mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge branch 'master' of github.com:Human-Connection/Human-Connection into 967-filter-post-by-category
This commit is contained in:
commit
a875fe0d5e
@ -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
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
88
backend/src/bootstrap/neode.js
Normal file
88
backend/src/bootstrap/neode.js
Normal 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
|
||||||
|
}
|
||||||
7
backend/src/helpers/encryptPassword.js
Normal file
7
backend/src/helpers/encryptPassword.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { hashSync } from 'bcryptjs'
|
||||||
|
|
||||||
|
export default function(args) {
|
||||||
|
args.encryptedPassword = hashSync(args.password, 10)
|
||||||
|
delete args.password
|
||||||
|
return args
|
||||||
|
}
|
||||||
@ -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}`,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}`
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
57
backend/src/middleware/email/emailMiddleware.js
Normal file
57
backend/src/middleware/email/emailMiddleware.js
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
85
backend/src/middleware/email/templates/passwordReset.js
Normal file
85
backend/src/middleware/email/templates/passwordReset.js
Normal 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
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
}
|
||||||
42
backend/src/middleware/email/templates/signup.js
Normal file
42
backend/src/middleware/email/templates/signup.js
Normal 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
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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',
|
||||||
|
|||||||
@ -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
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
22
backend/src/middleware/validation/validationMiddleware.js
Normal file
22
backend/src/middleware/validation/validationMiddleware.js
Normal 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),
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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 () => {
|
||||||
|
|||||||
@ -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',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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) || ''
|
||||||
const mailTemplate = user ? resetPasswordMail : wrongAccountMail
|
return { user, code, name, response: true }
|
||||||
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()
|
||||||
|
|||||||
@ -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 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
107
backend/src/schema/resolvers/registration.js
Normal file
107
backend/src/schema/resolvers/registration.js
Normal 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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
402
backend/src/schema/resolvers/registration.spec.js
Normal file
402
backend/src/schema/resolvers/registration.spec.js
Normal 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',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -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',
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|||||||
@ -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)',
|
||||||
|
}),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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!) {
|
|
||||||
CreateUser(name: $name, password: $password, email: $email) {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
describe('given valid password and email', () => {
|
|
||||||
const variables = {
|
|
||||||
name: 'John Doe',
|
|
||||||
password: '123',
|
|
||||||
email: '123@123.de',
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('unauthenticated', () => {
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
await factory.create('User', { name: 'Johnny', email: 'any-email-address@example.org' })
|
||||||
|
})
|
||||||
|
|
||||||
|
const query = `query($email: String) { User(email: $email) { name } }`
|
||||||
|
const variables = { email: 'any-email-address@example.org' }
|
||||||
|
beforeEach(() => {
|
||||||
client = new GraphQLClient(host)
|
client = new GraphQLClient(host)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('is not allowed to create users', async () => {
|
it('is forbidden', async () => {
|
||||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
await expect(client.request(query, variables)).rejects.toThrow('Not Authorised')
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('authenticated admin', () => {
|
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',
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
23
backend/src/schema/types/type/EmailAddress.gql
Normal file
23
backend/src/schema/types/type/EmailAddress.gql
Normal 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
|
||||||
|
}
|
||||||
13
backend/src/schema/types/type/InvitationCode.gql
Normal file
13
backend/src/schema/types/type/InvitationCode.gql
Normal 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
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
if (factory) {
|
||||||
|
this.lastResponse = await factory({ args, neodeInstance })
|
||||||
|
} else {
|
||||||
this.lastResponse = await this.graphQLClient.request(mutation, variables)
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
}
|
||||||
) {
|
defaults.slug = slugify(defaults.name, { lower: true })
|
||||||
CreateUser(
|
args = {
|
||||||
id: $id
|
...defaults,
|
||||||
name: $name
|
...args,
|
||||||
slug: $slug
|
}
|
||||||
password: $password
|
args = await encryptPassword(args)
|
||||||
email: $email
|
const user = await neodeInstance.create('User', args)
|
||||||
avatar: $avatar
|
return user.toJson()
|
||||||
about: $about
|
},
|
||||||
role: $role
|
|
||||||
) {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
slug
|
|
||||||
email
|
|
||||||
avatar
|
|
||||||
role
|
|
||||||
deleted
|
|
||||||
disabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
variables: { id, name, slug, password, email, avatar, about, role },
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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!"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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!"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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 ''
|
||||||
|
|||||||
77
yarn.lock
77
yarn.lock
@ -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"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user