Merge branch 'master' of github.com:Human-Connection/Human-Connection into 967-filter-post-by-category

This commit is contained in:
Matt Rider 2019-07-04 20:33:24 -03:00
commit a875fe0d5e
48 changed files with 1527 additions and 451 deletions

View File

@ -29,9 +29,10 @@ script:
- docker-compose exec backend yarn run test:jest --ci --verbose=false --coverage - docker-compose exec backend yarn run test:jest --ci --verbose=false --coverage
- docker-compose exec backend yarn run db:reset - docker-compose exec backend yarn run db:reset
- docker-compose exec backend yarn run db:seed - docker-compose exec backend yarn run db:seed
- docker-compose exec backend yarn run test:cucumber --tags "not @wip" # ActivityPub cucumber testing temporarily disabled because it's too buggy
- docker-compose exec backend yarn run db:reset # - docker-compose exec backend yarn run test:cucumber --tags "not @wip"
- docker-compose exec backend yarn run db:seed # - docker-compose exec backend yarn run db:reset
# - docker-compose exec backend yarn run db:seed
# Frontend # Frontend
- docker-compose exec webapp yarn run lint - docker-compose exec webapp yarn run lint
- docker-compose exec webapp yarn run test --ci --verbose=false --coverage - docker-compose exec webapp yarn run test --ci --verbose=false --coverage

View File

@ -42,17 +42,18 @@
] ]
}, },
"dependencies": { "dependencies": {
"@hapi/joi": "^15.1.0",
"activitystrea.ms": "~2.1.3", "activitystrea.ms": "~2.1.3",
"apollo-cache-inmemory": "~1.6.2", "apollo-cache-inmemory": "~1.6.2",
"apollo-client": "~2.6.3", "apollo-client": "~2.6.3",
"apollo-link-context": "~1.0.18", "apollo-link-context": "~1.0.18",
"apollo-link-http": "~1.5.15", "apollo-link-http": "~1.5.15",
"apollo-server": "~2.6.7", "apollo-server": "~2.6.6",
"bcryptjs": "~2.4.3", "bcryptjs": "~2.4.3",
"cheerio": "~1.0.0-rc.3", "cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5", "cors": "~2.8.5",
"cross-env": "~5.2.0", "cross-env": "~5.2.0",
"date-fns": "2.0.0-beta.2", "date-fns": "2.0.0-beta.1",
"debug": "~4.1.1", "debug": "~4.1.1",
"dotenv": "~8.0.0", "dotenv": "~8.0.0",
"express": "~4.17.1", "express": "~4.17.1",
@ -71,6 +72,7 @@
"merge-graphql-schemas": "^1.5.8", "merge-graphql-schemas": "^1.5.8",
"neo4j-driver": "~1.7.4", "neo4j-driver": "~1.7.4",
"neo4j-graphql-js": "^2.6.3", "neo4j-graphql-js": "^2.6.3",
"neode": "^0.2.16",
"node-fetch": "~2.6.0", "node-fetch": "~2.6.0",
"nodemailer": "^6.2.1", "nodemailer": "^6.2.1",
"npm-run-all": "~4.1.5", "npm-run-all": "~4.1.5",

View File

@ -1,5 +1,6 @@
import { v1 as neo4j } from 'neo4j-driver' import { v1 as neo4j } from 'neo4j-driver'
import CONFIG from './../config' import CONFIG from './../config'
import setupNeode from './neode'
let driver let driver
@ -14,3 +15,12 @@ export function getDriver(options = {}) {
} }
return driver return driver
} }
let neodeInstance
export function neode() {
if (!neodeInstance) {
const { NEO4J_URI: uri, NEO4J_USERNAME: username, NEO4J_PASSWORD: password } = CONFIG
neodeInstance = setupNeode({ uri, username, password })
}
return neodeInstance
}

View File

@ -0,0 +1,88 @@
import Neode from 'neode'
import uuid from 'uuid/v4'
export default function setupNeode(options) {
const { uri, username, password } = options
const neodeInstance = new Neode(uri, username, password)
neodeInstance.model('InvitationCode', {
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
token: { type: 'string', primary: true, token: true },
generatedBy: {
type: 'relationship',
relationship: 'GENERATED',
target: 'User',
direction: 'in',
},
activated: {
type: 'relationship',
relationship: 'ACTIVATED',
target: 'EmailAddress',
direction: 'out',
},
})
neodeInstance.model('EmailAddress', {
email: { type: 'string', primary: true, lowercase: true, email: true },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
verifiedAt: { type: 'string', isoDate: true },
nonce: { type: 'string', token: true },
belongsTo: {
type: 'relationship',
relationship: 'BELONGS_TO',
target: 'User',
direction: 'out',
},
})
neodeInstance.model('User', {
id: { type: 'string', primary: true, default: uuid }, // TODO: should be type: 'uuid' but simplified for our tests
actorId: { type: 'string', allow: [null] },
name: { type: 'string', min: 3 },
email: { type: 'string', lowercase: true, email: true },
slug: 'string',
encryptedPassword: 'string',
avatar: { type: 'string', allow: [null] },
coverImg: { type: 'string', allow: [null] },
deleted: { type: 'boolean', default: false },
disabled: { type: 'boolean', default: false },
role: 'string',
publicKey: 'string',
privateKey: 'string',
wasInvited: 'boolean',
wasSeeded: 'boolean',
locationName: { type: 'string', allow: [null] },
about: { type: 'string', allow: [null] },
primaryEmail: {
type: 'relationship',
relationship: 'PRIMARY_EMAIL',
target: 'EmailAddress',
direction: 'out',
},
following: {
type: 'relationship',
relationship: 'FOLLOWS',
target: 'User',
direction: 'out',
},
followedBy: {
type: 'relationship',
relationship: 'FOLLOWS',
target: 'User',
direction: 'in',
},
friends: { type: 'relationship', relationship: 'FRIENDS', target: 'User', direction: 'both' },
disabledBy: {
type: 'relationship',
relationship: 'DISABLED',
target: 'User',
direction: 'in',
},
invitedBy: { type: 'relationship', relationship: 'INVITED', target: 'User', direction: 'in' },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
updatedAt: {
type: 'string',
isoDate: true,
required: true,
default: () => new Date().toISOString(),
},
})
return neodeInstance
}

View File

@ -0,0 +1,7 @@
import { hashSync } from 'bcryptjs'
export default function(args) {
args.encryptedPassword = hashSync(args.password, 10)
delete args.password
return args
}

View File

