Merge remote-tracking branch 'origin/master' into 106_authorization

This commit is contained in:
Robert Schäfer 2019-01-15 19:59:52 +01:00
commit a07f48826b
19 changed files with 211 additions and 30 deletions

View File

@ -6,4 +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"

View File

@ -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

View File

@ -17,8 +17,9 @@ 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
neo4j:
image: humanconnection/neo4j:latest

View File

@ -98,7 +98,9 @@ export const resolvers = {
const session = driver.session()
return session.run(
'MATCH (user:User {email: $userEmail}) ' +
'RETURN user {.id, .slug, .name, .avatar, .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) {

View File

@ -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<void>}
*/
async function asyncForEach (array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array)
}
}
export default asyncForEach

View File

@ -1,5 +1,3 @@
import format from 'date-fns/format'
export default {
Mutation: {
CreateUser: async (resolve, root, args, context, info) => {
@ -31,22 +29,22 @@ export default {
return result
},
UpdateUser: async (resolve, root, args, context, info) => {
args.updatedAt = format(new Date())
args.updatedAt = (new Date()).toISOString()
const result = await resolve(root, args, context, info)
return result
},
UpdatePost: async (resolve, root, args, context, info) => {
args.updatedAt = format(new Date())
args.updatedAt = (new Date()).toISOString()
const result = await resolve(root, args, context, info)
return result
},
UpdateComment: async (resolve, root, args, context, info) => {
args.updatedAt = format(new Date())
args.updatedAt = (new Date()).toISOString()
const result = await resolve(root, args, context, info)
return result
},
UpdateOrganization: async (resolve, root, args, context, info) => {
args.updatedAt = format(new Date())
args.updatedAt = (new Date()).toISOString()
const result = await resolve(root, args, context, info)
return result
}

View File

@ -6,6 +6,7 @@ import excerptMiddleware from './excerptMiddleware'
import dateTimeMiddleware from './dateTimeMiddleware'
import xssMiddleware from './xssMiddleware'
import permissionsMiddleware from './permissionsMiddleware'
import userMiddleware from './userMiddleware'
export default schema => {
let middleware = [
@ -15,7 +16,8 @@ export default schema => {
excerptMiddleware,
xssMiddleware,
fixImageUrlsMiddleware,
softDeleteMiddleware
softDeleteMiddleware,
userMiddleware
]
// add permisions middleware at the first position (unless we're seeding)

View File

@ -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

View File

@ -1,5 +1,5 @@
import bcrypt from 'bcryptjs'
import walkRecursive from './helpers/walkRecursive'
import walkRecursive from '../helpers/walkRecursive'
export default {
Mutation: {

View File

@ -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
})

View File

@ -0,0 +1,16 @@
import createOrUpdateLocations from './nodes/locations'
export default {
Mutation: {
CreateUser: async (resolve, root, args, context, info) => {
const result = await resolve(root, args, context, info)
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(args.id, args.locationName, context.driver)
return result
}
}
}

View File

@ -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'

View File

@ -13,6 +13,8 @@ type LoggedInUser {
avatar:String!
email: String!
role: String!
locationName: String
about: String
token: String!
}
@ -44,6 +46,23 @@ enum UserGroupEnum {
user
}
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
parent: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
}
type User {
id: ID!
name: String
@ -54,6 +73,11 @@ type User {
deleted: Boolean
disabled: Boolean
role: UserGroupEnum
location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
locationName: String
about: String
createdAt: String
updatedAt: String

View File

@ -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

View File

@ -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) {

View File

@ -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<void>}
*/
asyncForEach: async (array, callback) => {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array)
}
},
genInviteCode: () => {
const chars = '23456789abcdefghkmnpqrstuvwxyzABCDEFGHJKLMNPRSTUVWXYZ'
let code = ''

View File

@ -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,
@ -52,6 +59,7 @@ const createServer = (options) => {
return payload
},
schema: schema,
debug: debug,
tracing: debug,
middlewares: middleware(schema),
mocks: (process.env.MOCK === 'true') ? mocks : false