From e9bc848a334e30d4717c73325dd71f3d2bd39922 Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Fri, 4 Jan 2019 15:14:51 +0100 Subject: [PATCH 01/17] Improved locale handling and moved MAPBOX_TOKEN to env --- .env.template | 1 + src/middleware/userMiddleware.js | 59 ++++++++++++++++++++++---------- src/schema.graphql | 9 ++--- src/server.js | 1 + 4 files changed, 48 insertions(+), 22 deletions(-) diff --git a/.env.template b/.env.template index 3ec0cbce9..8f05f50c1 100644 --- a/.env.template +++ b/.env.template @@ -7,3 +7,4 @@ CLIENT_URI=http://localhost:3000 MOCK=false JWT_SECRET=b/&&7b78BF&fv/Vd +MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ" diff --git a/src/middleware/userMiddleware.js b/src/middleware/userMiddleware.js index 5e1f14493..56bb524c8 100644 --- a/src/middleware/userMiddleware.js +++ b/src/middleware/userMiddleware.js @@ -1,5 +1,11 @@ import request from 'request' -import zipObject from 'lodash/zipObject' + + +const asyncForEach = async (array, callback) => { + for (let index = 0; index < array.length; index++) { + await callback(array[index], index, array) + } +} const fetch = url => { return new Promise((resolve, reject) => { @@ -13,15 +19,12 @@ const fetch = url => { }) } -const createOrUpdateLocations = async (userId, locationId, driver) =>{ - if (!locationId) { +const createOrUpdateLocations = async (userId, locationName, driver) =>{ + if (!locationName) { return } - console.log('userId', userId) - console.log('locationId', locationId) - - const mapboxToken = 'pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ' - const res = await fetch(`https://api.mapbox.com/geocoding/v5/mapbox.places/${locationId}.json?access_token=${mapboxToken}&language=de`) + const mapboxToken = process.env.MAPBOX_TOKEN + const res = await fetch(`https://api.mapbox.com/geocoding/v5/mapbox.places/${locationName}.json?access_token=${mapboxToken}&language=de`) // TODO: create location in db @@ -29,15 +32,35 @@ const createOrUpdateLocations = async (userId, locationId, driver) =>{ const data = res.features[0] const session = driver.session() - const r = await session.run(`MERGE (l:Location {id: "${data.id}"}) SET l.name = "${data.place_name}", l.type = "${data.place_type[0]}", l.lat = "${data.center[0]}", l.lng = "${data.center[1]}" RETURN l.id, l.name, l.type, l.lat, l.lng`) - // let location = r.records[0]._fields ? zipObject([ - // 'id', - // 'name', - // 'type', - // 'lat', - // 'lng' - // ], r.records[0]._fields) : null + await session.run( + `MERGE (l:Location {id: "${data.id}"}) ` + + `SET l.name = "${data.text}", ` + + `l.type = "${data.place_type[0].toLowerCase()}", ` + + `l.lat = "${data.center[0]}", ` + + `l.lng = "${data.center[1]}" ` + + 'RETURN l.id, l.name, l.type, l.lat, l.lng' + ) + let parent = data + + if (data.context) { + await asyncForEach(data.context, async ctx => { + const type = ctx.id.split('.')[0].toLowerCase() + await session.run( + `MERGE (l:Location {id: "${ctx.id}"}) ` + + `SET l.name = "${ctx.text}", ` + + `l.type = "${type}", ` + + `l.shortCode = "${ctx.short_code}" ` + + 'RETURN l.id, l.name, l.type' + ) + await session.run( + `MATCH (parent:Location {id: "${parent.id}"}), (child:Location {id: "${ctx.id}"}) ` + + 'MERGE (child)<-[:IS_IN]-(parent) ' + + 'RETURN child.id, parent.id') + + parent = ctx + }) + } // delete all current locations from user await session.run(`MATCH (u:User {id: "${userId}"})-[r:IS_IN]->(l:Location) DETACH DELETE r`) // connect user with location @@ -49,12 +72,12 @@ export default { Mutation: { CreateUser: async (resolve, root, args, context, info) => { const result = await resolve(root, args, context, info) - await createOrUpdateLocations(context.user.id, args.locationId, context.driver) + await createOrUpdateLocations(context.user.id, args.locationName, context.driver) return result }, UpdateUser: async (resolve, root, args, context, info) => { const result = await resolve(root, args, context, info) - await createOrUpdateLocations(context.user.id, args.locationId, context.driver) + await createOrUpdateLocations(context.user.id, args.locationName, context.driver) return result } } diff --git a/src/schema.graphql b/src/schema.graphql index 5ee0412a9..48b3c4302 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -50,8 +50,9 @@ type Location { id: ID! name: String! type: String! - lat: Float! - lng: Float! + lat: Float + lng: Float + parent: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") } type User { @@ -65,8 +66,8 @@ type User { disabled: Boolean role: UserGroupEnum - location: [Location] @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") - locationId: String + location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") + locationName: String about: String createdAt: String diff --git a/src/server.js b/src/server.js index 3b0e0a561..1175509e7 100644 --- a/src/server.js +++ b/src/server.js @@ -51,6 +51,7 @@ const createServer = (options) => { }, schema: schema, tracing: true, + debug: process.env.NODE_ENV !== 'production', middlewares: middleware(schema), mocks: (process.env.MOCK === 'true') ? mocks : false } From 30d4318d1738796b4c1f299a15fa8248e7da7002 Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Fri, 4 Jan 2019 15:15:07 +0100 Subject: [PATCH 02/17] Throw an error if requried env vars are missing --- src/server.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/server.js b/src/server.js index 1175509e7..eff18f8d0 100644 --- a/src/server.js +++ b/src/server.js @@ -14,6 +14,13 @@ import jwtStrategy from './jwt/strategy' import jwt from 'jsonwebtoken' dotenv.config() +// check env and warn +const requiredEnvVars = ['MAPBOX_TOKEN', 'JWT_SECRET'] +requiredEnvVars.forEach(env => { + if (!process.env[env]) { + throw new Error(`ERROR: "${env}" env variable is missing`) + } +}) let schema = makeExecutableSchema({ typeDefs, From 77e77229b90f2babc6b9770e8bce63d7bc49de02 Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Mon, 7 Jan 2019 17:30:54 +0100 Subject: [PATCH 03/17] Fixed login and schema --- src/graphql-schema.js | 5 +++-- src/schema.graphql | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/graphql-schema.js b/src/graphql-schema.js index 9f6b4fc29..cbeb6bb55 100644 --- a/src/graphql-schema.js +++ b/src/graphql-schema.js @@ -98,11 +98,12 @@ export const resolvers = { const session = driver.session() return session.run( 'MATCH (user:User {email: $userEmail}) ' + - 'RETURN user {.id, .slug, .name, .avatar, .locationId, .about, .email, .password, .role} as user LIMIT 1', { userEmail: email }) + 'RETURN user {.id, .slug, .name, .avatar, .locationName, .about, .email, .password, .role} as user LIMIT 1', { + userEmail: email + }) .then(async (result) => { session.close() const [currentUser] = await result.records.map(function (record) { - console.log(record.get('user')) return record.get('user') }) if (currentUser && await bcrypt.compareSync(password, currentUser.password)) { diff --git a/src/schema.graphql b/src/schema.graphql index 48b3c4302..cdc8aa7a2 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -13,7 +13,7 @@ type LoggedInUser { avatar:String! email: String! role: String! - locationId: String + locationName: String about: String token: String! } From 9fab4a8fe712e7ff7a41e5e881d515321555a7eb Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Mon, 7 Jan 2019 17:34:18 +0100 Subject: [PATCH 04/17] Fixed injection issues --- src/middleware/userMiddleware.js | 49 ++++++++++++++++++++++---------- src/server.js | 2 +- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/middleware/userMiddleware.js b/src/middleware/userMiddleware.js index 56bb524c8..5bf0d5af9 100644 --- a/src/middleware/userMiddleware.js +++ b/src/middleware/userMiddleware.js @@ -33,12 +33,18 @@ const createOrUpdateLocations = async (userId, locationName, driver) =>{ const data = res.features[0] const session = driver.session() await session.run( - `MERGE (l:Location {id: "${data.id}"}) ` + - `SET l.name = "${data.text}", ` + - `l.type = "${data.place_type[0].toLowerCase()}", ` + - `l.lat = "${data.center[0]}", ` + - `l.lng = "${data.center[1]}" ` + - 'RETURN l.id, l.name, l.type, l.lat, l.lng' + 'MERGE (l:Location {id: $id}) ' + + 'SET l.name = $name, ' + + 'l.type = $type, ' + + 'l.lat = $lat, ' + + 'l.lng = $lng ' + + 'RETURN l.id, l.name, l.type, l.lat, l.lng', { + id: data.id, + name: data.text, + type: data.place_type[0].toLowerCase(), + lat: data.center[0], + lng: data.center[1] + } ) let parent = data @@ -47,24 +53,37 @@ const createOrUpdateLocations = async (userId, locationName, driver) =>{ await asyncForEach(data.context, async ctx => { const type = ctx.id.split('.')[0].toLowerCase() await session.run( - `MERGE (l:Location {id: "${ctx.id}"}) ` + - `SET l.name = "${ctx.text}", ` + - `l.type = "${type}", ` + - `l.shortCode = "${ctx.short_code}" ` + - 'RETURN l.id, l.name, l.type' + 'MERGE (l:Location {id: $id}) ' + + 'SET l.name = $name, ' + + 'l.type = $type, ' + + 'l.shortCode = $short_code ' + + 'RETURN l.id, l.name, l.type', { + id: ctx.id, + name: ctx.text, + type: type, + shortCode: ctx.short_code + } ) await session.run( - `MATCH (parent:Location {id: "${parent.id}"}), (child:Location {id: "${ctx.id}"}) ` + + 'MATCH (parent:Location {id: $parentId}), (child:Location {id: $childId}) ' + 'MERGE (child)<-[:IS_IN]-(parent) ' + - 'RETURN child.id, parent.id') + 'RETURN child.id, parent.id', { + parentId: parent.id, + childId: ctx.id + }) parent = ctx }) } // delete all current locations from user - await session.run(`MATCH (u:User {id: "${userId}"})-[r:IS_IN]->(l:Location) DETACH DELETE r`) + await session.run('MATCH (u:User {id: $userId})-[r:IS_IN]->(l:Location) DETACH DELETE r', { + userId: userId + }) // connect user with location - await session.run(`MATCH (u:User {id: "${userId}"}), (l:Location {id: "${data.id}"}) MERGE (u)-[:IS_IN]->(l) RETURN l.id, u.id`) + await session.run('MATCH (u:User {id: $userId}), (l:Location {id: $locationId}) MERGE (u)-[:IS_IN]->(l) RETURN l.id, u.id', { + userId: userId, + locationId: data.id + }) session.close() } diff --git a/src/server.js b/src/server.js index eff18f8d0..1ecab800e 100644 --- a/src/server.js +++ b/src/server.js @@ -18,7 +18,7 @@ dotenv.config() const requiredEnvVars = ['MAPBOX_TOKEN', 'JWT_SECRET'] requiredEnvVars.forEach(env => { if (!process.env[env]) { - throw new Error(`ERROR: "${env}" env variable is missing`) + throw new Error(`ERROR: "${env}" env variable is missing.`) } }) From cffce564ea265eb629161ea055245fe83c02d73d Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Mon, 7 Jan 2019 17:58:27 +0100 Subject: [PATCH 05/17] Fixed short_code --- src/middleware/userMiddleware.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/userMiddleware.js b/src/middleware/userMiddleware.js index 5bf0d5af9..d0cefc139 100644 --- a/src/middleware/userMiddleware.js +++ b/src/middleware/userMiddleware.js @@ -56,7 +56,7 @@ const createOrUpdateLocations = async (userId, locationName, driver) =>{ 'MERGE (l:Location {id: $id}) ' + 'SET l.name = $name, ' + 'l.type = $type, ' + - 'l.shortCode = $short_code ' + + 'l.shortCode = $shortCode ' + 'RETURN l.id, l.name, l.type', { id: ctx.id, name: ctx.text, From a313f941c5e13ecc36f6096e5f24c99a22763193 Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Mon, 7 Jan 2019 20:25:05 +0100 Subject: [PATCH 06/17] Fixed eslint issue --- src/middleware/userMiddleware.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/middleware/userMiddleware.js b/src/middleware/userMiddleware.js index d0cefc139..28c419837 100644 --- a/src/middleware/userMiddleware.js +++ b/src/middleware/userMiddleware.js @@ -1,6 +1,5 @@ import request from 'request' - const asyncForEach = async (array, callback) => { for (let index = 0; index < array.length; index++) { await callback(array[index], index, array) @@ -19,7 +18,7 @@ const fetch = url => { }) } -const createOrUpdateLocations = async (userId, locationName, driver) =>{ +const createOrUpdateLocations = async (userId, locationName, driver) => { if (!locationName) { return } From b5649ca2e3fda41d6c4d2ebc8a1c23248dcbf860 Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Tue, 8 Jan 2019 14:34:00 +0100 Subject: [PATCH 07/17] Internationalized Locations --- src/middleware/userMiddleware.js | 53 ++++++++++++++++++++++++++------ src/schema.graphql | 8 +++++ 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/middleware/userMiddleware.js b/src/middleware/userMiddleware.js index 28c419837..c4cba0c63 100644 --- a/src/middleware/userMiddleware.js +++ b/src/middleware/userMiddleware.js @@ -1,4 +1,5 @@ import request from 'request' +import { UserInputError } from 'apollo-server' const asyncForEach = async (array, callback) => { for (let index = 0; index < array.length; index++) { @@ -23,23 +24,38 @@ const createOrUpdateLocations = async (userId, locationName, driver) => { return } const mapboxToken = process.env.MAPBOX_TOKEN - const res = await fetch(`https://api.mapbox.com/geocoding/v5/mapbox.places/${locationName}.json?access_token=${mapboxToken}&language=de`) + const res = await fetch(`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(locationName)}.json?access_token=${mapboxToken}&types=region,place,country&language=en,de,fr,nl,it,es,pt,pl&limit=1`) - // TODO: create location in db - - // TODO: get related region, district and country to build the location tree + if (!res || !res.features || !res.features[0]) { + throw new UserInputError('locationName is invalid') + } const data = res.features[0] const session = driver.session() await session.run( 'MERGE (l:Location {id: $id}) ' + - 'SET l.name = $name, ' + + 'SET l.name = $nameEN, ' + + 'l.nameEN = $nameEN, ' + + 'l.nameDE = $nameDE, ' + + 'l.nameFR = $nameFR, ' + + 'l.nameNL = $nameNL, ' + + 'l.nameIT = $nameIT, ' + + 'l.nameES = $nameES, ' + + 'l.namePT = $namePT, ' + + 'l.namePL = $namePL, ' + 'l.type = $type, ' + 'l.lat = $lat, ' + 'l.lng = $lng ' + - 'RETURN l.id, l.name, l.type, l.lat, l.lng', { + 'RETURN l.id', { id: data.id, - name: data.text, + nameEN: data.text_en, + nameDE: data.text_de, + nameFR: data.text_fr, + nameNL: data.text_nl, + nameIT: data.text_it, + nameES: data.text_es, + namePT: data.text_pt, + namePL: data.text_pl, type: data.place_type[0].toLowerCase(), lat: data.center[0], lng: data.center[1] @@ -53,12 +69,27 @@ const createOrUpdateLocations = async (userId, locationName, driver) => { const type = ctx.id.split('.')[0].toLowerCase() await session.run( 'MERGE (l:Location {id: $id}) ' + - 'SET l.name = $name, ' + + 'SET l.name = $nameEN, ' + + 'l.nameEN = $nameEN, ' + + 'l.nameDE = $nameDE, ' + + 'l.nameFR = $nameFR, ' + + 'l.nameNL = $nameNL, ' + + 'l.nameIT = $nameIT, ' + + 'l.nameES = $nameES, ' + + 'l.namePT = $namePT, ' + + 'l.namePL = $namePL, ' + 'l.type = $type, ' + 'l.shortCode = $shortCode ' + - 'RETURN l.id, l.name, l.type', { + 'RETURN l.id', { id: ctx.id, - name: ctx.text, + nameEN: ctx.text_en, + nameDE: ctx.text_de, + nameFR: ctx.text_fr, + nameNL: ctx.text_nl, + nameIT: ctx.text_it, + nameES: ctx.text_es, + namePT: ctx.text_pt, + namePL: ctx.text_pl, type: type, shortCode: ctx.short_code } @@ -94,6 +125,8 @@ export default { return result }, UpdateUser: async (resolve, root, args, context, info) => { + console.log(context.req.headers['accept-language']) + console.log(context.req.headers) const result = await resolve(root, args, context, info) await createOrUpdateLocations(context.user.id, args.locationName, context.driver) return result diff --git a/src/schema.graphql b/src/schema.graphql index cdc8aa7a2..3031ac0fe 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -49,6 +49,14 @@ enum UserGroupEnum { type Location { id: ID! name: String! + nameEN: String + nameDE: String + nameFR: String + nameNL: String + nameIT: String + nameES: String + namePT: String + namePL: String type: String! lat: Float lng: Float From 0bfa047a5a39e2014493c2ec38ea1153aa599a3b Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Tue, 8 Jan 2019 15:12:44 +0100 Subject: [PATCH 08/17] Fixed issues for non german / english languages --- src/middleware/userMiddleware.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/middleware/userMiddleware.js b/src/middleware/userMiddleware.js index c4cba0c63..2d8de6a77 100644 --- a/src/middleware/userMiddleware.js +++ b/src/middleware/userMiddleware.js @@ -24,13 +24,27 @@ const createOrUpdateLocations = async (userId, locationName, driver) => { return } const mapboxToken = process.env.MAPBOX_TOKEN - const res = await fetch(`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(locationName)}.json?access_token=${mapboxToken}&types=region,place,country&language=en,de,fr,nl,it,es,pt,pl&limit=1`) + const res = await fetch(`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(locationName)}.json?access_token=${mapboxToken}&types=region,place,country&language=en,de,fr,nl,it,es,pt,pl`) if (!res || !res.features || !res.features[0]) { throw new UserInputError('locationName is invalid') } - const data = res.features[0] + let data + + res.features.forEach(item => { + if (item.matching_place_name === locationName) { + data = item + } + }) + if (!data) { + data = res.features[0] + } + + if (!data) { + throw new UserInputError('locationName is invalid') + } + const session = driver.session() await session.run( 'MERGE (l:Location {id: $id}) ' + @@ -125,8 +139,6 @@ export default { return result }, UpdateUser: async (resolve, root, args, context, info) => { - console.log(context.req.headers['accept-language']) - console.log(context.req.headers) const result = await resolve(root, args, context, info) await createOrUpdateLocations(context.user.id, args.locationName, context.driver) return result From 9629ca649ac92580af02e51252bb1aac96e1daf1 Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Wed, 9 Jan 2019 17:20:25 +0100 Subject: [PATCH 09/17] Fixed user middleware --- src/middleware/userMiddleware.js | 7 ++++--- src/nodes/users/userMiddleware.js | 0 2 files changed, 4 insertions(+), 3 deletions(-) delete mode 100644 src/nodes/users/userMiddleware.js diff --git a/src/middleware/userMiddleware.js b/src/middleware/userMiddleware.js index 2d8de6a77..adb6eb46f 100644 --- a/src/middleware/userMiddleware.js +++ b/src/middleware/userMiddleware.js @@ -1,5 +1,6 @@ import request from 'request' import { UserInputError } from 'apollo-server' +import isEmpty from 'lodash/isEmpty' const asyncForEach = async (array, callback) => { for (let index = 0; index < array.length; index++) { @@ -20,7 +21,7 @@ const fetch = url => { } const createOrUpdateLocations = async (userId, locationName, driver) => { - if (!locationName) { + if (isEmpty(locationName)) { return } const mapboxToken = process.env.MAPBOX_TOKEN @@ -135,12 +136,12 @@ export default { Mutation: { CreateUser: async (resolve, root, args, context, info) => { const result = await resolve(root, args, context, info) - await createOrUpdateLocations(context.user.id, args.locationName, context.driver) + await createOrUpdateLocations(args.id, args.locationName, context.driver) return result }, UpdateUser: async (resolve, root, args, context, info) => { const result = await resolve(root, args, context, info) - await createOrUpdateLocations(context.user.id, args.locationName, context.driver) + await createOrUpdateLocations(args.id, args.locationName, context.driver) return result } } diff --git a/src/nodes/users/userMiddleware.js b/src/nodes/users/userMiddleware.js deleted file mode 100644 index e69de29bb..000000000 From ced8bd78bbca0a3a1fa308a8fa1dcd85fc69eb30 Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Wed, 9 Jan 2019 17:21:30 +0100 Subject: [PATCH 10/17] Added locations to seeded users --- src/seed/data/users.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/seed/data/users.js b/src/seed/data/users.js index 2c148bc0c..8106a75f4 100644 --- a/src/seed/data/users.js +++ b/src/seed/data/users.js @@ -9,6 +9,7 @@ export default function (data) { password: "1234", email: "admin@example.org", avatar: "${faker.internet.avatar()}", + locationName: "Hamburg, Germany", role: admin, disabled: false, deleted: false) { @@ -24,6 +25,7 @@ export default function (data) { password: "1234", email: "moderator@example.org", avatar: "${faker.internet.avatar()}", + locationName: "USA", role: moderator, disabled: false, deleted: false) { From 887cc8fddf35dc3c82f2f2578169cc9daeb56ce3 Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Thu, 10 Jan 2019 15:59:10 +0100 Subject: [PATCH 11/17] Refactoring --- src/helpers/asyncForEach.js | 14 ++ src/{middleware => }/helpers/walkRecursive.js | 0 src/middleware/nodes/locations.js | 124 ++++++++++++++++ src/middleware/passwordMiddleware.js | 2 +- src/middleware/userMiddleware.js | 134 +----------------- src/middleware/xssMiddleware.js | 2 +- src/seed/data/index.js | 4 +- src/seed/seed-helpers.js | 18 +-- 8 files changed, 146 insertions(+), 152 deletions(-) create mode 100644 src/helpers/asyncForEach.js rename src/{middleware => }/helpers/walkRecursive.js (100%) create mode 100644 src/middleware/nodes/locations.js diff --git a/src/helpers/asyncForEach.js b/src/helpers/asyncForEach.js new file mode 100644 index 000000000..1f05ea915 --- /dev/null +++ b/src/helpers/asyncForEach.js @@ -0,0 +1,14 @@ +/** + * Provide a way to iterate for each element in an array while waiting for async functions to finish + * + * @param array + * @param callback + * @returns {Promise} + */ +async function asyncForEach (array, callback) { + for (let index = 0; index < array.length; index++) { + await callback(array[index], index, array) + } +} + +export default asyncForEach diff --git a/src/middleware/helpers/walkRecursive.js b/src/helpers/walkRecursive.js similarity index 100% rename from src/middleware/helpers/walkRecursive.js rename to src/helpers/walkRecursive.js diff --git a/src/middleware/nodes/locations.js b/src/middleware/nodes/locations.js new file mode 100644 index 000000000..735b047dd --- /dev/null +++ b/src/middleware/nodes/locations.js @@ -0,0 +1,124 @@ + +import request from 'request' +import { UserInputError } from 'apollo-server' +import isEmpty from 'lodash/isEmpty' +import asyncForEach from '../../helpers/asyncForEach' + +const fetch = url => { + return new Promise((resolve, reject) => { + request(url, function (error, response, body) { + if (error) { + reject(error) + } else { + resolve(JSON.parse(body)) + } + }) + }) +} + +const locales = [ + 'en', + 'de', + 'fr', + 'nl', + 'it', + 'es', + 'pt', + 'pl' +] + +const createLocation = async (session, mapboxData) => { + const data = { + id: mapboxData.id, + nameEN: mapboxData.text_en, + nameDE: mapboxData.text_de, + nameFR: mapboxData.text_fr, + nameNL: mapboxData.text_nl, + nameIT: mapboxData.text_it, + nameES: mapboxData.text_es, + namePT: mapboxData.text_pt, + namePL: mapboxData.text_pl, + type: mapboxData.id.split('.')[0].toLowerCase(), + lat: (mapboxData.center && mapboxData.center.length) ? mapboxData.center[0] : null, + lng: (mapboxData.center && mapboxData.center.length) ? mapboxData.center[1] : null + } + + let query = 'MERGE (l:Location {id: $id}) ' + + 'SET l.name = $nameEN, ' + + 'l.nameEN = $nameEN, ' + + 'l.nameDE = $nameDE, ' + + 'l.nameFR = $nameFR, ' + + 'l.nameNL = $nameNL, ' + + 'l.nameIT = $nameIT, ' + + 'l.nameES = $nameES, ' + + 'l.namePT = $namePT, ' + + 'l.namePL = $namePL, ' + + 'l.type = $type' + + if (data.lat && data.lng) { + query += ', l.lat = $lat, l.lng = $lng' + } + query += ' RETURN l.id' + + await session.run(query, data) +} + +const createOrUpdateLocations = async (userId, locationName, driver) => { + if (isEmpty(locationName)) { + return + } + const mapboxToken = process.env.MAPBOX_TOKEN + const res = await fetch(`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(locationName)}.json?access_token=${mapboxToken}&types=region,place,country&language=${locales.join(',')}`) + + if (!res || !res.features || !res.features[0]) { + throw new UserInputError('locationName is invalid') + } + + let data + + res.features.forEach(item => { + if (item.matching_place_name === locationName) { + data = item + } + }) + if (!data) { + data = res.features[0] + } + + if (!data || !data.place_type || !data.place_type.length) { + throw new UserInputError('locationName is invalid') + } + + const session = driver.session() + await createLocation(session, data) + + let parent = data + + if (data.context) { + await asyncForEach(data.context, async ctx => { + await createLocation(session, ctx) + + await session.run( + 'MATCH (parent:Location {id: $parentId}), (child:Location {id: $childId}) ' + + 'MERGE (child)<-[:IS_IN]-(parent) ' + + 'RETURN child.id, parent.id', { + parentId: parent.id, + childId: ctx.id + }) + + parent = ctx + }) + } + // delete all current locations from user + await session.run('MATCH (u:User {id: $userId})-[r:IS_IN]->(l:Location) DETACH DELETE r', { + userId: userId + }) + // connect user with location + await session.run('MATCH (u:User {id: $userId}), (l:Location {id: $locationId}) MERGE (u)-[:IS_IN]->(l) RETURN l.id, u.id', { + userId: userId, + locationId: data.id + }) + session.close() +} + +export default createOrUpdateLocations diff --git a/src/middleware/passwordMiddleware.js b/src/middleware/passwordMiddleware.js index 4f551fe5d..16480b126 100644 --- a/src/middleware/passwordMiddleware.js +++ b/src/middleware/passwordMiddleware.js @@ -1,5 +1,5 @@ import bcrypt from 'bcryptjs' -import walkRecursive from './helpers/walkRecursive' +import walkRecursive from '../helpers/walkRecursive' export default { Mutation: { diff --git a/src/middleware/userMiddleware.js b/src/middleware/userMiddleware.js index adb6eb46f..55b181bc9 100644 --- a/src/middleware/userMiddleware.js +++ b/src/middleware/userMiddleware.js @@ -1,136 +1,4 @@ -import request from 'request' -import { UserInputError } from 'apollo-server' -import isEmpty from 'lodash/isEmpty' - -const asyncForEach = async (array, callback) => { - for (let index = 0; index < array.length; index++) { - await callback(array[index], index, array) - } -} - -const fetch = url => { - return new Promise((resolve, reject) => { - request(url, function (error, response, body) { - if (error) { - reject(error) - } else { - resolve(JSON.parse(body)) - } - }) - }) -} - -const createOrUpdateLocations = async (userId, locationName, driver) => { - if (isEmpty(locationName)) { - return - } - const mapboxToken = process.env.MAPBOX_TOKEN - const res = await fetch(`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(locationName)}.json?access_token=${mapboxToken}&types=region,place,country&language=en,de,fr,nl,it,es,pt,pl`) - - if (!res || !res.features || !res.features[0]) { - throw new UserInputError('locationName is invalid') - } - - let data - - res.features.forEach(item => { - if (item.matching_place_name === locationName) { - data = item - } - }) - if (!data) { - data = res.features[0] - } - - if (!data) { - throw new UserInputError('locationName is invalid') - } - - const session = driver.session() - await session.run( - 'MERGE (l:Location {id: $id}) ' + - 'SET l.name = $nameEN, ' + - 'l.nameEN = $nameEN, ' + - 'l.nameDE = $nameDE, ' + - 'l.nameFR = $nameFR, ' + - 'l.nameNL = $nameNL, ' + - 'l.nameIT = $nameIT, ' + - 'l.nameES = $nameES, ' + - 'l.namePT = $namePT, ' + - 'l.namePL = $namePL, ' + - 'l.type = $type, ' + - 'l.lat = $lat, ' + - 'l.lng = $lng ' + - 'RETURN l.id', { - id: data.id, - nameEN: data.text_en, - nameDE: data.text_de, - nameFR: data.text_fr, - nameNL: data.text_nl, - nameIT: data.text_it, - nameES: data.text_es, - namePT: data.text_pt, - namePL: data.text_pl, - type: data.place_type[0].toLowerCase(), - lat: data.center[0], - lng: data.center[1] - } - ) - - let parent = data - - if (data.context) { - await asyncForEach(data.context, async ctx => { - const type = ctx.id.split('.')[0].toLowerCase() - await session.run( - 'MERGE (l:Location {id: $id}) ' + - 'SET l.name = $nameEN, ' + - 'l.nameEN = $nameEN, ' + - 'l.nameDE = $nameDE, ' + - 'l.nameFR = $nameFR, ' + - 'l.nameNL = $nameNL, ' + - 'l.nameIT = $nameIT, ' + - 'l.nameES = $nameES, ' + - 'l.namePT = $namePT, ' + - 'l.namePL = $namePL, ' + - 'l.type = $type, ' + - 'l.shortCode = $shortCode ' + - 'RETURN l.id', { - id: ctx.id, - nameEN: ctx.text_en, - nameDE: ctx.text_de, - nameFR: ctx.text_fr, - nameNL: ctx.text_nl, - nameIT: ctx.text_it, - nameES: ctx.text_es, - namePT: ctx.text_pt, - namePL: ctx.text_pl, - type: type, - shortCode: ctx.short_code - } - ) - await session.run( - 'MATCH (parent:Location {id: $parentId}), (child:Location {id: $childId}) ' + - 'MERGE (child)<-[:IS_IN]-(parent) ' + - 'RETURN child.id, parent.id', { - parentId: parent.id, - childId: ctx.id - }) - - parent = ctx - }) - } - // delete all current locations from user - await session.run('MATCH (u:User {id: $userId})-[r:IS_IN]->(l:Location) DETACH DELETE r', { - userId: userId - }) - // connect user with location - await session.run('MATCH (u:User {id: $userId}), (l:Location {id: $locationId}) MERGE (u)-[:IS_IN]->(l) RETURN l.id, u.id', { - userId: userId, - locationId: data.id - }) - session.close() -} +import createOrUpdateLocations from './nodes/locations' export default { Mutation: { diff --git a/src/middleware/xssMiddleware.js b/src/middleware/xssMiddleware.js index ac71f3421..59ce8800f 100644 --- a/src/middleware/xssMiddleware.js +++ b/src/middleware/xssMiddleware.js @@ -1,4 +1,4 @@ -import walkRecursive from './helpers/walkRecursive' +import walkRecursive from '../helpers/walkRecursive' // import { getByDot, setByDot, getItems, replaceItems } from 'feathers-hooks-common' import sanitizeHtml from 'sanitize-html' // import { isEmpty, intersection } from 'lodash' diff --git a/src/seed/data/index.js b/src/seed/data/index.js index e50fececc..33bc56f36 100644 --- a/src/seed/data/index.js +++ b/src/seed/data/index.js @@ -1,5 +1,5 @@ import gql from 'graphql-tag' -import helper from '../seed-helpers' +import asyncForEach from '../../helpers/asyncForEach' const seed = { Badge: require('./badges.js').default, @@ -22,7 +22,7 @@ let data = {} export default async function (client) { // iterate through seeds - await helper.asyncForEach(Object.keys(seed), async key => { + await asyncForEach(Object.keys(seed), async key => { const mutations = seed[key] try { const res = await client diff --git a/src/seed/seed-helpers.js b/src/seed/seed-helpers.js index 48b9ba01d..23bde40ae 100644 --- a/src/seed/seed-helpers.js +++ b/src/seed/seed-helpers.js @@ -46,7 +46,7 @@ export default { let randomIds = _.shuffle(ids) return items[randomIds.pop()] }, - randomItems: (items, key = '_id', min = 1, max = 1) => { + randomItems: (items, key = 'id', min = 1, max = 1) => { let randomIds = _.shuffle(_.keys(items)) let res = [] @@ -54,7 +54,7 @@ export default { for (let i = 0; i < count; i++) { let r = items[randomIds.pop()][key] - if (key === '_id') { + if (key === 'id') { r = r.toString() } res.push(r) @@ -117,22 +117,10 @@ export default { mapIdsByKey: (items, values, key) => { let res = [] values.forEach(value => { - res.push(_.find(items, [key, value])._id.toString()) + res.push(_.find(items, [key, value]).id.toString()) }) return res }, - /** - * Provide a way to iterate for each element in an array while waiting for async functions to finish - * - * @param array - * @param callback - * @returns {Promise} - */ - asyncForEach: async (array, callback) => { - for (let index = 0; index < array.length; index++) { - await callback(array[index], index, array) - } - }, genInviteCode: () => { const chars = '23456789abcdefghkmnpqrstuvwxyzABCDEFGHJKLMNPRSTUVWXYZ' let code = '' From fb3c169f65cafbb639b9901eef38553dea3c3c53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 11 Jan 2019 14:29:56 +0100 Subject: [PATCH 12/17] Add missing env variable to `docker-compose.yml` @appinteractive always run the docker-compose up workflow at least once. --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 459496173..639592b88 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,7 @@ services: - GRAPHQL_URI=http://localhost:4000 - CLIENT_URI=http://localhost:3000 - JWT_SECRET=b/&&7b78BF&fv/Vd + - MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ" - MOCK=false neo4j: From f4f69cfe0f0b74fe35873542cba6a9d0d8240285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 11 Jan 2019 17:59:06 +0100 Subject: [PATCH 13/17] Quote JWT Token (consistency) --- .env.template | 2 +- docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.template b/.env.template index 8f05f50c1..42211b184 100644 --- a/.env.template +++ b/.env.template @@ -6,5 +6,5 @@ GRAPHQL_URI=http://localhost:4000 CLIENT_URI=http://localhost:3000 MOCK=false -JWT_SECRET=b/&&7b78BF&fv/Vd +JWT_SECRET="b/&&7b78BF&fv/Vd" MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ" diff --git a/docker-compose.yml b/docker-compose.yml index 639592b88..755169b2d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,7 @@ services: - GRAPHQL_PORT=4000 - GRAPHQL_URI=http://localhost:4000 - CLIENT_URI=http://localhost:3000 - - JWT_SECRET=b/&&7b78BF&fv/Vd + - JWT_SECRET="b/&&7b78BF&fv/Vd" - MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ" - MOCK=false From aa9cfceba98b662a606cf43dec0be8b473db039f Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Sat, 12 Jan 2019 15:50:51 +0100 Subject: [PATCH 14/17] Try to fix build issues --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index ed6c274ce..750d284dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,7 @@ CMD ["yarn", "run", "start"] FROM base as builder RUN yarn install --frozen-lockfile --non-interactive COPY . . +RUN cp .env.template .env RUN yarn run build # reduce image size with a multistage build From 79749e657035d65e5adbaeceb07c72acf0a4ab0f Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Mon, 14 Jan 2019 17:07:20 +0100 Subject: [PATCH 15/17] Fixed isOwner permission --- src/middleware/permissionsMiddleware.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index 5b0cb87d2..6f786583e 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -14,7 +14,7 @@ const isModerator = rule()(async (parent, args, ctx, info) => { }) */ -const isOwner = rule()(async (parent, args, ctx, info) => { +const isOwner = rule({ cache: 'no_cache' })(async (parent, args, ctx, info) => { return ctx.user.id === parent.id }) From 75ee9bf9b44ab2edc1ca489a0c5a35d8b3d9fccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 14 Jan 2019 21:50:42 +0100 Subject: [PATCH 16/17] Fix seed data in docker environment @appinteractive Running ``` docker-compose -f ../Nitro-Backend/docker-compose.yml -f ../Nitro-Backend/docker-compose.travis.yml up ``` from the Nitro-Web folder gives me an error when running seed data. The configuration in this commit fixes it on my machine. I guess that will fix the frontend build, too. --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 755169b2d..8e4a6c498 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,9 +17,9 @@ services: - GRAPHQL_PORT=4000 - GRAPHQL_URI=http://localhost:4000 - CLIENT_URI=http://localhost:3000 - - JWT_SECRET="b/&&7b78BF&fv/Vd" - - MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ" + - JWT_SECRET=b/&&7b78BF&fv/Vd - MOCK=false + - MAPBOX_TOKEN=pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ neo4j: image: humanconnection/neo4j:latest From 9e28704d9b520482f0e2656597abbf33821358b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 15 Jan 2019 01:20:32 +0100 Subject: [PATCH 17/17] Fix all cypress tests but one @appinteractive, it's super strange but apparently the JWT has to be enclosed in quotes whereas the MAPBOX_TOKEN must not be enclosed. This commit fixes almost all tests running in the docker containers on my machine. The remaining test case is: ``` About me and and location I set my location to "Mecklenburg-Vorpommern" (example #2) ``` @appinteractive when I try to repeat the cypress test on my machine I get no results for "Mecklenburg-Vorpommern" only cities and states. can you check if regions really work? --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8e4a6c498..5a7650aa1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,7 @@ services: - GRAPHQL_PORT=4000 - GRAPHQL_URI=http://localhost:4000 - CLIENT_URI=http://localhost:3000 - - JWT_SECRET=b/&&7b78BF&fv/Vd + - JWT_SECRET="b/&&7b78BF&fv/Vd" - MOCK=false - MAPBOX_TOKEN=pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