@ -4,12 +4,13 @@ import { request } from 'graphql-request'
// not to be confused with the seeder host // not to be confused with the seeder host
export const host = 'http://127.0.0.1:4123' export const host = 'http://127.0.0.1:4123'
export async function login({ email, password }) { export async function login(variables) {
const mutation = ` const mutation = `
mutation { mutation($email: String!, $password: String!) {
login(email:"${email}", password:"${password}") login(email: $email, password: $password)
}` }
const response = await request(host, mutation) `
const response = await request(host, mutation, variables)
return { return {
authorization: `Bearer ${response.login}`, authorization: `Bearer ${response.login}`,
} }

View File

@ -46,7 +46,7 @@ export default {
} }
return post return post
}, },
CreateUser: async (resolve, root, args, context, info) => { SignupVerification: async (resolve, root, args, context, info) => {
const keys = generateRsaKeyPair() const keys = generateRsaKeyPair()
Object.assign(args, keys) Object.assign(args, keys)
args.actorId = `${activityPub.host}/activitypub/users/${args.slug}` args.actorId = `${activityPub.host}/activitypub/users/${args.slug}`

View File

@ -9,7 +9,6 @@ const setUpdatedAt = (resolve, root, args, context, info) => {
export default { export default {
Mutation: { Mutation: {
CreateUser: setCreatedAt,
CreatePost: setCreatedAt, CreatePost: setCreatedAt,
CreateComment: setCreatedAt, CreateComment: setCreatedAt,
CreateOrganization: setCreatedAt, CreateOrganization: setCreatedAt,

View File

@ -0,0 +1,57 @@
import CONFIG from '../../config'
import nodemailer from 'nodemailer'
import { resetPasswordMail, wrongAccountMail } from './templates/passwordReset'
import { signupTemplate } from './templates/signup'
const transporter = () => {
const configs = {
host: CONFIG.SMTP_HOST,
port: CONFIG.SMTP_PORT,
ignoreTLS: CONFIG.SMTP_IGNORE_TLS,
secure: false, // true for 465, false for other ports
}
const { SMTP_USERNAME: user, SMTP_PASSWORD: pass } = CONFIG
if (user && pass) {
configs.auth = { user, pass }
}
return nodemailer.createTransport(configs)
}
const returnResponse = async (resolve, root, args, context, resolveInfo) => {
const { response } = await resolve(root, args, context, resolveInfo)
delete response.nonce
return response
}
const sendSignupMail = async (resolve, root, args, context, resolveInfo) => {
const { email } = args
const { response, nonce } = await resolve(root, args, context, resolveInfo)
delete response.nonce
await transporter().sendMail(signupTemplate({ email, nonce }))
return response
}
export default function({ isEnabled }) {
if (!isEnabled)
return {
Mutation: {
requestPasswordReset: returnResponse,
Signup: returnResponse,
SignupByInvitation: returnResponse,
},
}
return {
Mutation: {
requestPasswordReset: async (resolve, root, args, context, resolveInfo) => {
const { email } = args
const { response, user, code, name } = await resolve(root, args, context, resolveInfo)
const mailTemplate = user ? resetPasswordMail : wrongAccountMail
await transporter().sendMail(mailTemplate({ email, code, name }))
return response
},
Signup: sendSignupMail,
SignupByInvitation: sendSignupMail,
},
}
}

View File

@ -0,0 +1,85 @@
import CONFIG from '../../../config'
export const from = '"Human Connection" <info@human-connection.org>'
export const resetPasswordMail = options => {
const {
name,
email,
code,
subject = 'Use this link to reset your password. The link is only valid for 24 hours.',
supportUrl = 'https://human-connection.org/en/contact/',
} = options
const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('code', code)
actionUrl.searchParams.set('email', email)
return {
to: email,
subject,
text: `
Hi ${name}!
You recently requested to reset your password for your Human Connection account.
Use the link below to reset it. This password reset is only valid for the next
24 hours.
${actionUrl}
If you did not request a password reset, please ignore this email or contact
support if you have questions:
${supportUrl}
Thanks,
The Human Connection Team
If you're having trouble with the link above, you can manually copy and
paste the following code into your browser window:
${code}
Human Connection gemeinnützige GmbH
Bahnhofstr. 11
73235 Weilheim / Teck
Deutschland
`,
}
}
export const wrongAccountMail = options => {
const {
email,
subject = `We received a request to reset your password with this email address (${email})`,
supportUrl = 'https://human-connection.org/en/contact/',
} = options
const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI)
return {
to: email,
subject,
text: `
We received a request to reset the password to access Human Connection with your
email address, but we were unable to find an account associated with this
address.
If you use Human Connection and were expecting this email, consider trying to
request a password reset using the email address associated with your account.
Try a different email:
${actionUrl}
If you do not use Human Connection or did not request a password reset, please
ignore this email. Feel free to contact support if you have further questions:
${supportUrl}
Thanks,
The Human Connection Team
Human Connection gemeinnützige GmbH
Bahnhofstr. 11
73235 Weilheim / Teck
Deutschland
`,
}
}

View File

@ -0,0 +1,42 @@
import CONFIG from '../../../config'
export const from = '"Human Connection" <info@human-connection.org>'
export const signupTemplate = options => {
const {
email,
nonce,
subject = 'Signup link',
supportUrl = 'https://human-connection.org/en/contact/',
} = options
const actionUrl = new URL('/registration/create-user-account', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('nonce', nonce)
return {
to: email,
subject,
text: `
Welcome to Human Connection! Use this link to complete the registration process
and create a user account:
${actionUrl}
You can also copy+paste this verification code in your browser window:
${nonce}
If you did not signed up for Human Connection, please ignore this email or
contact support if you have questions:
${supportUrl}
Thanks,
The Human Connection Team
Human Connection gemeinnützige GmbH
Bahnhofstr. 11
73235 Weilheim / Teck
Deutschland
`,
}
}

View File

@ -1,6 +1,5 @@
import CONFIG from './../config' import CONFIG from './../config'
import activityPub from './activityPubMiddleware' import activityPub from './activityPubMiddleware'
import password from './passwordMiddleware'
import softDelete from './softDeleteMiddleware' import softDelete from './softDeleteMiddleware'
import sluggify from './sluggifyMiddleware' import sluggify from './sluggifyMiddleware'
import excerpt from './excerptMiddleware' import excerpt from './excerptMiddleware'
@ -10,14 +9,14 @@ import permissions from './permissionsMiddleware'
import user from './userMiddleware' import user from './userMiddleware'
import includedFields from './includedFieldsMiddleware' import includedFields from './includedFieldsMiddleware'
import orderBy from './orderByMiddleware' import orderBy from './orderByMiddleware'
import validation from './validation' import validation from './validation/validationMiddleware'
import notifications from './notifications' import notifications from './notifications'
import email from './email/emailMiddleware'
export default schema => { export default schema => {
const middlewares = { const middlewares = {
permissions: permissions, permissions: permissions,
activityPub: activityPub, activityPub: activityPub,
password: password,
dateTime: dateTime, dateTime: dateTime,
validation: validation, validation: validation,
sluggify: sluggify, sluggify: sluggify,
@ -28,16 +27,17 @@ export default schema => {
user: user, user: user,
includedFields: includedFields, includedFields: includedFields,
orderBy: orderBy, orderBy: orderBy,
email: email({ isEnabled: CONFIG.SMTP_HOST && CONFIG.SMTP_PORT }),
} }
let order = [ let order = [
'permissions', 'permissions',
'activityPub', // 'activityPub', disabled temporarily
'password',
'dateTime', 'dateTime',
'validation', 'validation',
'sluggify', 'sluggify',
'excerpt', 'excerpt',
'email',
'notifications', 'notifications',
'xss', 'xss',
'softDelete', 'softDelete',

View File

@ -1,21 +0,0 @@
import bcrypt from 'bcryptjs'
import walkRecursive from '../helpers/walkRecursive'
export default {
Mutation: {
CreateUser: async (resolve, root, args, context, info) => {
args.password = await bcrypt.hashSync(args.password, 10)
const result = await resolve(root, args, context, info)
result.password = '*****'
return result
},
},
Query: async (resolve, root, args, context, info) => {
let result = await resolve(root, args, context, info)
result = walkRecursive(result, ['password', 'privateKey'], () => {
// replace password with asterisk
return '*****'
})
return result
},
}

View File

@ -1,4 +1,4 @@
import { rule, shield, deny, allow, or } from 'graphql-shield' import { rule, shield, deny, allow, and, or, not } from 'graphql-shield'
/* /*
* TODO: implement * TODO: implement
@ -70,6 +70,29 @@ const onlyEnabledContent = rule({
return !(disabled || deleted) return !(disabled || deleted)
}) })
const invitationLimitReached = rule({
cache: 'no_cache',
})(async (parent, args, { user, driver }) => {
const session = driver.session()
try {
const result = await session.run(
`
MATCH (user:User {id:$id})-[:GENERATED]->(i:InvitationCode)
RETURN COUNT(i) >= 3 as limitReached
`,
{ id: user.id },
)
const [limitReached] = result.records.map(record => {
return record.get('limitReached')
})
return limitReached
} catch (e) {
throw e
} finally {
session.close()
}
})
const isAuthor = rule({ const isAuthor = rule({
cache: 'no_cache', cache: 'no_cache',
})(async (parent, args, { user, driver }) => { })(async (parent, args, { user, driver }) => {
@ -101,6 +124,12 @@ const isDeletingOwnAccount = rule({
return context.user.id === args.id return context.user.id === args.id
}) })
const noEmailFilter = rule({
cache: 'no_cache',
})(async (_, args) => {
return !('email' in args)
})
// Permissions // Permissions
const permissions = shield( const permissions = shield(
{ {
@ -115,14 +144,17 @@ const permissions = shield(
currentUser: allow, currentUser: allow,
Post: or(onlyEnabledContent, isModerator), Post: or(onlyEnabledContent, isModerator),
Comment: allow, Comment: allow,
User: allow, User: or(noEmailFilter, isAdmin),
isLoggedIn: allow, isLoggedIn: allow,
}, },
Mutation: { Mutation: {
'*': deny, '*': deny,
login: allow, login: allow,
SignupByInvitation: allow,
Signup: isAdmin,
SignupVerification: allow,
CreateInvitationCode: and(isAuthenticated, or(not(invitationLimitReached), isAdmin)),
UpdateNotification: belongsToMe, UpdateNotification: belongsToMe,
CreateUser: isAdmin,
UpdateUser: onlyYourself, UpdateUser: onlyYourself,
CreatePost: isAuthenticated, CreatePost: isAuthenticated,
UpdatePost: isAuthor, UpdatePost: isAuthor,
@ -131,7 +163,6 @@ const permissions = shield(
CreateBadge: isAdmin, CreateBadge: isAdmin,
UpdateBadge: isAdmin, UpdateBadge: isAdmin,
DeleteBadge: isAdmin, DeleteBadge: isAdmin,
AddUserBadges: isAdmin,
CreateSocialMedia: isAuthenticated, CreateSocialMedia: isAuthenticated,
DeleteSocialMedia: isAuthenticated, DeleteSocialMedia: isAuthenticated,
// AddBadgeRewarded: isAdmin, // AddBadgeRewarded: isAdmin,
@ -154,8 +185,6 @@ const permissions = shield(
}, },
User: { User: {
email: isMyOwn, email: isMyOwn,
password: isMyOwn,
privateKey: isMyOwn,
}, },
}, },
{ {

View File

@ -13,6 +13,10 @@ const isUniqueFor = (context, type) => {
export default { export default {
Mutation: { Mutation: {
SignupVerification: async (resolve, root, args, context, info) => {
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'User')))
return resolve(root, args, context, info)
},
CreatePost: async (resolve, root, args, context, info) => { CreatePost: async (resolve, root, args, context, info) => {
args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post'))) args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
return resolve(root, args, context, info) return resolve(root, args, context, info)
@ -21,10 +25,6 @@ export default {
args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post'))) args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
return resolve(root, args, context, info) return resolve(root, args, context, info)
}, },
CreateUser: async (resolve, root, args, context, info) => {
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'User')))
return resolve(root, args, context, info)
},
CreateOrganization: async (resolve, root, args, context, info) => { CreateOrganization: async (resolve, root, args, context, info) => {
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Organization'))) args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Organization')))
return resolve(root, args, context, info) return resolve(root, args, context, info)

View File

@ -1,10 +1,12 @@
import { GraphQLClient } from 'graphql-request' import { GraphQLClient } from 'graphql-request'
import Factory from '../seed/factories' import Factory from '../seed/factories'
import { host, login } from '../jest/helpers' import { host, login } from '../jest/helpers'
import { neode } from '../bootstrap/neo4j'
let authenticatedClient let authenticatedClient
let headers let headers
const factory = Factory() const factory = Factory()
const instance = neode()
beforeEach(async () => { beforeEach(async () => {
const adminParams = { role: 'admin', email: 'admin@example.org', password: '1234' } const adminParams = { role: 'admin', email: 'admin@example.org', password: '1234' }
@ -76,33 +78,41 @@ describe('slugify', () => {
}) })
}) })
describe('CreateUser', () => { describe('SignupVerification', () => {
const action = async (mutation, params) => { const mutation = `mutation($password: String!, $email: String!, $name: String!, $slug: String, $nonce: String!) {
return authenticatedClient.request(`mutation { SignupVerification(email: $email, password: $password, name: $name, slug: $slug, nonce: $nonce) { slug }
${mutation}(password: "yo", email: "123@123.de", ${params}) { slug }
}`)
} }
`
const action = async variables => {
// required for SignupVerification
await instance.create('EmailAddress', { email: '123@example.org', nonce: '123456' })
const defaultVariables = { nonce: '123456', password: 'yo', email: '123@example.org' }
return authenticatedClient.request(mutation, { ...defaultVariables, ...variables })
}
it('generates a slug based on name', async () => { it('generates a slug based on name', async () => {
await expect(action('CreateUser', 'name: "I am a user"')).resolves.toEqual({ await expect(action({ name: 'I am a user' })).resolves.toEqual({
CreateUser: { slug: 'i-am-a-user' }, SignupVerification: { slug: 'i-am-a-user' },
}) })
}) })
describe('if slug exists', () => { describe('if slug exists', () => {
beforeEach(async () => { beforeEach(async () => {
await action('CreateUser', 'name: "Pre-existing user", slug: "pre-existing-user"') await factory.create('User', { name: 'pre-existing user', slug: 'pre-existing-user' })
}) })
it('chooses another slug', async () => { it('chooses another slug', async () => {
await expect(action('CreateUser', 'name: "pre-existing-user"')).resolves.toEqual({ await expect(action({ name: 'pre-existing-user' })).resolves.toEqual({
CreateUser: { slug: 'pre-existing-user-1' }, SignupVerification: { slug: 'pre-existing-user-1' },
}) })
}) })
describe('but if the client specifies a slug', () => { describe('but if the client specifies a slug', () => {
it('rejects CreateUser', async () => { it('rejects SignupVerification', async () => {
await expect( await expect(
action('CreateUser', 'name: "Pre-existing user", slug: "pre-existing-user"'), action({ name: 'Pre-existing user', slug: 'pre-existing-user' }),
).rejects.toThrow('already exists') ).rejects.toThrow('already exists')
}) })
}) })

View File

@ -2,7 +2,7 @@ import createOrUpdateLocations from './nodes/locations'
export default { export default {
Mutation: { Mutation: {
CreateUser: async (resolve, root, args, context, info) => { SignupVerification: async (resolve, root, args, context, info) => {
const result = await resolve(root, args, context, info) const result = await resolve(root, args, context, info)
await createOrUpdateLocations(args.id, args.locationName, context.driver) await createOrUpdateLocations(args.id, args.locationName, context.driver)
return result return result

View File

@ -1,16 +1,5 @@
import { UserInputError } from 'apollo-server' import { UserInputError } from 'apollo-server'
const USERNAME_MIN_LENGTH = 3
const validateUsername = async (resolve, root, args, context, info) => {
if (!('name' in args) || (args.name && args.name.length >= USERNAME_MIN_LENGTH)) {
/* eslint-disable-next-line no-return-await */
return await resolve(root, args, context, info)
} else {
throw new UserInputError(`Username must be at least ${USERNAME_MIN_LENGTH} characters long!`)
}
}
const validateUrl = async (resolve, root, args, context, info) => { const validateUrl = async (resolve, root, args, context, info) => {
const { url } = args const { url } = args
const isValid = url.match(/^(?:https?:\/\/)(?:[^@\n])?(?:www\.)?([^:/\n?]+)/g) const isValid = url.match(/^(?:https?:\/\/)(?:[^@\n])?(?:www\.)?([^:/\n?]+)/g)
@ -24,8 +13,6 @@ const validateUrl = async (resolve, root, args, context, info) => {
export default { export default {
Mutation: { Mutation: {
CreateUser: validateUsername,
UpdateUser: validateUsername,
CreateSocialMedia: validateUrl, CreateSocialMedia: validateUrl,
}, },
} }

View File

@ -0,0 +1,22 @@
import { UserInputError } from 'apollo-server'
import Joi from '@hapi/joi'
const validate = schema => {
return async (resolve, root, args, context, info) => {
const validation = schema.validate(args)
if (validation.error) throw new UserInputError(validation.error)
return resolve(root, args, context, info)
}
}
const socialMediaSchema = Joi.object().keys({
url: Joi.string()
.uri()
.required(),
})
export default {
Mutation: {
CreateSocialMedia: validate(socialMediaSchema),
},
}

View File

@ -12,10 +12,12 @@ export default applyScalars(
resolvers, resolvers,
config: { config: {
query: { query: {
exclude: ['Notfication', 'Statistics', 'LoggedInUser'], exclude: ['InvitationCode', 'EmailAddress', 'Notfication', 'Statistics', 'LoggedInUser'],
// add 'User' here as soon as possible
}, },
mutation: { mutation: {
exclude: ['Notfication', 'Statistics', 'LoggedInUser'], exclude: ['InvitationCode', 'EmailAddress', 'Notfication', 'Statistics', 'LoggedInUser'],
// add 'User' here as soon as possible
}, },
debug: CONFIG.DEBUG, debug: CONFIG.DEBUG,
}, },

View File

@ -9,12 +9,16 @@ let createCommentVariables
let createPostVariables let createPostVariables
let createCommentVariablesSansPostId let createCommentVariablesSansPostId
let createCommentVariablesWithNonExistentPost let createCommentVariablesWithNonExistentPost
let userParams
let authorParams
beforeEach(async () => { beforeEach(async () => {
await factory.create('User', { userParams = {
name: 'TestUser',
email: 'test@example.org', email: 'test@example.org',
password: '1234', password: '1234',
}) }
await factory.create('User', userParams)
}) })
afterEach(async () => { afterEach(async () => {
@ -53,10 +57,7 @@ describe('CreateComment', () => {
describe('authenticated', () => { describe('authenticated', () => {
let headers let headers
beforeEach(async () => { beforeEach(async () => {
headers = await login({ headers = await login(userParams)
email: 'test@example.org',
password: '1234',
})
client = new GraphQLClient(host, { client = new GraphQLClient(host, {
headers, headers,
}) })
@ -89,7 +90,7 @@ describe('CreateComment', () => {
const { User } = await client.request(gql` const { User } = await client.request(gql`
{ {
User(email: "test@example.org") { User(name: "TestUser") {
comments { comments {
content content
} }
@ -201,15 +202,13 @@ describe('DeleteComment', () => {
} }
beforeEach(async () => { beforeEach(async () => {
authorParams = {
email: 'author@example.org',
password: '1234',
}
const asAuthor = Factory() const asAuthor = Factory()
await asAuthor.create('User', { await asAuthor.create('User', authorParams)
email: 'author@example.org', await asAuthor.authenticateAs(authorParams)
password: '1234',
})
await asAuthor.authenticateAs({
email: 'author@example.org',
password: '1234',
})
await asAuthor.create('Post', { await asAuthor.create('Post', {
id: 'p1', id: 'p1',
content: 'Post to be commented', content: 'Post to be commented',
@ -233,13 +232,8 @@ describe('DeleteComment', () => {
describe('authenticated but not the author', () => { describe('authenticated but not the author', () => {
beforeEach(async () => { beforeEach(async () => {
let headers let headers
headers = await login({ headers = await login(userParams)
email: 'test@example.org', client = new GraphQLClient(host, { headers })
password: '1234',
})
client = new GraphQLClient(host, {
headers,
})
}) })
it('throws authorization error', async () => { it('throws authorization error', async () => {
@ -252,13 +246,8 @@ describe('DeleteComment', () => {
describe('authenticated as author', () => { describe('authenticated as author', () => {
beforeEach(async () => { beforeEach(async () => {
let headers let headers
headers = await login({ headers = await login(authorParams)
email: 'author@example.org', client = new GraphQLClient(host, { headers })
password: '1234',
})
client = new GraphQLClient(host, {
headers,
})
}) })
it('deletes the comment', async () => { it('deletes the comment', async () => {

View File

@ -254,7 +254,7 @@ describe('enable', () => {
beforeEach(async () => { beforeEach(async () => {
authenticateClient = setupAuthenticateClient({ authenticateClient = setupAuthenticateClient({
role: 'moderator', role: 'moderator',
email: 'someUser@example.org', email: 'someuser@example.org',
password: '1234', password: '1234',
}) })
}) })

View File

@ -1,22 +1,5 @@
import uuid from 'uuid/v4' import uuid from 'uuid/v4'
import bcrypt from 'bcryptjs' import bcrypt from 'bcryptjs'
import CONFIG from '../../config'
import nodemailer from 'nodemailer'
import { resetPasswordMail, wrongAccountMail } from './passwordReset/emailTemplates'
const transporter = () => {
const configs = {
host: CONFIG.SMTP_HOST,
port: CONFIG.SMTP_PORT,
ignoreTLS: CONFIG.SMTP_IGNORE_TLS,
secure: false, // true for 465, false for other ports
}
const { SMTP_USERNAME: user, SMTP_PASSWORD: pass } = CONFIG
if (user && pass) {
configs.auth = { user, pass }
}
return nodemailer.createTransport(configs)
}
export async function createPasswordReset(options) { export async function createPasswordReset(options) {
const { driver, code, email, issuedAt = new Date() } = options const { driver, code, email, issuedAt = new Date() } = options
@ -42,27 +25,28 @@ export default {
requestPasswordReset: async (_, { email }, { driver }) => { requestPasswordReset: async (_, { email }, { driver }) => {
const code = uuid().substring(0, 6) const code = uuid().substring(0, 6)
const [user] = await createPasswordReset({ driver, code, email }) const [user] = await createPasswordReset({ driver, code, email })
if (CONFIG.SMTP_HOST && CONFIG.SMTP_PORT) { const name = (user && user.name) || ''
const name = (user && user.name) || '' return { user, code, name, response: true }
const mailTemplate = user ? resetPasswordMail : wrongAccountMail
await transporter().sendMail(mailTemplate({ email, code, name }))
}
return true
}, },
resetPassword: async (_, { email, code, newPassword }, { driver }) => { resetPassword: async (_, { email, code, newPassword }, { driver }) => {
const session = driver.session() const session = driver.session()
const stillValid = new Date() const stillValid = new Date()
stillValid.setDate(stillValid.getDate() - 1) stillValid.setDate(stillValid.getDate() - 1)
const newHashedPassword = await bcrypt.hashSync(newPassword, 10) const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10)
const cypher = ` const cypher = `
MATCH (pr:PasswordReset {code: $code}) MATCH (pr:PasswordReset {code: $code})
MATCH (u:User {email: $email})-[:REQUESTED]->(pr) MATCH (u:User {email: $email})-[:REQUESTED]->(pr)
WHERE duration.between(pr.issuedAt, datetime()).days <= 0 AND pr.usedAt IS NULL WHERE duration.between(pr.issuedAt, datetime()).days <= 0 AND pr.usedAt IS NULL
SET pr.usedAt = datetime() SET pr.usedAt = datetime()
SET u.password = $newHashedPassword SET u.encryptedPassword = $encryptedNewPassword
RETURN pr RETURN pr
` `
let transactionRes = await session.run(cypher, { stillValid, email, code, newHashedPassword }) let transactionRes = await session.run(cypher, {
stillValid,
email,
code,
encryptedNewPassword,
})
const [reset] = transactionRes.records.map(record => record.get('pr')) const [reset] = transactionRes.records.map(record => record.get('pr'))
const result = !!(reset && reset.properties.usedAt) const result = !!(reset && reset.properties.usedAt)
session.close() session.close()

View File

@ -4,6 +4,9 @@ import { host, login } from '../../jest/helpers'
const factory = Factory() const factory = Factory()
let client let client
let userParams
let authorParams
const postTitle = 'I am a title' const postTitle = 'I am a title'
const postContent = 'Some content' const postContent = 'Some content'
const oldTitle = 'Old title' const oldTitle = 'Old title'
@ -53,10 +56,16 @@ const postQueryFilteredByCategoryVariables = {
name: postCategoriesFilterParam, name: postCategoriesFilterParam,
} }
beforeEach(async () => { beforeEach(async () => {
await factory.create('User', { userParams = {
name: 'TestUser',
email: 'test@example.org', email: 'test@example.org',
password: '1234', password: '1234',
}) }
authorParams = {
email: 'author@example.org',
password: '1234',
}
await factory.create('User', userParams)
}) })
afterEach(async () => { afterEach(async () => {
@ -86,7 +95,7 @@ describe('CreatePost', () => {
describe('authenticated', () => { describe('authenticated', () => {
let headers let headers
beforeEach(async () => { beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' }) headers = await login(userParams)
client = new GraphQLClient(host, { headers }) client = new GraphQLClient(host, { headers })
}) })
@ -104,7 +113,7 @@ describe('CreatePost', () => {
await client.request(mutation, createPostVariables) await client.request(mutation, createPostVariables)
const { User } = await client.request( const { User } = await client.request(
`{ `{
User(email:"test@example.org") { User(name: "TestUser") {
contributions { contributions {
title title
} }
@ -209,14 +218,8 @@ describe('UpdatePost', () => {
let updatePostVariables let updatePostVariables
beforeEach(async () => { beforeEach(async () => {
const asAuthor = Factory() const asAuthor = Factory()
await asAuthor.create('User', { await asAuthor.create('User', authorParams)
email: 'author@example.org', await asAuthor.authenticateAs(authorParams)
password: '1234',
})
await asAuthor.authenticateAs({
email: 'author@example.org',
password: '1234',
})
await asAuthor.create('Post', { await asAuthor.create('Post', {
id: 'p1', id: 'p1',
title: oldTitle, title: oldTitle,
@ -251,7 +254,7 @@ describe('UpdatePost', () => {
describe('authenticated but not the author', () => { describe('authenticated but not the author', () => {
let headers let headers
beforeEach(async () => { beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' }) headers = await login(userParams)
client = new GraphQLClient(host, { headers }) client = new GraphQLClient(host, { headers })
}) })
@ -265,7 +268,7 @@ describe('UpdatePost', () => {
describe('authenticated as author', () => { describe('authenticated as author', () => {
let headers let headers
beforeEach(async () => { beforeEach(async () => {
headers = await login({ email: 'author@example.org', password: '1234' }) headers = await login(authorParams)
client = new GraphQLClient(host, { headers }) client = new GraphQLClient(host, { headers })
}) })
@ -343,14 +346,8 @@ describe('DeletePost', () => {
beforeEach(async () => { beforeEach(async () => {
const asAuthor = Factory() const asAuthor = Factory()
await asAuthor.create('User', { await asAuthor.create('User', authorParams)
email: 'author@example.org', await asAuthor.authenticateAs(authorParams)
password: '1234',
})
await asAuthor.authenticateAs({
email: 'author@example.org',
password: '1234',
})
await asAuthor.create('Post', { await asAuthor.create('Post', {
id: 'p1', id: 'p1',
content: 'To be deleted', content: 'To be deleted',
@ -367,7 +364,7 @@ describe('DeletePost', () => {
describe('authenticated but not the author', () => { describe('authenticated but not the author', () => {
let headers let headers
beforeEach(async () => { beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' }) headers = await login(userParams)
client = new GraphQLClient(host, { headers }) client = new GraphQLClient(host, { headers })
}) })
@ -379,7 +376,7 @@ describe('DeletePost', () => {
describe('authenticated as author', () => { describe('authenticated as author', () => {
let headers let headers
beforeEach(async () => { beforeEach(async () => {
headers = await login({ email: 'author@example.org', password: '1234' }) headers = await login(authorParams)
client = new GraphQLClient(host, { headers }) client = new GraphQLClient(host, { headers })
}) })

View File

@ -0,0 +1,107 @@
import { UserInputError } from 'apollo-server'
import uuid from 'uuid/v4'
import { neode } from '../../bootstrap/neo4j'
import fileUpload from './fileUpload'
import encryptPassword from '../../helpers/encryptPassword'
const instance = neode()
/*
* TODO: remove this function as soon type `User` has no `email` property
* anymore
*/
const checkEmailDoesNotExist = async ({ email }) => {
email = email.toLowerCase()
const users = await instance.all('User', { email })
if (users.length > 0) throw new UserInputError('User account with this email already exists.')
}
export default {
Mutation: {
CreateInvitationCode: async (parent, args, context, resolveInfo) => {
args.token = uuid().substring(0, 6)
const {
user: { id: userId },
} = context
let response
try {
const [user, invitationCode] = await Promise.all([
instance.find('User', userId),
instance.create('InvitationCode', args),
])
await invitationCode.relateTo(user, 'generatedBy')
response = invitationCode.toJson()
response.generatedBy = user.toJson()
} catch (e) {
throw new UserInputError(e)
}
return response
},
Signup: async (parent, args, context, resolveInfo) => {
const nonce = uuid().substring(0, 6)
args.nonce = nonce
await checkEmailDoesNotExist({ email: args.email })
try {
const emailAddress = await instance.create('EmailAddress', args)
return { response: emailAddress.toJson(), nonce }
} catch (e) {
throw new UserInputError(e.message)
}
},
SignupByInvitation: async (parent, args, context, resolveInfo) => {
const { token } = args
const nonce = uuid().substring(0, 6)
args.nonce = nonce
await checkEmailDoesNotExist({ email: args.email })
try {
const result = await instance.cypher(
`
MATCH (invitationCode:InvitationCode {token:{token}})
WHERE NOT (invitationCode)-[:ACTIVATED]->()
RETURN invitationCode
`,
{ token },
)
const validInvitationCode = instance.hydrateFirst(
result,
'invitationCode',
instance.model('InvitationCode'),
)
if (!validInvitationCode)
throw new UserInputError('Invitation code already used or does not exist.')
const emailAddress = await instance.create('EmailAddress', args)
await validInvitationCode.relateTo(emailAddress, 'activated')
return { response: emailAddress.toJson(), nonce }
} catch (e) {
throw new UserInputError(e)
}
},
SignupVerification: async (object, args, context, resolveInfo) => {
let { nonce, email } = args
email = email.toLowerCase()
const result = await instance.cypher(
`
MATCH(email:EmailAddress {nonce: {nonce}, email: {email}})
WHERE NOT (email)-[:BELONGS_TO]->()
RETURN email
`,
{ nonce, email },
)
const emailAddress = await instance.hydrateFirst(result, 'email', instance.model('Email'))
if (!emailAddress) throw new UserInputError('Invalid email or nonce')
args = await fileUpload(args, { file: 'avatarUpload', url: 'avatar' })
args = await encryptPassword(args)
try {
const user = await instance.create('User', args)
await Promise.all([
user.relateTo(emailAddress, 'primaryEmail'),
emailAddress.relateTo(user, 'belongsTo'),
emailAddress.update({ verifiedAt: new Date().toISOString() }),
])
return user.toJson()
} catch (e) {
throw new UserInputError(e.message)
}
},
},
}

View File

@ -0,0 +1,402 @@
import { GraphQLClient } from 'graphql-request'
import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
import { neode } from '../../bootstrap/neo4j'
let factory
let client
let variables
let action
let userParams
const instance = neode()
beforeEach(async () => {
variables = {}
factory = Factory()
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('CreateInvitationCode', () => {
const mutation = `mutation { CreateInvitationCode { token } }`
it('throws Authorization error', async () => {
const client = new GraphQLClient(host)
await expect(client.request(mutation)).rejects.toThrow('Not Authorised!')
})
describe('authenticated', () => {
beforeEach(async () => {
userParams = {
id: 'i123',
name: 'Inviter',
email: 'inviter@example.org',
password: '1234',
}
action = async () => {
const factory = Factory()
await factory.create('User', userParams)
const headers = await login(userParams)
client = new GraphQLClient(host, { headers })
return client.request(mutation)
}
})
it('resolves', async () => {
await expect(action()).resolves.toEqual({
CreateInvitationCode: { token: expect.any(String) },
})
})
it('creates an InvitationCode with a `createdAt` attribute', async () => {
await action()
const codes = await instance.all('InvitationCode')
const invitation = await codes.first().toJson()
expect(invitation.createdAt).toBeTruthy()
expect(Date.parse(invitation.createdAt)).toEqual(expect.any(Number))
})
it('relates inviting User to InvitationCode', async () => {
await action()
const result = await instance.cypher(
'MATCH(code:InvitationCode)<-[:GENERATED]-(user:User) RETURN user',
)
const inviter = instance.hydrateFirst(result, 'user', instance.model('User'))
await expect(inviter.toJson()).resolves.toEqual(expect.objectContaining({ name: 'Inviter' }))
})
describe('who has invited a lot of users already', () => {
beforeEach(() => {
action = async () => {
const factory = Factory()
await factory.create('User', userParams)
const headers = await login(userParams)
client = new GraphQLClient(host, { headers })
await Promise.all(
[1, 2, 3].map(() => {
return client.request(mutation)
}),
)
return client.request(mutation, variables)
}
})
describe('as ordinary `user`', () => {
it('throws `Not Authorised` because of maximum number of invitations', async () => {
await expect(action()).rejects.toThrow('Not Authorised')
})
it('creates no additional invitation codes', async done => {
try {
await action()
} catch (e) {
const invitationCodes = await instance.all('InvitationCode')
await expect(invitationCodes.toJson()).resolves.toHaveLength(3)
done()
}
})
})
describe('as a strong donator', () => {
beforeEach(() => {
// What is the setup?
})
it.todo('can invite more people')
// it('can invite more people', async () => {
// await action()
// const invitationQuery = `{ User { createdAt } }`
// const { User: users } = await client.request(invitationQuery )
// expect(users).toHaveLength(3 + 1 + 1)
// })
})
})
})
})
describe('SignupByInvitation', () => {
const mutation = `mutation($email: String!, $token: String!) {
SignupByInvitation(email: $email, token: $token) { email }
}`
beforeEach(() => {
client = new GraphQLClient(host)
action = async () => {
return client.request(mutation, variables)
}
})
describe('with valid email but invalid InvitationCode', () => {
beforeEach(() => {
variables.email = 'any-email@example.org'
variables.token = 'wut?'
})
it('throws UserInputError', async () => {
await expect(action()).rejects.toThrow('Invitation code already used or does not exist.')
})
})
describe('with valid InvitationCode', () => {
beforeEach(async () => {
const inviterParams = {
name: 'Inviter',
email: 'inviter@example.org',
password: '1234',
}
const factory = Factory()
await factory.create('User', inviterParams)
const headersOfInviter = await login(inviterParams)
const anotherClient = new GraphQLClient(host, { headers: headersOfInviter })
const invitationMutation = `mutation { CreateInvitationCode { token } }`
const {
CreateInvitationCode: { token },
} = await anotherClient.request(invitationMutation)
variables.token = token
})
describe('given an invalid email', () => {
beforeEach(() => {
variables.email = 'someuser'
})
it('throws `email is not a valid email`', async () => {
await expect(action()).rejects.toThrow('"email" must be a valid email')
})
it('creates no EmailAddress node', async done => {
try {
await action()
} catch (e) {
const emailAddresses = await instance.all('EmailAddress')
expect(emailAddresses).toHaveLength(0)
done()
}
})
})
describe('given a valid email', () => {
beforeEach(() => {
variables.email = 'someUser@example.org'
})
it('resolves', async () => {
await expect(action()).resolves.toEqual({
SignupByInvitation: { email: 'someuser@example.org' },
})
})
describe('creates a EmailAddress node', () => {
it('with a `createdAt` attribute', async () => {
await action()
const emailAddresses = await instance.all('EmailAddress')
const emailAddress = await emailAddresses.first().toJson()
expect(emailAddress.createdAt).toBeTruthy()
expect(Date.parse(emailAddress.createdAt)).toEqual(expect.any(Number))
})
it('with a cryptographic `nonce`', async () => {
await action()
const emailAddresses = await instance.all('EmailAddress')
const emailAddress = await emailAddresses.first().toJson()
expect(emailAddress.nonce).toEqual(expect.any(String))
})
it('connects inviter through invitation code', async () => {
await action()
const result = await instance.cypher(
'MATCH(inviter:User)-[:GENERATED]->(:InvitationCode)-[:ACTIVATED]->(email:EmailAddress {email: {email}}) RETURN inviter',
{ email: 'someuser@example.org' },
)
const inviter = instance.hydrateFirst(result, 'inviter', instance.model('User'))
await expect(inviter.toJson()).resolves.toEqual(
expect.objectContaining({ name: 'Inviter' }),
)
})
describe('using the same InvitationCode twice', () => {
it('rejects because codes can be used only once', async done => {
await action()
try {
await action()
} catch (e) {
expect(e.message).toMatch(/Invitation code already used/)
done()
}
})
})
describe('if a user account with the given email already exists', () => {
beforeEach(async () => {
await factory.create('User', { email: 'someuser@example.org' })
})
it('throws unique violation error', async () => {
await expect(action()).rejects.toThrow('User account with this email already exists.')
})
})
describe('if the EmailAddress already exists but without user account', () => {
// shall we re-send the registration email?
it.todo('decide what to do')
})
})
})
})
})
describe('Signup', () => {
const mutation = `mutation($email: String!) {
Signup(email: $email) { email }
}`
it('throws AuthorizationError', async () => {
client = new GraphQLClient(host)
await expect(
client.request(mutation, { email: 'get-me-a-user-account@example.org' }),
).rejects.toThrow('Not Authorised')
})
describe('as admin', () => {
beforeEach(async () => {
userParams = {
role: 'admin',
email: 'admin@example.org',
password: '1234',
}
variables.email = 'someuser@example.org'
const factory = Factory()
await factory.create('User', userParams)
const headers = await login(userParams)
client = new GraphQLClient(host, { headers })
action = async () => {
return client.request(mutation, variables)
}
})
it('is allowed to signup users by email', async () => {
await expect(action()).resolves.toEqual({ Signup: { email: 'someuser@example.org' } })
})
it('creates a Signup with a cryptographic `nonce`', async () => {
await action()
const emailAddresses = await instance.all('EmailAddress')
const emailAddress = await emailAddresses.first().toJson()
expect(emailAddress.nonce).toEqual(expect.any(String))
})
})
})
describe('SignupVerification', () => {
const mutation = `
mutation($name: String!, $password: String!, $email: String!, $nonce: String!) {
SignupVerification(name: $name, password: $password, email: $email, nonce: $nonce) {
id
}
}
`
describe('given valid password and email', () => {
let variables = {
nonce: '123456',
name: 'John Doe',
password: '123',
email: 'john@example.org',
}
describe('unauthenticated', () => {
beforeEach(async () => {
client = new GraphQLClient(host)
})
describe('EmailAddress exists, but is already related to a user account', () => {
beforeEach(async () => {
const { email, nonce } = variables
const [emailAddress, user] = await Promise.all([
instance.model('EmailAddress').create({ email, nonce }),
instance
.model('User')
.create({ name: 'Somebody', password: '1234', email: 'john@example.org' }),
])
await emailAddress.relateTo(user, 'belongsTo')
})
describe('sending a valid nonce', () => {
beforeEach(() => {
variables.nonce = '123456'
})
it('rejects', async () => {
await expect(client.request(mutation, variables)).rejects.toThrow(
'Invalid email or nonce',
)
})
})
})
describe('disconnected EmailAddress exists', () => {
beforeEach(async () => {
const args = {
email: 'john@example.org',
nonce: '123456',
}
await instance.model('EmailAddress').create(args)
})
describe('sending a valid nonce', () => {
it('creates a user account', async () => {
const expected = {
SignupVerification: {
id: expect.any(String),
},
}
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
})
it('sets `verifiedAt` attribute of EmailAddress', async () => {
await client.request(mutation, variables)
const email = await instance.first('EmailAddress', { email: 'john@example.org' })
await expect(email.toJson()).resolves.toEqual(
expect.objectContaining({
verifiedAt: expect.any(String),
}),
)
})
it('connects User with EmailAddress', async () => {
const cypher = `
MATCH(email:EmailAddress)-[:BELONGS_TO]->(u:User {name: {name}})
RETURN email
`
await client.request(mutation, variables)
const { records: emails } = await instance.cypher(cypher, { name: 'John Doe' })
expect(emails).toHaveLength(1)
})
it('marks the EmailAddress as primary', async () => {
const cypher = `
MATCH(email:EmailAddress)<-[:PRIMARY_EMAIL]-(u:User {name: {name}})
RETURN email
`
await client.request(mutation, variables)
const { records: emails } = await instance.cypher(cypher, { name: 'John Doe' })
expect(emails).toHaveLength(1)
})
})
describe('sending invalid nonce', () => {
beforeEach(() => {
variables.nonce = 'wut2'
})
it('rejects', async () => {
await expect(client.request(mutation, variables)).rejects.toThrow(
'Invalid email or nonce',
)
})
})
})
})
})
})

View File

@ -98,14 +98,19 @@ describe('SocialMedia', () => {
const variables = { const variables = {
url: '', url: '',
} }
await expect(client.request(mutationC, variables)).rejects.toThrow('Input is not a URL') await expect(client.request(mutationC, variables)).rejects.toThrow(
'"url" is not allowed to be empty',
)
}) })
it('validates URLs', async () => { it('validates URLs', async () => {
const variables = { const variables = {
url: 'not-a-url', url: 'not-a-url',
} }
await expect(client.request(mutationC, variables)).rejects.toThrow('Input is not a URL')
await expect(client.request(mutationC, variables)).rejects.toThrow(
'"url" must be a valid uri',
)
}) })
}) })
}) })

View File

@ -5,7 +5,7 @@ import { neo4jgraphql } from 'neo4j-graphql-js'
export default { export default {
Query: { Query: {
isLoggedIn: (parent, args, { driver, user }) => { isLoggedIn: (_, args, { driver, user }) => {
return Boolean(user && user.id) return Boolean(user && user.id)
}, },
currentUser: async (object, params, ctx, resolveInfo) => { currentUser: async (object, params, ctx, resolveInfo) => {
@ -15,40 +15,29 @@ export default {
}, },
}, },
Mutation: { Mutation: {
signup: async (parent, { email, password }, { req }) => { login: async (_, { email, password }, { driver, req, user }) => {
// if (data[email]) {
// throw new Error('Another User with same email exists.')
// }
// data[email] = {
// password: await bcrypt.hashSync(password, 10),
// }
return true
},
login: async (parent, { email, password }, { driver, req, user }) => {
// if (user && user.id) { // if (user && user.id) {
// throw new Error('Already logged in.') // throw new Error('Already logged in.')
// } // }
const session = driver.session() const session = driver.session()
const result = await session.run( const result = await session.run(
'MATCH (user:User {email: $userEmail}) ' + 'MATCH (user:User {email: $userEmail}) ' +
'RETURN user {.id, .slug, .name, .avatar, .email, .password, .role, .disabled} as user LIMIT 1', 'RETURN user {.id, .slug, .name, .avatar, .email, .encryptedPassword, .role, .disabled} as user LIMIT 1',
{ {
userEmail: email, userEmail: email,
}, },
) )
session.close() session.close()
const [currentUser] = await result.records.map(function(record) { const [currentUser] = await result.records.map(record => {
return record.get('user') return record.get('user')
}) })
if ( if (
currentUser && currentUser &&
(await bcrypt.compareSync(password, currentUser.password)) && (await bcrypt.compareSync(password, currentUser.encryptedPassword)) &&
!currentUser.disabled !currentUser.disabled
) { ) {
delete currentUser.password delete currentUser.encryptedPassword
return encode(currentUser) return encode(currentUser)
} else if (currentUser && currentUser.disabled) { } else if (currentUser && currentUser.disabled) {
throw new AuthenticationError('Your account has been disabled.') throw new AuthenticationError('Your account has been disabled.')
@ -60,7 +49,7 @@ export default {
const session = driver.session() const session = driver.session()
let result = await session.run( let result = await session.run(
`MATCH (user:User {email: $userEmail}) `MATCH (user:User {email: $userEmail})
RETURN user {.id, .email, .password}`, RETURN user {.id, .email, .encryptedPassword}`,
{ {
userEmail: user.email, userEmail: user.email,
}, },
@ -70,22 +59,22 @@ export default {
return record.get('user') return record.get('user')
}) })
if (!(await bcrypt.compareSync(oldPassword, currentUser.password))) { if (!(await bcrypt.compareSync(oldPassword, currentUser.encryptedPassword))) {
throw new AuthenticationError('Old password is not correct') throw new AuthenticationError('Old password is not correct')
} }
if (await bcrypt.compareSync(newPassword, currentUser.password)) { if (await bcrypt.compareSync(newPassword, currentUser.encryptedPassword)) {
throw new AuthenticationError('Old password and new password should be different') throw new AuthenticationError('Old password and new password should be different')
} else { } else {
const newHashedPassword = await bcrypt.hashSync(newPassword, 10) const newEncryptedPassword = await bcrypt.hashSync(newPassword, 10)
session.run( session.run(
`MATCH (user:User {email: $userEmail}) `MATCH (user:User {email: $userEmail})
SET user.password = $newHashedPassword SET user.encryptedPassword = $newEncryptedPassword
RETURN user RETURN user
`, `,
{ {
userEmail: user.email, userEmail: user.email,
newHashedPassword, newEncryptedPassword,
}, },
) )
session.close() session.close()

View File

@ -1,4 +1,3 @@
import gql from 'graphql-tag'
import { GraphQLClient, request } from 'graphql-request' import { GraphQLClient, request } from 'graphql-request'
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import CONFIG from './../../config' import CONFIG from './../../config'
@ -311,121 +310,3 @@ describe('change password', () => {
}) })
}) })
}) })
describe('do not expose private RSA key', () => {
let headers
let client
let authenticatedClient
const queryUserPuplicKey = gql`
query($queriedUserSlug: String) {
User(slug: $queriedUserSlug) {
id
publicKey
}
}
`
const queryUserPrivateKey = gql`
query($queriedUserSlug: String) {
User(slug: $queriedUserSlug) {
id
privateKey
}
}
`
const generateUserWithKeys = async authenticatedClient => {
// Generate user with "privateKey" via 'CreateUser' mutation instead of using the factories "factory.create('User', {...})", see above.
const variables = {
id: 'bcb2d923-f3af-479e-9f00-61b12e864667',
password: 'xYz',
slug: 'apfel-strudel',
name: 'Apfel Strudel',
email: 'apfel-strudel@test.org',
}
await authenticatedClient.request(
gql`
mutation($id: ID, $password: String!, $slug: String, $name: String, $email: String!) {
CreateUser(id: $id, password: $password, slug: $slug, name: $name, email: $email) {
id
}
}
`,
variables,
)
}
beforeEach(async () => {
const adminParams = {
role: 'admin',
email: 'admin@example.org',
password: '1234',
}
// create an admin user who has enough permissions to create other users
await factory.create('User', adminParams)
const headers = await login(adminParams)
authenticatedClient = new GraphQLClient(host, { headers })
// but also create an unauthenticated client to issue the `User` query
client = new GraphQLClient(host)
})
describe('unauthenticated query of "publicKey" (does the RSA key pair get generated at all?)', () => {
it('returns publicKey', async () => {
await generateUserWithKeys(authenticatedClient)
await expect(
await client.request(queryUserPuplicKey, { queriedUserSlug: 'apfel-strudel' }),
).toEqual(
expect.objectContaining({
User: [
{
id: 'bcb2d923-f3af-479e-9f00-61b12e864667',
publicKey: expect.any(String),
},
],
}),
)
})
})
describe('unauthenticated query of "privateKey"', () => {
it('throws "Not Authorised!"', async () => {
await generateUserWithKeys(authenticatedClient)
await expect(
client.request(queryUserPrivateKey, { queriedUserSlug: 'apfel-strudel' }),
).rejects.toThrow('Not Authorised')
})
})
// authenticate
beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
describe('authenticated query of "publicKey"', () => {
it('returns publicKey', async () => {
await generateUserWithKeys(authenticatedClient)
await expect(
await client.request(queryUserPuplicKey, { queriedUserSlug: 'apfel-strudel' }),
).toEqual(
expect.objectContaining({
User: [
{
id: 'bcb2d923-f3af-479e-9f00-61b12e864667',
publicKey: expect.any(String),
},
],
}),
)
})
})
describe('authenticated query of "privateKey"', () => {
it('throws "Not Authorised!"', async () => {
await generateUserWithKeys(authenticatedClient)
await expect(
client.request(queryUserPrivateKey, { queriedUserSlug: 'apfel-strudel' }),
).rejects.toThrow('Not Authorised')
})
})
})

View File

@ -1,15 +1,84 @@
import { neo4jgraphql } from 'neo4j-graphql-js' import { neo4jgraphql } from 'neo4j-graphql-js'
import fileUpload from './fileUpload' import fileUpload from './fileUpload'
import { neode } from '../../bootstrap/neo4j'
import { UserInputError } from 'apollo-server'
const instance = neode()
const _has = (resolvers, { key, connection }, { returnType }) => {
return async (parent, params, context, resolveInfo) => {
if (typeof parent[key] !== 'undefined') return parent[key]
const { id } = parent
const statement = `MATCH(u:User {id: {id}})${connection} RETURN related`
const result = await instance.cypher(statement, { id })
let response = result.records.map(r => r.get('related').properties)
if (returnType === 'object') response = response[0] || null
return response
}
}
const count = obj => {
const resolvers = {}
for (const [key, connection] of Object.entries(obj)) {
resolvers[key] = async (parent, params, context, resolveInfo) => {
if (typeof parent[key] !== 'undefined') return parent[key]
const { id } = parent
const statement = `
MATCH(u:User {id: {id}})${connection}
WHERE NOT related.deleted = true AND NOT related.disabled = true
RETURN COUNT(DISTINCT(related)) as count
`
const result = await instance.cypher(statement, { id })
const [response] = result.records.map(r => r.get('count').toNumber())
return response
}
}
return resolvers
}
const undefinedToNull = list => {
const resolvers = {}
list.forEach(key => {
resolvers[key] = async (parent, params, context, resolveInfo) => {
return typeof parent[key] === 'undefined' ? null : parent[key]
}
})
return resolvers
}
export const hasMany = obj => {
const resolvers = {}
for (const [key, connection] of Object.entries(obj)) {
resolvers[key] = _has(resolvers, { key, connection }, { returnType: 'iterable' })
}
return resolvers
}
export const hasOne = obj => {
const resolvers = {}
for (const [key, connection] of Object.entries(obj)) {
resolvers[key] = _has(resolvers, { key, connection }, { returnType: 'object' })
}
return resolvers
}
export default { export default {
Mutation: { Query: {
UpdateUser: async (object, params, context, resolveInfo) => { User: async (object, args, context, resolveInfo) => {
params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar' }) return neo4jgraphql(object, args, context, resolveInfo, false)
return neo4jgraphql(object, params, context, resolveInfo, false)
}, },
CreateUser: async (object, params, context, resolveInfo) => { },
params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar' }) Mutation: {
return neo4jgraphql(object, params, context, resolveInfo, false) UpdateUser: async (object, args, context, resolveInfo) => {
args = await fileUpload(args, { file: 'avatarUpload', url: 'avatar' })
try {
let user = await instance.find('User', args.id)
if (!user) return null
await user.update(args)
return user.toJson()
} catch (e) {
throw new UserInputError(e.message)
}
}, },
DeleteUser: async (object, params, context, resolveInfo) => { DeleteUser: async (object, params, context, resolveInfo) => {
const { resource } = params const { resource } = params
@ -34,4 +103,43 @@ export default {
return neo4jgraphql(object, params, context, resolveInfo, false) return neo4jgraphql(object, params, context, resolveInfo, false)
}, },
}, },
User: {
...undefinedToNull([
'actorId',
'avatar',
'coverImg',
'deleted',
'disabled',
'locationName',
'about',
]),
...count({
contributionsCount: '-[:WROTE]->(related:Post)',
friendsCount: '<-[:FRIENDS]->(related:User)',
followingCount: '-[:FOLLOWS]->(related:User)',
followedByCount: '<-[:FOLLOWS]-(related:User)',
commentsCount: '-[:WROTE]->(r:Comment)',
commentedCount: '-[:WROTE]->(:Comment)-[:COMMENTS]->(related:Post)',
shoutedCount: '-[:SHOUTED]->(related:Post)',
badgesCount: '<-[:REWARDED]-(related:Badge)',
}),
...hasOne({
invitedBy: '<-[:INVITED]-(related:User)',
disabledBy: '<-[:DISABLED]-(related:User)',
}),
...hasMany({
followedBy: '<-[:FOLLOWS]-(related:User)',
following: '-[:FOLLOWS]->(related:User)',
friends: '-[:FRIENDS]-(related:User)',
blacklisted: '-[:BLACKLISTED]->(related:User)',
socialMedia: '-[:OWNED]->(related:SocialMedia)',
contributions: '-[:WROTE]->(related:Post)',
comments: '-[:WROTE]->(related:Comment)',
shouted: '-[:SHOUTED]->(related:Post)',
organizationsCreated: '-[:CREATED_ORGA]->(related:Organization)',
organizationsOwned: '-[:OWNING_ORGA]->(related:Organization)',
categories: '-[:CATEGORIZED]->(related:Category)',
badges: '-[:REWARDED]->(related:Badge)',
}),
},
} }

View File

@ -11,50 +11,39 @@ afterEach(async () => {
}) })
describe('users', () => { describe('users', () => {
describe('CreateUser', () => { describe('User', () => {
const mutation = ` describe('query by email address', () => {
mutation($name: String, $password: String!, $email: String!) { beforeEach(async () => {
CreateUser(name: $name, password: $password, email: $email) { await factory.create('User', { name: 'Johnny', email: 'any-email-address@example.org' })
id
}
}
`
describe('given valid password and email', () => {
const variables = {
name: 'John Doe',
password: '123',
email: '123@123.de',
}
describe('unauthenticated', () => {
beforeEach(async () => {
client = new GraphQLClient(host)
})
it('is not allowed to create users', async () => {
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
}) })
describe('authenticated admin', () => { const query = `query($email: String) { User(email: $email) { name } }`
const variables = { email: 'any-email-address@example.org' }
beforeEach(() => {
client = new GraphQLClient(host)
})
it('is forbidden', async () => {
await expect(client.request(query, variables)).rejects.toThrow('Not Authorised')
})
describe('as admin', () => {
beforeEach(async () => { beforeEach(async () => {
const adminParams = { const userParams = {
role: 'admin', role: 'admin',
email: 'admin@example.org', email: 'admin@example.org',
password: '1234', password: '1234',
} }
await factory.create('User', adminParams) const factory = Factory()
const headers = await login(adminParams) await factory.create('User', userParams)
const headers = await login(userParams)
client = new GraphQLClient(host, { headers }) client = new GraphQLClient(host, { headers })
}) })
it('is allowed to create new users', async () => { it('is permitted', async () => {
const expected = { await expect(client.request(query, variables)).resolves.toEqual({
CreateUser: { User: [{ name: 'Johnny' }],
id: expect.any(String), })
},
}
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
}) })
}) })
}) })
@ -88,7 +77,7 @@ describe('users', () => {
describe('as another user', () => { describe('as another user', () => {
beforeEach(async () => { beforeEach(async () => {
const someoneElseParams = { const someoneElseParams = {
email: 'someoneElse@example.org', email: 'someone-else@example.org',
password: '1234', password: '1234',
name: 'James Doe', name: 'James Doe',
} }
@ -119,12 +108,12 @@ describe('users', () => {
await expect(client.request(mutation, variables)).resolves.toEqual(expected) await expect(client.request(mutation, variables)).resolves.toEqual(expected)
}) })
it('with no name', async () => { it('with `null` as name', async () => {
const variables = { const variables = {
id: 'u47', id: 'u47',
name: null, name: null,
} }
const expected = 'Username must be at least 3 characters long!' const expected = '"name" must be a string'
await expect(client.request(mutation, variables)).rejects.toThrow(expected) await expect(client.request(mutation, variables)).rejects.toThrow(expected)
}) })
@ -133,7 +122,7 @@ describe('users', () => {
id: 'u47', id: 'u47',
name: ' ', name: ' ',
} }
const expected = 'Username must be at least 3 characters long!' const expected = '"name" length must be at least 3 characters long'
await expect(client.request(mutation, variables)).rejects.toThrow(expected) await expect(client.request(mutation, variables)).rejects.toThrow(expected)
}) })
}) })
@ -164,7 +153,7 @@ describe('users', () => {
id: 'u343', id: 'u343',
}) })
await factory.create('User', { await factory.create('User', {
email: 'friendsAccount@example.org', email: 'friends-account@example.org',
password: '1234', password: '1234',
id: 'u565', id: 'u565',
}) })

View File

@ -17,13 +17,11 @@ type Query {
LIMIT $limit LIMIT $limit
""" """
) )
CommentByPost(postId: ID!): [Comment]!
} }
type Mutation { type Mutation {
# Get a JWT Token for the given Email and password # Get a JWT Token for the given Email and password
login(email: String!, password: String!): String! login(email: String!, password: String!): String!
signup(email: String!, password: String!): Boolean!
changePassword(oldPassword: String!, newPassword: String!): String! changePassword(oldPassword: String!, newPassword: String!): String!
requestPasswordReset(email: String!): Boolean! requestPasswordReset(email: String!): Boolean!
resetPassword(email: String!, code: String!, newPassword: String!): Boolean! resetPassword(email: String!, code: String!, newPassword: String!): Boolean!
@ -40,7 +38,6 @@ type Mutation {
follow(id: ID!, type: FollowTypeEnum): Boolean! follow(id: ID!, type: FollowTypeEnum): Boolean!
# Unfollow the given Type and ID # Unfollow the given Type and ID
unfollow(id: ID!, type: FollowTypeEnum): Boolean! unfollow(id: ID!, type: FollowTypeEnum): Boolean!
DeleteUser(id: ID!, resource: [Deletable]): User
} }
type Statistics { type Statistics {

View File

@ -0,0 +1,23 @@
type EmailAddress {
id: ID!
email: String!
verifiedAt: String
createdAt: String
}
type Mutation {
Signup(email: String!): EmailAddress
SignupByInvitation(email: String!, token: String!): EmailAddress
SignupVerification(
nonce: String!
name: String!
email: String!
password: String!
slug: String
avatar: String
coverImg: String
avatarUpload: Upload
locationName: String
about: String
): User
}

View File

@ -0,0 +1,13 @@
type InvitationCode {
id: ID!
token: String
generatedBy: User @relation(name: "GENERATED", direction: "IN")
#createdAt: DateTime
#usedAt: DateTime
createdAt: String
}
type Mutation {
CreateInvitationCode: InvitationCode
}

View File

@ -3,20 +3,16 @@ type User {
actorId: String actorId: String
name: String name: String
email: String! email: String!
slug: String slug: String!
password: String!
avatar: String avatar: String
coverImg: String coverImg: String
avatarUpload: Upload
deleted: Boolean deleted: Boolean
disabled: Boolean disabled: Boolean
disabledBy: User @relation(name: "DISABLED", direction: "IN") disabledBy: User @relation(name: "DISABLED", direction: "IN")
role: UserGroup role: UserGroup
publicKey: String publicKey: String
privateKey: String invitedBy: User @relation(name: "INVITED", direction: "IN")
invited: [User] @relation(name: "INVITED", direction: "OUT")
wasInvited: Boolean
wasSeeded: Boolean
location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
locationName: String locationName: String
@ -78,3 +74,89 @@ type User {
badges: [Badge]! @relation(name: "REWARDED", direction: "IN") badges: [Badge]! @relation(name: "REWARDED", direction: "IN")
badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)") badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)")
} }
input _UserFilter {
AND: [_UserFilter!]
OR: [_UserFilter!]
id: ID
id_not: ID
id_in: [ID!]
id_not_in: [ID!]
id_contains: ID
id_not_contains: ID
id_starts_with: ID
id_not_starts_with: ID
id_ends_with: ID
id_not_ends_with: ID
friends: _UserFilter
friends_not: _UserFilter
friends_in: [_UserFilter!]
friends_not_in: [_UserFilter!]
friends_some: _UserFilter
friends_none: _UserFilter
friends_single: _UserFilter
friends_every: _UserFilter
following: _UserFilter
following_not: _UserFilter
following_in: [_UserFilter!]
following_not_in: [_UserFilter!]
following_some: _UserFilter
following_none: _UserFilter
following_single: _UserFilter
following_every: _UserFilter
followedBy: _UserFilter
followedBy_not: _UserFilter
followedBy_in: [_UserFilter!]
followedBy_not_in: [_UserFilter!]
followedBy_some: _UserFilter
followedBy_none: _UserFilter
followedBy_single: _UserFilter
followedBy_every: _UserFilter
}
type Query {
User(
id: ID
email: String
actorId: String
name: String
slug: String
avatar: String
coverImg: String
role: UserGroup
locationName: String
about: String
createdAt: String
updatedAt: String
friendsCount: Int
followingCount: Int
followedByCount: Int
followedByCurrentUser: Boolean
contributionsCount: Int
commentsCount: Int
commentedCount: Int
shoutedCount: Int
badgesCount: Int
first: Int
offset: Int
orderBy: [_UserOrdering]
filter: _UserFilter
): [User]
}
type Mutation {
UpdateUser (
id: ID!
name: String
email: String
slug: String
avatar: String
coverImg: String
avatarUpload: Upload
locationName: String
about: String
): User
DeleteUser(id: ID!, resource: [Deletable]): User
}

View File

@ -1,5 +1,5 @@
import { GraphQLClient, request } from 'graphql-request' import { GraphQLClient, request } from 'graphql-request'
import { getDriver } from '../../bootstrap/neo4j' import { getDriver, neode } from '../../bootstrap/neo4j'
import createBadge from './badges.js' import createBadge from './badges.js'
import createUser from './users.js' import createUser from './users.js'
import createOrganization from './organizations.js' import createOrganization from './organizations.js'
@ -48,7 +48,11 @@ export const cleanDatabase = async (options = {}) => {
} }
export default function Factory(options = {}) { export default function Factory(options = {}) {
const { neo4jDriver = getDriver(), seedServerHost = 'http://127.0.0.1:4001' } = options let {
seedServerHost = 'http://127.0.0.1:4001',
neo4jDriver = getDriver(),
neodeInstance = neode(),
} = options
const graphQLClient = new GraphQLClient(seedServerHost) const graphQLClient = new GraphQLClient(seedServerHost)
@ -58,19 +62,23 @@ export default function Factory(options = {}) {
graphQLClient, graphQLClient,
factories, factories,
lastResponse: null, lastResponse: null,
neodeInstance,
async authenticateAs({ email, password }) { async authenticateAs({ email, password }) {
const headers = await authenticatedHeaders({ email, password }, seedServerHost) const headers = await authenticatedHeaders({ email, password }, seedServerHost)
this.lastResponse = headers this.lastResponse = headers
this.graphQLClient = new GraphQLClient(seedServerHost, { headers }) this.graphQLClient = new GraphQLClient(seedServerHost, { headers })
return this return this
}, },
async create(node, properties) { async create(node, args = {}) {
const { mutation, variables } = this.factories[node](properties) const { factory, mutation, variables } = this.factories[node](args)
this.lastResponse = await this.graphQLClient.request(mutation, variables) if (factory) {
this.lastResponse = await factory({ args, neodeInstance })
} else {
this.lastResponse = await this.graphQLClient.request(mutation, variables)
}
return this return this
}, },
async relate(node, relationship, properties) { async relate(node, relationship, { from, to }) {
const { from, to } = properties
const mutation = ` const mutation = `
mutation { mutation {
Add${node}${relationship}( Add${node}${relationship}(
@ -112,6 +120,11 @@ export default function Factory(options = {}) {
this.lastResponse = await this.graphQLClient.request(mutation) this.lastResponse = await this.graphQLClient.request(mutation)
return this return this
}, },
async invite({ email }) {
const mutation = ` mutation($email: String!) { invite( email: $email) } `
this.lastResponse = await this.graphQLClient.request(mutation, { email })
return this
},
async cleanDatabase() { async cleanDatabase() {
this.lastResponse = await cleanDatabase({ driver: this.neo4jDriver }) this.lastResponse = await cleanDatabase({ driver: this.neo4jDriver })
return this return this
@ -121,6 +134,9 @@ export default function Factory(options = {}) {
result.create.bind(result) result.create.bind(result)
result.relate.bind(result) result.relate.bind(result)
result.mutate.bind(result) result.mutate.bind(result)
result.shout.bind(result)
result.follow.bind(result)
result.invite.bind(result)
result.cleanDatabase.bind(result) result.cleanDatabase.bind(result)
return result return result
} }

View File

@ -1,51 +1,28 @@
import faker from 'faker' import faker from 'faker'
import uuid from 'uuid/v4' import uuid from 'uuid/v4'
import encryptPassword from '../../helpers/encryptPassword'
import slugify from 'slug'
export default function create(params) { export default function create(params) {
const {
id = uuid(),
name = faker.name.findName(),
slug = '',
email = faker.internet.email(),
password = '1234',
role = 'user',
avatar = faker.internet.avatar(),
about = faker.lorem.paragraph(),
} = params
return { return {
mutation: ` factory: async ({ args, neodeInstance }) => {
mutation( const defaults = {
$id: ID! id: uuid(),
$name: String name: faker.name.findName(),
$slug: String email: faker.internet.email(),
$password: String! password: '1234',
$email: String! role: 'user',
$avatar: String avatar: faker.internet.avatar(),
$about: String about: faker.lorem.paragraph(),
$role: UserGroup
) {
CreateUser(
id: $id
name: $name
slug: $slug
password: $password
email: $email
avatar: $avatar
about: $about
role: $role
) {
id
name
slug
email
avatar
role
deleted
disabled
}
} }
`, defaults.slug = slugify(defaults.name, { lower: true })
variables: { id, name, slug, password, email, avatar, about, role }, args = {
...defaults,
...args,
}
args = await encryptPassword(args)
const user = await neodeInstance.create('User', args)
return user.toJson()
},
} }
} }

View File

@ -349,6 +349,7 @@ import Factory from './factories'
]) ])
/* eslint-disable-next-line no-console */ /* eslint-disable-next-line no-console */
console.log('Seeded Data...') console.log('Seeded Data...')
process.exit(0)
} catch (err) { } catch (err) {
/* eslint-disable-next-line no-console */ /* eslint-disable-next-line no-console */
console.error(err) console.error(err)

View File

@ -704,6 +704,13 @@
dependencies: dependencies:
regenerator-runtime "^0.13.2" regenerator-runtime "^0.13.2"
"@babel/runtime@^7.4.4":
version "7.4.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12"
integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==
dependencies:
regenerator-runtime "^0.13.2"
"@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.4.4": "@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.4.4":
version "7.4.4" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237"
@ -745,6 +752,43 @@
exec-sh "^0.3.2" exec-sh "^0.3.2"
minimist "^1.2.0" minimist "^1.2.0"
"@hapi/address@2.x.x":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.0.0.tgz#9f05469c88cb2fd3dcd624776b54ee95c312126a"
integrity sha512-mV6T0IYqb0xL1UALPFplXYQmR0twnXG0M6jUswpquqT2sD12BOiCiLy3EvMp/Fy7s3DZElC4/aPjEjo2jeZpvw==
"@hapi/hoek@6.x.x":
version "6.2.4"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-6.2.4.tgz#4b95fbaccbfba90185690890bdf1a2fbbda10595"
integrity sha512-HOJ20Kc93DkDVvjwHyHawPwPkX44sIrbXazAUDiUXaY2R9JwQGo2PhFfnQtdrsIe4igjG2fPgMra7NYw7qhy0A==
"@hapi/hoek@8.x.x":
version "8.0.1"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.0.1.tgz#9712fa2ad124ac64668ab06ba847b1eaf83a03fd"
integrity sha512-cctMYH5RLbElaUpZn3IJaUj9QNQD8iXDnl7xNY6KB1aFD2ciJrwpo3kvZowIT75uA+silJFDnSR2kGakALUymg==
"@hapi/joi@^15.1.0":
version "15.1.0"
resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-15.1.0.tgz#940cb749b5c55c26ab3b34ce362e82b6162c8e7a"
integrity sha512-n6kaRQO8S+kepUTbXL9O/UOL788Odqs38/VOfoCrATDtTvyfiO3fgjlSRaNkHabpTLgM7qru9ifqXlXbXk8SeQ==
dependencies:
"@hapi/address" "2.x.x"
"@hapi/hoek" "6.x.x"
"@hapi/marker" "1.x.x"
"@hapi/topo" "3.x.x"
"@hapi/marker@1.x.x":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@hapi/marker/-/marker-1.0.0.tgz#65b0b2b01d1be06304886ce9b4b77b1bfb21a769"
integrity sha512-JOfdekTXnJexfE8PyhZFyHvHjt81rBFSAbTIRAhF2vv/2Y1JzoKsGqxH/GpZJoF7aEfYok8JVcAHmSz1gkBieA==
"@hapi/topo@3.x.x":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-3.1.2.tgz#57cc1317be1a8c5f47c124f9b0e3c49cd78424d2"
integrity sha512-r+aumOqJ5QbD6aLPJWqVjMAPsx5pZKz+F5yPqXZ/WWG9JTtHbQqlzrJoknJ0iJxLj9vlXtmpSdjlkszseeG8OA==
dependencies:
"@hapi/hoek" "8.x.x"
"@jest/console@^24.7.1": "@jest/console@^24.7.1":
version "24.7.1" version "24.7.1"
resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.7.1.tgz#32a9e42535a97aedfe037e725bd67e954b459545" resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.7.1.tgz#32a9e42535a97aedfe037e725bd67e954b459545"
@ -1342,6 +1386,18 @@ apollo-engine-reporting-protobuf@0.3.1:
dependencies: dependencies:
protobufjs "^6.8.6" protobufjs "^6.8.6"
apollo-engine-reporting@1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.3.4.tgz#65e12f94221d80b3b1740c26e82ce9bb6bdfb7ee"
integrity sha512-DJdYghyUBzT0/LcPLwuQNXDCw06r1RfxkVfNTGKoTv6a+leVvjhDJmXvc+jSuBPwaNsc+RYRnfyQ2qUn9fmfyA==
dependencies:
apollo-engine-reporting-protobuf "0.3.1"
apollo-graphql "^0.3.2"
apollo-server-core "2.6.6"
apollo-server-env "2.4.0"
async-retry "^1.2.1"
graphql-extensions "0.7.5"
apollo-engine-reporting@1.3.5: apollo-engine-reporting@1.3.5:
version "1.3.5" version "1.3.5"
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.3.5.tgz#075424d39dfe77a20f96e8e33b7ae52d58c38e1e" resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.3.5.tgz#075424d39dfe77a20f96e8e33b7ae52d58c38e1e"
@ -1371,6 +1427,14 @@ apollo-errors@^1.9.0:
assert "^1.4.1" assert "^1.4.1"
extendable-error "^0.1.5" extendable-error "^0.1.5"
apollo-graphql@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.3.2.tgz#8881a87f1d5fcf80837b34dba90737e664eabe9a"
integrity sha512-YbzYGR14GV0023m//EU66vOzZ3i7c04V/SF8Qk+60vf1sOWyKgO6mxZJ4BKhw10qWUayirhSDxq3frYE+qSG0A==
dependencies:
apollo-env "0.5.1"
lodash.sortby "^4.7.0"
apollo-graphql@^0.3.3: apollo-graphql@^0.3.3:
version "0.3.3" version "0.3.3"
resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.3.3.tgz#ce1df194f6e547ad3ce1e35b42f9c211766e1658" resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.3.3.tgz#ce1df194f6e547ad3ce1e35b42f9c211766e1658"
@ -1422,6 +1486,32 @@ apollo-server-caching@0.4.0:
dependencies: dependencies:
lru-cache "^5.0.0" lru-cache "^5.0.0"
apollo-server-core@2.6.6:
version "2.6.6"
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.6.tgz#55fea7980a943948c49dea20d81b9bbfc0e04f87"
integrity sha512-PFSjJbqkV1eetfFJxu11gzklQYC8BrF0RZfvC1d1mhvtxAOKl25uhPHxltN0Omyjp7LW4YeoC6zwl9rLWuhZFQ==
dependencies:
"@apollographql/apollo-tools" "^0.3.6"
"@apollographql/graphql-playground-html" "1.6.20"
"@types/ws" "^6.0.0"
apollo-cache-control "0.7.4"
apollo-datasource "0.5.0"
apollo-engine-reporting "1.3.4"
apollo-server-caching "0.4.0"
apollo-server-env "2.4.0"
apollo-server-errors "2.3.0"
apollo-server-plugin-base "0.5.5"
apollo-tracing "0.7.3"
fast-json-stable-stringify "^2.0.0"
graphql-extensions "0.7.5"
graphql-subscriptions "^1.0.0"
graphql-tag "^2.9.2"
graphql-tools "^4.0.0"
graphql-upload "^8.0.2"
sha.js "^2.4.11"
subscriptions-transport-ws "^0.9.11"
ws "^6.0.0"
apollo-server-core@2.6.7: apollo-server-core@2.6.7:
version "2.6.7" version "2.6.7"
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.7.tgz#85b0310f40cfec43a702569c73af16d88776a6f0" resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.7.tgz#85b0310f40cfec43a702569c73af16d88776a6f0"
@ -1470,10 +1560,10 @@ apollo-server-errors@2.3.0:
resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.0.tgz#700622b66a16dffcad3b017e4796749814edc061" resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.0.tgz#700622b66a16dffcad3b017e4796749814edc061"
integrity sha512-rUvzwMo2ZQgzzPh2kcJyfbRSfVKRMhfIlhY7BzUfM4x6ZT0aijlgsf714Ll3Mbf5Fxii32kD0A/DmKsTecpccw== integrity sha512-rUvzwMo2ZQgzzPh2kcJyfbRSfVKRMhfIlhY7BzUfM4x6ZT0aijlgsf714Ll3Mbf5Fxii32kD0A/DmKsTecpccw==
apollo-server-express@2.6.7: apollo-server-express@2.6.6:
version "2.6.7" version "2.6.6"
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.6.7.tgz#22307e08b75be1553f4099d00028abe52597767d" resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.6.6.tgz#ec2b955354d7dd4d12fe01ea7e983d302071d5b9"
integrity sha512-qbCQM+8LxXpwPNN5Sdvcb+Sne8zuCORFt25HJtPJRkHlyBUzOd7JA7SEnUn5e2geTiiGoVIU5leh+++C51udTw== integrity sha512-bY/xrr9lZH+hsjchiQuSXpW3ivXfL1h81M5VE9Ppus1PVwwEIar/irBN+PFp97WxERZPDjVZzrRKa+lRHjtJsA==
dependencies: dependencies:
"@apollographql/graphql-playground-html" "1.6.20" "@apollographql/graphql-playground-html" "1.6.20"
"@types/accepts" "^1.3.5" "@types/accepts" "^1.3.5"
@ -1481,7 +1571,7 @@ apollo-server-express@2.6.7:
"@types/cors" "^2.8.4" "@types/cors" "^2.8.4"
"@types/express" "4.17.0" "@types/express" "4.17.0"
accepts "^1.3.5" accepts "^1.3.5"
apollo-server-core "2.6.7" apollo-server-core "2.6.6"
body-parser "^1.18.3" body-parser "^1.18.3"
cors "^2.8.4" cors "^2.8.4"
graphql-subscriptions "^1.0.0" graphql-subscriptions "^1.0.0"
@ -1509,6 +1599,11 @@ apollo-server-module-graphiql@^1.3.4, apollo-server-module-graphiql@^1.4.0:
resolved "https://registry.yarnpkg.com/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.4.0.tgz#c559efa285578820709f1769bb85d3b3eed3d8ec" resolved "https://registry.yarnpkg.com/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.4.0.tgz#c559efa285578820709f1769bb85d3b3eed3d8ec"
integrity sha512-GmkOcb5he2x5gat+TuiTvabnBf1m4jzdecal3XbXBh/Jg+kx4hcvO3TTDFQ9CuTprtzdcVyA11iqG7iOMOt7vA== integrity sha512-GmkOcb5he2x5gat+TuiTvabnBf1m4jzdecal3XbXBh/Jg+kx4hcvO3TTDFQ9CuTprtzdcVyA11iqG7iOMOt7vA==
apollo-server-plugin-base@0.5.5:
version "0.5.5"
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.5.tgz#364e4a2fca4d95ddeb9fd3e78940ed1da58865c2"
integrity sha512-agiuhknyu3lnnEsqUh99tzxwPCGp+TuDK+TSRTkXU1RUG6lY4C3uJp0JGJw03cP+M6ze73TbRjMA4E68g/ks5A==
apollo-server-plugin-base@0.5.6: apollo-server-plugin-base@0.5.6:
version "0.5.6" version "0.5.6"
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.6.tgz#3a7128437a0f845e7d873fa43ef091ff7bf27975" resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.6.tgz#3a7128437a0f845e7d873fa43ef091ff7bf27975"
@ -1521,13 +1616,13 @@ apollo-server-testing@~2.6.7:
dependencies: dependencies:
apollo-server-core "2.6.7" apollo-server-core "2.6.7"
apollo-server@~2.6.7: apollo-server@~2.6.6:
version "2.6.7" version "2.6.6"
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.6.7.tgz#b707ede529b4d45f2f00a74f3b457658b0e62e83" resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.6.6.tgz#0570fce4a682eb1de8bc1b86dbe2543de440cd4e"
integrity sha512-4wk9JykURLed6CnNIj9jhU6ueeTVmGBTyAnnvnlhRrOf50JAFszUErZIKg6lw5vVr5riaByrGFIkMBTySCHgPQ== integrity sha512-7Bulb3RnOO4/SGA66LXu3ZHCXIK8MYMrsxy4yti1/adDIUmcniolDqJwOYUGoTmv1AQjRxgJb4TVZ0Dk9nrrYg==
dependencies: dependencies:
apollo-server-core "2.6.7" apollo-server-core "2.6.6"
apollo-server-express "2.6.7" apollo-server-express "2.6.6"
express "^4.0.0" express "^4.0.0"
graphql-subscriptions "^1.0.0" graphql-subscriptions "^1.0.0"
graphql-tools "^4.0.0" graphql-tools "^4.0.0"
@ -2584,10 +2679,10 @@ data-urls@^1.0.0:
whatwg-mimetype "^2.2.0" whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0" whatwg-url "^7.0.0"
date-fns@2.0.0-beta.2: date-fns@2.0.0-beta.1:
version "2.0.0-beta.2" version "2.0.0-beta.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-beta.2.tgz#ccd556df832ef761baa88c600f53d2e829245999" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-beta.1.tgz#6f3209ea8be559211be5160e0a6379a7eade227b"
integrity sha512-4cicZF707RNerr3/Q3CcdLo+3OHMCfrRXE7h5iFgn7AMvX07sqKLxSf8Yp+WJW5bvKr2cy9/PkctXLv4iFtOaA== integrity sha512-ls5W/PUZmrtck53HD3Sd0564NlnNoQtcxNCwWcIzULJMNNgAPVKHoylVXPau7vdyu5/JTd25ljtan+iWnnUKkw==
debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
version "2.6.9" version "2.6.9"
@ -2816,6 +2911,11 @@ dotenv@^0.4.0:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-0.4.0.tgz#f6fb351363c2d92207245c737802c9ab5ae1495a" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-0.4.0.tgz#f6fb351363c2d92207245c737802c9ab5ae1495a"
integrity sha1-9vs1E2PC2SIHJFxzeALJq1rhSVo= integrity sha1-9vs1E2PC2SIHJFxzeALJq1rhSVo=
dotenv@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d"
integrity sha1-hk7xN5rO1Vzm+V3r7NzhefegzR0=
dotenv@~8.0.0: dotenv@~8.0.0:
version "8.0.0" version "8.0.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.0.0.tgz#ed310c165b4e8a97bb745b0a9d99c31bda566440" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.0.0.tgz#ed310c165b4e8a97bb745b0a9d99c31bda566440"
@ -3727,6 +3827,13 @@ graphql-extensions@0.7.4:
dependencies: dependencies:
"@apollographql/apollo-tools" "^0.3.6" "@apollographql/apollo-tools" "^0.3.6"
graphql-extensions@0.7.5:
version "0.7.5"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.5.tgz#fab2b9e53cf6014952e6547456d50680ff0ea579"
integrity sha512-B1m+/WEJa3IYKWqBPS9W/7OasfPmlHOSz5hpEAq2Jbn6T0FQ/d2YWFf2HBETHR3RR2qfT+55VMiYovl2ga3qcg==
dependencies:
"@apollographql/apollo-tools" "^0.3.6"
graphql-extensions@0.7.6: graphql-extensions@0.7.6:
version "0.7.6" version "0.7.6"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.6.tgz#80cdddf08b0af12525529d1922ee2ea0d0cc8ecf" resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.6.tgz#80cdddf08b0af12525529d1922ee2ea0d0cc8ecf"
@ -4916,7 +5023,7 @@ jmespath@0.15.0:
resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc= integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=
joi@^13.0.0: joi@^13.0.0, joi@^13.7.0:
version "13.7.0" version "13.7.0"
resolved "https://registry.yarnpkg.com/joi/-/joi-13.7.0.tgz#cfd85ebfe67e8a1900432400b4d03bbd93fb879f" resolved "https://registry.yarnpkg.com/joi/-/joi-13.7.0.tgz#cfd85ebfe67e8a1900432400b4d03bbd93fb879f"
integrity sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q== integrity sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==
@ -5604,6 +5711,15 @@ negotiator@0.6.2:
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
neo4j-driver@^1.6.3:
version "1.7.5"
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.5.tgz#c3fe3677f69c12f26944563d45e7e7d818a685e4"
integrity sha512-xCD2F5+tp/SD9r5avX5bSoY8u8RH2o793xJ9Ikjz1s5qQy7cFxFbbj2c52uz3BVGhRAx/NmB57VjOquYmmxGtw==
dependencies:
"@babel/runtime" "^7.4.4"
text-encoding-utf-8 "^1.0.2"
uri-js "^4.2.2"
neo4j-driver@^1.7.3, neo4j-driver@~1.7.4: neo4j-driver@^1.7.3, neo4j-driver@~1.7.4:
version "1.7.4" version "1.7.4"
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.4.tgz#9661cf643b63818bff85e82c4691918e75098c1e" resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.4.tgz#9661cf643b63818bff85e82c4691918e75098c1e"
@ -5623,6 +5739,16 @@ neo4j-graphql-js@^2.6.3:
lodash "^4.17.11" lodash "^4.17.11"
neo4j-driver "^1.7.3" neo4j-driver "^1.7.3"
neode@^0.2.16:
version "0.2.16"
resolved "https://registry.yarnpkg.com/neode/-/neode-0.2.16.tgz#20532cc67604fd00cc88de841d422f5238ae5bd3"
integrity sha512-L9p55IDKGzAZsQgHdXrfd2xasDuB46RipcrPw6NP7ESgkmfJMaMWRZ1F3Kv+f4V4U1WnhZ1IILvwVFhYPnpXEg==
dependencies:
dotenv "^4.0.0"
joi "^13.7.0"
neo4j-driver "^1.6.3"
uuid "^3.3.2"
next-tick@^1.0.0: next-tick@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
@ -7411,6 +7537,11 @@ test-exclude@^5.0.0:
read-pkg-up "^4.0.0" read-pkg-up "^4.0.0"
require-main-filename "^1.0.1" require-main-filename "^1.0.1"
text-encoding-utf-8@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13"
integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==
text-encoding@^0.6.4: text-encoding@^0.6.4:
version "0.6.4" version "0.6.4"
resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19" resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19"

View File

@ -1,5 +1,6 @@
import { Given, When, Then } from "cypress-cucumber-preprocessor/steps"; import { Given, When, Then } from "cypress-cucumber-preprocessor/steps";
import { getLangByName } from "../../support/helpers"; import { getLangByName } from "../../support/helpers";
import slugify from 'slug'
/* global cy */ /* global cy */
@ -11,6 +12,7 @@ let loginCredentials = {
}; };
const narratorParams = { const narratorParams = {
name: "Peter Pan", name: "Peter Pan",
slug: 'peter-pan',
avatar: "https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg", avatar: "https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg",
...loginCredentials ...loginCredentials
}; };
@ -171,10 +173,11 @@ When("I press {string}", label => {
}); });
Given("we have the following posts in our database:", table => { Given("we have the following posts in our database:", table => {
table.hashes().forEach(({ Author, ...postAttributes }) => { table.hashes().forEach(({ Author, ...postAttributes }, i) => {
Author = Author || `author-${i}`
const userAttributes = { const userAttributes = {
name: Author, name: Author,
email: `${Author}@example.org`, email: `${slugify(Author, {lower: true})}@example.org`,
password: "1234" password: "1234"
}; };
postAttributes.deleted = Boolean(postAttributes.deleted); postAttributes.deleted = Boolean(postAttributes.deleted);

View File

@ -6,9 +6,9 @@ Feature: Search
Background: Background:
Given I have a user account Given I have a user account
And we have the following posts in our database: And we have the following posts in our database:
| Author | id | title | content | | id | title | content |
| Brianna Wiest | p1 | 101 Essays that will change the way you think | 101 Essays, of course! | | p1 | 101 Essays that will change the way you think | 101 Essays, of course! |
| Brianna Wiest | p2 | No searched for content | will be found in this post, I guarantee | | p2 | No searched for content | will be found in this post, I guarantee |
Given I am logged in Given I am logged in
Scenario: Search for specific words Scenario: Search for specific words

View File

@ -1,12 +1,15 @@
import Factory from '../../backend/src/seed/factories' import Factory from '../../backend/src/seed/factories'
import { getDriver } from '../../backend/src/bootstrap/neo4j' import { getDriver } from '../../backend/src/bootstrap/neo4j'
import setupNeode from '../../backend/src/bootstrap/neode'
import neode from 'neode'
const neo4jDriver = getDriver({ const neo4jConfigs = {
uri: Cypress.env('NEO4J_URI'), uri: Cypress.env('NEO4J_URI'),
username: Cypress.env('NEO4J_USERNAME'), username: Cypress.env('NEO4J_USERNAME'),
password: Cypress.env('NEO4J_PASSWORD') password: Cypress.env('NEO4J_PASSWORD')
}) }
const factory = Factory({ neo4jDriver }) const neo4jDriver = getDriver(neo4jConfigs)
const factory = Factory({ seedServerHost, neo4jDriver, neodeInstance: setupNeode(neo4jConfigs)})
const seedServerHost = Cypress.env('SEED_SERVER_HOST') const seedServerHost = Cypress.env('SEED_SERVER_HOST')
beforeEach(async () => { beforeEach(async () => {
@ -14,7 +17,7 @@ beforeEach(async () => {
}) })
Cypress.Commands.add('factory', () => { Cypress.Commands.add('factory', () => {
return Factory({ seedServerHost }) return Factory({ seedServerHost, neo4jDriver, neodeInstance: setupNeode(neo4jConfigs) })
}) })
Cypress.Commands.add( Cypress.Commands.add(

View File

@ -19,6 +19,7 @@
"test:jest": "cd webapp && yarn test && cd ../backend && yarn test:jest && codecov" "test:jest": "cd webapp && yarn test && cd ../backend && yarn test:jest && codecov"
}, },
"devDependencies": { "devDependencies": {
"bcryptjs": "^2.4.3",
"codecov": "^3.5.0", "codecov": "^3.5.0",
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"cypress": "^3.3.2", "cypress": "^3.3.2",
@ -29,6 +30,8 @@
"faker": "Marak/faker.js#master", "faker": "Marak/faker.js#master",
"graphql-request": "^1.8.2", "graphql-request": "^1.8.2",
"neo4j-driver": "^1.7.5", "neo4j-driver": "^1.7.5",
"npm-run-all": "^4.1.5" "neode": "^0.2.16",
"npm-run-all": "^4.1.5",
"slug": "^1.1.0"
} }
} }

View File

@ -97,7 +97,7 @@
</ds-container> </ds-container>
</div> </div>
<ds-container> <ds-container style="word-break: break-all">
<div style="padding: 6rem 2rem 5rem;"> <div style="padding: 6rem 2rem 5rem;">
<nuxt /> <nuxt />
</div> </div>

View File

@ -73,7 +73,7 @@
"name": "Your data", "name": "Your data",
"labelName": "Your Name", "labelName": "Your Name",
"namePlaceholder": "Femanon Funny", "namePlaceholder": "Femanon Funny",
"labelCity": "Su ciudad o región", "labelCity": "Your City or Region",
"labelBio": "About You", "labelBio": "About You",
"success": "Your data was successfully updated!" "success": "Your data was successfully updated!"
}, },

View File

@ -43,7 +43,7 @@
"name": "Sus datos", "name": "Sus datos",
"labelName": "Su nombre", "labelName": "Su nombre",
"namePlaceholder": "Femanon Funny", "namePlaceholder": "Femanon Funny",
"labelCity": "Your City or Region", "labelCity": "Su ciudad o región",
"labelBio": "Acerca de usted", "labelBio": "Acerca de usted",
"success": "Sus datos han sido actualizados con éxito!" "success": "Sus datos han sido actualizados con éxito!"
}, },

View File

@ -2,7 +2,6 @@ import Vue from 'vue'
import { enUS, de, nl, fr, es } from 'date-fns/locale' import { enUS, de, nl, fr, es } from 'date-fns/locale'
import format from 'date-fns/format' import format from 'date-fns/format'
import addSeconds from 'date-fns/addSeconds'
import accounting from 'accounting' import accounting from 'accounting'
export default ({ app = {} }) => { export default ({ app = {} }) => {
@ -39,15 +38,6 @@ export default ({ app = {} }) => {
} }
return accounting.formatNumber(value || 0, precision, thousands, decimals) return accounting.formatNumber(value || 0, precision, thousands, decimals)
}, },
// format seconds or milliseconds to durations HH:mm:ss
duration: (value, unit = 's') => {
if (unit === 'ms') {
value = value / 1000
}
return value
? format(addSeconds(new Date('2000-01-01 00:00'), value), 'HH:mm:ss')
: '00:00:00'
},
truncate: (value = '', length = -1) => { truncate: (value = '', length = -1) => {
if (!value || typeof value !== 'string' || value.length <= 0) { if (!value || typeof value !== 'string' || value.length <= 0) {
return '' return ''

View File

@ -1040,6 +1040,11 @@ bcrypt-pbkdf@^1.0.0:
dependencies: dependencies:
tweetnacl "^0.14.3" tweetnacl "^0.14.3"
bcryptjs@^2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb"
integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=
becke-ch--regex--s0-0-v1--base--pl--lib@^1.2.0: becke-ch--regex--s0-0-v1--base--pl--lib@^1.2.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/becke-ch--regex--s0-0-v1--base--pl--lib/-/becke-ch--regex--s0-0-v1--base--pl--lib-1.4.0.tgz#429ceebbfa5f7e936e78d73fbdc7da7162b20e20" resolved "https://registry.yarnpkg.com/becke-ch--regex--s0-0-v1--base--pl--lib/-/becke-ch--regex--s0-0-v1--base--pl--lib-1.4.0.tgz#429ceebbfa5f7e936e78d73fbdc7da7162b20e20"
@ -2038,6 +2043,11 @@ domain-browser@^1.2.0:
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
dotenv@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d"
integrity sha1-hk7xN5rO1Vzm+V3r7NzhefegzR0=
dotenv@^8.0.0: dotenv@^8.0.0:
version "8.0.0" version "8.0.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.0.0.tgz#ed310c165b4e8a97bb745b0a9d99c31bda566440" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.0.0.tgz#ed310c165b4e8a97bb745b0a9d99c31bda566440"
@ -2658,6 +2668,16 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0" minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1" minimalistic-crypto-utils "^1.0.1"
hoek@5.x.x:
version "5.0.4"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-5.0.4.tgz#0f7fa270a1cafeb364a4b2ddfaa33f864e4157da"
integrity sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==
hoek@6.x.x:
version "6.1.3"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c"
integrity sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==
hosted-git-info@^2.1.4: hosted-git-info@^2.1.4:
version "2.7.1" version "2.7.1"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
@ -3033,6 +3053,13 @@ isarray@^2.0.4:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.4.tgz#38e7bcbb0f3ba1b7933c86ba1894ddfc3781bbb7" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.4.tgz#38e7bcbb0f3ba1b7933c86ba1894ddfc3781bbb7"
integrity sha512-GMxXOiUirWg1xTKRipM0Ek07rX+ubx4nNVElTJdNLYmNO/2YrDkgJGw9CljXn+r4EWiDQg/8lsRdHyg2PJuUaA== integrity sha512-GMxXOiUirWg1xTKRipM0Ek07rX+ubx4nNVElTJdNLYmNO/2YrDkgJGw9CljXn+r4EWiDQg/8lsRdHyg2PJuUaA==
isemail@3.x.x:
version "3.2.0"
resolved "https://registry.yarnpkg.com/isemail/-/isemail-3.2.0.tgz#59310a021931a9fb06bbb51e155ce0b3f236832c"
integrity sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==
dependencies:
punycode "2.x.x"
isexe@^2.0.0: isexe@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@ -3055,6 +3082,15 @@ isstream@~0.1.2:
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
joi@^13.7.0:
version "13.7.0"
resolved "https://registry.yarnpkg.com/joi/-/joi-13.7.0.tgz#cfd85ebfe67e8a1900432400b4d03bbd93fb879f"
integrity sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==
dependencies:
hoek "5.x.x"
isemail "3.x.x"
topo "3.x.x"
js-levenshtein@^1.1.3: js-levenshtein@^1.1.3:
version "1.1.6" version "1.1.6"
resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
@ -3524,7 +3560,7 @@ needle@^2.2.1:
iconv-lite "^0.4.4" iconv-lite "^0.4.4"
sax "^1.2.4" sax "^1.2.4"
neo4j-driver@^1.7.5: neo4j-driver@^1.6.3, neo4j-driver@^1.7.5:
version "1.7.5" version "1.7.5"
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.5.tgz#c3fe3677f69c12f26944563d45e7e7d818a685e4" resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.5.tgz#c3fe3677f69c12f26944563d45e7e7d818a685e4"
integrity sha512-xCD2F5+tp/SD9r5avX5bSoY8u8RH2o793xJ9Ikjz1s5qQy7cFxFbbj2c52uz3BVGhRAx/NmB57VjOquYmmxGtw== integrity sha512-xCD2F5+tp/SD9r5avX5bSoY8u8RH2o793xJ9Ikjz1s5qQy7cFxFbbj2c52uz3BVGhRAx/NmB57VjOquYmmxGtw==
@ -3533,6 +3569,16 @@ neo4j-driver@^1.7.5:
text-encoding-utf-8 "^1.0.2" text-encoding-utf-8 "^1.0.2"
uri-js "^4.2.2" uri-js "^4.2.2"
neode@^0.2.16:
version "0.2.16"
resolved "https://registry.yarnpkg.com/neode/-/neode-0.2.16.tgz#20532cc67604fd00cc88de841d422f5238ae5bd3"
integrity sha512-L9p55IDKGzAZsQgHdXrfd2xasDuB46RipcrPw6NP7ESgkmfJMaMWRZ1F3Kv+f4V4U1WnhZ1IILvwVFhYPnpXEg==
dependencies:
dotenv "^4.0.0"
joi "^13.7.0"
neo4j-driver "^1.6.3"
uuid "^3.3.2"
next-tick@^1.0.0: next-tick@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
@ -3965,16 +4011,16 @@ punycode@1.3.2:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
punycode@2.x.x, punycode@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
punycode@^1.3.2, punycode@^1.4.1: punycode@^1.3.2, punycode@^1.4.1:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
punycode@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
qs@~6.5.2: qs@~6.5.2:
version "6.5.2" version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
@ -4373,6 +4419,13 @@ slice-ansi@0.0.4:
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU= integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=
slug@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/slug/-/slug-1.1.0.tgz#73eef5710416f515077bdf70c683bde4915913c9"
integrity sha512-NuIOjDQeTMPm+/AUIHJ5636mF3jOsYLFnoEErl9Tdpt4kpt4fOrAJxscH9mUgX1LtPaEqgPCawBg7A4yhoSWRg==
dependencies:
unicode ">= 0.3.1"
snapdragon-node@^2.0.1: snapdragon-node@^2.0.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@ -4785,6 +4838,13 @@ to-regex@^3.0.1, to-regex@^3.0.2:
regex-not "^1.0.2" regex-not "^1.0.2"
safe-regex "^1.1.0" safe-regex "^1.1.0"
topo@3.x.x:
version "3.0.3"
resolved "https://registry.yarnpkg.com/topo/-/topo-3.0.3.tgz#d5a67fb2e69307ebeeb08402ec2a2a6f5f7ad95c"
integrity sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==
dependencies:
hoek "6.x.x"
tough-cookie@~2.4.3: tough-cookie@~2.4.3:
version "2.4.3" version "2.4.3"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
@ -4864,6 +4924,11 @@ unicode-property-aliases-ecmascript@^1.0.4:
resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz#a9cc6cc7ce63a0a3023fc99e341b94431d405a57" resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz#a9cc6cc7ce63a0a3023fc99e341b94431d405a57"
integrity sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw== integrity sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==
"unicode@>= 0.3.1":
version "11.0.1"
resolved "https://registry.yarnpkg.com/unicode/-/unicode-11.0.1.tgz#735bd422ec75cf28d396eb224d535d168d5f1db6"
integrity sha512-+cHtykLb+eF1yrSLWTwcYBrqJkTfX7Quoyg7Juhe6uylF43ZbMdxMuSHNYlnyLT8T7POAvavgBthzUF9AIaQvQ==
union-value@^1.0.0: union-value@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"