mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
merge master
This commit is contained in:
parent
f870b8ea29
commit
f7ec19f960
@ -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
|
||||||
|
|||||||
@ -4,9 +4,9 @@ Thanks so much for thinking of contributing to the Human Connection project, we
|
|||||||
|
|
||||||
## Getting Set Up
|
## Getting Set Up
|
||||||
|
|
||||||
Instructions for how to install all the necessary software can be found in our [documentation](https://docs.human-connection.org/human-connection/)
|
Instructions for how to install all the necessary software can be found in our [documentation](https://docs.human-connection.org/human-connection/).
|
||||||
|
|
||||||
We recommend that new folks should ideally work together with an existing developer. Please join our discord instance to chat with developers or just ask them in tickets in [Zenhub](https://app.zenhub.com/workspaces/human-connection-nitro-5c0154ecc699f60fc92cf11f/boards?repos=152252353):
|
We recommend that new folks should ideally work together with an existing developer. Please join our [discord](https://discord.gg/6ub73U3) instance to chat with developers or just ask them in tickets in [Zenhub](https://app.zenhub.com/workspaces/human-connection-nitro-5c0154ecc699f60fc92cf11f/boards?repos=152252353):
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@ -17,7 +17,7 @@ Here are some general notes on our development flow:
|
|||||||
* Currently operating in two week sprints
|
* Currently operating in two week sprints
|
||||||
* We are using ZenHub to coordinate
|
* We are using ZenHub to coordinate
|
||||||
* estimating time per issue is the crucial feature of [Zenhub](https://app.zenhub.com/workspaces/human-connection-nitro-5c0154ecc699f60fc92cf11f) that Github does not have
|
* estimating time per issue is the crucial feature of [Zenhub](https://app.zenhub.com/workspaces/human-connection-nitro-5c0154ecc699f60fc92cf11f) that Github does not have
|
||||||
* "up-for-grabs" links to [Github project](https://github.com/orgs/Human-Connection/projects/10?card_filter_query=label%3A"good+first+issue)
|
* "up-for-grabs" links to [Github project](https://github.com/Human-Connection/Human-Connection/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
|
||||||
* ordering on ZenHub not necessarily reflected on github projects
|
* ordering on ZenHub not necessarily reflected on github projects
|
||||||
* AgileVentures run open pairing sessions at 10:30am UTC each week on Tuesdays and Thursdays
|
* AgileVentures run open pairing sessions at 10:30am UTC each week on Tuesdays and Thursdays
|
||||||
* Core team
|
* Core team
|
||||||
@ -77,4 +77,3 @@ Matt makes point that new stories will have to be taken off the "New Issues" and
|
|||||||
Robert notes that everyone is invited to join the kickoff meetings
|
Robert notes that everyone is invited to join the kickoff meetings
|
||||||
|
|
||||||
Robert - difference between "important" \(creates a lot of value\) and "beginner friendly" \(easy to implement\)
|
Robert - difference between "important" \(creates a lot of value\) and "beginner friendly" \(easy to implement\)
|
||||||
|
|
||||||
|
|||||||
@ -46,7 +46,7 @@ Thank you lokalise for providing us with a premium account :raised_hands:.
|
|||||||
## Developer Chat
|
## Developer Chat
|
||||||
|
|
||||||
Join our friendly open-source community on [Discord](https://discord.gg/6ub73U3) :heart_eyes_cat:
|
Join our friendly open-source community on [Discord](https://discord.gg/6ub73U3) :heart_eyes_cat:
|
||||||
Just introduce yourself at `#user-presentation` and mention `@@Mentor` to get you onboard :neckbeard:
|
Just introduce yourself at `#introduce-yourself` and mention `@@Mentor` to get you onboard :neckbeard:
|
||||||
Check out the [contribution guideline](./CONTRIBUTING.md), too!
|
Check out the [contribution guideline](./CONTRIBUTING.md), too!
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
NEO4J_URI=bolt://localhost:7687
|
NEO4J_URI=bolt://localhost:7687
|
||||||
NEO4J_USER=neo4j
|
NEO4J_USERNAME=neo4j
|
||||||
NEO4J_PASSWORD=letmein
|
NEO4J_PASSWORD=letmein
|
||||||
GRAPHQL_PORT=4000
|
GRAPHQL_PORT=4000
|
||||||
GRAPHQL_URI=http://localhost:4000
|
GRAPHQL_URI=http://localhost:4000
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
FROM node:12.5-alpine as base
|
FROM node:12.6-alpine as base
|
||||||
LABEL Description="Backend of the Social Network Human-Connection.org" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)"
|
LABEL Description="Backend of the Social Network Human-Connection.org" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)"
|
||||||
|
|
||||||
EXPOSE 4000
|
EXPOSE 4000
|
||||||
|
|||||||
@ -42,35 +42,37 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"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.8",
|
||||||
"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",
|
||||||
"faker": "Marak/faker.js#master",
|
"faker": "Marak/faker.js#master",
|
||||||
"graphql": "~14.4.0",
|
"graphql": "~14.4.2",
|
||||||
"graphql-custom-directives": "~0.2.14",
|
"graphql-custom-directives": "~0.2.14",
|
||||||
"graphql-iso-date": "~3.6.1",
|
"graphql-iso-date": "~3.6.1",
|
||||||
"graphql-middleware": "~3.0.2",
|
"graphql-middleware": "~3.0.2",
|
||||||
"graphql-shield": "~6.0.2",
|
"graphql-shield": "~6.0.3",
|
||||||
"graphql-tag": "~2.10.1",
|
"graphql-tag": "~2.10.1",
|
||||||
"graphql-yoga": "~1.18.0",
|
"graphql-yoga": "~1.18.0",
|
||||||
"helmet": "~3.18.0",
|
"helmet": "~3.18.0",
|
||||||
"jsonwebtoken": "~8.5.1",
|
"jsonwebtoken": "~8.5.1",
|
||||||
"linkifyjs": "~2.1.8",
|
"linkifyjs": "~2.1.8",
|
||||||
"lodash": "~4.17.11",
|
"lodash": "~4.17.13",
|
||||||
"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",
|
||||||
@ -82,13 +84,13 @@
|
|||||||
"wait-on": "~3.2.0"
|
"wait-on": "~3.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "~7.4.4",
|
"@babel/cli": "~7.5.0",
|
||||||
"@babel/core": "~7.4.5",
|
"@babel/core": "~7.5.4",
|
||||||
"@babel/node": "~7.4.5",
|
"@babel/node": "~7.5.0",
|
||||||
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
|
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
|
||||||
"@babel/preset-env": "~7.4.5",
|
"@babel/preset-env": "~7.5.4",
|
||||||
"@babel/register": "~7.4.4",
|
"@babel/register": "~7.4.4",
|
||||||
"apollo-server-testing": "~2.6.7",
|
"apollo-server-testing": "~2.6.8",
|
||||||
"babel-core": "~7.0.0-0",
|
"babel-core": "~7.0.0-0",
|
||||||
"babel-eslint": "~10.0.2",
|
"babel-eslint": "~10.0.2",
|
||||||
"babel-jest": "~24.8.0",
|
"babel-jest": "~24.8.0",
|
||||||
@ -98,7 +100,7 @@
|
|||||||
"eslint-config-prettier": "~6.0.0",
|
"eslint-config-prettier": "~6.0.0",
|
||||||
"eslint-config-standard": "~12.0.0",
|
"eslint-config-standard": "~12.0.0",
|
||||||
"eslint-plugin-import": "~2.18.0",
|
"eslint-plugin-import": "~2.18.0",
|
||||||
"eslint-plugin-jest": "~22.7.1",
|
"eslint-plugin-jest": "~22.7.2",
|
||||||
"eslint-plugin-node": "~9.1.0",
|
"eslint-plugin-node": "~9.1.0",
|
||||||
"eslint-plugin-prettier": "~3.1.0",
|
"eslint-plugin-prettier": "~3.1.0",
|
||||||
"eslint-plugin-promise": "~4.2.1",
|
"eslint-plugin-promise": "~4.2.1",
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
9
backend/src/bootstrap/neode.js
Normal file
9
backend/src/bootstrap/neode.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import Neode from 'neode'
|
||||||
|
import models from '../models'
|
||||||
|
|
||||||
|
export default function setupNeode(options) {
|
||||||
|
const { uri, username, password } = options
|
||||||
|
const neodeInstance = new Neode(uri, username, password)
|
||||||
|
neodeInstance.with(models)
|
||||||
|
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
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
import extractMentionedUsers from './notifications/extractMentionedUsers'
|
||||||
|
import extractHashtags from './hashtags/extractHashtags'
|
||||||
|
|
||||||
|
const notify = async (postId, idsOfMentionedUsers, context) => {
|
||||||
|
const session = context.driver.session()
|
||||||
|
const createdAt = new Date().toISOString()
|
||||||
|
const cypher = `
|
||||||
|
match(u:User) where u.id in $idsOfMentionedUsers
|
||||||
|
match(p:Post) where p.id = $postId
|
||||||
|
create(n:Notification{id: apoc.create.uuid(), read: false, createdAt: $createdAt})
|
||||||
|
merge (n)-[:NOTIFIED]->(u)
|
||||||
|
merge (p)-[:NOTIFIED]->(n)
|
||||||
|
`
|
||||||
|
await session.run(cypher, {
|
||||||
|
idsOfMentionedUsers,
|
||||||
|
createdAt,
|
||||||
|
postId,
|
||||||
|
})
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateHashtagsOfPost = async (postId, hashtags, context) => {
|
||||||
|
const session = context.driver.session()
|
||||||
|
// We need two Cypher statements, because the 'MATCH' in the 'cypherDeletePreviousRelations' statement
|
||||||
|
// functions as an 'if'. In case there is no previous relation, the rest of the commands are omitted
|
||||||
|
// and no new Hashtags and relations will be created.
|
||||||
|
const cypherDeletePreviousRelations = `
|
||||||
|
MATCH (p:Post { id: $postId })-[previousRelations:TAGGED]->(t:Tag)
|
||||||
|
DELETE previousRelations
|
||||||
|
RETURN p, t
|
||||||
|
`
|
||||||
|
const cypherCreateNewTagsAndRelations = `
|
||||||
|
MATCH (p:Post { id: $postId})
|
||||||
|
UNWIND $hashtags AS tagName
|
||||||
|
MERGE (t:Tag { id: tagName, name: tagName, disabled: false, deleted: false })
|
||||||
|
MERGE (p)-[:TAGGED]->(t)
|
||||||
|
RETURN p, t
|
||||||
|
`
|
||||||
|
await session.run(cypherDeletePreviousRelations, {
|
||||||
|
postId,
|
||||||
|
})
|
||||||
|
await session.run(cypherCreateNewTagsAndRelations, {
|
||||||
|
postId,
|
||||||
|
hashtags,
|
||||||
|
})
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleContentData = async (resolve, root, args, context, resolveInfo) => {
|
||||||
|
// extract user ids before xss-middleware removes classes via the following "resolve" call
|
||||||
|
const idsOfMentionedUsers = extractMentionedUsers(args.content)
|
||||||
|
// extract tag (hashtag) ids before xss-middleware removes classes via the following "resolve" call
|
||||||
|
const hashtags = extractHashtags(args.content)
|
||||||
|
|
||||||
|
// removes classes from the content
|
||||||
|
const post = await resolve(root, args, context, resolveInfo)
|
||||||
|
|
||||||
|
await notify(post.id, idsOfMentionedUsers, context)
|
||||||
|
await updateHashtagsOfPost(post.id, hashtags, context)
|
||||||
|
|
||||||
|
return post
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Mutation: {
|
||||||
|
CreatePost: handleContentData,
|
||||||
|
UpdatePost: handleContentData,
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -0,0 +1,286 @@
|
|||||||
|
import { GraphQLClient } from 'graphql-request'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import { host, login } from '../../jest/helpers'
|
||||||
|
import Factory from '../../seed/factories'
|
||||||
|
|
||||||
|
const factory = Factory()
|
||||||
|
let client
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await factory.create('User', {
|
||||||
|
id: 'you',
|
||||||
|
name: 'Al Capone',
|
||||||
|
slug: 'al-capone',
|
||||||
|
email: 'test@example.org',
|
||||||
|
password: '1234',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await factory.cleanDatabase()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('currentUser { notifications }', () => {
|
||||||
|
const query = gql`
|
||||||
|
query($read: Boolean) {
|
||||||
|
currentUser {
|
||||||
|
notifications(read: $read, orderBy: createdAt_desc) {
|
||||||
|
read
|
||||||
|
post {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
describe('authenticated', () => {
|
||||||
|
let headers
|
||||||
|
beforeEach(async () => {
|
||||||
|
headers = await login({
|
||||||
|
email: 'test@example.org',
|
||||||
|
password: '1234',
|
||||||
|
})
|
||||||
|
client = new GraphQLClient(host, {
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given another user', () => {
|
||||||
|
let authorClient
|
||||||
|
let authorParams
|
||||||
|
let authorHeaders
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
authorParams = {
|
||||||
|
email: 'author@example.org',
|
||||||
|
password: '1234',
|
||||||
|
id: 'author',
|
||||||
|
}
|
||||||
|
await factory.create('User', authorParams)
|
||||||
|
authorHeaders = await login(authorParams)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('who mentions me in a post', () => {
|
||||||
|
let post
|
||||||
|
const title = 'Mentioning Al Capone'
|
||||||
|
const content =
|
||||||
|
'Hey <a class="mention" href="/profile/you/al-capone">@al-capone</a> how do you do?'
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const createPostMutation = gql`
|
||||||
|
mutation($title: String!, $content: String!) {
|
||||||
|
CreatePost(title: $title, content: $content) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
authorClient = new GraphQLClient(host, {
|
||||||
|
headers: authorHeaders,
|
||||||
|
})
|
||||||
|
const { CreatePost } = await authorClient.request(createPostMutation, {
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
})
|
||||||
|
post = CreatePost
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sends you a notification', async () => {
|
||||||
|
const expectedContent =
|
||||||
|
'Hey <a href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do?'
|
||||||
|
const expected = {
|
||||||
|
currentUser: {
|
||||||
|
notifications: [
|
||||||
|
{
|
||||||
|
read: false,
|
||||||
|
post: {
|
||||||
|
content: expectedContent,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
client.request(query, {
|
||||||
|
read: false,
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('who mentions me again', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
const updatedContent = `${post.content} One more mention to <a href="/profile/you" class="mention">@al-capone</a>`
|
||||||
|
// The response `post.content` contains a link but the XSSmiddleware
|
||||||
|
// should have the `mention` CSS class removed. I discovered this
|
||||||
|
// during development and thought: A feature not a bug! This way we
|
||||||
|
// can encode a re-mentioning of users when you edit your post or
|
||||||
|
// comment.
|
||||||
|
const updatePostMutation = gql`
|
||||||
|
mutation($id: ID!, $title: String!, $content: String!) {
|
||||||
|
UpdatePost(id: $id, content: $content, title: $title) {
|
||||||
|
title
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
authorClient = new GraphQLClient(host, {
|
||||||
|
headers: authorHeaders,
|
||||||
|
})
|
||||||
|
await authorClient.request(updatePostMutation, {
|
||||||
|
id: post.id,
|
||||||
|
title: post.title,
|
||||||
|
content: updatedContent,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates exactly one more notification', async () => {
|
||||||
|
const expectedContent =
|
||||||
|
'Hey <a href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do? One more mention to <a href="/profile/you" target="_blank">@al-capone</a>'
|
||||||
|
const expected = {
|
||||||
|
currentUser: {
|
||||||
|
notifications: [
|
||||||
|
{
|
||||||
|
read: false,
|
||||||
|
post: {
|
||||||
|
content: expectedContent,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
read: false,
|
||||||
|
post: {
|
||||||
|
content: expectedContent,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
client.request(query, {
|
||||||
|
read: false,
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Hashtags', () => {
|
||||||
|
const postId = 'p135'
|
||||||
|
const postTitle = 'Two Hashtags'
|
||||||
|
const postContent =
|
||||||
|
'<p>Hey Dude, <a class="hashtag" href="/search/hashtag/Democracy">#Democracy</a> should work equal for everybody!? That seems to be the only way to have equal <a class="hashtag" href="/search/hashtag/Liberty">#Liberty</a> for everyone.</p>'
|
||||||
|
const postWithHastagsQuery = gql`
|
||||||
|
query($id: ID) {
|
||||||
|
Post(id: $id) {
|
||||||
|
tags {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const postWithHastagsVariables = {
|
||||||
|
id: postId,
|
||||||
|
}
|
||||||
|
const createPostMutation = gql`
|
||||||
|
mutation($postId: ID, $postTitle: String!, $postContent: String!) {
|
||||||
|
CreatePost(id: $postId, title: $postTitle, content: $postContent) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
describe('authenticated', () => {
|
||||||
|
let headers
|
||||||
|
beforeEach(async () => {
|
||||||
|
headers = await login({
|
||||||
|
email: 'test@example.org',
|
||||||
|
password: '1234',
|
||||||
|
})
|
||||||
|
client = new GraphQLClient(host, {
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('create a Post with Hashtags', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await client.request(createPostMutation, {
|
||||||
|
postId,
|
||||||
|
postTitle,
|
||||||
|
postContent,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('both Hashtags are created with the "id" set to thier "name"', async () => {
|
||||||
|
const expected = [
|
||||||
|
{
|
||||||
|
id: 'Democracy',
|
||||||
|
name: 'Democracy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Liberty',
|
||||||
|
name: 'Liberty',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
await expect(
|
||||||
|
client.request(postWithHastagsQuery, postWithHastagsVariables),
|
||||||
|
).resolves.toEqual({
|
||||||
|
Post: [
|
||||||
|
{
|
||||||
|
tags: expect.arrayContaining(expected),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('afterwards update the Post by removing a Hashtag, leaving a Hashtag and add a Hashtag', () => {
|
||||||
|
// The already existing Hashtag has no class at this point.
|
||||||
|
const updatedPostContent =
|
||||||
|
'<p>Hey Dude, <a class="hashtag" href="/search/hashtag/Elections">#Elections</a> should work equal for everybody!? That seems to be the only way to have equal <a href="/search/hashtag/Liberty">#Liberty</a> for everyone.</p>'
|
||||||
|
const updatePostMutation = gql`
|
||||||
|
mutation($postId: ID!, $postTitle: String!, $updatedPostContent: String!) {
|
||||||
|
UpdatePost(id: $postId, title: $postTitle, content: $updatedPostContent) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
it('only one previous Hashtag and the new Hashtag exists', async () => {
|
||||||
|
await client.request(updatePostMutation, {
|
||||||
|
postId,
|
||||||
|
postTitle,
|
||||||
|
updatedPostContent,
|
||||||
|
})
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{
|
||||||
|
id: 'Elections',
|
||||||
|
name: 'Elections',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Liberty',
|
||||||
|
name: 'Liberty',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
await expect(
|
||||||
|
client.request(postWithHastagsQuery, postWithHastagsVariables),
|
||||||
|
).resolves.toEqual({
|
||||||
|
Post: [
|
||||||
|
{
|
||||||
|
tags: expect.arrayContaining(expected),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import cheerio from 'cheerio'
|
||||||
|
// formats of a Hashtag:
|
||||||
|
// https://en.wikipedia.org/w/index.php?title=Hashtag&oldid=905141980#Style
|
||||||
|
// here:
|
||||||
|
// 0. Search for whole string.
|
||||||
|
// 1. Hashtag has only 'a-z', 'A-Z', and '0-9'.
|
||||||
|
// 2. If it starts with a digit '0-9' than 'a-z', or 'A-Z' has to follow.
|
||||||
|
const ID_REGEX = /^\/search\/hashtag\/(([a-zA-Z]+[a-zA-Z0-9]*)|([0-9]+[a-zA-Z]+[a-zA-Z0-9]*))$/g
|
||||||
|
|
||||||
|
export default function(content) {
|
||||||
|
if (!content) return []
|
||||||
|
const $ = cheerio.load(content)
|
||||||
|
// We can not search for class '.hashtag', because the classes are removed at the 'xss' middleware.
|
||||||
|
// But we have to know, which Hashtags are removed from the content es well, so we search for the 'a' html-tag.
|
||||||
|
const urls = $('a')
|
||||||
|
.map((_, el) => {
|
||||||
|
return $(el).attr('href')
|
||||||
|
})
|
||||||
|
.get()
|
||||||
|
const hashtags = []
|
||||||
|
urls.forEach(url => {
|
||||||
|
let match
|
||||||
|
while ((match = ID_REGEX.exec(url)) != null) {
|
||||||
|
hashtags.push(match[1])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return hashtags
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
import extractHashtags from './extractHashtags'
|
||||||
|
|
||||||
|
describe('extractHashtags', () => {
|
||||||
|
describe('content undefined', () => {
|
||||||
|
it('returns empty array', () => {
|
||||||
|
expect(extractHashtags()).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('searches through links', () => {
|
||||||
|
it('finds links with and without ".hashtag" class and extracts Hashtag names', () => {
|
||||||
|
const content =
|
||||||
|
'<p><a class="hashtag" href="/search/hashtag/Elections">#Elections</a><a href="/search/hashtag/Democracy">#Democracy</a></p>'
|
||||||
|
expect(extractHashtags(content)).toEqual(['Elections', 'Democracy'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ignores mentions', () => {
|
||||||
|
const content =
|
||||||
|
'<p>Something inspirational about <a href="/profile/u2" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3/jenny-rostock" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
||||||
|
expect(extractHashtags(content)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('handles links', () => {
|
||||||
|
it('ignores links with domains', () => {
|
||||||
|
const content =
|
||||||
|
'<p><a class="hashtag" href="http://localhost:3000/search/hashtag/Elections">#Elections</a><a href="/search/hashtag/Democracy">#Democracy</a></p>'
|
||||||
|
expect(extractHashtags(content)).toEqual(['Democracy'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ignores Hashtag links with not allowed character combinations', () => {
|
||||||
|
const content =
|
||||||
|
'<p>Something inspirational about <a href="/search/hashtag/AbcDefXyz0123456789!*(),2" class="hashtag" target="_blank">#AbcDefXyz0123456789!*(),2</a>, <a href="/search/hashtag/0123456789" class="hashtag" target="_blank">#0123456789</a>, <a href="/search/hashtag/0123456789a" class="hashtag" target="_blank">#0123456789a</a> and <a href="/search/hashtag/AbcDefXyz0123456789" target="_blank">#AbcDefXyz0123456789</a>.</p>'
|
||||||
|
expect(extractHashtags(content)).toEqual(['0123456789a', 'AbcDefXyz0123456789'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('does not crash if', () => {
|
||||||
|
it('`href` contains no Hashtag name', () => {
|
||||||
|
const content =
|
||||||
|
'<p>Something inspirational about <a href="/search/hashtag/" target="_blank">#Democracy</a> and <a href="/search/hashtag" target="_blank">#liberty</a>.</p>'
|
||||||
|
expect(extractHashtags(content)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('`href` contains Hashtag as page anchor', () => {
|
||||||
|
const content =
|
||||||
|
'<p>Something inspirational about <a href="https://www.example.org/#anchor" target="_blank">#anchor</a>.</p>'
|
||||||
|
expect(extractHashtags(content)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('`href` is empty or invalid', () => {
|
||||||
|
const content =
|
||||||
|
'<p>Something inspirational about <a href="" class="hashtag" target="_blank">@bob-der-baumeister</a> and <a href="not-a-url" target="_blank">@jenny-rostock</a>.</p>'
|
||||||
|
expect(extractHashtags(content)).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import extractIds from '.'
|
import extractMentionedUsers from './extractMentionedUsers'
|
||||||
|
|
||||||
describe('extractIds', () => {
|
describe('extractMentionedUsers', () => {
|
||||||
describe('content undefined', () => {
|
describe('content undefined', () => {
|
||||||
it('returns empty array', () => {
|
it('returns empty array', () => {
|
||||||
expect(extractIds()).toEqual([])
|
expect(extractMentionedUsers()).toEqual([])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -11,33 +11,33 @@ describe('extractIds', () => {
|
|||||||
it('ignores links without .mention class', () => {
|
it('ignores links without .mention class', () => {
|
||||||
const content =
|
const content =
|
||||||
'<p>Something inspirational about <a href="/profile/u2" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3" target="_blank">@jenny-rostock</a>.</p>'
|
'<p>Something inspirational about <a href="/profile/u2" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3" target="_blank">@jenny-rostock</a>.</p>'
|
||||||
expect(extractIds(content)).toEqual([])
|
expect(extractMentionedUsers(content)).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('given a link with .mention class', () => {
|
describe('given a link with .mention class', () => {
|
||||||
it('extracts ids', () => {
|
it('extracts ids', () => {
|
||||||
const content =
|
const content =
|
||||||
'<p>Something inspirational about <a href="/profile/u2" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3/jenny-rostock" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
'<p>Something inspirational about <a href="/profile/u2" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3/jenny-rostock" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
||||||
expect(extractIds(content)).toEqual(['u2', 'u3'])
|
expect(extractMentionedUsers(content)).toEqual(['u2', 'u3'])
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('handles links', () => {
|
describe('handles links', () => {
|
||||||
it('with slug and id', () => {
|
it('with slug and id', () => {
|
||||||
const content =
|
const content =
|
||||||
'<p>Something inspirational about <a href="/profile/u2/bob-der-baumeister" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3/jenny-rostock/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
'<p>Something inspirational about <a href="/profile/u2/bob-der-baumeister" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3/jenny-rostock/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
||||||
expect(extractIds(content)).toEqual(['u2', 'u3'])
|
expect(extractMentionedUsers(content)).toEqual(['u2', 'u3'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('with domains', () => {
|
it('with domains', () => {
|
||||||
const content =
|
const content =
|
||||||
'<p>Something inspirational about <a href="http://localhost:3000/profile/u2/bob-der-baumeister" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="http://localhost:3000//profile/u3/jenny-rostock/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
'<p>Something inspirational about <a href="http://localhost:3000/profile/u2/bob-der-baumeister" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="http://localhost:3000//profile/u3/jenny-rostock/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
||||||
expect(extractIds(content)).toEqual(['u2', 'u3'])
|
expect(extractMentionedUsers(content)).toEqual(['u2', 'u3'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('special characters', () => {
|
it('special characters', () => {
|
||||||
const content =
|
const content =
|
||||||
'<p>Something inspirational about <a href="http://localhost:3000/profile/u!*(),2/bob-der-baumeister" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="http://localhost:3000//profile/u.~-3/jenny-rostock/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
'<p>Something inspirational about <a href="http://localhost:3000/profile/u!*(),2/bob-der-baumeister" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="http://localhost:3000//profile/u.~-3/jenny-rostock/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
||||||
expect(extractIds(content)).toEqual(['u!*(),2', 'u.~-3'])
|
expect(extractMentionedUsers(content)).toEqual(['u!*(),2', 'u.~-3'])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -45,13 +45,13 @@ describe('extractIds', () => {
|
|||||||
it('`href` contains no user id', () => {
|
it('`href` contains no user id', () => {
|
||||||
const content =
|
const content =
|
||||||
'<p>Something inspirational about <a href="/profile" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="/profile/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
'<p>Something inspirational about <a href="/profile" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="/profile/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
||||||
expect(extractIds(content)).toEqual([])
|
expect(extractMentionedUsers(content)).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('`href` is empty or invalid', () => {
|
it('`href` is empty or invalid', () => {
|
||||||
const content =
|
const content =
|
||||||
'<p>Something inspirational about <a href="" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="not-a-url" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
'<p>Something inspirational about <a href="" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="not-a-url" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
||||||
expect(extractIds(content)).toEqual([])
|
expect(extractMentionedUsers(content)).toEqual([])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -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,35 +9,36 @@ 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 handleContentData from './handleHtmlContent/handleContentData'
|
||||||
|
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,
|
||||||
excerpt: excerpt,
|
excerpt: excerpt,
|
||||||
notifications: notifications,
|
handleContentData: handleContentData,
|
||||||
xss: xss,
|
xss: xss,
|
||||||
softDelete: softDelete,
|
softDelete: softDelete,
|
||||||
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',
|
||||||
'notifications',
|
'email',
|
||||||
|
'handleContentData',
|
||||||
'xss',
|
'xss',
|
||||||
'softDelete',
|
'softDelete',
|
||||||
'user',
|
'user',
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
import extractIds from './extractIds'
|
|
||||||
|
|
||||||
const notify = async (resolve, root, args, context, resolveInfo) => {
|
|
||||||
// extract user ids before xss-middleware removes link classes
|
|
||||||
const ids = extractIds(args.content)
|
|
||||||
|
|
||||||
const post = await resolve(root, args, context, resolveInfo)
|
|
||||||
|
|
||||||
const session = context.driver.session()
|
|
||||||
const { id: postId } = post
|
|
||||||
const createdAt = new Date().toISOString()
|
|
||||||
const cypher = `
|
|
||||||
match(u:User) where u.id in $ids
|
|
||||||
match(p:Post) where p.id = $postId
|
|
||||||
create(n:Notification{id: apoc.create.uuid(), read: false, createdAt: $createdAt})
|
|
||||||
merge (n)-[:NOTIFIED]->(u)
|
|
||||||
merge (p)-[:NOTIFIED]->(n)
|
|
||||||
`
|
|
||||||
await session.run(cypher, { ids, createdAt, postId })
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
return post
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
Mutation: {
|
|
||||||
CreatePost: notify,
|
|
||||||
UpdatePost: notify,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@ -1,130 +0,0 @@
|
|||||||
import { GraphQLClient } from 'graphql-request'
|
|
||||||
import { host, login } from '../../jest/helpers'
|
|
||||||
import Factory from '../../seed/factories'
|
|
||||||
|
|
||||||
const factory = Factory()
|
|
||||||
let client
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await factory.create('User', {
|
|
||||||
id: 'you',
|
|
||||||
name: 'Al Capone',
|
|
||||||
slug: 'al-capone',
|
|
||||||
email: 'test@example.org',
|
|
||||||
password: '1234',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await factory.cleanDatabase()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('currentUser { notifications }', () => {
|
|
||||||
const query = `query($read: Boolean) {
|
|
||||||
currentUser {
|
|
||||||
notifications(read: $read, orderBy: createdAt_desc) {
|
|
||||||
read
|
|
||||||
post {
|
|
||||||
content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
|
|
||||||
describe('authenticated', () => {
|
|
||||||
let headers
|
|
||||||
beforeEach(async () => {
|
|
||||||
headers = await login({ email: 'test@example.org', password: '1234' })
|
|
||||||
client = new GraphQLClient(host, { headers })
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('given another user', () => {
|
|
||||||
let authorClient
|
|
||||||
let authorParams
|
|
||||||
let authorHeaders
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
authorParams = {
|
|
||||||
email: 'author@example.org',
|
|
||||||
password: '1234',
|
|
||||||
id: 'author',
|
|
||||||
}
|
|
||||||
await factory.create('User', authorParams)
|
|
||||||
authorHeaders = await login(authorParams)
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('who mentions me in a post', () => {
|
|
||||||
let post
|
|
||||||
const title = 'Mentioning Al Capone'
|
|
||||||
const content =
|
|
||||||
'Hey <a class="mention" href="/profile/you/al-capone">@al-capone</a> how do you do?'
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const createPostMutation = `
|
|
||||||
mutation($title: String!, $content: String!) {
|
|
||||||
CreatePost(title: $title, content: $content) {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
authorClient = new GraphQLClient(host, { headers: authorHeaders })
|
|
||||||
const { CreatePost } = await authorClient.request(createPostMutation, { title, content })
|
|
||||||
post = CreatePost
|
|
||||||
})
|
|
||||||
|
|
||||||
it('sends you a notification', async () => {
|
|
||||||
const expectedContent =
|
|
||||||
'Hey <a href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do?'
|
|
||||||
const expected = {
|
|
||||||
currentUser: {
|
|
||||||
notifications: [{ read: false, post: { content: expectedContent } }],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
await expect(client.request(query, { read: false })).resolves.toEqual(expected)
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('who mentions me again', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
const updatedContent = `${post.content} One more mention to <a href="/profile/you" class="mention">@al-capone</a>`
|
|
||||||
const updatedTitle = 'this post has been updated'
|
|
||||||
// The response `post.content` contains a link but the XSSmiddleware
|
|
||||||
// should have the `mention` CSS class removed. I discovered this
|
|
||||||
// during development and thought: A feature not a bug! This way we
|
|
||||||
// can encode a re-mentioning of users when you edit your post or
|
|
||||||
// comment.
|
|
||||||
const updatePostMutation = `
|
|
||||||
mutation($id: ID!, $title: String!, $content: String!) {
|
|
||||||
UpdatePost(id: $id, title: $title, content: $content) {
|
|
||||||
title
|
|
||||||
content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
authorClient = new GraphQLClient(host, { headers: authorHeaders })
|
|
||||||
await authorClient.request(updatePostMutation, {
|
|
||||||
id: post.id,
|
|
||||||
content: updatedContent,
|
|
||||||
title: updatedTitle,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('creates exactly one more notification', async () => {
|
|
||||||
const expectedContent =
|
|
||||||
'Hey <a href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do? One more mention to <a href="/profile/you" target="_blank">@al-capone</a>'
|
|
||||||
const expected = {
|
|
||||||
currentUser: {
|
|
||||||
notifications: [
|
|
||||||
{ read: false, post: { content: expectedContent } },
|
|
||||||
{ read: false, post: { content: expectedContent } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
await expect(client.request(query, { read: false })).resolves.toEqual(expected)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@ -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(
|
||||||
{
|
{
|
||||||
@ -108,21 +137,24 @@ const permissions = shield(
|
|||||||
'*': deny,
|
'*': deny,
|
||||||
findPosts: allow,
|
findPosts: allow,
|
||||||
Category: allow,
|
Category: allow,
|
||||||
Tag: isAdmin,
|
Tag: allow,
|
||||||
Report: isModerator,
|
Report: isModerator,
|
||||||
Notification: isAdmin,
|
Notification: isAdmin,
|
||||||
statistics: allow,
|
statistics: allow,
|
||||||
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
backend/src/models/EmailAddress.js
Normal file
12
backend/src/models/EmailAddress.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
module.exports = {
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
}
|
||||||
16
backend/src/models/InvitationCode.js
Normal file
16
backend/src/models/InvitationCode.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
}
|
||||||
54
backend/src/models/User.js
Normal file
54
backend/src/models/User.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import uuid from 'uuid/v4'
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
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: { type: 'string', default: 'user' },
|
||||||
|
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(),
|
||||||
|
},
|
||||||
|
}
|
||||||
20
backend/src/models/User.spec.js
Normal file
20
backend/src/models/User.spec.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import Factory from '../seed/factories'
|
||||||
|
import { neode } from '../bootstrap/neo4j'
|
||||||
|
|
||||||
|
const factory = Factory()
|
||||||
|
const instance = neode()
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await factory.cleanDatabase()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('role', () => {
|
||||||
|
it('defaults to `user`', async () => {
|
||||||
|
const user = await instance.create('User', { name: 'John' })
|
||||||
|
await expect(user.toJson()).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
role: 'user',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
7
backend/src/models/index.js
Normal file
7
backend/src/models/index.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// NOTE: We cannot use `fs` here to clean up the code. Cypress breaks on any npm
|
||||||
|
// module that is not browser-compatible. Node's `fs` module is server-side only
|
||||||
|
export default {
|
||||||
|
User: require('./User.js'),
|
||||||
|
InvitationCode: require('./InvitationCode.js'),
|
||||||
|
EmailAddress: require('./EmailAddress.js'),
|
||||||
|
}
|
||||||
@ -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'
|
||||||
@ -33,10 +36,16 @@ const postQueryWithCategories = `
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
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 () => {
|
||||||
@ -66,7 +75,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 })
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -84,7 +93,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
|
||||||
}
|
}
|
||||||
@ -163,14 +172,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,
|
||||||
@ -205,7 +208,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 })
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -219,7 +222,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 })
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -297,14 +300,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',
|
||||||
@ -321,7 +318,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 })
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -333,7 +330,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',
|
||||||
})
|
})
|
||||||
|
|||||||
@ -22,7 +22,6 @@ type Query {
|
|||||||
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!
|
||||||
@ -39,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
|
|
||||||
) {
|
|
||||||
CreateUser(
|
|
||||||
id: $id
|
|
||||||
name: $name
|
|
||||||
slug: $slug
|
|
||||||
password: $password
|
|
||||||
email: $email
|
|
||||||
avatar: $avatar
|
|
||||||
about: $about
|
|
||||||
role: $role
|
|
||||||
) {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
slug
|
|
||||||
email
|
|
||||||
avatar
|
|
||||||
role
|
|
||||||
deleted
|
|
||||||
disabled
|
|
||||||
}
|
}
|
||||||
|
defaults.slug = slugify(defaults.name, { lower: true })
|
||||||
|
args = {
|
||||||
|
...defaults,
|
||||||
|
...args,
|
||||||
}
|
}
|
||||||
`,
|
args = await encryptPassword(args)
|
||||||
variables: { id, name, slug, password, email, avatar, about, role },
|
const user = await neodeInstance.create('User', args)
|
||||||
|
return user.toJson()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,47 +69,144 @@ import Factory from './factories'
|
|||||||
role: 'user',
|
role: 'user',
|
||||||
email: 'user@example.org',
|
email: 'user@example.org',
|
||||||
}),
|
}),
|
||||||
f.create('User', { id: 'u4', name: 'Tick', role: 'user', email: 'tick@example.org' }),
|
f.create('User', {
|
||||||
f.create('User', { id: 'u5', name: 'Trick', role: 'user', email: 'trick@example.org' }),
|
id: 'u4',
|
||||||
f.create('User', { id: 'u6', name: 'Track', role: 'user', email: 'track@example.org' }),
|
name: 'Tick',
|
||||||
f.create('User', { id: 'u7', name: 'Dagobert', role: 'user', email: 'dagobert@example.org' }),
|
role: 'user',
|
||||||
|
email: 'tick@example.org',
|
||||||
|
}),
|
||||||
|
f.create('User', {
|
||||||
|
id: 'u5',
|
||||||
|
name: 'Trick',
|
||||||
|
role: 'user',
|
||||||
|
email: 'trick@example.org',
|
||||||
|
}),
|
||||||
|
f.create('User', {
|
||||||
|
id: 'u6',
|
||||||
|
name: 'Track',
|
||||||
|
role: 'user',
|
||||||
|
email: 'track@example.org',
|
||||||
|
}),
|
||||||
|
f.create('User', {
|
||||||
|
id: 'u7',
|
||||||
|
name: 'Dagobert',
|
||||||
|
role: 'user',
|
||||||
|
email: 'dagobert@example.org',
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
const [asAdmin, asModerator, asUser, asTick, asTrick, asTrack] = await Promise.all([
|
const [asAdmin, asModerator, asUser, asTick, asTrick, asTrack] = await Promise.all([
|
||||||
Factory().authenticateAs({ email: 'admin@example.org', password: '1234' }),
|
Factory().authenticateAs({
|
||||||
Factory().authenticateAs({ email: 'moderator@example.org', password: '1234' }),
|
email: 'admin@example.org',
|
||||||
Factory().authenticateAs({ email: 'user@example.org', password: '1234' }),
|
password: '1234',
|
||||||
Factory().authenticateAs({ email: 'tick@example.org', password: '1234' }),
|
}),
|
||||||
Factory().authenticateAs({ email: 'trick@example.org', password: '1234' }),
|
Factory().authenticateAs({
|
||||||
Factory().authenticateAs({ email: 'track@example.org', password: '1234' }),
|
email: 'moderator@example.org',
|
||||||
|
password: '1234',
|
||||||
|
}),
|
||||||
|
Factory().authenticateAs({
|
||||||
|
email: 'user@example.org',
|
||||||
|
password: '1234',
|
||||||
|
}),
|
||||||
|
Factory().authenticateAs({
|
||||||
|
email: 'tick@example.org',
|
||||||
|
password: '1234',
|
||||||
|
}),
|
||||||
|
Factory().authenticateAs({
|
||||||
|
email: 'trick@example.org',
|
||||||
|
password: '1234',
|
||||||
|
}),
|
||||||
|
Factory().authenticateAs({
|
||||||
|
email: 'track@example.org',
|
||||||
|
password: '1234',
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
f.relate('User', 'Badges', { from: 'b6', to: 'u1' }),
|
f.relate('User', 'Badges', {
|
||||||
f.relate('User', 'Badges', { from: 'b5', to: 'u2' }),
|
from: 'b6',
|
||||||
f.relate('User', 'Badges', { from: 'b4', to: 'u3' }),
|
to: 'u1',
|
||||||
f.relate('User', 'Badges', { from: 'b3', to: 'u4' }),
|
}),
|
||||||
f.relate('User', 'Badges', { from: 'b2', to: 'u5' }),
|
f.relate('User', 'Badges', {
|
||||||
f.relate('User', 'Badges', { from: 'b1', to: 'u6' }),
|
from: 'b5',
|
||||||
f.relate('User', 'Friends', { from: 'u1', to: 'u2' }),
|
to: 'u2',
|
||||||
f.relate('User', 'Friends', { from: 'u1', to: 'u3' }),
|
}),
|
||||||
f.relate('User', 'Friends', { from: 'u2', to: 'u3' }),
|
f.relate('User', 'Badges', {
|
||||||
f.relate('User', 'Blacklisted', { from: 'u7', to: 'u4' }),
|
from: 'b4',
|
||||||
f.relate('User', 'Blacklisted', { from: 'u7', to: 'u5' }),
|
to: 'u3',
|
||||||
f.relate('User', 'Blacklisted', { from: 'u7', to: 'u6' }),
|
}),
|
||||||
|
f.relate('User', 'Badges', {
|
||||||
|
from: 'b3',
|
||||||
|
to: 'u4',
|
||||||
|
}),
|
||||||
|
f.relate('User', 'Badges', {
|
||||||
|
from: 'b2',
|
||||||
|
to: 'u5',
|
||||||
|
}),
|
||||||
|
f.relate('User', 'Badges', {
|
||||||
|
from: 'b1',
|
||||||
|
to: 'u6',
|
||||||
|
}),
|
||||||
|
f.relate('User', 'Friends', {
|
||||||
|
from: 'u1',
|
||||||
|
to: 'u2',
|
||||||
|
}),
|
||||||
|
f.relate('User', 'Friends', {
|
||||||
|
from: 'u1',
|
||||||
|
to: 'u3',
|
||||||
|
}),
|
||||||
|
f.relate('User', 'Friends', {
|
||||||
|
from: 'u2',
|
||||||
|
to: 'u3',
|
||||||
|
}),
|
||||||
|
f.relate('User', 'Blacklisted', {
|
||||||
|
from: 'u7',
|
||||||
|
to: 'u4',
|
||||||
|
}),
|
||||||
|
f.relate('User', 'Blacklisted', {
|
||||||
|
from: 'u7',
|
||||||
|
to: 'u5',
|
||||||
|
}),
|
||||||
|
f.relate('User', 'Blacklisted', {
|
||||||
|
from: 'u7',
|
||||||
|
to: 'u6',
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
asAdmin.follow({ id: 'u3', type: 'User' }),
|
asAdmin.follow({
|
||||||
asModerator.follow({ id: 'u4', type: 'User' }),
|
id: 'u3',
|
||||||
asUser.follow({ id: 'u4', type: 'User' }),
|
type: 'User',
|
||||||
asTick.follow({ id: 'u6', type: 'User' }),
|
}),
|
||||||
asTrick.follow({ id: 'u4', type: 'User' }),
|
asModerator.follow({
|
||||||
asTrack.follow({ id: 'u3', type: 'User' }),
|
id: 'u4',
|
||||||
|
type: 'User',
|
||||||
|
}),
|
||||||
|
asUser.follow({
|
||||||
|
id: 'u4',
|
||||||
|
type: 'User',
|
||||||
|
}),
|
||||||
|
asTick.follow({
|
||||||
|
id: 'u6',
|
||||||
|
type: 'User',
|
||||||
|
}),
|
||||||
|
asTrick.follow({
|
||||||
|
id: 'u4',
|
||||||
|
type: 'User',
|
||||||
|
}),
|
||||||
|
asTrack.follow({
|
||||||
|
id: 'u3',
|
||||||
|
type: 'User',
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
f.create('Category', { id: 'cat1', name: 'Just For Fun', slug: 'justforfun', icon: 'smile' }),
|
f.create('Category', {
|
||||||
|
id: 'cat1',
|
||||||
|
name: 'Just For Fun',
|
||||||
|
slug: 'justforfun',
|
||||||
|
icon: 'smile',
|
||||||
|
}),
|
||||||
f.create('Category', {
|
f.create('Category', {
|
||||||
id: 'cat2',
|
id: 'cat2',
|
||||||
name: 'Happyness & Values',
|
name: 'Happyness & Values',
|
||||||
@ -203,10 +300,22 @@ import Factory from './factories'
|
|||||||
])
|
])
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
f.create('Tag', { id: 't1', name: 'Umwelt' }),
|
f.create('Tag', {
|
||||||
f.create('Tag', { id: 't2', name: 'Naturschutz' }),
|
id: 'Umwelt',
|
||||||
f.create('Tag', { id: 't3', name: 'Demokratie' }),
|
name: 'Umwelt',
|
||||||
f.create('Tag', { id: 't4', name: 'Freiheit' }),
|
}),
|
||||||
|
f.create('Tag', {
|
||||||
|
id: 'Naturschutz',
|
||||||
|
name: 'Naturschutz',
|
||||||
|
}),
|
||||||
|
f.create('Tag', {
|
||||||
|
id: 'Demokratie',
|
||||||
|
name: 'Demokratie',
|
||||||
|
}),
|
||||||
|
f.create('Tag', {
|
||||||
|
id: 'Freiheit',
|
||||||
|
name: 'Freiheit',
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
const mention1 = 'Hey <a class="mention" href="/profile/u3">@jenny-rostock</a>, what\'s up?'
|
const mention1 = 'Hey <a class="mention" href="/profile/u3">@jenny-rostock</a>, what\'s up?'
|
||||||
@ -214,108 +323,347 @@ import Factory from './factories'
|
|||||||
'Hey <a class="mention" href="/profile/u3">@jenny-rostock</a>, here is another notification for you!'
|
'Hey <a class="mention" href="/profile/u3">@jenny-rostock</a>, here is another notification for you!'
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
asAdmin.create('Post', { id: 'p0', image: faker.image.unsplash.food() }),
|
asAdmin.create('Post', {
|
||||||
asModerator.create('Post', { id: 'p1', image: faker.image.unsplash.technology() }),
|
id: 'p0',
|
||||||
asUser.create('Post', { id: 'p2' }),
|
image: faker.image.unsplash.food(),
|
||||||
asTick.create('Post', { id: 'p3' }),
|
}),
|
||||||
asTrick.create('Post', { id: 'p4' }),
|
asModerator.create('Post', {
|
||||||
asTrack.create('Post', { id: 'p5' }),
|
id: 'p1',
|
||||||
asAdmin.create('Post', { id: 'p6', image: faker.image.unsplash.buildings() }),
|
image: faker.image.unsplash.technology(),
|
||||||
asModerator.create('Post', { id: 'p7', content: `${mention1} ${faker.lorem.paragraph()}` }),
|
}),
|
||||||
asUser.create('Post', { id: 'p8', image: faker.image.unsplash.nature() }),
|
asUser.create('Post', {
|
||||||
asTick.create('Post', { id: 'p9' }),
|
id: 'p2',
|
||||||
asTrick.create('Post', { id: 'p10' }),
|
}),
|
||||||
asTrack.create('Post', { id: 'p11', image: faker.image.unsplash.people() }),
|
asTick.create('Post', {
|
||||||
asAdmin.create('Post', { id: 'p12', content: `${mention2} ${faker.lorem.paragraph()}` }),
|
id: 'p3',
|
||||||
asModerator.create('Post', { id: 'p13' }),
|
}),
|
||||||
asUser.create('Post', { id: 'p14', image: faker.image.unsplash.objects() }),
|
asTrick.create('Post', {
|
||||||
asTick.create('Post', { id: 'p15' }),
|
id: 'p4',
|
||||||
|
}),
|
||||||
|
asTrack.create('Post', {
|
||||||
|
id: 'p5',
|
||||||
|
}),
|
||||||
|
asAdmin.create('Post', {
|
||||||
|
id: 'p6',
|
||||||
|
image: faker.image.unsplash.buildings(),
|
||||||
|
}),
|
||||||
|
asModerator.create('Post', {
|
||||||
|
id: 'p7',
|
||||||
|
content: `${mention1} ${faker.lorem.paragraph()}`,
|
||||||
|
}),
|
||||||
|
asUser.create('Post', {
|
||||||
|
id: 'p8',
|
||||||
|
image: faker.image.unsplash.nature(),
|
||||||
|
}),
|
||||||
|
asTick.create('Post', {
|
||||||
|
id: 'p9',
|
||||||
|
}),
|
||||||
|
asTrick.create('Post', {
|
||||||
|
id: 'p10',
|
||||||
|
}),
|
||||||
|
asTrack.create('Post', {
|
||||||
|
id: 'p11',
|
||||||
|
image: faker.image.unsplash.people(),
|
||||||
|
}),
|
||||||
|
asAdmin.create('Post', {
|
||||||
|
id: 'p12',
|
||||||
|
content: `${mention2} ${faker.lorem.paragraph()}`,
|
||||||
|
}),
|
||||||
|
asModerator.create('Post', {
|
||||||
|
id: 'p13',
|
||||||
|
}),
|
||||||
|
asUser.create('Post', {
|
||||||
|
id: 'p14',
|
||||||
|
image: faker.image.unsplash.objects(),
|
||||||
|
}),
|
||||||
|
asTick.create('Post', {
|
||||||
|
id: 'p15',
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
f.relate('Post', 'Categories', { from: 'p0', to: 'cat16' }),
|
f.relate('Post', 'Categories', {
|
||||||
f.relate('Post', 'Categories', { from: 'p1', to: 'cat1' }),
|
from: 'p0',
|
||||||
f.relate('Post', 'Categories', { from: 'p2', to: 'cat2' }),
|
to: 'cat16',
|
||||||
f.relate('Post', 'Categories', { from: 'p3', to: 'cat3' }),
|
}),
|
||||||
f.relate('Post', 'Categories', { from: 'p4', to: 'cat4' }),
|
f.relate('Post', 'Categories', {
|
||||||
f.relate('Post', 'Categories', { from: 'p5', to: 'cat5' }),
|
from: 'p1',
|
||||||
f.relate('Post', 'Categories', { from: 'p6', to: 'cat6' }),
|
to: 'cat1',
|
||||||
f.relate('Post', 'Categories', { from: 'p7', to: 'cat7' }),
|
}),
|
||||||
f.relate('Post', 'Categories', { from: 'p8', to: 'cat8' }),
|
f.relate('Post', 'Categories', {
|
||||||
f.relate('Post', 'Categories', { from: 'p9', to: 'cat9' }),
|
from: 'p2',
|
||||||
f.relate('Post', 'Categories', { from: 'p10', to: 'cat10' }),
|
to: 'cat2',
|
||||||
f.relate('Post', 'Categories', { from: 'p11', to: 'cat11' }),
|
}),
|
||||||
f.relate('Post', 'Categories', { from: 'p12', to: 'cat12' }),
|
f.relate('Post', 'Categories', {
|
||||||
f.relate('Post', 'Categories', { from: 'p13', to: 'cat13' }),
|
from: 'p3',
|
||||||
f.relate('Post', 'Categories', { from: 'p14', to: 'cat14' }),
|
to: 'cat3',
|
||||||
f.relate('Post', 'Categories', { from: 'p15', to: 'cat15' }),
|
}),
|
||||||
|
f.relate('Post', 'Categories', {
|
||||||
|
from: 'p4',
|
||||||
|
to: 'cat4',
|
||||||
|
}),
|
||||||
|
f.relate('Post', 'Categories', {
|
||||||
|
from: 'p5',
|
||||||
|
to: 'cat5',
|
||||||
|
}),
|
||||||
|
f.relate('Post', 'Categories', {
|
||||||
|
from: 'p6',
|
||||||
|
to: 'cat6',
|
||||||
|
}),
|
||||||
|
f.relate('Post', 'Categories', {
|
||||||
|
from: 'p7',
|
||||||
|
to: 'cat7',
|
||||||
|
}),
|
||||||
|
f.relate('Post', 'Categories', {
|
||||||
|
from: 'p8',
|
||||||
|
to: 'cat8',
|
||||||
|
}),
|
||||||
|
f.relate('Post', 'Categories', {
|
||||||
|
from: 'p9',
|
||||||
|
to: 'cat9',
|
||||||
|
}),
|
||||||
|
f.relate('Post', 'Categories', {
|
||||||
|
from: 'p10',
|
||||||
|
to: 'cat10',
|
||||||
|
}),
|
||||||
|
f.relate('Post', 'Categories', {
|
||||||
|
from: 'p11',
|
||||||
|
to: 'cat11',
|
||||||
|
}),
|
||||||
|
f.relate('Post', 'Categories', {
|
||||||
|
from: 'p12',
|
||||||
|
to: 'cat12',
|
||||||
|
}),
|
||||||
|
f.relate('Post', 'Categories', {
|
||||||
|
from: 'p13',
|
||||||
|
to: 'cat13',
|
||||||
|
}),
|
||||||
|
f.relate('Post', 'Categories', {
|
||||||
|
from: 'p14',
|
||||||
|
to: 'cat14',
|
||||||
|
}),
|
||||||
|
f.relate('Post', 'Categories', {
|
||||||
|
from: 'p15',
|
||||||
|
to: 'cat15',
|
||||||
|
}),
|
||||||
|
|
||||||
f.relate('Post', 'Tags', { from: 'p0', to: 't4' }),
|
f.relate('Post', 'Tags', {
|
||||||
f.relate('Post', 'Tags', { from: 'p1', to: 't1' }),
|
from: 'p0',
|
||||||
f.relate('Post', 'Tags', { from: 'p2', to: 't2' }),
|
to: 'Freiheit',
|
||||||
f.relate('Post', 'Tags', { from: 'p3', to: 't3' }),
|
}),
|
||||||
f.relate('Post', 'Tags', { from: 'p4', to: 't4' }),
|
f.relate('Post', 'Tags', {
|
||||||
f.relate('Post', 'Tags', { from: 'p5', to: 't1' }),
|
from: 'p1',
|
||||||
f.relate('Post', 'Tags', { from: 'p6', to: 't2' }),
|
to: 'Umwelt',
|
||||||
f.relate('Post', 'Tags', { from: 'p7', to: 't3' }),
|
}),
|
||||||
f.relate('Post', 'Tags', { from: 'p8', to: 't4' }),
|
f.relate('Post', 'Tags', {
|
||||||
f.relate('Post', 'Tags', { from: 'p9', to: 't1' }),
|
from: 'p2',
|
||||||
f.relate('Post', 'Tags', { from: 'p10', to: 't2' }),
|
to: 'Naturschutz',
|
||||||
f.relate('Post', 'Tags', { from: 'p11', to: 't3' }),
|
}),
|
||||||
f.relate('Post', 'Tags', { from: 'p12', to: 't4' }),
|
f.relate('Post', 'Tags', {
|
||||||
f.relate('Post', 'Tags', { from: 'p13', to: 't1' }),
|
from: 'p3',
|
||||||
f.relate('Post', 'Tags', { from: 'p14', to: 't2' }),
|
to: 'Demokratie',
|
||||||
f.relate('Post', 'Tags', { from: 'p15', to: 't3' }),
|
}),
|
||||||
|
f.relate('Post', 'Tags', {
|
||||||
|
from: 'p4',
|
||||||
|
to: 'Freiheit',
|
||||||
|
}),
|
||||||
|
f.relate('Post', 'Tags', {
|
||||||
|
from: 'p5',
|
||||||
|
to: 'Umwelt',
|
||||||
|
}),
|
||||||
|
f.relate('Post', 'Tags', {
|
||||||
|
from: 'p6',
|
||||||
|
to: 'Naturschutz',
|
||||||
|
}),
|
||||||
|
f.relate('Post', 'Tags', {
|
||||||
|
from: 'p7',
|
||||||
|
to: 'Demokratie',
|
||||||
|
}),
|
||||||
|
f.relate('Post', 'Tags', {
|
||||||
|
from: 'p8',
|
||||||
|
to: 'Freiheit',
|
||||||
|
}),
|
||||||
|
f.relate('Post', 'Tags', {
|
||||||
|
from: 'p9',
|
||||||
|
to: 'Umwelt',
|
||||||
|
}),
|
||||||
|
f.relate('Post', 'Tags', {
|
||||||
|
from: 'p10',
|
||||||
|
to: 'Naturschutz',
|
||||||
|
}),
|
||||||
|
f.relate('Post', 'Tags', {
|
||||||
|
from: 'p11',
|
||||||
|
to: 'Demokratie',
|
||||||
|
}),
|
||||||
|
f.relate('Post', 'Tags', {
|
||||||
|
from: 'p12',
|
||||||
|
to: 'Freiheit',
|
||||||
|
}),
|
||||||
|
f.relate('Post', 'Tags', {
|
||||||
|
from: 'p13',
|
||||||
|
to: 'Umwelt',
|
||||||
|
}),
|
||||||
|
f.relate('Post', 'Tags', {
|
||||||
|
from: 'p14',
|
||||||
|
to: 'Naturschutz',
|
||||||
|
}),
|
||||||
|
f.relate('Post', 'Tags', {
|
||||||
|
from: 'p15',
|
||||||
|
to: 'Demokratie',
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
asAdmin.shout({ id: 'p2', type: 'Post' }),
|
asAdmin.shout({
|
||||||
asAdmin.shout({ id: 'p6', type: 'Post' }),
|
id: 'p2',
|
||||||
asModerator.shout({ id: 'p0', type: 'Post' }),
|
type: 'Post',
|
||||||
asModerator.shout({ id: 'p6', type: 'Post' }),
|
}),
|
||||||
asUser.shout({ id: 'p6', type: 'Post' }),
|
asAdmin.shout({
|
||||||
asUser.shout({ id: 'p7', type: 'Post' }),
|
id: 'p6',
|
||||||
asTick.shout({ id: 'p8', type: 'Post' }),
|
type: 'Post',
|
||||||
asTick.shout({ id: 'p9', type: 'Post' }),
|
}),
|
||||||
asTrack.shout({ id: 'p10', type: 'Post' }),
|
asModerator.shout({
|
||||||
|
id: 'p0',
|
||||||
|
type: 'Post',
|
||||||
|
}),
|
||||||
|
asModerator.shout({
|
||||||
|
id: 'p6',
|
||||||
|
type: 'Post',
|
||||||
|
}),
|
||||||
|
asUser.shout({
|
||||||
|
id: 'p6',
|
||||||
|
type: 'Post',
|
||||||
|
}),
|
||||||
|
asUser.shout({
|
||||||
|
id: 'p7',
|
||||||
|
type: 'Post',
|
||||||
|
}),
|
||||||
|
asTick.shout({
|
||||||
|
id: 'p8',
|
||||||
|
type: 'Post',
|
||||||
|
}),
|
||||||
|
asTick.shout({
|
||||||
|
id: 'p9',
|
||||||
|
type: 'Post',
|
||||||
|
}),
|
||||||
|
asTrack.shout({
|
||||||
|
id: 'p10',
|
||||||
|
type: 'Post',
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
asAdmin.shout({ id: 'p2', type: 'Post' }),
|
asAdmin.shout({
|
||||||
asAdmin.shout({ id: 'p6', type: 'Post' }),
|
id: 'p2',
|
||||||
asModerator.shout({ id: 'p0', type: 'Post' }),
|
type: 'Post',
|
||||||
asModerator.shout({ id: 'p6', type: 'Post' }),
|
}),
|
||||||
asUser.shout({ id: 'p6', type: 'Post' }),
|
asAdmin.shout({
|
||||||
asUser.shout({ id: 'p7', type: 'Post' }),
|
id: 'p6',
|
||||||
asTick.shout({ id: 'p8', type: 'Post' }),
|
type: 'Post',
|
||||||
asTick.shout({ id: 'p9', type: 'Post' }),
|
}),
|
||||||
asTrack.shout({ id: 'p10', type: 'Post' }),
|
asModerator.shout({
|
||||||
|
id: 'p0',
|
||||||
|
type: 'Post',
|
||||||
|
}),
|
||||||
|
asModerator.shout({
|
||||||
|
id: 'p6',
|
||||||
|
type: 'Post',
|
||||||
|
}),
|
||||||
|
asUser.shout({
|
||||||
|
id: 'p6',
|
||||||
|
type: 'Post',
|
||||||
|
}),
|
||||||
|
asUser.shout({
|
||||||
|
id: 'p7',
|
||||||
|
type: 'Post',
|
||||||
|
}),
|
||||||
|
asTick.shout({
|
||||||
|
id: 'p8',
|
||||||
|
type: 'Post',
|
||||||
|
}),
|
||||||
|
asTick.shout({
|
||||||
|
id: 'p9',
|
||||||
|
type: 'Post',
|
||||||
|
}),
|
||||||
|
asTrack.shout({
|
||||||
|
id: 'p10',
|
||||||
|
type: 'Post',
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
asUser.create('Comment', { id: 'c1', postId: 'p1' }),
|
asUser.create('Comment', {
|
||||||
asTick.create('Comment', { id: 'c2', postId: 'p1' }),
|
id: 'c1',
|
||||||
asTrack.create('Comment', { id: 'c3', postId: 'p3' }),
|
postId: 'p1',
|
||||||
asTrick.create('Comment', { id: 'c4', postId: 'p2' }),
|
}),
|
||||||
asModerator.create('Comment', { id: 'c5', postId: 'p3' }),
|
asTick.create('Comment', {
|
||||||
asAdmin.create('Comment', { id: 'c6', postId: 'p4' }),
|
id: 'c2',
|
||||||
asUser.create('Comment', { id: 'c7', postId: 'p2' }),
|
postId: 'p1',
|
||||||
asTick.create('Comment', { id: 'c8', postId: 'p15' }),
|
}),
|
||||||
asTrick.create('Comment', { id: 'c9', postId: 'p15' }),
|
asTrack.create('Comment', {
|
||||||
asTrack.create('Comment', { id: 'c10', postId: 'p15' }),
|
id: 'c3',
|
||||||
asUser.create('Comment', { id: 'c11', postId: 'p15' }),
|
postId: 'p3',
|
||||||
asUser.create('Comment', { id: 'c12', postId: 'p15' }),
|
}),
|
||||||
|
asTrick.create('Comment', {
|
||||||
|
id: 'c4',
|
||||||
|
postId: 'p2',
|
||||||
|
}),
|
||||||
|
asModerator.create('Comment', {
|
||||||
|
id: 'c5',
|
||||||
|
postId: 'p3',
|
||||||
|
}),
|
||||||
|
asAdmin.create('Comment', {
|
||||||
|
id: 'c6',
|
||||||
|
postId: 'p4',
|
||||||
|
}),
|
||||||
|
asUser.create('Comment', {
|
||||||
|
id: 'c7',
|
||||||
|
postId: 'p2',
|
||||||
|
}),
|
||||||
|
asTick.create('Comment', {
|
||||||
|
id: 'c8',
|
||||||
|
postId: 'p15',
|
||||||
|
}),
|
||||||
|
asTrick.create('Comment', {
|
||||||
|
id: 'c9',
|
||||||
|
postId: 'p15',
|
||||||
|
}),
|
||||||
|
asTrack.create('Comment', {
|
||||||
|
id: 'c10',
|
||||||
|
postId: 'p15',
|
||||||
|
}),
|
||||||
|
asUser.create('Comment', {
|
||||||
|
id: 'c11',
|
||||||
|
postId: 'p15',
|
||||||
|
}),
|
||||||
|
asUser.create('Comment', {
|
||||||
|
id: 'c12',
|
||||||
|
postId: 'p15',
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
const disableMutation = 'mutation($id: ID!) { disable(id: $id) }'
|
const disableMutation = 'mutation($id: ID!) { disable(id: $id) }'
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
asModerator.mutate(disableMutation, { id: 'p11' }),
|
asModerator.mutate(disableMutation, {
|
||||||
asModerator.mutate(disableMutation, { id: 'c5' }),
|
id: 'p11',
|
||||||
|
}),
|
||||||
|
asModerator.mutate(disableMutation, {
|
||||||
|
id: 'c5',
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
asTick.create('Report', { description: "I don't like this comment", id: 'c1' }),
|
asTick.create('Report', {
|
||||||
asTrick.create('Report', { description: "I don't like this post", id: 'p1' }),
|
description: "I don't like this comment",
|
||||||
asTrack.create('Report', { description: "I don't like this user", id: 'u1' }),
|
id: 'c1',
|
||||||
|
}),
|
||||||
|
asTrick.create('Report', {
|
||||||
|
description: "I don't like this post",
|
||||||
|
id: 'p1',
|
||||||
|
}),
|
||||||
|
asTrack.create('Report', {
|
||||||
|
description: "I don't like this user",
|
||||||
|
id: 'u1',
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@ -342,13 +690,26 @@ import Factory from './factories'
|
|||||||
])
|
])
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
f.relate('Organization', 'CreatedBy', { from: 'u1', to: 'o1' }),
|
f.relate('Organization', 'CreatedBy', {
|
||||||
f.relate('Organization', 'CreatedBy', { from: 'u1', to: 'o2' }),
|
from: 'u1',
|
||||||
f.relate('Organization', 'OwnedBy', { from: 'u2', to: 'o2' }),
|
to: 'o1',
|
||||||
f.relate('Organization', 'OwnedBy', { from: 'u2', to: 'o3' }),
|
}),
|
||||||
|
f.relate('Organization', 'CreatedBy', {
|
||||||
|
from: 'u1',
|
||||||
|
to: 'o2',
|
||||||
|
}),
|
||||||
|
f.relate('Organization', 'OwnedBy', {
|
||||||
|
from: 'u2',
|
||||||
|
to: 'o2',
|
||||||
|
}),
|
||||||
|
f.relate('Organization', 'OwnedBy', {
|
||||||
|
from: 'u2',
|
||||||
|
to: 'o3',
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
/* 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)
|
||||||
|
|||||||
@ -14,10 +14,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.20.tgz#bf9f2acdf319c0959fad8ec1239741dd2ead4e8d"
|
resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.20.tgz#bf9f2acdf319c0959fad8ec1239741dd2ead4e8d"
|
||||||
integrity sha512-3LWZa80HcP70Pl+H4KhLDJ7S0px+9/c8GTXdl6SpunRecUaB27g/oOQnAjNHLHdbWuGE0uyqcuGiTfbKB3ilaQ==
|
integrity sha512-3LWZa80HcP70Pl+H4KhLDJ7S0px+9/c8GTXdl6SpunRecUaB27g/oOQnAjNHLHdbWuGE0uyqcuGiTfbKB3ilaQ==
|
||||||
|
|
||||||
"@babel/cli@~7.4.4":
|
"@babel/cli@~7.5.0":
|
||||||
version "7.4.4"
|
version "7.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.4.4.tgz#5454bb7112f29026a4069d8e6f0e1794e651966c"
|
resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.5.0.tgz#f403c930692e28ecfa3bf02a9e7562b474f38271"
|
||||||
integrity sha512-XGr5YjQSjgTa6OzQZY57FAJsdeVSAKR/u/KA5exWIz66IKtv/zXtHy+fIZcMry/EgYegwuHE7vzGnrFhjdIAsQ==
|
integrity sha512-qNH55fWbKrEsCwID+Qc/3JDPnsSGpIIiMDbppnR8Z6PxLAqMQCFNqBctkIkBrMH49Nx+qqVTrHRWUR+ho2k+qQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
commander "^2.8.1"
|
commander "^2.8.1"
|
||||||
convert-source-map "^1.1.0"
|
convert-source-map "^1.1.0"
|
||||||
@ -38,18 +38,18 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/highlight" "^7.0.0"
|
"@babel/highlight" "^7.0.0"
|
||||||
|
|
||||||
"@babel/core@^7.1.0", "@babel/core@~7.4.5":
|
"@babel/core@^7.1.0", "@babel/core@~7.5.4":
|
||||||
version "7.4.5"
|
version "7.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.5.tgz#081f97e8ffca65a9b4b0fdc7e274e703f000c06a"
|
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.4.tgz#4c32df7ad5a58e9ea27ad025c11276324e0b4ddd"
|
||||||
integrity sha512-OvjIh6aqXtlsA8ujtGKfC7LYWksYSX8yQcM8Ay3LuvVeQ63lcOKgoZWVqcpFwkd29aYU9rVx7jxhfhiEDV9MZA==
|
integrity sha512-+DaeBEpYq6b2+ZmHx3tHspC+ZRflrvLqwfv8E3hNr5LVQoyBnL8RPKSBCg+rK2W2My9PWlujBiqd0ZPsR9Q6zQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/code-frame" "^7.0.0"
|
"@babel/code-frame" "^7.0.0"
|
||||||
"@babel/generator" "^7.4.4"
|
"@babel/generator" "^7.5.0"
|
||||||
"@babel/helpers" "^7.4.4"
|
"@babel/helpers" "^7.5.4"
|
||||||
"@babel/parser" "^7.4.5"
|
"@babel/parser" "^7.5.0"
|
||||||
"@babel/template" "^7.4.4"
|
"@babel/template" "^7.4.4"
|
||||||
"@babel/traverse" "^7.4.5"
|
"@babel/traverse" "^7.5.0"
|
||||||
"@babel/types" "^7.4.4"
|
"@babel/types" "^7.5.0"
|
||||||
convert-source-map "^1.1.0"
|
convert-source-map "^1.1.0"
|
||||||
debug "^4.1.0"
|
debug "^4.1.0"
|
||||||
json5 "^2.1.0"
|
json5 "^2.1.0"
|
||||||
@ -58,12 +58,12 @@
|
|||||||
semver "^5.4.1"
|
semver "^5.4.1"
|
||||||
source-map "^0.5.0"
|
source-map "^0.5.0"
|
||||||
|
|
||||||
"@babel/generator@^7.0.0", "@babel/generator@^7.4.4":
|
"@babel/generator@^7.0.0", "@babel/generator@^7.5.0":
|
||||||
version "7.4.4"
|
version "7.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.4.4.tgz#174a215eb843fc392c7edcaabeaa873de6e8f041"
|
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.5.0.tgz#f20e4b7a91750ee8b63656073d843d2a736dca4a"
|
||||||
integrity sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ==
|
integrity sha512-1TTVrt7J9rcG5PMjvO7VEG3FrEoEJNHxumRq66GemPmzboLWtIjjcJgk8rokuAS7IiRSpgVSu5Vb9lc99iJkOA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.4.4"
|
"@babel/types" "^7.5.0"
|
||||||
jsesc "^2.5.1"
|
jsesc "^2.5.1"
|
||||||
lodash "^4.17.11"
|
lodash "^4.17.11"
|
||||||
source-map "^0.5.0"
|
source-map "^0.5.0"
|
||||||
@ -260,14 +260,14 @@
|
|||||||
"@babel/traverse" "^7.1.0"
|
"@babel/traverse" "^7.1.0"
|
||||||
"@babel/types" "^7.2.0"
|
"@babel/types" "^7.2.0"
|
||||||
|
|
||||||
"@babel/helpers@^7.4.4":
|
"@babel/helpers@^7.5.4":
|
||||||
version "7.4.4"
|
version "7.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.4.4.tgz#868b0ef59c1dd4e78744562d5ce1b59c89f2f2a5"
|
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.5.4.tgz#2f00608aa10d460bde0ccf665d6dcf8477357cf0"
|
||||||
integrity sha512-igczbR/0SeuPR8RFfC7tGrbdTbFL3QTvH6D+Z6zNxnTe//GyqmtHmDkzrqDmyZ3eSwPqB/LhyKoU5DXsp+Vp2A==
|
integrity sha512-6LJ6xwUEJP51w0sIgKyfvFMJvIb9mWAfohJp0+m6eHJigkFdcH8duZ1sfhn0ltJRzwUIT/yqqhdSfRpCpL7oow==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/template" "^7.4.4"
|
"@babel/template" "^7.4.4"
|
||||||
"@babel/traverse" "^7.4.4"
|
"@babel/traverse" "^7.5.0"
|
||||||
"@babel/types" "^7.4.4"
|
"@babel/types" "^7.5.0"
|
||||||
|
|
||||||
"@babel/highlight@^7.0.0":
|
"@babel/highlight@^7.0.0":
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
@ -278,10 +278,10 @@
|
|||||||
esutils "^2.0.2"
|
esutils "^2.0.2"
|
||||||
js-tokens "^4.0.0"
|
js-tokens "^4.0.0"
|
||||||
|
|
||||||
"@babel/node@~7.4.5":
|
"@babel/node@~7.5.0":
|
||||||
version "7.4.5"
|
version "7.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/node/-/node-7.4.5.tgz#bce71bb44d902bfdd4da0b9c839a8a90fc084056"
|
resolved "https://registry.yarnpkg.com/@babel/node/-/node-7.5.0.tgz#bcc5a286317ad771703889fb658e1f768c0a2a2e"
|
||||||
integrity sha512-nDXPT0KwYMycDHhFG9wKlkipCR+iXzzoX9bD2aF2UABLhQ13AKhNi5Y61W8ASGPPll/7p9GrHesmlOgTUJVcfw==
|
integrity sha512-VBlCrbJp7HDrKt4HRbtfq4Rs/XjBokvkfxXRQs4qA1C6eV3JycSOMELx4BFTPFRd9QnNA4PsIRfnvJqe/3tHow==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/polyfill" "^7.0.0"
|
"@babel/polyfill" "^7.0.0"
|
||||||
"@babel/register" "^7.0.0"
|
"@babel/register" "^7.0.0"
|
||||||
@ -290,10 +290,10 @@
|
|||||||
node-environment-flags "^1.0.5"
|
node-environment-flags "^1.0.5"
|
||||||
v8flags "^3.1.1"
|
v8flags "^3.1.1"
|
||||||
|
|
||||||
"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.4.4", "@babel/parser@^7.4.5":
|
"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.4.4", "@babel/parser@^7.5.0":
|
||||||
version "7.4.5"
|
version "7.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.5.tgz#04af8d5d5a2b044a2a1bffacc1e5e6673544e872"
|
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.0.tgz#3e0713dff89ad6ae37faec3b29dcfc5c979770b7"
|
||||||
integrity sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew==
|
integrity sha512-I5nW8AhGpOXGCCNYGc+p7ExQIBxRFnS2fd/d862bNOKvmoEPjYPcfIjsfdy0ujagYOIYPczKgD9l3FsgTkAzKA==
|
||||||
|
|
||||||
"@babel/plugin-proposal-async-generator-functions@^7.2.0":
|
"@babel/plugin-proposal-async-generator-functions@^7.2.0":
|
||||||
version "7.2.0"
|
version "7.2.0"
|
||||||
@ -304,6 +304,14 @@
|
|||||||
"@babel/helper-remap-async-to-generator" "^7.1.0"
|
"@babel/helper-remap-async-to-generator" "^7.1.0"
|
||||||
"@babel/plugin-syntax-async-generators" "^7.2.0"
|
"@babel/plugin-syntax-async-generators" "^7.2.0"
|
||||||
|
|
||||||
|
"@babel/plugin-proposal-dynamic-import@^7.5.0":
|
||||||
|
version "7.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz#e532202db4838723691b10a67b8ce509e397c506"
|
||||||
|
integrity sha512-x/iMjggsKTFHYC6g11PL7Qy58IK8H5zqfm9e6hu4z1iH2IRyAp9u9dL80zA6R76yFovETFLKz2VJIC2iIPBuFw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
|
"@babel/plugin-syntax-dynamic-import" "^7.2.0"
|
||||||
|
|
||||||
"@babel/plugin-proposal-json-strings@^7.2.0":
|
"@babel/plugin-proposal-json-strings@^7.2.0":
|
||||||
version "7.2.0"
|
version "7.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317"
|
||||||
@ -312,10 +320,10 @@
|
|||||||
"@babel/helper-plugin-utils" "^7.0.0"
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
"@babel/plugin-syntax-json-strings" "^7.2.0"
|
"@babel/plugin-syntax-json-strings" "^7.2.0"
|
||||||
|
|
||||||
"@babel/plugin-proposal-object-rest-spread@^7.4.4":
|
"@babel/plugin-proposal-object-rest-spread@^7.5.4":
|
||||||
version "7.4.4"
|
version "7.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.4.4.tgz#1ef173fcf24b3e2df92a678f027673b55e7e3005"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.5.4.tgz#250de35d867ce8260a31b1fdac6c4fc1baa99331"
|
||||||
integrity sha512-dMBG6cSPBbHeEBdFXeQ2QLc5gUpg4Vkaz8octD4aoW/ISO+jBOcsuxYL7bsb5WSu8RLP6boxrBIALEHgoHtO9g==
|
integrity sha512-KCx0z3y7y8ipZUMAEEJOyNi11lMb/FOPUjjB113tfowgw0c16EGYos7worCKBcUAh2oG+OBnoUhsnTSoLpV9uA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-plugin-utils" "^7.0.0"
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
"@babel/plugin-syntax-object-rest-spread" "^7.2.0"
|
"@babel/plugin-syntax-object-rest-spread" "^7.2.0"
|
||||||
@ -352,6 +360,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-plugin-utils" "^7.0.0"
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
|
|
||||||
|
"@babel/plugin-syntax-dynamic-import@^7.2.0":
|
||||||
|
version "7.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz#69c159ffaf4998122161ad8ebc5e6d1f55df8612"
|
||||||
|
integrity sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
|
|
||||||
"@babel/plugin-syntax-json-strings@^7.2.0":
|
"@babel/plugin-syntax-json-strings@^7.2.0":
|
||||||
version "7.2.0"
|
version "7.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz#72bd13f6ffe1d25938129d2a186b11fd62951470"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz#72bd13f6ffe1d25938129d2a186b11fd62951470"
|
||||||
@ -387,10 +402,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-plugin-utils" "^7.0.0"
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
|
|
||||||
"@babel/plugin-transform-async-to-generator@^7.4.4":
|
"@babel/plugin-transform-async-to-generator@^7.5.0":
|
||||||
version "7.4.4"
|
version "7.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.4.4.tgz#a3f1d01f2f21cadab20b33a82133116f14fb5894"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.5.0.tgz#89a3848a0166623b5bc481164b5936ab947e887e"
|
||||||
integrity sha512-YiqW2Li8TXmzgbXw+STsSqPBPFnGviiaSp6CYOq55X8GQ2SGVLrXB6pNid8HkqkZAzOH6knbai3snhP7v0fNwA==
|
integrity sha512-mqvkzwIGkq0bEF1zLRRiTdjfomZJDV33AH3oQzHVGkI2VzEmXLpKKOBvEVaFZBJdN0XTyH38s9j/Kiqr68dggg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-module-imports" "^7.0.0"
|
"@babel/helper-module-imports" "^7.0.0"
|
||||||
"@babel/helper-plugin-utils" "^7.0.0"
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
@ -432,10 +447,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-plugin-utils" "^7.0.0"
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
|
|
||||||
"@babel/plugin-transform-destructuring@^7.4.4":
|
"@babel/plugin-transform-destructuring@^7.5.0":
|
||||||
version "7.4.4"
|
version "7.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.4.4.tgz#9d964717829cc9e4b601fc82a26a71a4d8faf20f"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.5.0.tgz#f6c09fdfe3f94516ff074fe877db7bc9ef05855a"
|
||||||
integrity sha512-/aOx+nW0w8eHiEHm+BTERB2oJn5D127iye/SUQl7NjHy0lf+j7h4MKMMSOwdazGq9OxgiNADncE+SRJkCxjZpQ==
|
integrity sha512-YbYgbd3TryYYLGyC7ZR+Tq8H/+bCmwoaxHfJHupom5ECstzbRLTch6gOQbhEY9Z4hiCNHEURgq06ykFv9JZ/QQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-plugin-utils" "^7.0.0"
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
|
|
||||||
@ -448,10 +463,10 @@
|
|||||||
"@babel/helper-regex" "^7.4.4"
|
"@babel/helper-regex" "^7.4.4"
|
||||||
regexpu-core "^4.5.4"
|
regexpu-core "^4.5.4"
|
||||||
|
|
||||||
"@babel/plugin-transform-duplicate-keys@^7.2.0":
|
"@babel/plugin-transform-duplicate-keys@^7.5.0":
|
||||||
version "7.2.0"
|
version "7.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.2.0.tgz#d952c4930f312a4dbfff18f0b2914e60c35530b3"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.5.0.tgz#c5dbf5106bf84cdf691222c0974c12b1df931853"
|
||||||
integrity sha512-q+yuxW4DsTjNceUiTzK0L+AfQ0zD9rWaTLiUqHA8p0gxx7lu1EylenfzjeIWNkPy6e/0VG/Wjw9uf9LueQwLOw==
|
integrity sha512-igcziksHizyQPlX9gfSjHkE2wmoCH3evvD2qR5w29/Dk0SMKE/eOI7f1HhBdNhR/zxJDqrgpoDTq5YSLH/XMsQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-plugin-utils" "^7.0.0"
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
|
|
||||||
@ -492,30 +507,33 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-plugin-utils" "^7.0.0"
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
|
|
||||||
"@babel/plugin-transform-modules-amd@^7.2.0":
|
"@babel/plugin-transform-modules-amd@^7.5.0":
|
||||||
version "7.2.0"
|
version "7.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.2.0.tgz#82a9bce45b95441f617a24011dc89d12da7f4ee6"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.5.0.tgz#ef00435d46da0a5961aa728a1d2ecff063e4fb91"
|
||||||
integrity sha512-mK2A8ucqz1qhrdqjS9VMIDfIvvT2thrEsIQzbaTdc5QFzhDjQv2CkJJ5f6BXIkgbmaoax3zBr2RyvV/8zeoUZw==
|
integrity sha512-n20UsQMKnWrltocZZm24cRURxQnWIvsABPJlw/fvoy9c6AgHZzoelAIzajDHAQrDpuKFFPPcFGd7ChsYuIUMpg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-module-transforms" "^7.1.0"
|
"@babel/helper-module-transforms" "^7.1.0"
|
||||||
"@babel/helper-plugin-utils" "^7.0.0"
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
|
babel-plugin-dynamic-import-node "^2.3.0"
|
||||||
|
|
||||||
"@babel/plugin-transform-modules-commonjs@^7.4.4":
|
"@babel/plugin-transform-modules-commonjs@^7.5.0":
|
||||||
version "7.4.4"
|
version "7.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.4.4.tgz#0bef4713d30f1d78c2e59b3d6db40e60192cac1e"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.5.0.tgz#425127e6045231360858eeaa47a71d75eded7a74"
|
||||||
integrity sha512-4sfBOJt58sEo9a2BQXnZq+Q3ZTSAUXyK3E30o36BOGnJ+tvJ6YSxF0PG6kERvbeISgProodWuI9UVG3/FMY6iw==
|
integrity sha512-xmHq0B+ytyrWJvQTc5OWAC4ii6Dhr0s22STOoydokG51JjWhyYo5mRPXoi+ZmtHQhZZwuXNN+GG5jy5UZZJxIQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-module-transforms" "^7.4.4"
|
"@babel/helper-module-transforms" "^7.4.4"
|
||||||
"@babel/helper-plugin-utils" "^7.0.0"
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
"@babel/helper-simple-access" "^7.1.0"
|
"@babel/helper-simple-access" "^7.1.0"
|
||||||
|
babel-plugin-dynamic-import-node "^2.3.0"
|
||||||
|
|
||||||
"@babel/plugin-transform-modules-systemjs@^7.4.4":
|
"@babel/plugin-transform-modules-systemjs@^7.5.0":
|
||||||
version "7.4.4"
|
version "7.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.4.4.tgz#dc83c5665b07d6c2a7b224c00ac63659ea36a405"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.5.0.tgz#e75266a13ef94202db2a0620977756f51d52d249"
|
||||||
integrity sha512-MSiModfILQc3/oqnG7NrP1jHaSPryO6tA2kOMmAQApz5dayPxWiHqmq4sWH2xF5LcQK56LlbKByCd8Aah/OIkQ==
|
integrity sha512-Q2m56tyoQWmuNGxEtUyeEkm6qJYFqs4c+XyXH5RAuYxObRNz9Zgj/1g2GMnjYp2EUyEy7YTrxliGCXzecl/vJg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-hoist-variables" "^7.4.4"
|
"@babel/helper-hoist-variables" "^7.4.4"
|
||||||
"@babel/helper-plugin-utils" "^7.0.0"
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
|
babel-plugin-dynamic-import-node "^2.3.0"
|
||||||
|
|
||||||
"@babel/plugin-transform-modules-umd@^7.2.0":
|
"@babel/plugin-transform-modules-umd@^7.2.0":
|
||||||
version "7.2.0"
|
version "7.2.0"
|
||||||
@ -631,39 +649,41 @@
|
|||||||
core-js "^2.5.7"
|
core-js "^2.5.7"
|
||||||
regenerator-runtime "^0.12.0"
|
regenerator-runtime "^0.12.0"
|
||||||
|
|
||||||
"@babel/preset-env@~7.4.5":
|
"@babel/preset-env@~7.5.4":
|
||||||
version "7.4.5"
|
version "7.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.4.5.tgz#2fad7f62983d5af563b5f3139242755884998a58"
|
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.5.4.tgz#64bc15041a3cbb0798930319917e70fcca57713d"
|
||||||
integrity sha512-f2yNVXM+FsR5V8UwcFeIHzHWgnhXg3NpRmy0ADvALpnhB0SLbCvrCRr4BLOUYbQNLS+Z0Yer46x9dJXpXewI7w==
|
integrity sha512-hFnFnouyRNiH1rL8YkX1ANCNAUVC8Djwdqfev8i1415tnAG+7hlA5zhZ0Q/3Q5gkop4HioIPbCEWAalqcbxRoQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-module-imports" "^7.0.0"
|
"@babel/helper-module-imports" "^7.0.0"
|
||||||
"@babel/helper-plugin-utils" "^7.0.0"
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
"@babel/plugin-proposal-async-generator-functions" "^7.2.0"
|
"@babel/plugin-proposal-async-generator-functions" "^7.2.0"
|
||||||
|
"@babel/plugin-proposal-dynamic-import" "^7.5.0"
|
||||||
"@babel/plugin-proposal-json-strings" "^7.2.0"
|
"@babel/plugin-proposal-json-strings" "^7.2.0"
|
||||||
"@babel/plugin-proposal-object-rest-spread" "^7.4.4"
|
"@babel/plugin-proposal-object-rest-spread" "^7.5.4"
|
||||||
"@babel/plugin-proposal-optional-catch-binding" "^7.2.0"
|
"@babel/plugin-proposal-optional-catch-binding" "^7.2.0"
|
||||||
"@babel/plugin-proposal-unicode-property-regex" "^7.4.4"
|
"@babel/plugin-proposal-unicode-property-regex" "^7.4.4"
|
||||||
"@babel/plugin-syntax-async-generators" "^7.2.0"
|
"@babel/plugin-syntax-async-generators" "^7.2.0"
|
||||||
|
"@babel/plugin-syntax-dynamic-import" "^7.2.0"
|
||||||
"@babel/plugin-syntax-json-strings" "^7.2.0"
|
"@babel/plugin-syntax-json-strings" "^7.2.0"
|
||||||
"@babel/plugin-syntax-object-rest-spread" "^7.2.0"
|
"@babel/plugin-syntax-object-rest-spread" "^7.2.0"
|
||||||
"@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
|
"@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
|
||||||
"@babel/plugin-transform-arrow-functions" "^7.2.0"
|
"@babel/plugin-transform-arrow-functions" "^7.2.0"
|
||||||
"@babel/plugin-transform-async-to-generator" "^7.4.4"
|
"@babel/plugin-transform-async-to-generator" "^7.5.0"
|
||||||
"@babel/plugin-transform-block-scoped-functions" "^7.2.0"
|
"@babel/plugin-transform-block-scoped-functions" "^7.2.0"
|
||||||
"@babel/plugin-transform-block-scoping" "^7.4.4"
|
"@babel/plugin-transform-block-scoping" "^7.4.4"
|
||||||
"@babel/plugin-transform-classes" "^7.4.4"
|
"@babel/plugin-transform-classes" "^7.4.4"
|
||||||
"@babel/plugin-transform-computed-properties" "^7.2.0"
|
"@babel/plugin-transform-computed-properties" "^7.2.0"
|
||||||
"@babel/plugin-transform-destructuring" "^7.4.4"
|
"@babel/plugin-transform-destructuring" "^7.5.0"
|
||||||
"@babel/plugin-transform-dotall-regex" "^7.4.4"
|
"@babel/plugin-transform-dotall-regex" "^7.4.4"
|
||||||
"@babel/plugin-transform-duplicate-keys" "^7.2.0"
|
"@babel/plugin-transform-duplicate-keys" "^7.5.0"
|
||||||
"@babel/plugin-transform-exponentiation-operator" "^7.2.0"
|
"@babel/plugin-transform-exponentiation-operator" "^7.2.0"
|
||||||
"@babel/plugin-transform-for-of" "^7.4.4"
|
"@babel/plugin-transform-for-of" "^7.4.4"
|
||||||
"@babel/plugin-transform-function-name" "^7.4.4"
|
"@babel/plugin-transform-function-name" "^7.4.4"
|
||||||
"@babel/plugin-transform-literals" "^7.2.0"
|
"@babel/plugin-transform-literals" "^7.2.0"
|
||||||
"@babel/plugin-transform-member-expression-literals" "^7.2.0"
|
"@babel/plugin-transform-member-expression-literals" "^7.2.0"
|
||||||
"@babel/plugin-transform-modules-amd" "^7.2.0"
|
"@babel/plugin-transform-modules-amd" "^7.5.0"
|
||||||
"@babel/plugin-transform-modules-commonjs" "^7.4.4"
|
"@babel/plugin-transform-modules-commonjs" "^7.5.0"
|
||||||
"@babel/plugin-transform-modules-systemjs" "^7.4.4"
|
"@babel/plugin-transform-modules-systemjs" "^7.5.0"
|
||||||
"@babel/plugin-transform-modules-umd" "^7.2.0"
|
"@babel/plugin-transform-modules-umd" "^7.2.0"
|
||||||
"@babel/plugin-transform-named-capturing-groups-regex" "^7.4.5"
|
"@babel/plugin-transform-named-capturing-groups-regex" "^7.4.5"
|
||||||
"@babel/plugin-transform-new-target" "^7.4.4"
|
"@babel/plugin-transform-new-target" "^7.4.4"
|
||||||
@ -678,7 +698,7 @@
|
|||||||
"@babel/plugin-transform-template-literals" "^7.4.4"
|
"@babel/plugin-transform-template-literals" "^7.4.4"
|
||||||
"@babel/plugin-transform-typeof-symbol" "^7.2.0"
|
"@babel/plugin-transform-typeof-symbol" "^7.2.0"
|
||||||
"@babel/plugin-transform-unicode-regex" "^7.4.4"
|
"@babel/plugin-transform-unicode-regex" "^7.4.4"
|
||||||
"@babel/types" "^7.4.4"
|
"@babel/types" "^7.5.0"
|
||||||
browserslist "^4.6.0"
|
browserslist "^4.6.0"
|
||||||
core-js-compat "^3.1.1"
|
core-js-compat "^3.1.1"
|
||||||
invariant "^2.2.2"
|
invariant "^2.2.2"
|
||||||
@ -704,6 +724,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"
|
||||||
@ -713,25 +740,25 @@
|
|||||||
"@babel/parser" "^7.4.4"
|
"@babel/parser" "^7.4.4"
|
||||||
"@babel/types" "^7.4.4"
|
"@babel/types" "^7.4.4"
|
||||||
|
|
||||||
"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.4", "@babel/traverse@^7.4.5":
|
"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.4", "@babel/traverse@^7.5.0":
|
||||||
version "7.4.5"
|
version "7.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.5.tgz#4e92d1728fd2f1897dafdd321efbff92156c3216"
|
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.5.0.tgz#4216d6586854ef5c3c4592dab56ec7eb78485485"
|
||||||
integrity sha512-Vc+qjynwkjRmIFGxy0KYoPj4FdVDxLej89kMHFsWScq999uX+pwcX4v9mWRjW0KcAYTPAuVQl2LKP1wEVLsp+A==
|
integrity sha512-SnA9aLbyOCcnnbQEGwdfBggnc142h/rbqqsXcaATj2hZcegCl903pUD/lfpsNBlBSuWow/YDfRyJuWi2EPR5cg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/code-frame" "^7.0.0"
|
"@babel/code-frame" "^7.0.0"
|
||||||
"@babel/generator" "^7.4.4"
|
"@babel/generator" "^7.5.0"
|
||||||
"@babel/helper-function-name" "^7.1.0"
|
"@babel/helper-function-name" "^7.1.0"
|
||||||
"@babel/helper-split-export-declaration" "^7.4.4"
|
"@babel/helper-split-export-declaration" "^7.4.4"
|
||||||
"@babel/parser" "^7.4.5"
|
"@babel/parser" "^7.5.0"
|
||||||
"@babel/types" "^7.4.4"
|
"@babel/types" "^7.5.0"
|
||||||
debug "^4.1.0"
|
debug "^4.1.0"
|
||||||
globals "^11.1.0"
|
globals "^11.1.0"
|
||||||
lodash "^4.17.11"
|
lodash "^4.17.11"
|
||||||
|
|
||||||
"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.4":
|
"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.4", "@babel/types@^7.5.0":
|
||||||
version "7.4.4"
|
version "7.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.4.tgz#8db9e9a629bb7c29370009b4b779ed93fe57d5f0"
|
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.5.0.tgz#e47d43840c2e7f9105bc4d3a2c371b4d0c7832ab"
|
||||||
integrity sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==
|
integrity sha512-UFpDVqRABKsW01bvw7/wSUe56uy6RXM5+VJibVVAybDGxEW25jdwiFJEf7ASvSaC7sN7rbE/l3cLp2izav+CtQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
esutils "^2.0.2"
|
esutils "^2.0.2"
|
||||||
lodash "^4.17.11"
|
lodash "^4.17.11"
|
||||||
@ -745,6 +772,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"
|
||||||
@ -1110,10 +1174,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.9.tgz#693e76a52f61a2f1e7fb48c0eef167b95ea4ffd0"
|
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.9.tgz#693e76a52f61a2f1e7fb48c0eef167b95ea4ffd0"
|
||||||
integrity sha512-sCZy4SxP9rN2w30Hlmg5dtdRwgYQfYRiLo9usw8X9cxlf+H4FqM1xX7+sNH7NNKVdbXMJWqva7iyy+fxh/V7fA==
|
integrity sha512-sCZy4SxP9rN2w30Hlmg5dtdRwgYQfYRiLo9usw8X9cxlf+H4FqM1xX7+sNH7NNKVdbXMJWqva7iyy+fxh/V7fA==
|
||||||
|
|
||||||
"@types/yup@0.26.20":
|
"@types/yup@0.26.21":
|
||||||
version "0.26.20"
|
version "0.26.21"
|
||||||
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.20.tgz#3b85a05f5dd76e2e8475abb6a8aeae7777627143"
|
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.21.tgz#bfca27a02a0631495bfd25b6c63647a125e6944e"
|
||||||
integrity sha512-LpCsA6NG7vIU7Umv1k4w3YGIBH5ZLZRPEKo8vJLHVbBUqRy2WaJ002kbsRqcwODpkICAOMuyGOqLQJa5isZ8+g==
|
integrity sha512-1C45M7hZrVsl8bXxYV01bitRp0r35djt+eX5HWFH3NdH+8ejqta3KM7rmQLRLrupkhF7jRkAtXl2EgDsriIqwA==
|
||||||
|
|
||||||
"@types/zen-observable@^0.5.3":
|
"@types/zen-observable@^0.5.3":
|
||||||
version "0.5.4"
|
version "0.5.4"
|
||||||
@ -1448,6 +1512,32 @@ apollo-server-core@2.6.7:
|
|||||||
subscriptions-transport-ws "^0.9.11"
|
subscriptions-transport-ws "^0.9.11"
|
||||||
ws "^6.0.0"
|
ws "^6.0.0"
|
||||||
|
|
||||||
|
apollo-server-core@2.6.8:
|
||||||
|
version "2.6.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.8.tgz#c8758b5f26b5f3b9fef51b911265b80a9ce5251d"
|
||||||
|
integrity sha512-Jxw+6R/2I2LiZ6kjRFTzPpdjw7HfuVLfNU+svgNlxioLducxBH/wqUs3qYTf4eVUUtWy+nSS/BUf/Ullo+Ur0Q==
|
||||||
|
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.5"
|
||||||
|
apollo-server-caching "0.4.0"
|
||||||
|
apollo-server-env "2.4.0"
|
||||||
|
apollo-server-errors "2.3.0"
|
||||||
|
apollo-server-plugin-base "0.5.7"
|
||||||
|
apollo-tracing "0.7.3"
|
||||||
|
fast-json-stable-stringify "^2.0.0"
|
||||||
|
graphql-extensions "0.7.6"
|
||||||
|
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@^1.3.6, apollo-server-core@^1.4.0:
|
apollo-server-core@^1.3.6, apollo-server-core@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-1.4.0.tgz#4faff7f110bfdd6c3f47008302ae24140f94c592"
|
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-1.4.0.tgz#4faff7f110bfdd6c3f47008302ae24140f94c592"
|
||||||
@ -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.8:
|
||||||
version "2.6.7"
|
version "2.6.8"
|
||||||
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.8.tgz#9f3e29f7087af669f05d75dfd335b4a9383ba48e"
|
||||||
integrity sha512-qbCQM+8LxXpwPNN5Sdvcb+Sne8zuCORFt25HJtPJRkHlyBUzOd7JA7SEnUn5e2geTiiGoVIU5leh+++C51udTw==
|
integrity sha512-LQzVHknQDkHWffc2qK9dr/qNxQ/WecSKiye5/w10tXrOy3aruTFe67ysG/vMnFZ/puroqiZ2njHzhHZztqQ4sA==
|
||||||
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.8"
|
||||||
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"
|
||||||
@ -1514,20 +1604,25 @@ apollo-server-plugin-base@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"
|
||||||
integrity sha512-wJvcPqfm/kiBwY5JZT85t2A4pcHv24xdQIpWMNt1zsnx77lIZqJmhsc22eSUSrlnYqUMXC4XMVgSUfAO4oI9wg==
|
integrity sha512-wJvcPqfm/kiBwY5JZT85t2A4pcHv24xdQIpWMNt1zsnx77lIZqJmhsc22eSUSrlnYqUMXC4XMVgSUfAO4oI9wg==
|
||||||
|
|
||||||
apollo-server-testing@~2.6.7:
|
apollo-server-plugin-base@0.5.7:
|
||||||
version "2.6.7"
|
version "0.5.7"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.6.7.tgz#cfc6366921eb99fd0cbc5d02552a8a5b268787d5"
|
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.7.tgz#267faeb5c2de7fa8d3be469cb99f82f601be7aed"
|
||||||
integrity sha512-lqgZuSqBd5hkRILeVEleo2ScJjukR/E71Mv67vPBUs01s0gEHNnjSRnuOJJOM3cAFBQOdKPc42cHGANzf2ZZTw==
|
integrity sha512-HeEwEZ92c2XYRV+0CFLbstW3vUJ4idCxR9E9Q3wwvhXrq8gaGzqyDoC8EzAzRxCJUKcEn7xQOpT/AUTC/KtkRA==
|
||||||
dependencies:
|
|
||||||
apollo-server-core "2.6.7"
|
|
||||||
|
|
||||||
apollo-server@~2.6.7:
|
apollo-server-testing@~2.6.8:
|
||||||
version "2.6.7"
|
version "2.6.8"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.6.7.tgz#b707ede529b4d45f2f00a74f3b457658b0e62e83"
|
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.6.8.tgz#e75364df7fdc2d6a11023f8a0f72a14309b90800"
|
||||||
integrity sha512-4wk9JykURLed6CnNIj9jhU6ueeTVmGBTyAnnvnlhRrOf50JAFszUErZIKg6lw5vVr5riaByrGFIkMBTySCHgPQ==
|
integrity sha512-pch2I+8QhdXBMnGDctVth4BcZ5hocwY/ogtBMoQuv7H2HBnlDOz7dCM9BH4TW3+Tk6cFgvLTaDtLJ+NxMCtyVA==
|
||||||
dependencies:
|
dependencies:
|
||||||
apollo-server-core "2.6.7"
|
apollo-server-core "2.6.8"
|
||||||
apollo-server-express "2.6.7"
|
|
||||||
|
apollo-server@~2.6.8:
|
||||||
|
version "2.6.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.6.8.tgz#5f3cf5cf4f2feccbded0cb03fa37dcd8260e5c6a"
|
||||||
|
integrity sha512-BxwaGxnD3GPuZAAqsexVHFvDlF/s2X8pILgYQ4x+VhUkMeQ12DHQtKPuxn2v2GYwH0U/GDXNohkgwxF/5eTDsQ==
|
||||||
|
dependencies:
|
||||||
|
apollo-server-core "2.6.8"
|
||||||
|
apollo-server-express "2.6.8"
|
||||||
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"
|
||||||
@ -1806,6 +1901,13 @@ babel-jest@^24.8.0, babel-jest@~24.8.0:
|
|||||||
chalk "^2.4.2"
|
chalk "^2.4.2"
|
||||||
slash "^2.0.0"
|
slash "^2.0.0"
|
||||||
|
|
||||||
|
babel-plugin-dynamic-import-node@^2.3.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f"
|
||||||
|
integrity sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==
|
||||||
|
dependencies:
|
||||||
|
object.assign "^4.1.0"
|
||||||
|
|
||||||
babel-plugin-istanbul@^5.1.0:
|
babel-plugin-istanbul@^5.1.0:
|
||||||
version "5.1.0"
|
version "5.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.1.0.tgz#6892f529eff65a3e2d33d87dc5888ffa2ecd4a30"
|
resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.1.0.tgz#6892f529eff65a3e2d33d87dc5888ffa2ecd4a30"
|
||||||
@ -2584,10 +2686,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 +2918,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"
|
||||||
@ -3053,10 +3160,10 @@ eslint-plugin-import@~2.18.0:
|
|||||||
read-pkg-up "^2.0.0"
|
read-pkg-up "^2.0.0"
|
||||||
resolve "^1.11.0"
|
resolve "^1.11.0"
|
||||||
|
|
||||||
eslint-plugin-jest@~22.7.1:
|
eslint-plugin-jest@~22.7.2:
|
||||||
version "22.7.1"
|
version "22.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.7.1.tgz#5dcdf8f7a285f98040378220d6beca581f0ab2a1"
|
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.7.2.tgz#7ab118a66a34e46ae5e16a128b5d24fd28b43dca"
|
||||||
integrity sha512-CrT3AzA738neimv8G8iK2HCkrCwHnAJeeo7k5TEHK86VMItKl6zdJT/tHBDImfnVVAYsVs4Y6BUdBZQCCgfiyw==
|
integrity sha512-Aecqe3ulBVI7amgOycVI8ZPL8o0SnGHOf3zn2/Ciu8TXyXDHcjtwD3hOs3ss/Qh/VAwlW/DMcuiXg5btgF+XMA==
|
||||||
|
|
||||||
eslint-plugin-node@~9.1.0:
|
eslint-plugin-node@~9.1.0:
|
||||||
version "9.1.0"
|
version "9.1.0"
|
||||||
@ -3788,12 +3895,12 @@ graphql-request@~1.8.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
cross-fetch "2.2.2"
|
cross-fetch "2.2.2"
|
||||||
|
|
||||||
graphql-shield@~6.0.2:
|
graphql-shield@~6.0.3:
|
||||||
version "6.0.2"
|
version "6.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-6.0.2.tgz#3ebad8faacbada91b8e576029732e91b5a041c7f"
|
resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-6.0.3.tgz#a79ca8b2fe58fb9558ffc0e64fa8aa19f63af1b3"
|
||||||
integrity sha512-3qV2qjeNZla1Fyg6Q2NR5J9AsMaNePLbUboOwhRXB7IcMnTnrxSiVn2R//8VnjnmBjF9rcvgAIAvETZ8AKGfsg==
|
integrity sha512-+yVT/dRWsRqeJOTHcEElJVfvIRPrhBqPlg5FHLmSkWNdGMR4AFqAQGrJteuZuNDvJ3bt380CZ96Bvf4J9hUpKA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/yup" "0.26.20"
|
"@types/yup" "0.26.21"
|
||||||
lightercollective "^0.3.0"
|
lightercollective "^0.3.0"
|
||||||
object-hash "^1.3.1"
|
object-hash "^1.3.1"
|
||||||
yup "^0.27.0"
|
yup "^0.27.0"
|
||||||
@ -3866,10 +3973,10 @@ graphql-yoga@~1.18.0:
|
|||||||
graphql-upload "^8.0.0"
|
graphql-upload "^8.0.0"
|
||||||
subscriptions-transport-ws "^0.9.8"
|
subscriptions-transport-ws "^0.9.8"
|
||||||
|
|
||||||
"graphql@^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0", graphql@^14.2.1, graphql@~14.4.0:
|
"graphql@^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0", graphql@^14.2.1, graphql@~14.4.2:
|
||||||
version "14.4.0"
|
version "14.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.4.0.tgz#e97086acfc0338e4fdc8f7dba519c6b8a6badfd9"
|
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.4.2.tgz#553a7d546d524663eda49ed6df77577be3203ae3"
|
||||||
integrity sha512-E55z1oK6e4cGxCqlSsRWytYDPcIUxky3XkbuQUf6TIjCmn6C7CuBJpmkMF1066q95yPAGOZVPTVT7jABKbRFSA==
|
integrity sha512-6uQadiRgnpnSS56hdZUSvFrVcQ6OF9y6wkxJfKquFtHlnl7+KSuWwSJsdwiK1vybm1HgcdbpGkCpvhvsVQ0UZQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
iterall "^1.2.2"
|
iterall "^1.2.2"
|
||||||
|
|
||||||
@ -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==
|
||||||
@ -5294,10 +5401,10 @@ lodash@=3.10.1:
|
|||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
|
||||||
integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=
|
integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=
|
||||||
|
|
||||||
lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.11:
|
lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.13:
|
||||||
version "4.17.11"
|
version "4.17.13"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.13.tgz#0bdc3a6adc873d2f4e0c4bac285df91b64fc7b93"
|
||||||
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
|
integrity sha512-vm3/XWXfWtRua0FkUyEHBZy8kCPjErNBT9fJx8Zvs+U6zjqPbTUOpkaoum3O5uiA8sm+yNMHXfYkTUHFoMxFNA==
|
||||||
|
|
||||||
long@^4.0.0:
|
long@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
@ -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"
|
||||||
@ -5860,6 +5986,11 @@ object-hash@^1.3.1:
|
|||||||
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df"
|
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df"
|
||||||
integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==
|
integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==
|
||||||
|
|
||||||
|
object-keys@^1.0.11:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
|
||||||
|
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
|
||||||
|
|
||||||
object-keys@^1.0.12:
|
object-keys@^1.0.12:
|
||||||
version "1.0.12"
|
version "1.0.12"
|
||||||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2"
|
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2"
|
||||||
@ -5877,6 +6008,16 @@ object-visit@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
isobject "^3.0.0"
|
isobject "^3.0.0"
|
||||||
|
|
||||||
|
object.assign@^4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
|
||||||
|
integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
|
||||||
|
dependencies:
|
||||||
|
define-properties "^1.1.2"
|
||||||
|
function-bind "^1.1.1"
|
||||||
|
has-symbols "^1.0.0"
|
||||||
|
object-keys "^1.0.11"
|
||||||
|
|
||||||
object.getownpropertydescriptors@^2.0.3:
|
object.getownpropertydescriptors@^2.0.3:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16"
|
resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16"
|
||||||
@ -7411,6 +7552,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"
|
||||||
|
|||||||
@ -16,7 +16,7 @@ The following features will be implemented. This gets done in three steps:
|
|||||||
|
|
||||||
### User Account
|
### User Account
|
||||||
|
|
||||||
[Cucumber Features](./integration/user_account)
|
[Cucumber Features](https://github.com/Human-Connection/Human-Connection/tree/master/cypress/integration/user_account)
|
||||||
|
|
||||||
* Sign-up
|
* Sign-up
|
||||||
* Agree to Data Privacy Statement
|
* Agree to Data Privacy Statement
|
||||||
@ -34,7 +34,7 @@ The following features will be implemented. This gets done in three steps:
|
|||||||
|
|
||||||
### User Profile
|
### User Profile
|
||||||
|
|
||||||
[Cucumber Features](./integration/user_profile)
|
[Cucumber Features](https://github.com/Human-Connection/Human-Connection/tree/master/cypress/integration/user_profile)
|
||||||
|
|
||||||
* Upload and Change Avatar
|
* Upload and Change Avatar
|
||||||
* Upload and Change Profile Picture
|
* Upload and Change Profile Picture
|
||||||
@ -59,7 +59,7 @@ The following features will be implemented. This gets done in three steps:
|
|||||||
|
|
||||||
### Posts
|
### Posts
|
||||||
|
|
||||||
[Cucumber Features](./integration/post/)
|
[Cucumber Features](https://github.com/Human-Connection/Human-Connection/tree/master/cypress/integration/post)
|
||||||
|
|
||||||
* Creating Posts
|
* Creating Posts
|
||||||
* Persistent Links
|
* Persistent Links
|
||||||
@ -84,7 +84,7 @@ The following features will be implemented. This gets done in three steps:
|
|||||||
* Upvote comments of others
|
* Upvote comments of others
|
||||||
|
|
||||||
### Notifications
|
### Notifications
|
||||||
[Cucumber features](./integration/notifications)
|
[Cucumber features](https://github.com/Human-Connection/Human-Connection/tree/master/cypress/integration/notifications)
|
||||||
|
|
||||||
* User @-mentionings
|
* User @-mentionings
|
||||||
* Notify authors for comments
|
* Notify authors for comments
|
||||||
@ -116,7 +116,7 @@ The following features will be implemented. This gets done in three steps:
|
|||||||
|
|
||||||
### Search
|
### Search
|
||||||
|
|
||||||
[Cucumber Features](./integration/search)
|
[Cucumber Features](https://github.com/Human-Connection/Human-Connection/tree/master/cypress/integration/search)
|
||||||
|
|
||||||
* Search for Categories
|
* Search for Categories
|
||||||
* Search for Tags
|
* Search for Tags
|
||||||
@ -186,7 +186,7 @@ The following features will be implemented. This gets done in three steps:
|
|||||||
|
|
||||||
### More Info
|
### More Info
|
||||||
|
|
||||||
Shows autmatically releated information for existing post.
|
Shows automatically related information for existing post.
|
||||||
|
|
||||||
* Show related Posts
|
* Show related Posts
|
||||||
* Show Pros and Cons
|
* Show Pros and Cons
|
||||||
@ -237,7 +237,7 @@ Shows automatically related actions for existing post.
|
|||||||
|
|
||||||
### Moderation
|
### Moderation
|
||||||
|
|
||||||
[Cucumber Features](./integration/moderation)
|
[Cucumber Features](https://github.com/Human-Connection/Human-Connection/tree/master/cypress/integration/moderation)
|
||||||
|
|
||||||
* Report Button for users for doubtful Content
|
* Report Button for users for doubtful Content
|
||||||
* Moderator Panel
|
* Moderator Panel
|
||||||
@ -262,7 +262,7 @@ Shows automatically related actions for existing post.
|
|||||||
|
|
||||||
### Internationalization
|
### Internationalization
|
||||||
|
|
||||||
[Cucumber Features](./integration/internationalization)
|
[Cucumber Features](https://github.com/Human-Connection/Human-Connection/tree/master/cypress/integration/internationalization)
|
||||||
|
|
||||||
* Frontend UI
|
* Frontend UI
|
||||||
* Backend Error Messages
|
* Backend Error Messages
|
||||||
@ -276,4 +276,3 @@ Shows automatically related actions for existing post.
|
|||||||
* Receiving Undo and Delete Activities for Articles and Notes
|
* Receiving Undo and Delete Activities for Articles and Notes
|
||||||
* Serving Webfinger records and Actor Objects
|
* Serving Webfinger records and Actor Objects
|
||||||
* Serving Followers, Following and Outbox collections
|
* Serving Followers, Following and Outbox collections
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
@ -273,9 +276,9 @@ When("I fill the password form with:", table => {
|
|||||||
table = table.rowsHash();
|
table = table.rowsHash();
|
||||||
cy.get("input[id=oldPassword]")
|
cy.get("input[id=oldPassword]")
|
||||||
.type(table["Your old password"])
|
.type(table["Your old password"])
|
||||||
.get("input[id=newPassword]")
|
.get("input[id=password]")
|
||||||
.type(table["Your new passsword"])
|
.type(table["Your new passsword"])
|
||||||
.get("input[id=confirmPassword]")
|
.get("input[id=passwordConfirmation]")
|
||||||
.type(table["Confirm new password"]);
|
.type(table["Confirm new password"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -32,21 +32,9 @@
|
|||||||
value: 1G
|
value: 1G
|
||||||
- name: NEO4J_dbms_memory_heap_max__size
|
- name: NEO4J_dbms_memory_heap_max__size
|
||||||
value: 1G
|
value: 1G
|
||||||
- name: NEO4J_URI
|
envFrom:
|
||||||
valueFrom:
|
- configMapRef:
|
||||||
configMapKeyRef:
|
|
||||||
name: configmap
|
name: configmap
|
||||||
key: NEO4J_URI
|
|
||||||
- name: NEO4J_USER
|
|
||||||
valueFrom:
|
|
||||||
configMapKeyRef:
|
|
||||||
name: configmap
|
|
||||||
key: NEO4J_USER
|
|
||||||
- name: NEO4J_AUTH
|
|
||||||
valueFrom:
|
|
||||||
configMapKeyRef:
|
|
||||||
name: configmap
|
|
||||||
key: NEO4J_AUTH
|
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 7687
|
- containerPort: 7687
|
||||||
- containerPort: 7474
|
- containerPort: 7474
|
||||||
|
|||||||
@ -10,7 +10,8 @@
|
|||||||
GRAPHQL_URI: "http://nitro-backend.human-connection:4000"
|
GRAPHQL_URI: "http://nitro-backend.human-connection:4000"
|
||||||
MOCKS: "false"
|
MOCKS: "false"
|
||||||
NEO4J_URI: "bolt://nitro-neo4j.human-connection:7687"
|
NEO4J_URI: "bolt://nitro-neo4j.human-connection:7687"
|
||||||
NEO4J_USER: "neo4j"
|
NEO4J_USERNAME: "neo4j"
|
||||||
|
NEO4J_PASSWORD: "neo4j"
|
||||||
NEO4J_AUTH: "none"
|
NEO4J_AUTH: "none"
|
||||||
CLIENT_URI: "https://nitro-staging.human-connection.org"
|
CLIENT_URI: "https://nitro-staging.human-connection.org"
|
||||||
metadata:
|
metadata:
|
||||||
|
|||||||
@ -1,12 +1,6 @@
|
|||||||
# Edit this Documentation
|
# Edit this Documentation
|
||||||
|
|
||||||
Go to the section and theme you want to change: On the left navigator.
|
Find the [**table of contents** for this documentation on GitHub](https://github.com/Human-Connection/Human-Connection/blob/master/SUMMARY.md) and navigate to the file you need to update.
|
||||||
|
|
||||||
Click **Edit on GitHub** on the right.
|
|
||||||
|
|
||||||
On the **Issue** tab you’ll find the open issues. Read what need to be done by clicking on the issue you like to fix.
|
|
||||||
|
|
||||||
By going backwards in the browser **\(!\)**, again go to the **Code** tab.
|
|
||||||
|
|
||||||
Click on the **edit pencil** on the right side directly above the text to edit this file on your fork of Human Connection \(HC\).
|
Click on the **edit pencil** on the right side directly above the text to edit this file on your fork of Human Connection \(HC\).
|
||||||
|
|
||||||
@ -14,7 +8,7 @@ You can see a preview of your changes by clicking the **Preview changes** tab as
|
|||||||
|
|
||||||
If you are ready, fill in the **Propose file change** at the end of the webpage.
|
If you are ready, fill in the **Propose file change** at the end of the webpage.
|
||||||
|
|
||||||
After that you have to send your change to the HC basis with a pull request. Here make a comment which issue you have fixed. At least the number.
|
After that you have to send your change to the HC basis with a pull request. Here make a comment which issue you have fixed. (If you are working on one of our [open issues](https://github.com/Human-Connection/Human-Connection/issues) please include the number.)
|
||||||
|
|
||||||
## Markdown your documentation
|
## Markdown your documentation
|
||||||
|
|
||||||
@ -117,4 +111,3 @@ TODO: How to modify screenshots in Linux ...
|
|||||||
{% endhint %}
|
{% endhint %}
|
||||||
{% endtab %}
|
{% endtab %}
|
||||||
{% endtabs %}
|
{% endtabs %}
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@ for an interactive cypher shell and a visualization of the graph.
|
|||||||
|
|
||||||
## Installation without Docker
|
## Installation without Docker
|
||||||
|
|
||||||
Install community edition of [Neo4J]() along with the plugin
|
Install the community edition of [Neo4j](https://neo4j.com/) along with the plugin
|
||||||
[Apoc](https://github.com/neo4j-contrib/neo4j-apoc-procedures) on your system.
|
[Apoc](https://github.com/neo4j-contrib/neo4j-apoc-procedures) on your system.
|
||||||
|
|
||||||
To do so, go to [releases](https://neo4j.com/download-center/#releases), choose
|
To do so, go to [releases](https://neo4j.com/download-center/#releases), choose
|
||||||
@ -28,7 +28,13 @@ To do so, go to [releases](https://neo4j.com/download-center/#releases), choose
|
|||||||
and unpack the files.
|
and unpack the files.
|
||||||
|
|
||||||
Download [Neo4j Apoc](https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases)
|
Download [Neo4j Apoc](https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases)
|
||||||
and drop the file into the `plugins` folder of the just extracted Neo4j-Server.
|
and drop the `.jar` file into the `plugins` folder of the just extracted Neo4j-Server.
|
||||||
|
|
||||||
|
Then make sure to allow Apoc procedures by adding the following line to your Neo4j configuration \(`conf/neo4j.conf`\):
|
||||||
|
|
||||||
|
```
|
||||||
|
dbms.security.procedures.unrestricted=apoc.*
|
||||||
|
```
|
||||||
|
|
||||||
### Alternatives
|
### Alternatives
|
||||||
|
|
||||||
@ -59,6 +65,6 @@ $ cp .env.template .env
|
|||||||
$ ./db_setup.sh
|
$ ./db_setup.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Otherwise if you don't have `cypher-shell` available, simply copy the cypher
|
Otherwise, if you don't have `cypher-shell` available, copy the cypher
|
||||||
statements [from the script](./neo4j/db_setup.sh) and paste the scripts into your
|
statements [from the `db_setup.sh` script](https://github.com/Human-Connection/Human-Connection/blob/master/neo4j/db_setup.sh) and paste the scripts into your
|
||||||
database [browser frontend](http://localhost:7474).
|
[database browser frontend](http://localhost:7474).
|
||||||
|
|||||||
@ -19,9 +19,10 @@
|
|||||||
"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.4.0",
|
||||||
"cypress-cucumber-preprocessor": "^1.12.0",
|
"cypress-cucumber-preprocessor": "^1.12.0",
|
||||||
"cypress-file-upload": "^3.2.0",
|
"cypress-file-upload": "^3.2.0",
|
||||||
"cypress-plugin-retries": "^1.2.2",
|
"cypress-plugin-retries": "^1.2.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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
FROM node:12.5-alpine as base
|
FROM node:12.6-alpine as base
|
||||||
LABEL Description="Web Frontend of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)"
|
LABEL Description="Web Frontend of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)"
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|||||||
@ -41,4 +41,4 @@ All reusable Components \(for example avatar\) should be done inside the [Nitro-
|
|||||||
|
|
||||||
More information can be found here: [https://github.com/Human-Connection/Nitro-Styleguide](https://github.com/Human-Connection/Nitro-Styleguide)
|
More information can be found here: [https://github.com/Human-Connection/Nitro-Styleguide](https://github.com/Human-Connection/Nitro-Styleguide)
|
||||||
|
|
||||||
If you need to change something in the styleguide and want to see the effects on the frontend immediately, then we have you covered. You need to clone the styleguide to the parent directory `../Nitro-Styleguide` and run `yarn && yarn run dev`. After that you run `yarn run dev:styleguide` instead of `yarn run dev` and you will see your changes reflected inside the fronten!
|
If you need to change something in the styleguide and want to see the effects on the frontend immediately, then we have you covered. You need to clone the styleguide to the parent directory `../Nitro-Styleguide` and run `yarn && yarn run dev`. After that you run `yarn run dev:styleguide` instead of `yarn run dev` and you will see your changes reflected inside the frontend!
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
# ASSETS
|
# ASSETS
|
||||||
|
|
||||||
**This directory is not required, you can delete it if you don't want to use it.**
|
This directory contains your un-compiled assets such as LESS, SASS, or JavaScript – in our case SCSS styles.
|
||||||
|
|
||||||
This directory contains your un-compiled assets such as LESS, SASS, or JavaScript.
|
|
||||||
|
|
||||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked).
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked).
|
||||||
|
|
||||||
|
|||||||
@ -66,7 +66,8 @@ blockquote {
|
|||||||
border-left: 3px dotted $color-neutral-70;
|
border-left: 3px dotted $color-neutral-70;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '\201C'; /*Unicode for Left Double Quote*/
|
content: '\201C';
|
||||||
|
/*Unicode for Left Double Quote*/
|
||||||
|
|
||||||
/*Font*/
|
/*Font*/
|
||||||
font-size: $font-size-xxxx-large;
|
font-size: $font-size-xxxx-large;
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
# COMPONENTS
|
# COMPONENTS
|
||||||
|
|
||||||
**This directory is not required, you can delete it if you don't want to use it.**
|
|
||||||
|
|
||||||
The components directory contains your Vue.js Components.
|
The components directory contains your Vue.js Components.
|
||||||
|
|
||||||
_Nuxt.js doesn't supercharge these components._
|
_Nuxt.js doesn't supercharge these components._
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,12 @@
|
|||||||
<ds-input model="title" class="post-title" placeholder="Title" name="title" autofocus />
|
<ds-input model="title" class="post-title" placeholder="Title" name="title" autofocus />
|
||||||
<small style="width:100%;position:relative;left:90%">{{ form.title.length }}/64</small>
|
<small style="width:100%;position:relative;left:90%">{{ form.title.length }}/64</small>
|
||||||
<no-ssr>
|
<no-ssr>
|
||||||
<hc-editor :users="users" :value="form.content" @input="updateEditorContent" />
|
<hc-editor
|
||||||
|
:users="users"
|
||||||
|
:hashtags="hashtags"
|
||||||
|
:value="form.content"
|
||||||
|
@input="updateEditorContent"
|
||||||
|
/>
|
||||||
<small style="width:100%;position:relative;left:90%">{{ form.contentLength }}/2000</small>
|
<small style="width:100%;position:relative;left:90%">{{ form.contentLength }}/2000</small>
|
||||||
</no-ssr>
|
</no-ssr>
|
||||||
<ds-space margin-bottom="xxx-large" />
|
<ds-space margin-bottom="xxx-large" />
|
||||||
@ -34,18 +39,19 @@
|
|||||||
/>
|
/>
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
</ds-flex>
|
</ds-flex>
|
||||||
|
<ds-space />
|
||||||
<div slot="footer" style="text-align: right">
|
<div slot="footer" style="text-align: right">
|
||||||
<ds-button
|
<ds-button
|
||||||
|
class="cancel-button"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
ghost
|
ghost
|
||||||
class="cancel-button"
|
|
||||||
@click.prevent="$router.back()"
|
@click.prevent="$router.back()"
|
||||||
>
|
>
|
||||||
{{ $t('actions.cancel') }}
|
{{ $t('actions.cancel') }}
|
||||||
</ds-button>
|
</ds-button>
|
||||||
<ds-button
|
<ds-button
|
||||||
icon="check"
|
|
||||||
type="submit"
|
type="submit"
|
||||||
|
icon="check"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:disabled="disabled || errors"
|
:disabled="disabled || errors"
|
||||||
primary
|
primary
|
||||||
@ -61,7 +67,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import HcEditor from '~/components/Editor'
|
import HcEditor from '~/components/Editor/Editor'
|
||||||
import orderBy from 'lodash/orderBy'
|
import orderBy from 'lodash/orderBy'
|
||||||
import locales from '~/locales'
|
import locales from '~/locales'
|
||||||
import PostMutations from '~/graphql/PostMutations.js'
|
import PostMutations from '~/graphql/PostMutations.js'
|
||||||
@ -99,6 +105,7 @@ export default {
|
|||||||
slug: null,
|
slug: null,
|
||||||
users: [],
|
users: [],
|
||||||
n: 0,
|
n: 0,
|
||||||
|
hashtags: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@ -205,17 +212,34 @@ export default {
|
|||||||
apollo: {
|
apollo: {
|
||||||
User: {
|
User: {
|
||||||
query() {
|
query() {
|
||||||
return gql(`{
|
return gql`
|
||||||
|
{
|
||||||
User(orderBy: slug_asc) {
|
User(orderBy: slug_asc) {
|
||||||
id
|
id
|
||||||
slug
|
slug
|
||||||
}
|
}
|
||||||
}`)
|
}
|
||||||
|
`
|
||||||
},
|
},
|
||||||
result(result) {
|
result(result) {
|
||||||
this.users = result.data.User
|
this.users = result.data.User
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Tag: {
|
||||||
|
query() {
|
||||||
|
return gql`
|
||||||
|
{
|
||||||
|
Tag(orderBy: name_asc) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
},
|
||||||
|
result(result) {
|
||||||
|
this.hashtags = result.data.Tag
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { mount, createLocalVue } from '@vue/test-utils'
|
import { mount, createLocalVue } from '@vue/test-utils'
|
||||||
import Editor from './'
|
import Editor from './Editor'
|
||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
|
|
||||||
import Styleguide from '@human-connection/styleguide'
|
import Styleguide from '@human-connection/styleguide'
|
||||||
@ -36,7 +36,9 @@ describe('Editor.vue', () => {
|
|||||||
propsData,
|
propsData,
|
||||||
localVue,
|
localVue,
|
||||||
sync: false,
|
sync: false,
|
||||||
stubs: { transition: false },
|
stubs: {
|
||||||
|
transition: false,
|
||||||
|
},
|
||||||
store,
|
store,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -1,18 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
|
<!-- Mention and Hashtag Suggestions Menu -->
|
||||||
<div v-show="showSuggestions" ref="suggestions" class="suggestion-list">
|
<div v-show="showSuggestions" ref="suggestions" class="suggestion-list">
|
||||||
|
<!-- "filteredItems" array is not empty -->
|
||||||
<template v-if="hasResults">
|
<template v-if="hasResults">
|
||||||
<div
|
<div
|
||||||
v-for="(user, index) in filteredUsers"
|
v-for="(item, index) in filteredItems"
|
||||||
:key="user.id"
|
:key="item.id"
|
||||||
class="suggestion-list__item"
|
class="suggestion-list__item"
|
||||||
:class="{ 'is-selected': navigatedUserIndex === index }"
|
:class="{ 'is-selected': navigatedItemIndex === index }"
|
||||||
@click="selectUser(user)"
|
@click="selectItem(item)"
|
||||||
>
|
>
|
||||||
@{{ user.slug }}
|
<div v-if="isMention">@{{ item.slug }}</div>
|
||||||
|
<div v-if="isHashtag">#{{ item.name }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="isHashtag">
|
||||||
|
<!-- if query is not empty and is find fully in the suggestions array ... -->
|
||||||
|
<div v-if="query && !filteredItems.find(el => el.name === query)">
|
||||||
|
<div class="suggestion-list__item is-empty">{{ $t('editor.hashtag.addHashtag') }}</div>
|
||||||
|
<div class="suggestion-list__item" @click="selectItem({ name: query })">
|
||||||
|
#{{ query }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- otherwise if sanitized query is empty advice the user to add a char -->
|
||||||
|
<div v-else-if="!query">
|
||||||
|
<div class="suggestion-list__item is-empty">{{ $t('editor.hashtag.addLetter') }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="suggestion-list__item is-empty">No users found</div>
|
<!-- if "!hasResults" -->
|
||||||
|
<div v-else>
|
||||||
|
<div v-if="isMention" class="suggestion-list__item is-empty">
|
||||||
|
{{ $t('editor.mention.noUsersFound') }}
|
||||||
|
</div>
|
||||||
|
<div v-if="isHashtag">
|
||||||
|
<div v-if="query === ''" class="suggestion-list__item is-empty">
|
||||||
|
{{ $t('editor.hashtag.noHashtagsFound') }}
|
||||||
|
</div>
|
||||||
|
<!-- if "query" is not empty -->
|
||||||
|
<div v-else>
|
||||||
|
<div class="suggestion-list__item is-empty">{{ $t('editor.hashtag.addHashtag') }}</div>
|
||||||
|
<div class="suggestion-list__item" @click="selectItem({ name: query })">
|
||||||
|
#{{ query }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<editor-menu-bubble :editor="editor">
|
<editor-menu-bubble :editor="editor">
|
||||||
@ -173,6 +206,7 @@ import {
|
|||||||
History,
|
History,
|
||||||
} from 'tiptap-extensions'
|
} from 'tiptap-extensions'
|
||||||
import Mention from './nodes/Mention.js'
|
import Mention from './nodes/Mention.js'
|
||||||
|
import Hashtag from './nodes/Hashtag.js'
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
let throttleInputEvent
|
let throttleInputEvent
|
||||||
@ -185,6 +219,7 @@ export default {
|
|||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
users: { type: Array, default: () => [] },
|
users: { type: Array, default: () => [] },
|
||||||
|
hashtags: { type: Array, default: () => [] },
|
||||||
value: { type: String, default: '' },
|
value: { type: String, default: '' },
|
||||||
doc: { type: Object, default: () => {} },
|
doc: { type: Object, default: () => {} },
|
||||||
},
|
},
|
||||||
@ -215,34 +250,40 @@ export default {
|
|||||||
}),
|
}),
|
||||||
new History(),
|
new History(),
|
||||||
new Mention({
|
new Mention({
|
||||||
|
// a list of all suggested items
|
||||||
items: () => {
|
items: () => {
|
||||||
return this.users
|
return this.users
|
||||||
},
|
},
|
||||||
|
// is called when a suggestion starts
|
||||||
onEnter: ({ items, query, range, command, virtualNode }) => {
|
onEnter: ({ items, query, range, command, virtualNode }) => {
|
||||||
|
this.suggestionType = this.mentionSuggestionType
|
||||||
|
|
||||||
this.query = query
|
this.query = query
|
||||||
this.filteredUsers = items
|
this.filteredItems = items
|
||||||
this.suggestionRange = range
|
this.suggestionRange = range
|
||||||
this.renderPopup(virtualNode)
|
this.renderPopup(virtualNode)
|
||||||
// we save the command for inserting a selected mention
|
// we save the command for inserting a selected mention
|
||||||
// this allows us to call it inside of our custom popup
|
// this allows us to call it inside of our custom popup
|
||||||
// via keyboard navigation and on click
|
// via keyboard navigation and on click
|
||||||
this.insertMention = command
|
this.insertMentionOrHashtag = command
|
||||||
},
|
},
|
||||||
// is called when a suggestion has changed
|
// is called when a suggestion has changed
|
||||||
onChange: ({ items, query, range, virtualNode }) => {
|
onChange: ({ items, query, range, virtualNode }) => {
|
||||||
this.query = query
|
this.query = query
|
||||||
this.filteredUsers = items
|
this.filteredItems = items
|
||||||
this.suggestionRange = range
|
this.suggestionRange = range
|
||||||
this.navigatedUserIndex = 0
|
this.navigatedItemIndex = 0
|
||||||
this.renderPopup(virtualNode)
|
this.renderPopup(virtualNode)
|
||||||
},
|
},
|
||||||
// is called when a suggestion is cancelled
|
// is called when a suggestion is cancelled
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
|
this.suggestionType = this.nullSuggestionType
|
||||||
|
|
||||||
// reset all saved values
|
// reset all saved values
|
||||||
this.query = null
|
this.query = null
|
||||||
this.filteredUsers = []
|
this.filteredItems = []
|
||||||
this.suggestionRange = null
|
this.suggestionRange = null
|
||||||
this.navigatedUserIndex = 0
|
this.navigatedItemIndex = 0
|
||||||
this.destroyPopup()
|
this.destroyPopup()
|
||||||
},
|
},
|
||||||
// is called on every keyDown event while a suggestion is active
|
// is called on every keyDown event while a suggestion is active
|
||||||
@ -279,6 +320,83 @@ export default {
|
|||||||
return fuse.search(query)
|
return fuse.search(query)
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
new Hashtag({
|
||||||
|
// a list of all suggested items
|
||||||
|
items: () => {
|
||||||
|
return this.hashtags
|
||||||
|
},
|
||||||
|
// is called when a suggestion starts
|
||||||
|
onEnter: ({ items, query, range, command, virtualNode }) => {
|
||||||
|
this.suggestionType = this.hashtagSuggestionType
|
||||||
|
|
||||||
|
this.query = this.sanitizedQuery(query)
|
||||||
|
this.filteredItems = items
|
||||||
|
this.suggestionRange = range
|
||||||
|
this.renderPopup(virtualNode)
|
||||||
|
// we save the command for inserting a selected mention
|
||||||
|
// this allows us to call it inside of our custom popup
|
||||||
|
// via keyboard navigation and on click
|
||||||
|
this.insertMentionOrHashtag = command
|
||||||
|
},
|
||||||
|
// is called when a suggestion has changed
|
||||||
|
onChange: ({ items, query, range, virtualNode }) => {
|
||||||
|
this.query = this.sanitizedQuery(query)
|
||||||
|
this.filteredItems = items
|
||||||
|
this.suggestionRange = range
|
||||||
|
this.navigatedItemIndex = 0
|
||||||
|
this.renderPopup(virtualNode)
|
||||||
|
},
|
||||||
|
// is called when a suggestion is cancelled
|
||||||
|
onExit: () => {
|
||||||
|
this.suggestionType = this.nullSuggestionType
|
||||||
|
|
||||||
|
// reset all saved values
|
||||||
|
this.query = null
|
||||||
|
this.filteredItems = []
|
||||||
|
this.suggestionRange = null
|
||||||
|
this.navigatedItemIndex = 0
|
||||||
|
this.destroyPopup()
|
||||||
|
},
|
||||||
|
// is called on every keyDown event while a suggestion is active
|
||||||
|
onKeyDown: ({ event }) => {
|
||||||
|
// pressing up arrow
|
||||||
|
if (event.keyCode === 38) {
|
||||||
|
this.upHandler()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// pressing down arrow
|
||||||
|
if (event.keyCode === 40) {
|
||||||
|
this.downHandler()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// pressing enter
|
||||||
|
if (event.keyCode === 13) {
|
||||||
|
this.enterHandler()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// pressing space
|
||||||
|
if (event.keyCode === 32) {
|
||||||
|
this.spaceHandler()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
// is called when a suggestion has changed
|
||||||
|
// this function is optional because there is basic filtering built-in
|
||||||
|
// you can overwrite it if you prefer your own filtering
|
||||||
|
// in this example we use fuse.js with support for fuzzy search
|
||||||
|
onFilter: (items, query) => {
|
||||||
|
query = this.sanitizedQuery(query)
|
||||||
|
if (!query) {
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
return items.filter(item =>
|
||||||
|
JSON.stringify(item)
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(query.toLowerCase()),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
onUpdate: e => {
|
onUpdate: e => {
|
||||||
clearTimeout(throttleInputEvent)
|
clearTimeout(throttleInputEvent)
|
||||||
@ -287,22 +405,32 @@ export default {
|
|||||||
}),
|
}),
|
||||||
linkUrl: null,
|
linkUrl: null,
|
||||||
linkMenuIsActive: false,
|
linkMenuIsActive: false,
|
||||||
|
nullSuggestionType: '',
|
||||||
|
mentionSuggestionType: 'mention',
|
||||||
|
hashtagSuggestionType: 'hashtag',
|
||||||
|
suggestionType: this.nullSuggestionType,
|
||||||
query: null,
|
query: null,
|
||||||
suggestionRange: null,
|
suggestionRange: null,
|
||||||
filteredUsers: [],
|
filteredItems: [],
|
||||||
navigatedUserIndex: 0,
|
navigatedItemIndex: 0,
|
||||||
insertMention: () => {},
|
insertMentionOrHashtag: () => {},
|
||||||
observer: null,
|
observer: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({ placeholder: 'editor/placeholder' }),
|
...mapGetters({ placeholder: 'editor/placeholder' }),
|
||||||
hasResults() {
|
hasResults() {
|
||||||
return this.filteredUsers.length
|
return this.filteredItems.length
|
||||||
},
|
},
|
||||||
showSuggestions() {
|
showSuggestions() {
|
||||||
return this.query || this.hasResults
|
return this.query || this.hasResults
|
||||||
},
|
},
|
||||||
|
isMention() {
|
||||||
|
return this.suggestionType === this.mentionSuggestionType
|
||||||
|
},
|
||||||
|
isHashtag() {
|
||||||
|
return this.suggestionType === this.hashtagSuggestionType
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
value: {
|
value: {
|
||||||
@ -330,33 +458,54 @@ export default {
|
|||||||
this.editor.destroy()
|
this.editor.destroy()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
sanitizedQuery(query) {
|
||||||
|
// remove all not allowed chars
|
||||||
|
query = query.replace(/[^a-zA-Z0-9]/gm, '')
|
||||||
|
// if the query is only made of digits, make it empty
|
||||||
|
return query.replace(/[0-9]/gm, '') === '' ? '' : query
|
||||||
|
},
|
||||||
// navigate to the previous item
|
// navigate to the previous item
|
||||||
// if it's the first item, navigate to the last one
|
// if it's the first item, navigate to the last one
|
||||||
upHandler() {
|
upHandler() {
|
||||||
this.navigatedUserIndex =
|
this.navigatedItemIndex =
|
||||||
(this.navigatedUserIndex + this.filteredUsers.length - 1) % this.filteredUsers.length
|
(this.navigatedItemIndex + this.filteredItems.length - 1) % this.filteredItems.length
|
||||||
},
|
},
|
||||||
// navigate to the next item
|
// navigate to the next item
|
||||||
// if it's the last item, navigate to the first one
|
// if it's the last item, navigate to the first one
|
||||||
downHandler() {
|
downHandler() {
|
||||||
this.navigatedUserIndex = (this.navigatedUserIndex + 1) % this.filteredUsers.length
|
this.navigatedItemIndex = (this.navigatedItemIndex + 1) % this.filteredItems.length
|
||||||
},
|
},
|
||||||
|
// Handles pressing of enter.
|
||||||
enterHandler() {
|
enterHandler() {
|
||||||
const user = this.filteredUsers[this.navigatedUserIndex]
|
const item = this.filteredItems[this.navigatedItemIndex]
|
||||||
if (user) {
|
if (item) {
|
||||||
this.selectUser(user)
|
this.selectItem(item)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// For hashtags handles pressing of space.
|
||||||
|
spaceHandler() {
|
||||||
|
if (this.suggestionType === this.hashtagSuggestionType && this.query !== '') {
|
||||||
|
this.selectItem({ name: this.query })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// we have to replace our suggestion text with a mention
|
// we have to replace our suggestion text with a mention
|
||||||
// so it's important to pass also the position of your suggestion text
|
// so it's important to pass also the position of your suggestion text
|
||||||
selectUser(user) {
|
selectItem(item) {
|
||||||
this.insertMention({
|
const typeAttrs = {
|
||||||
range: this.suggestionRange,
|
mention: {
|
||||||
attrs: {
|
|
||||||
// TODO: use router here
|
// TODO: use router here
|
||||||
url: `/profile/${user.id}`,
|
url: `/profile/${item.id}`,
|
||||||
label: user.slug,
|
label: item.slug,
|
||||||
},
|
},
|
||||||
|
hashtag: {
|
||||||
|
// TODO: Fill up with input hashtag in search field
|
||||||
|
url: `/search/hashtag/${item.name}`,
|
||||||
|
label: item.name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
this.insertMentionOrHashtag({
|
||||||
|
range: this.suggestionRange,
|
||||||
|
attrs: typeAttrs[this.suggestionType],
|
||||||
})
|
})
|
||||||
this.editor.focus()
|
this.editor.focus()
|
||||||
},
|
},
|
||||||
@ -535,6 +684,12 @@ li > p {
|
|||||||
.mention-suggestion {
|
.mention-suggestion {
|
||||||
color: $color-primary;
|
color: $color-primary;
|
||||||
}
|
}
|
||||||
|
.hashtag {
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
.hashtag-suggestion {
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
&__floating-menu {
|
&__floating-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
margin-top: -0.25rem;
|
margin-top: -0.25rem;
|
||||||
44
webapp/components/Editor/nodes/Hashtag.js
Normal file
44
webapp/components/Editor/nodes/Hashtag.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { Mention as TipTapMention } from 'tiptap-extensions'
|
||||||
|
|
||||||
|
export default class Hashtag extends TipTapMention {
|
||||||
|
get name() {
|
||||||
|
return 'hashtag'
|
||||||
|
}
|
||||||
|
|
||||||
|
get defaultOptions() {
|
||||||
|
return {
|
||||||
|
matcher: {
|
||||||
|
char: '#',
|
||||||
|
allowSpaces: false,
|
||||||
|
startOfLine: false,
|
||||||
|
},
|
||||||
|
mentionClass: 'hashtag',
|
||||||
|
suggestionClass: 'hashtag-suggestion',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get schema() {
|
||||||
|
const patchedSchema = super.schema
|
||||||
|
|
||||||
|
patchedSchema.attrs = {
|
||||||
|
url: {},
|
||||||
|
label: {},
|
||||||
|
}
|
||||||
|
patchedSchema.toDOM = node => {
|
||||||
|
return [
|
||||||
|
'a',
|
||||||
|
{
|
||||||
|
class: this.options.mentionClass,
|
||||||
|
href: node.attrs.url,
|
||||||
|
target: '_blank',
|
||||||
|
// contenteditable: 'true',
|
||||||
|
},
|
||||||
|
`${this.options.matcher.char}${node.attrs.label}`,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
patchedSchema.parseDOM = [
|
||||||
|
// this is not implemented
|
||||||
|
]
|
||||||
|
return patchedSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,10 @@
|
|||||||
import { Mention as TipTapMention } from 'tiptap-extensions'
|
import { Mention as TipTapMention } from 'tiptap-extensions'
|
||||||
|
|
||||||
export default class Mention extends TipTapMention {
|
export default class Mention extends TipTapMention {
|
||||||
|
get name() {
|
||||||
|
return 'mention'
|
||||||
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
const patchedSchema = super.schema
|
const patchedSchema = super.schema
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<ds-card>
|
<ds-card class="filter-menu-card">
|
||||||
<ds-flex>
|
<ds-flex>
|
||||||
<ds-flex-item class="filter-menu-title">
|
<ds-flex-item class="filter-menu-title">
|
||||||
<ds-heading size="h3">{{ $t('filter-menu.title') }}</ds-heading>
|
<ds-heading size="h3">{{ $t('filter-menu.title') }}</ds-heading>
|
||||||
@ -20,6 +20,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
</ds-flex>
|
</ds-flex>
|
||||||
|
<div v-if="hashtag">
|
||||||
|
<ds-space margin-bottom="x-small" />
|
||||||
|
<ds-flex>
|
||||||
|
<ds-flex-item>
|
||||||
|
<ds-heading size="h3">{{ $t('filter-menu.hashtag-search', { hashtag }) }}</ds-heading>
|
||||||
|
</ds-flex-item>
|
||||||
|
<ds-flex-item>
|
||||||
|
<div class="filter-menu-buttons">
|
||||||
|
<ds-button
|
||||||
|
v-tooltip="{
|
||||||
|
content: this.$t('filter-menu.clearSearch'),
|
||||||
|
placement: 'left',
|
||||||
|
delay: { show: 500 },
|
||||||
|
}"
|
||||||
|
name="filter-by-followed-authors-only"
|
||||||
|
icon="close"
|
||||||
|
@click="clearSearch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ds-flex-item>
|
||||||
|
</ds-flex>
|
||||||
|
</div>
|
||||||
</ds-card>
|
</ds-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -27,6 +49,7 @@
|
|||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
user: { type: Object, required: true },
|
user: { type: Object, required: true },
|
||||||
|
hashtag: { type: Object, default: null },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -50,11 +73,18 @@ export default {
|
|||||||
: { author: { followedBy_some: { id: this.user.id } } }
|
: { author: { followedBy_some: { id: this.user.id } } }
|
||||||
this.$emit('changeFilterBubble', this.filter)
|
this.$emit('changeFilterBubble', this.filter)
|
||||||
},
|
},
|
||||||
|
clearSearch() {
|
||||||
|
this.$emit('clearSearch')
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
.filter-menu-card {
|
||||||
|
background-color: $background-color-soft;
|
||||||
|
}
|
||||||
|
|
||||||
.filter-menu-title {
|
.filter-menu-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -54,7 +54,7 @@ describe('ChangePassword.vue', () => {
|
|||||||
describe('match', () => {
|
describe('match', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper.find('input#oldPassword').setValue('some secret')
|
wrapper.find('input#oldPassword').setValue('some secret')
|
||||||
wrapper.find('input#newPassword').setValue('some secret')
|
wrapper.find('input#password').setValue('some secret')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('invalid', () => {
|
it('invalid', () => {
|
||||||
@ -90,8 +90,8 @@ describe('ChangePassword.vue', () => {
|
|||||||
describe('given valid input', () => {
|
describe('given valid input', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper.find('input#oldPassword').setValue('supersecret')
|
wrapper.find('input#oldPassword').setValue('supersecret')
|
||||||
wrapper.find('input#newPassword').setValue('superdupersecret')
|
wrapper.find('input#password').setValue('superdupersecret')
|
||||||
wrapper.find('input#confirmPassword').setValue('superdupersecret')
|
wrapper.find('input#passwordConfirmation').setValue('superdupersecret')
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('submit form', () => {
|
describe('submit form', () => {
|
||||||
@ -109,8 +109,8 @@ describe('ChangePassword.vue', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
variables: {
|
variables: {
|
||||||
oldPassword: 'supersecret',
|
oldPassword: 'supersecret',
|
||||||
newPassword: 'superdupersecret',
|
password: 'superdupersecret',
|
||||||
confirmPassword: 'superdupersecret',
|
passwordConfirmation: 'superdupersecret',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -135,8 +135,8 @@ describe('ChangePassword.vue', () => {
|
|||||||
/* describe('mutation rejects', () => {
|
/* describe('mutation rejects', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await wrapper.find('input#oldPassword').setValue('supersecret')
|
await wrapper.find('input#oldPassword').setValue('supersecret')
|
||||||
await wrapper.find('input#newPassword').setValue('supersecret')
|
await wrapper.find('input#password').setValue('supersecret')
|
||||||
await wrapper.find('input#confirmPassword').setValue('supersecret')
|
await wrapper.find('input#passwordConfirmation').setValue('supersecret')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('displays error message', async () => {
|
it('displays error message', async () => {
|
||||||
|
|||||||
@ -1,12 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<ds-form
|
<ds-form v-model="formData" :schema="formSchema" @submit="handleSubmit">
|
||||||
v-model="formData"
|
<template slot-scope="{ errors }">
|
||||||
:schema="formSchema"
|
|
||||||
@submit="handleSubmit"
|
|
||||||
@input="handleInput"
|
|
||||||
@input-valid="handleInputValid"
|
|
||||||
>
|
|
||||||
<template>
|
|
||||||
<ds-input
|
<ds-input
|
||||||
id="oldPassword"
|
id="oldPassword"
|
||||||
model="oldPassword"
|
model="oldPassword"
|
||||||
@ -15,22 +9,22 @@
|
|||||||
:label="$t('settings.security.change-password.label-old-password')"
|
:label="$t('settings.security.change-password.label-old-password')"
|
||||||
/>
|
/>
|
||||||
<ds-input
|
<ds-input
|
||||||
id="newPassword"
|
id="password"
|
||||||
model="newPassword"
|
model="password"
|
||||||
type="password"
|
type="password"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:label="$t('settings.security.change-password.label-new-password')"
|
:label="$t('settings.security.change-password.label-new-password')"
|
||||||
/>
|
/>
|
||||||
<ds-input
|
<ds-input
|
||||||
id="confirmPassword"
|
id="passwordConfirmation"
|
||||||
model="confirmPassword"
|
model="passwordConfirmation"
|
||||||
type="password"
|
type="password"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:label="$t('settings.security.change-password.label-new-password-confirm')"
|
:label="$t('settings.security.change-password.label-new-password-confirm')"
|
||||||
/>
|
/>
|
||||||
<password-strength :password="formData.newPassword" />
|
<password-strength :password="formData.password" />
|
||||||
<ds-space margin-top="base">
|
<ds-space margin-top="base">
|
||||||
<ds-button :loading="loading" :disabled="disabled" primary>
|
<ds-button :loading="loading" :disabled="errors" primary>
|
||||||
{{ $t('settings.security.change-password.button') }}
|
{{ $t('settings.security.change-password.button') }}
|
||||||
</ds-button>
|
</ds-button>
|
||||||
</ds-space>
|
</ds-space>
|
||||||
@ -41,6 +35,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import PasswordStrength from './Strength'
|
import PasswordStrength from './Strength'
|
||||||
|
import PasswordForm from '~/components/utils/PasswordFormHelper'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ChangePassword',
|
name: 'ChangePassword',
|
||||||
@ -48,11 +43,11 @@ export default {
|
|||||||
PasswordStrength,
|
PasswordStrength,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
const passwordForm = PasswordForm({ translate: this.$t })
|
||||||
return {
|
return {
|
||||||
formData: {
|
formData: {
|
||||||
oldPassword: '',
|
oldPassword: '',
|
||||||
newPassword: '',
|
...passwordForm.formData,
|
||||||
confirmPassword: '',
|
|
||||||
},
|
},
|
||||||
formSchema: {
|
formSchema: {
|
||||||
oldPassword: {
|
oldPassword: {
|
||||||
@ -60,38 +55,18 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
message: this.$t('settings.security.change-password.message-old-password-required'),
|
message: this.$t('settings.security.change-password.message-old-password-required'),
|
||||||
},
|
},
|
||||||
newPassword: {
|
...passwordForm.formSchema,
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
message: this.$t('settings.security.change-password.message-new-password-required'),
|
|
||||||
},
|
|
||||||
confirmPassword: [
|
|
||||||
{ validator: this.matchPassword },
|
|
||||||
{
|
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
message: this.$t(
|
|
||||||
'settings.security.change-password.message-new-password-confirm-required',
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async handleInput(data) {
|
|
||||||
this.disabled = true
|
|
||||||
},
|
|
||||||
async handleInputValid(data) {
|
|
||||||
this.disabled = false
|
|
||||||
},
|
|
||||||
async handleSubmit(data) {
|
async handleSubmit(data) {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
const mutation = gql`
|
const mutation = gql`
|
||||||
mutation($oldPassword: String!, $newPassword: String!) {
|
mutation($oldPassword: String!, $password: String!) {
|
||||||
changePassword(oldPassword: $oldPassword, newPassword: $newPassword)
|
changePassword(oldPassword: $oldPassword, newPassword: $password)
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
const variables = this.formData
|
const variables = this.formData
|
||||||
@ -102,8 +77,8 @@ export default {
|
|||||||
this.$toast.success(this.$t('settings.security.change-password.success'))
|
this.$toast.success(this.$t('settings.security.change-password.success'))
|
||||||
this.formData = {
|
this.formData = {
|
||||||
oldPassword: '',
|
oldPassword: '',
|
||||||
newPassword: '',
|
password: '',
|
||||||
confirmPassword: '',
|
passwordConfirmation: '',
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.$toast.error(err.message)
|
this.$toast.error(err.message)
|
||||||
@ -111,15 +86,6 @@ export default {
|
|||||||
this.loading = false
|
this.loading = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
matchPassword(rule, value, callback, source, options) {
|
|
||||||
var errors = []
|
|
||||||
if (this.formData.newPassword !== value) {
|
|
||||||
errors.push(
|
|
||||||
new Error(this.$t('settings.security.change-password.message-new-password-missmatch')),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
callback(errors)
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -47,8 +47,8 @@ describe('ChangePassword ', () => {
|
|||||||
describe('submitting new password', () => {
|
describe('submitting new password', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
wrapper.find('input#newPassword').setValue('supersecret')
|
wrapper.find('input#password').setValue('supersecret')
|
||||||
wrapper.find('input#confirmPassword').setValue('supersecret')
|
wrapper.find('input#passwordConfirmation').setValue('supersecret')
|
||||||
wrapper.find('form').trigger('submit')
|
wrapper.find('form').trigger('submit')
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -58,7 +58,7 @@ describe('ChangePassword ', () => {
|
|||||||
|
|
||||||
it('delivers new password to backend', () => {
|
it('delivers new password to backend', () => {
|
||||||
const expected = expect.objectContaining({
|
const expected = expect.objectContaining({
|
||||||
variables: { code: '123456', email: 'mail@example.org', newPassword: 'supersecret' },
|
variables: { code: '123456', email: 'mail@example.org', password: 'supersecret' },
|
||||||
})
|
})
|
||||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,36 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<ds-card class="verify-code">
|
<ds-card class="verify-code">
|
||||||
<ds-space margin="large">
|
<ds-space margin="large">
|
||||||
<template>
|
|
||||||
<ds-form
|
<ds-form
|
||||||
v-if="!changePasswordResult"
|
v-if="!changePasswordResult"
|
||||||
v-model="formData"
|
v-model="formData"
|
||||||
:schema="formSchema"
|
:schema="formSchema"
|
||||||
@submit="handleSubmitPassword"
|
@submit="handleSubmitPassword"
|
||||||
@input="handleInput"
|
|
||||||
@input-valid="handleInputValid"
|
|
||||||
class="change-password"
|
class="change-password"
|
||||||
>
|
>
|
||||||
|
<template slot-scope="{ errors }">
|
||||||
<ds-input
|
<ds-input
|
||||||
id="newPassword"
|
id="password"
|
||||||
model="newPassword"
|
model="password"
|
||||||
type="password"
|
type="password"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:label="$t('settings.security.change-password.label-new-password')"
|
:label="$t('settings.security.change-password.label-new-password')"
|
||||||
/>
|
/>
|
||||||
<ds-input
|
<ds-input
|
||||||
id="confirmPassword"
|
id="passwordConfirmation"
|
||||||
model="confirmPassword"
|
model="passwordConfirmation"
|
||||||
type="password"
|
type="password"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:label="$t('settings.security.change-password.label-new-password-confirm')"
|
:label="$t('settings.security.change-password.label-new-password-confirm')"
|
||||||
/>
|
/>
|
||||||
<password-strength :password="formData.newPassword" />
|
<password-strength :password="formData.password" />
|
||||||
<ds-space margin-top="base">
|
<ds-space margin-top="base">
|
||||||
<ds-button :loading="$apollo.loading" :disabled="disabled" primary>
|
<ds-button :loading="$apollo.loading" :disabled="errors" primary>
|
||||||
{{ $t('settings.security.change-password.button') }}
|
{{ $t('settings.security.change-password.button') }}
|
||||||
</ds-button>
|
</ds-button>
|
||||||
</ds-space>
|
</ds-space>
|
||||||
|
</template>
|
||||||
</ds-form>
|
</ds-form>
|
||||||
<ds-text v-else>
|
<ds-text v-else>
|
||||||
<template v-if="changePasswordResult === 'success'">
|
<template v-if="changePasswordResult === 'success'">
|
||||||
@ -48,7 +47,6 @@
|
|||||||
<a href="mailto:support@human-connection.org">support@human-connection.org</a>
|
<a href="mailto:support@human-connection.org">support@human-connection.org</a>
|
||||||
</template>
|
</template>
|
||||||
</ds-text>
|
</ds-text>
|
||||||
</template>
|
|
||||||
</ds-space>
|
</ds-space>
|
||||||
</ds-card>
|
</ds-card>
|
||||||
</template>
|
</template>
|
||||||
@ -57,6 +55,7 @@
|
|||||||
import PasswordStrength from '../Password/Strength'
|
import PasswordStrength from '../Password/Strength'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
||||||
|
import PasswordForm from '~/components/utils/PasswordFormHelper'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -68,48 +67,28 @@ export default {
|
|||||||
code: { type: String, required: true },
|
code: { type: String, required: true },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
const passwordForm = PasswordForm({ translate: this.$t })
|
||||||
return {
|
return {
|
||||||
formData: {
|
formData: {
|
||||||
newPassword: '',
|
...passwordForm.formData,
|
||||||
confirmPassword: '',
|
|
||||||
},
|
},
|
||||||
formSchema: {
|
formSchema: {
|
||||||
newPassword: {
|
...passwordForm.formSchema,
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
message: this.$t('settings.security.change-password.message-new-password-required'),
|
|
||||||
},
|
|
||||||
confirmPassword: [
|
|
||||||
{ validator: this.matchPassword },
|
|
||||||
{
|
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
message: this.$t(
|
|
||||||
'settings.security.change-password.message-new-password-confirm-required',
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
disabled: true,
|
disabled: true,
|
||||||
changePasswordResult: null,
|
changePasswordResult: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async handleInput() {
|
|
||||||
this.disabled = true
|
|
||||||
},
|
|
||||||
async handleInputValid() {
|
|
||||||
this.disabled = false
|
|
||||||
},
|
|
||||||
async handleSubmitPassword() {
|
async handleSubmitPassword() {
|
||||||
const mutation = gql`
|
const mutation = gql`
|
||||||
mutation($code: String!, $email: String!, $newPassword: String!) {
|
mutation($code: String!, $email: String!, $password: String!) {
|
||||||
resetPassword(code: $code, email: $email, newPassword: $newPassword)
|
resetPassword(code: $code, email: $email, newPassword: $password)
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
const { newPassword } = this.formData
|
const { password } = this.formData
|
||||||
const { email, code } = this
|
const { email, code } = this
|
||||||
const variables = { newPassword, email, code }
|
const variables = { password, email, code }
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
data: { resetPassword },
|
data: { resetPassword },
|
||||||
@ -119,22 +98,13 @@ export default {
|
|||||||
this.$emit('passwordResetResponse', this.changePasswordResult)
|
this.$emit('passwordResetResponse', this.changePasswordResult)
|
||||||
}, 3000)
|
}, 3000)
|
||||||
this.formData = {
|
this.formData = {
|
||||||
newPassword: '',
|
password: '',
|
||||||
confirmPassword: '',
|
passwordConfirmation: '',
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.$toast.error(err.message)
|
this.$toast.error(err.message)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
matchPassword(rule, value, callback, source, options) {
|
|
||||||
var errors = []
|
|
||||||
if (this.formData.newPassword !== value) {
|
|
||||||
errors.push(
|
|
||||||
new Error(this.$t('settings.security.change-password.message-new-password-missmatch')),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
callback(errors)
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -128,6 +128,15 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
.ds-card-image img {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 300px;
|
||||||
|
-o-object-fit: cover;
|
||||||
|
object-fit: cover;
|
||||||
|
-o-object-position: center;
|
||||||
|
object-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
.post-card {
|
.post-card {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
132
webapp/components/Registration/CreateUserAccount.spec.js
Normal file
132
webapp/components/Registration/CreateUserAccount.spec.js
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { mount, createLocalVue } from '@vue/test-utils'
|
||||||
|
import CreateUserAccount, { SignupVerificationMutation } from './CreateUserAccount'
|
||||||
|
import Styleguide from '@human-connection/styleguide'
|
||||||
|
|
||||||
|
const localVue = createLocalVue()
|
||||||
|
|
||||||
|
localVue.use(Styleguide)
|
||||||
|
|
||||||
|
describe('CreateUserAccount', () => {
|
||||||
|
let wrapper
|
||||||
|
let Wrapper
|
||||||
|
let mocks
|
||||||
|
let propsData
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks = {
|
||||||
|
$toast: {
|
||||||
|
success: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
},
|
||||||
|
$t: jest.fn(),
|
||||||
|
$apollo: {
|
||||||
|
loading: false,
|
||||||
|
mutate: jest.fn(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
propsData = {}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
Wrapper = () => {
|
||||||
|
return mount(CreateUserAccount, {
|
||||||
|
mocks,
|
||||||
|
propsData,
|
||||||
|
localVue,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('given email and nonce', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
propsData.nonce = '666777'
|
||||||
|
propsData.email = 'sixseven@example.org'
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders a form to create a new user', () => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
expect(wrapper.find('.create-user-account').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('submit', () => {
|
||||||
|
let action
|
||||||
|
beforeEach(() => {
|
||||||
|
action = async () => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
wrapper.find('input#name').setValue('John Doe')
|
||||||
|
wrapper.find('input#password').setValue('hellopassword')
|
||||||
|
wrapper.find('input#passwordConfirmation').setValue('hellopassword')
|
||||||
|
await wrapper.find('form').trigger('submit')
|
||||||
|
await wrapper.html()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls CreateUserAccount graphql mutation', async () => {
|
||||||
|
await action()
|
||||||
|
const expected = expect.objectContaining({ mutation: SignupVerificationMutation })
|
||||||
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('delivers data to backend', async () => {
|
||||||
|
await action()
|
||||||
|
const expected = expect.objectContaining({
|
||||||
|
variables: {
|
||||||
|
about: '',
|
||||||
|
name: 'John Doe',
|
||||||
|
email: 'sixseven@example.org',
|
||||||
|
nonce: '666777',
|
||||||
|
password: 'hellopassword',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('in case mutation resolves', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.$apollo.mutate = jest.fn().mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
SignupVerification: {
|
||||||
|
id: 'u1',
|
||||||
|
name: 'John Doe',
|
||||||
|
slug: 'john-doe',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays success', async () => {
|
||||||
|
await action()
|
||||||
|
expect(mocks.$t).toHaveBeenCalledWith('registration.create-user-account.success')
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('after timeout', () => {
|
||||||
|
beforeEach(jest.useFakeTimers)
|
||||||
|
|
||||||
|
it('emits `userCreated` with { password, email }', async () => {
|
||||||
|
await action()
|
||||||
|
jest.runAllTimers()
|
||||||
|
expect(wrapper.emitted('userCreated')).toEqual([
|
||||||
|
[
|
||||||
|
{
|
||||||
|
email: 'sixseven@example.org',
|
||||||
|
password: 'hellopassword',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('in case mutation rejects', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.$apollo.mutate = jest.fn().mockRejectedValue(new Error('Invalid nonce'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays form errors', async () => {
|
||||||
|
await action()
|
||||||
|
expect(wrapper.find('.backendErrors').text()).toContain('Invalid nonce')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
142
webapp/components/Registration/CreateUserAccount.vue
Normal file
142
webapp/components/Registration/CreateUserAccount.vue
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
<template>
|
||||||
|
<ds-card v-if="success" class="success">
|
||||||
|
<ds-space>
|
||||||
|
<sweetalert-icon icon="success" />
|
||||||
|
<ds-text align="center" bold color="success">
|
||||||
|
{{ $t('registration.create-user-account.success') }}
|
||||||
|
</ds-text>
|
||||||
|
</ds-space>
|
||||||
|
</ds-card>
|
||||||
|
<ds-form
|
||||||
|
v-else
|
||||||
|
class="create-user-account"
|
||||||
|
v-model="formData"
|
||||||
|
:schema="formSchema"
|
||||||
|
@submit="submit"
|
||||||
|
>
|
||||||
|
<template slot-scope="{ errors }">
|
||||||
|
<ds-card :header="$t('registration.create-user-account.title')">
|
||||||
|
<ds-input
|
||||||
|
id="name"
|
||||||
|
model="name"
|
||||||
|
icon="user"
|
||||||
|
:label="$t('settings.data.labelName')"
|
||||||
|
:placeholder="$t('settings.data.namePlaceholder')"
|
||||||
|
/>
|
||||||
|
<ds-input
|
||||||
|
id="bio"
|
||||||
|
model="about"
|
||||||
|
type="textarea"
|
||||||
|
rows="3"
|
||||||
|
:label="$t('settings.data.labelBio')"
|
||||||
|
:placeholder="$t('settings.data.labelBio')"
|
||||||
|
/>
|
||||||
|
<ds-input
|
||||||
|
id="password"
|
||||||
|
model="password"
|
||||||
|
type="password"
|
||||||
|
autocomplete="off"
|
||||||
|
:label="$t('settings.security.change-password.label-new-password')"
|
||||||
|
/>
|
||||||
|
<ds-input
|
||||||
|
id="passwordConfirmation"
|
||||||
|
model="passwordConfirmation"
|
||||||
|
type="password"
|
||||||
|
autocomplete="off"
|
||||||
|
:label="$t('settings.security.change-password.label-new-password-confirm')"
|
||||||
|
/>
|
||||||
|
<password-strength :password="formData.password" />
|
||||||
|
<template slot="footer">
|
||||||
|
<ds-space class="backendErrors" v-if="backendErrors">
|
||||||
|
<ds-text align="center" bold color="danger">
|
||||||
|
{{ backendErrors.message }}
|
||||||
|
</ds-text>
|
||||||
|
</ds-space>
|
||||||
|
<ds-button
|
||||||
|
style="float: right;"
|
||||||
|
icon="check"
|
||||||
|
type="submit"
|
||||||
|
:loading="$apollo.loading"
|
||||||
|
:disabled="errors"
|
||||||
|
primary
|
||||||
|
>
|
||||||
|
{{ $t('actions.save') }}
|
||||||
|
</ds-button>
|
||||||
|
</template>
|
||||||
|
</ds-card>
|
||||||
|
</template>
|
||||||
|
</ds-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import PasswordStrength from '../Password/Strength'
|
||||||
|
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
||||||
|
import PasswordForm from '~/components/utils/PasswordFormHelper'
|
||||||
|
|
||||||
|
export const SignupVerificationMutation = gql`
|
||||||
|
mutation($nonce: String!, $name: String!, $email: String!, $password: String!) {
|
||||||
|
SignupVerification(nonce: $nonce, email: $email, name: $name, password: $password) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
PasswordStrength,
|
||||||
|
SweetalertIcon,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
const passwordForm = PasswordForm({ translate: this.$t })
|
||||||
|
return {
|
||||||
|
formData: {
|
||||||
|
name: '',
|
||||||
|
about: '',
|
||||||
|
...passwordForm.formData,
|
||||||
|
},
|
||||||
|
formSchema: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
min: 3,
|
||||||
|
},
|
||||||
|
about: {
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
...passwordForm.formSchema,
|
||||||
|
},
|
||||||
|
disabled: true,
|
||||||
|
success: null,
|
||||||
|
backendErrors: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
nonce: { type: String, required: true },
|
||||||
|
email: { type: String, required: true },
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async submit() {
|
||||||
|
const { name, password, about } = this.formData
|
||||||
|
const { email, nonce } = this
|
||||||
|
try {
|
||||||
|
await this.$apollo.mutate({
|
||||||
|
mutation: SignupVerificationMutation,
|
||||||
|
variables: { name, password, about, email, nonce },
|
||||||
|
})
|
||||||
|
this.success = true
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$emit('userCreated', {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
}, 3000)
|
||||||
|
} catch (err) {
|
||||||
|
this.backendErrors = err
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
146
webapp/components/Registration/Signup.spec.js
Normal file
146
webapp/components/Registration/Signup.spec.js
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { mount, createLocalVue } from '@vue/test-utils'
|
||||||
|
import Signup, { SignupMutation, SignupByInvitationMutation } from './Signup'
|
||||||
|
import Styleguide from '@human-connection/styleguide'
|
||||||
|
|
||||||
|
const localVue = createLocalVue()
|
||||||
|
|
||||||
|
localVue.use(Styleguide)
|
||||||
|
|
||||||
|
describe('Signup', () => {
|
||||||
|
let wrapper
|
||||||
|
let Wrapper
|
||||||
|
let mocks
|
||||||
|
let propsData
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks = {
|
||||||
|
$toast: {
|
||||||
|
success: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
},
|
||||||
|
$t: jest.fn(),
|
||||||
|
$apollo: {
|
||||||
|
loading: false,
|
||||||
|
mutate: jest.fn().mockResolvedValue({ data: { Signup: { email: 'mail@example.org' } } }),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
propsData = {}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(jest.useFakeTimers)
|
||||||
|
|
||||||
|
Wrapper = () => {
|
||||||
|
return mount(Signup, {
|
||||||
|
mocks,
|
||||||
|
propsData,
|
||||||
|
localVue,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('without invitation code', () => {
|
||||||
|
it('renders signup form', () => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
expect(wrapper.find('.signup').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('submit', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
wrapper.find('input#email').setValue('mail@example.org')
|
||||||
|
await wrapper.find('form').trigger('submit')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls Signup graphql mutation', () => {
|
||||||
|
const expected = expect.objectContaining({ mutation: SignupMutation })
|
||||||
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('delivers email to backend', () => {
|
||||||
|
const expected = expect.objectContaining({
|
||||||
|
variables: { email: 'mail@example.org', token: null },
|
||||||
|
})
|
||||||
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hides form to avoid re-submission', () => {
|
||||||
|
expect(wrapper.find('form').exists()).not.toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays a message that a mail for email verification was sent', () => {
|
||||||
|
const expected = ['registration.signup.form.success', { email: 'mail@example.org' }]
|
||||||
|
expect(mocks.$t).toHaveBeenCalledWith(...expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('after animation', () => {
|
||||||
|
beforeEach(jest.runAllTimers)
|
||||||
|
|
||||||
|
it('emits `handleSubmitted`', () => {
|
||||||
|
expect(wrapper.emitted('handleSubmitted')).toEqual([[{ email: 'mail@example.org' }]])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with invitation code', () => {
|
||||||
|
let action
|
||||||
|
beforeEach(() => {
|
||||||
|
propsData.token = '666777'
|
||||||
|
action = async () => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
wrapper.find('input#email').setValue('mail@example.org')
|
||||||
|
await wrapper.find('form').trigger('submit')
|
||||||
|
await wrapper.html()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('submit', () => {
|
||||||
|
it('calls SignupByInvitation graphql mutation', async () => {
|
||||||
|
await action()
|
||||||
|
const expected = expect.objectContaining({ mutation: SignupByInvitationMutation })
|
||||||
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('delivers invitation token to backend', async () => {
|
||||||
|
await action()
|
||||||
|
const expected = expect.objectContaining({
|
||||||
|
variables: { email: 'mail@example.org', token: '666777' },
|
||||||
|
})
|
||||||
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('in case a user account with the email already exists', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.$apollo.mutate = jest
|
||||||
|
.fn()
|
||||||
|
.mockRejectedValue(
|
||||||
|
new Error('UserInputError: User account with this email already exists.'),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('explains the error', async () => {
|
||||||
|
await action()
|
||||||
|
expect(mocks.$t).toHaveBeenCalledWith('registration.signup.form.errors.email-exists')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('in case the invitation code was incorrect', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.$apollo.mutate = jest
|
||||||
|
.fn()
|
||||||
|
.mockRejectedValue(
|
||||||
|
new Error('UserInputError: Invitation code already used or does not exist.'),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('explains the error', async () => {
|
||||||
|
await action()
|
||||||
|
expect(mocks.$t).toHaveBeenCalledWith(
|
||||||
|
'registration.signup.form.errors.invalid-invitation-token',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
141
webapp/components/Registration/Signup.vue
Normal file
141
webapp/components/Registration/Signup.vue
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
<template>
|
||||||
|
<ds-card class="signup">
|
||||||
|
<ds-space margin="large">
|
||||||
|
<ds-form
|
||||||
|
v-if="!success && !error"
|
||||||
|
@input="handleInput"
|
||||||
|
@input-valid="handleInputValid"
|
||||||
|
v-model="formData"
|
||||||
|
:schema="formSchema"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
>
|
||||||
|
<h1>{{ $t('registration.signup.title') }}</h1>
|
||||||
|
<ds-space v-if="token" margin-botton="large">
|
||||||
|
<ds-text v-html="$t('registration.signup.form.invitation-code', { code: token })" />
|
||||||
|
</ds-space>
|
||||||
|
<ds-space margin-botton="large">
|
||||||
|
<ds-text>
|
||||||
|
{{ $t('registration.signup.form.description') }}
|
||||||
|
</ds-text>
|
||||||
|
</ds-space>
|
||||||
|
<ds-input
|
||||||
|
:placeholder="$t('login.email')"
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
model="email"
|
||||||
|
name="email"
|
||||||
|
icon="envelope"
|
||||||
|
/>
|
||||||
|
<ds-button
|
||||||
|
:disabled="disabled"
|
||||||
|
:loading="$apollo.loading"
|
||||||
|
primary
|
||||||
|
fullwidth
|
||||||
|
name="submit"
|
||||||
|
type="submit"
|
||||||
|
icon="envelope"
|
||||||
|
>
|
||||||
|
{{ $t('registration.signup.form.submit') }}
|
||||||
|
</ds-button>
|
||||||
|
</ds-form>
|
||||||
|
<div v-else>
|
||||||
|
<template v-if="!error">
|
||||||
|
<sweetalert-icon icon="info" />
|
||||||
|
<ds-text align="center" v-html="submitMessage" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<sweetalert-icon icon="error" />
|
||||||
|
<ds-text align="center">
|
||||||
|
{{ error.message }}
|
||||||
|
</ds-text>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</ds-space>
|
||||||
|
</ds-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
||||||
|
|
||||||
|
export const SignupMutation = gql`
|
||||||
|
mutation($email: String!) {
|
||||||
|
Signup(email: $email) {
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
export const SignupByInvitationMutation = gql`
|
||||||
|
mutation($email: String!, $token: String!) {
|
||||||
|
SignupByInvitation(email: $email, token: $token) {
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
SweetalertIcon,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
token: { type: String, default: null },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
formData: {
|
||||||
|
email: '',
|
||||||
|
},
|
||||||
|
formSchema: {
|
||||||
|
email: {
|
||||||
|
type: 'email',
|
||||||
|
required: true,
|
||||||
|
message: this.$t('common.validations.email'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disabled: true,
|
||||||
|
success: false,
|
||||||
|
error: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
submitMessage() {
|
||||||
|
const { email } = this.formData
|
||||||
|
return this.$t('registration.signup.form.success', { email })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleInput() {
|
||||||
|
this.disabled = true
|
||||||
|
},
|
||||||
|
handleInputValid() {
|
||||||
|
this.disabled = false
|
||||||
|
},
|
||||||
|
async handleSubmit() {
|
||||||
|
const mutation = this.token ? SignupByInvitationMutation : SignupMutation
|
||||||
|
const { email } = this.formData
|
||||||
|
const { token } = this
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$apollo.mutate({ mutation, variables: { email, token } })
|
||||||
|
this.success = true
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$emit('handleSubmitted', { email })
|
||||||
|
}, 3000)
|
||||||
|
} catch (err) {
|
||||||
|
const { message } = err
|
||||||
|
const mapping = {
|
||||||
|
'User account with this email already exists': 'email-exists',
|
||||||
|
'Invitation code already used or does not exist': 'invalid-invitation-token',
|
||||||
|
}
|
||||||
|
for (const [pattern, key] of Object.entries(mapping)) {
|
||||||
|
if (message.includes(pattern))
|
||||||
|
this.error = { key, message: this.$t(`registration.signup.form.errors.${key}`) }
|
||||||
|
}
|
||||||
|
if (!this.error) {
|
||||||
|
this.$toast.error(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import HcEditor from '~/components/Editor'
|
import HcEditor from '~/components/Editor/Editor'
|
||||||
import PostCommentsQuery from '~/graphql/PostCommentsQuery.js'
|
import PostCommentsQuery from '~/graphql/PostCommentsQuery.js'
|
||||||
import CommentMutations from '~/graphql/CommentMutations.js'
|
import CommentMutations from '~/graphql/CommentMutations.js'
|
||||||
|
|
||||||
|
|||||||
36
webapp/components/utils/PasswordFormHelper.js
Normal file
36
webapp/components/utils/PasswordFormHelper.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
export default function PasswordForm({ translate }) {
|
||||||
|
const passwordMismatchMessage = translate(
|
||||||
|
'settings.security.change-password.message-new-password-missmatch',
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
formData: {
|
||||||
|
password: '',
|
||||||
|
passwordConfirmation: '',
|
||||||
|
},
|
||||||
|
formSchema: {
|
||||||
|
password: {
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
message: translate('settings.security.change-password.message-new-password-required'),
|
||||||
|
},
|
||||||
|
passwordConfirmation: [
|
||||||
|
{
|
||||||
|
validator(rule, value, callback, source, options) {
|
||||||
|
var errors = []
|
||||||
|
if (source.password !== value) {
|
||||||
|
errors.push(new Error(passwordMismatchMessage))
|
||||||
|
}
|
||||||
|
callback(errors)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
message: translate(
|
||||||
|
'settings.security.change-password.message-new-password-confirm-required',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -50,7 +50,6 @@ export default () => {
|
|||||||
content
|
content
|
||||||
contentExcerpt
|
contentExcerpt
|
||||||
language
|
language
|
||||||
image
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
# LAYOUTS
|
# LAYOUTS
|
||||||
|
|
||||||
**This directory is not required, you can delete it if you don't want to use it.**
|
|
||||||
|
|
||||||
This directory contains your Application Layouts.
|
This directory contains your Application Layouts.
|
||||||
|
|
||||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/views#layouts).
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/views#layouts).
|
||||||
|
|
||||||
|
|||||||
@ -5,9 +5,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<ds-flex>
|
<ds-flex>
|
||||||
<ds-flex-item :width="{ base: '49px', md: '150px' }">
|
<ds-flex-item :width="{ base: '49px', md: '150px' }">
|
||||||
<a v-router-link style="display: inline-flex" href="/">
|
<nuxt-link to="/">
|
||||||
<ds-logo />
|
<ds-logo />
|
||||||
</a>
|
</nuxt-link>
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
<ds-flex-item>
|
<ds-flex-item>
|
||||||
<div id="nav-search-box" v-on:click="unfolded" @blur.capture="foldedup">
|
<div id="nav-search-box" v-on:click="unfolded" @blur.capture="foldedup">
|
||||||
@ -86,13 +86,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</ds-container>
|
</ds-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ds-container style="word-break: break-all">
|
<ds-container style="word-break: break-all">
|
||||||
<div style="padding: 6rem 2rem 5rem;">
|
<div class="main-container" :width="{ base: '100%', md: '96%' }">
|
||||||
<nuxt />
|
<nuxt />
|
||||||
</div>
|
</div>
|
||||||
</ds-container>
|
</ds-container>
|
||||||
|
<div id="footer" class="ds-footer">
|
||||||
|
<a href="https://human-connection.org" target="_blank" v-html="$t('site.made')"></a>
|
||||||
|
-
|
||||||
|
<nuxt-link to="/imprint">{{ $t('site.imprint') }}</nuxt-link>
|
||||||
|
‑
|
||||||
|
<nuxt-link to="/terms-and-conditions">{{ $t('site.termsAc') }}</nuxt-link>
|
||||||
|
‑
|
||||||
|
<nuxt-link to="/privacy">{{ $t('site.privacy') }}</nuxt-link>
|
||||||
|
‑
|
||||||
|
<nuxt-link to="/changelog">{{ $t('site.changelog') }}</nuxt-link>
|
||||||
|
</div>
|
||||||
<div id="overlay" />
|
<div id="overlay" />
|
||||||
<no-ssr>
|
<no-ssr>
|
||||||
<modal />
|
<modal />
|
||||||
@ -217,6 +226,11 @@ export default {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
padding-top: 6rem;
|
||||||
|
padding-botton: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.main-navigation {
|
.main-navigation {
|
||||||
a {
|
a {
|
||||||
color: $text-color-soft;
|
color: $text-color-soft;
|
||||||
@ -271,4 +285,13 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.ds-footer {
|
||||||
|
text-align: center;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0px;
|
||||||
|
z-index: 10;
|
||||||
|
background-color: white;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,6 +1,23 @@
|
|||||||
{
|
{
|
||||||
"filter-menu": {
|
"filter-menu": {
|
||||||
"title": "Deine Filterblase"
|
"title": "Deine Filterblase",
|
||||||
|
"hashtag-search": "Suche nach #{hashtag}",
|
||||||
|
"clearSearch": "Suche löschen"
|
||||||
|
},
|
||||||
|
"site": {
|
||||||
|
"made": "Mit ❤ gemacht",
|
||||||
|
"imprint": "Impressum",
|
||||||
|
"termsAc": "Nutzungsbedingungen",
|
||||||
|
"privacy": "Datenschutz",
|
||||||
|
"changelog": "Änderungen & Verlauf",
|
||||||
|
"contact": "Kontakt",
|
||||||
|
"tribunal": "Registergericht",
|
||||||
|
"register": "Registernummer",
|
||||||
|
"director": "Geschäftsführer",
|
||||||
|
"taxident": "Umsatzsteuer-Identifikationsnummer gemäß § 27 a Umsatzsteuergesetz (Deutschland)",
|
||||||
|
"responsible": "Verantwortlicher gemäß § 55 Abs. 2 RStV ",
|
||||||
|
"bank": "Bankverbindung",
|
||||||
|
"germany": "Deutschland"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"copy": "Wenn Du bereits ein Konto bei Human Connection hast, melde Dich bitte hier an.",
|
"copy": "Wenn Du bereits ein Konto bei Human Connection hast, melde Dich bitte hier an.",
|
||||||
@ -12,7 +29,8 @@
|
|||||||
"moreInfo": "Was ist Human Connection?",
|
"moreInfo": "Was ist Human Connection?",
|
||||||
"moreInfoURL": "https://human-connection.org",
|
"moreInfoURL": "https://human-connection.org",
|
||||||
"moreInfoHint": "zur Präsentationsseite",
|
"moreInfoHint": "zur Präsentationsseite",
|
||||||
"hello": "Hallo"
|
"hello": "Hallo",
|
||||||
|
"success": "Du bist eingeloggt!"
|
||||||
},
|
},
|
||||||
"password-reset": {
|
"password-reset": {
|
||||||
"title": "Passwort zurücksetzen",
|
"title": "Passwort zurücksetzen",
|
||||||
@ -22,6 +40,24 @@
|
|||||||
"submitted": "Eine E-Mail mit weiteren Instruktionen wurde verschickt an <b>{email}</b>"
|
"submitted": "Eine E-Mail mit weiteren Instruktionen wurde verschickt an <b>{email}</b>"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"registration": {
|
||||||
|
"signup": {
|
||||||
|
"title": "Mach mit bei Human Connection!",
|
||||||
|
"form": {
|
||||||
|
"description": "Um loszulegen, gib deine E-Mail Adresse ein:",
|
||||||
|
"errors": {
|
||||||
|
"email-exists": "Es gibt schon ein Benutzerkonto mit dieser E-Mail Adresse!",
|
||||||
|
"invalid-invitation-token": "Es sieht so aus, als ob der Einladungscode schon eingelöst wurde. Jeder Code kann nur einmalig benutzt werden."
|
||||||
|
},
|
||||||
|
"submit": "Konto erstellen",
|
||||||
|
"success": "Eine Mail mit einem Bestätigungslink für die Registrierung wurde an <b>{email}</b> geschickt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"create-user-account": {
|
||||||
|
"title": "Benutzerkonto anlegen",
|
||||||
|
"success": "Dein Benutzerkonto wurde erstellt!"
|
||||||
|
}
|
||||||
|
},
|
||||||
"verify-code": {
|
"verify-code": {
|
||||||
"form": {
|
"form": {
|
||||||
"code": "Code eingeben",
|
"code": "Code eingeben",
|
||||||
@ -35,7 +71,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"editor": {
|
"editor": {
|
||||||
"placeholder": "Schreib etwas Inspirierendes..."
|
"placeholder": "Schreib etwas Inspirierendes...",
|
||||||
|
"mention": {
|
||||||
|
"noUsersFound": "Keine Benutzer gefunden"
|
||||||
|
},
|
||||||
|
"hashtag": {
|
||||||
|
"noHashtagsFound": "Keine Hashtags gefunden",
|
||||||
|
"addHashtag": "Neuer Hashtag",
|
||||||
|
"addLetter": "Tippe einen Buchstaben"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"name": "Mein Profil",
|
"name": "Mein Profil",
|
||||||
@ -164,6 +208,11 @@
|
|||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"name": "Einstellungen"
|
"name": "Einstellungen"
|
||||||
|
},
|
||||||
|
"invites": {
|
||||||
|
"name": "Benutzer einladen",
|
||||||
|
"title": "Benutzer als Admin anmelden",
|
||||||
|
"description": "Dieses Anmeldeformular ist zu sehen sobald die Anmeldung öffentlich zugänglich ist."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"post": {
|
"post": {
|
||||||
@ -343,6 +392,9 @@
|
|||||||
"categories": {
|
"categories": {
|
||||||
"infoSelectedNoOfMaxCategories": "{chosen} von {max} Kategorien ausgewählt"
|
"infoSelectedNoOfMaxCategories": "{chosen} von {max} Kategorien ausgewählt"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"terms": {
|
||||||
|
"text": "<div ><ol><li><strong>UNFALLGEFAHR: </strong>Das ist eine Testversion! Alle Daten, Dein Profil und die Server können jederzeit komplett vernichtet, verloren, verbrannt und vielleicht auch in der Nähe von Alpha Centauri synchronisiert werden. Die Benutzung läuft auf eigene Gefahr. Mit kommerziellen Nebenwirkungen ist jedoch nicht zu rechnen.</li><br><li><strong>DU UND DEINE DATEN: </strong>Bitte beachte, dass wir die Inhalte der Alphaversion zu Werbezwecken, Webpräsentationen usw. verwenden, aber wir glauben, dass das auch in Deinem Interesse ist. Am besten keinen Nachnamen eingeben und bei noch mehr Datensparsamkeit ein Profilfoto ohne Identität verwenden. Mehr in unserer <a href='/pages/privacy' target='_blank'>Datenschutzerklärung</a>.</li><br><li><strong>BAUSTELLEN: </strong>Das ist immer noch eine Testversion. Wenn etwas nicht funktioniert, blockiert, irritiert, falsch angezeigt, verbogen oder nicht anklickbar ist, bitten wir dies zu entschuldigen. Fehler, Käfer und Bugs bitte melden! <a href='http://localhost:3000/%22https://human-connection.org/alpha/#bugreport%5C%22' target='_blank'>https://human-connection.org/support</a></li><br><li><strong>VERHALTENSCODEX</strong>: Die Verhaltensregeln dienen als Leitsätze für den persönlichen Auftritt und den Umgang untereinander. Wer als Nutzer im Human Connection Netzwerk aktiv ist, Beiträge verfasst, kommentiert oder mit anderen Nutzern, auch außerhalb des Netzwerkes, Kontakt aufnimmt, erkennt diese Verhaltensregeln als verbindlich an: <a href='https://alpha.human-connection.org/pages/code-of-conduct' target='_blank'>https://alpha.human-connection.org/pages/code-of-conduct</a></li><br><li><strong>MODERATION: </strong>Solange kein Community-Moderationssystem lauffähig ist, entscheidet ein Regenbogen-Einhorn darüber, ob Du körperlich und psychisch stabil genug bist, unsere Testversion zu bedienen. Das Einhorn kann Dich jederzeit von der Alpha entfernen. Also sei nett und lass Regenbogenfutter da!</li><br><li><strong>FAIRNESS: </strong>Sollte Dir die Alphaversion unseres Netzwerks wider Erwarten, egal aus welchen Gründen, nicht gefallen, überweisen wir Dir Deine gespendeten Monatsbeiträge innerhalb der ersten 2 Monate gerne zurück. Einfach Mail an: <a href='mailto:info@human-connection.org' target='_blank'>info@human-connection.org </a><strong>Achtung: Viele Funktionen werden erst nach und nach eingebaut. </strong></li><br><li><strong>FRAGEN?</strong> Die Termine und Links zu den Zoom-Räumen findest Du hier: <a href='http://localhost:3000/%22https://human-connection.org/events-und-news//%22' target='_blank'>https://human-connection.org/veranstaltungen/</a></li><br><li><strong>VON MENSCHEN FÜR MENSCHEN: </strong>Bitte hilf uns weitere monatlichen Spender für Human Connection zu bekommen, damit das Netzwerk so schnell wie möglich offiziell an den Start gehen kann. <a href='http://localhost:3000/%22https://human-connection.org/alpha/#bugreport%5C%22' target='_blank'>https://human-connection.org</a></li></ol><p>Jetzt aber viel Spaß mit der Alpha von Human Connection! Für den ersten Weltfrieden. ♥︎</p><br><p><strong>Herzlichst,</strong></p><p><strong>Euer Human Connection Team</strong></p></div>"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,23 @@
|
|||||||
{
|
{
|
||||||
"filter-menu": {
|
"filter-menu": {
|
||||||
"title": "Your filter bubble"
|
"title": "Your filter bubble",
|
||||||
|
"hashtag-search": "Searching for #{hashtag}",
|
||||||
|
"clearSearch": "Clear search"
|
||||||
|
},
|
||||||
|
"site": {
|
||||||
|
"made": "Made with ❤",
|
||||||
|
"imprint": "Imprint",
|
||||||
|
"termsAc": "Terms and conditions",
|
||||||
|
"privacy": "Data privacy",
|
||||||
|
"changelog": "Changes & History",
|
||||||
|
"contact": "Contact",
|
||||||
|
"tribunal": "Registry court",
|
||||||
|
"register": "Registry number",
|
||||||
|
"director": "Managing Director",
|
||||||
|
"taxident": "Value added tax identification number according to § 27 a Value Added Tax Act (Germany)",
|
||||||
|
"responsible": "Responsible according to § 55 Abs. 2 RStV (Germany) ",
|
||||||
|
"bank": "bank account",
|
||||||
|
"germany": "Germany"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"copy": "If you already have a human-connection account, login here.",
|
"copy": "If you already have a human-connection account, login here.",
|
||||||
@ -12,7 +29,8 @@
|
|||||||
"moreInfo": "What is Human Connection?",
|
"moreInfo": "What is Human Connection?",
|
||||||
"moreInfoURL": "https://human-connection.org/en/",
|
"moreInfoURL": "https://human-connection.org/en/",
|
||||||
"moreInfoHint": "to the presentation page",
|
"moreInfoHint": "to the presentation page",
|
||||||
"hello": "Hello"
|
"hello": "Hello",
|
||||||
|
"success": "You are logged in!"
|
||||||
},
|
},
|
||||||
"password-reset": {
|
"password-reset": {
|
||||||
"title": "Reset your password",
|
"title": "Reset your password",
|
||||||
@ -22,6 +40,25 @@
|
|||||||
"submitted": "A mail with further instruction has been sent to <b>{email}</b>"
|
"submitted": "A mail with further instruction has been sent to <b>{email}</b>"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"registration": {
|
||||||
|
"signup": {
|
||||||
|
"title": "Join Human Connection!",
|
||||||
|
"form": {
|
||||||
|
"description": "To get started, enter your email address:",
|
||||||
|
"invitation-code": "Your invitation code is: <b>{code}</b>",
|
||||||
|
"errors": {
|
||||||
|
"email-exists": "There is already a user account with this email address!",
|
||||||
|
"invalid-invitation-token": "It looks like as if the invitation has been used already. Invitation links can only be used once."
|
||||||
|
},
|
||||||
|
"submit": "Create an account",
|
||||||
|
"success": "A mail with a link to complete your registration has been sent to <b>{email}</b>"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"create-user-account": {
|
||||||
|
"title": "Create user account",
|
||||||
|
"success": "Your account has been created!"
|
||||||
|
}
|
||||||
|
},
|
||||||
"verify-code": {
|
"verify-code": {
|
||||||
"form": {
|
"form": {
|
||||||
"code": "Enter your code",
|
"code": "Enter your code",
|
||||||
@ -35,7 +72,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"editor": {
|
"editor": {
|
||||||
"placeholder": "Leave your inspirational thoughts..."
|
"placeholder": "Leave your inspirational thoughts...",
|
||||||
|
"mention": {
|
||||||
|
"noUsersFound": "No users found"
|
||||||
|
},
|
||||||
|
"hashtag": {
|
||||||
|
"noHashtagsFound": "No hashtags found",
|
||||||
|
"addHashtag": "New hashtag",
|
||||||
|
"addLetter": "Type a letter"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"name": "My Profile",
|
"name": "My Profile",
|
||||||
@ -73,7 +118,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!"
|
||||||
},
|
},
|
||||||
@ -164,6 +209,11 @@
|
|||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"name": "Settings"
|
"name": "Settings"
|
||||||
|
},
|
||||||
|
"invites": {
|
||||||
|
"name": "Invite users",
|
||||||
|
"title": "Signup users as admin",
|
||||||
|
"description": "This registration form will be visible as soon as the registration is open to the public."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"post": {
|
"post": {
|
||||||
@ -343,5 +393,8 @@
|
|||||||
"categories": {
|
"categories": {
|
||||||
"infoSelectedNoOfMaxCategories": "{chosen} of {max} categories selected"
|
"infoSelectedNoOfMaxCategories": "{chosen} of {max} categories selected"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"terms": {
|
||||||
|
"text": "<div><ol><li><strong>RISK OF ACCIDENT:</strong> This is a test version! All data, your profile and the server can be completely destroyed, wiped out, lost, burnt and eventually synchronised near Alpha Centauri at any time. Use on your own risk. Commercial effects are not likely though.</li><br><li><strong>YOU AND YOUR DATA:</strong> Please notice, that the content of the alpha version will be used for publicity and web presentations etc. but we are sure, this is to your interest. If you like use no surnames and if you want to disclose less data use a profile picture without identity. You can find more information in our <a href='/pages/privacy' target='_blank'>privacy policy</a>.</li><br><li><strong>SITE:</strong> This is still a test version. Please excuse if some applications are not working, blocking, irritating, displayed falsely or not able to be clicked on. Please report faults and bugs! <a href='https://human-connection.org/support' target='_blank'>https://human-connection.org/support</a></li><br><li><strong>CODE OF CONDUCT</strong>: The code of conduct serves as guiding principles for our personal appearance and interaction with one another. Anyone who is active as a user in the Human Connection Network, writes articles, comments or contacts other users, including those outside the network, acknowledges these rules of conduct as binding: <a href='https://alpha.human-connection.org/pages/code-of-conduct' target='_blank'>https://alpha.human-connection.org/pages/code-of-conduct</a></li><br><li><strong>MODERATION:</strong> As long as there is no community moderation-system in operation, a rainbow colored unicorn decides, if you are physically and mentally stable enough to operate our test version. The unicorn can delete you from the alpha version at any time. So be so kind and leave rainbow food!</li><br><li><strong>FAIRNESS:</strong> If, against all expectations, our alpha version is not to your liking, we return your monthly payment within the first two months. Please send a mail to: <a href='mailto:info@human-connection.org' target='_blank'>info@human-connection.org</a>. <strong>Attention: Pleace note that more features are build in on regular basis.</strong></li><br><li><strong>QUESTIONS?</strong> You can find the dates and links to our zoom-rooms here: <a href='https://human-connection.org/events-und-news/' target='_blank'>https://human-connection.org/events-und-news/</a></li><br><li><strong>FROM HUMAN BEING TO HUMAN BEING: </strong>Please help us to get new donators for Human Connection, so the network can take off as soon as possible. <a href='https://human-connection.org/' target='_blank'>https://human-connection.org</a></li></ol><br><p>Now have fun with the alpha version of Human Connection! For the first universal peace.<strong> ♥︎</strong></p><p><strong> </strong></p><br><p><strong>Thank you very much, </strong></p><p><strong>your Human Connection Team</strong></p></div>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,21 @@
|
|||||||
"filter-menu": {
|
"filter-menu": {
|
||||||
"title": "Su burbuja de filtro"
|
"title": "Su burbuja de filtro"
|
||||||
},
|
},
|
||||||
|
"site": {
|
||||||
|
"made": "Con ❤ realizado",
|
||||||
|
"imprint": "Pie de imprenta",
|
||||||
|
"termsAc": "términos y condiciones",
|
||||||
|
"privacy": "protección de datos",
|
||||||
|
"changelog": "Cambios e historia",
|
||||||
|
"contact": "Contacto",
|
||||||
|
"tribunal": "tribunal de registro",
|
||||||
|
"register": "número de registro",
|
||||||
|
"director": "Director General",
|
||||||
|
"taxident": "Número de identificación del impuesto sobre el valor añadido según el § 27 a de la Ley del Impuesto sobre el Valor Añadido (Alemania)",
|
||||||
|
"responsible": "Responsable según § 55 Abs. 2 RStV (Alemania)",
|
||||||
|
"bank": "cuenta",
|
||||||
|
"germany": "Alemania"
|
||||||
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"copy": "Si ya tiene una cuenta de Human Connection, inicie sesión aquí.",
|
"copy": "Si ya tiene una cuenta de Human Connection, inicie sesión aquí.",
|
||||||
"login": "Iniciar sesión",
|
"login": "Iniciar sesión",
|
||||||
@ -43,7 +58,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,6 +2,21 @@
|
|||||||
"filter-menu": {
|
"filter-menu": {
|
||||||
"title": "Votre bulle de filtre"
|
"title": "Votre bulle de filtre"
|
||||||
},
|
},
|
||||||
|
"site": {
|
||||||
|
"made": "Avec ❤ fait",
|
||||||
|
"imprint": "Mentions légales",
|
||||||
|
"termsAc": "modalités et conditions",
|
||||||
|
"privacy": "protection des données",
|
||||||
|
"changelog": "Changements et historique",
|
||||||
|
"contact": "Contacter",
|
||||||
|
"tribunal": "tribunal de registre",
|
||||||
|
"register": "numéro de registre",
|
||||||
|
"director": "Directeur Général",
|
||||||
|
"taxident": "Numéro d'identification à la taxe sur la valeur ajoutée selon § 27 a de la loi sur la taxe sur la valeur ajoutée (Allemagne)",
|
||||||
|
"responsible": "Responsable selon § 55 Abs. 2 RStV (Allemagne)",
|
||||||
|
"bank": "Bankverbindung",
|
||||||
|
"germany": "Allemagne"
|
||||||
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"copy": "Si vous avez déjà un compte human-connection, connectez-vous ici.",
|
"copy": "Si vous avez déjà un compte human-connection, connectez-vous ici.",
|
||||||
"login": "Connexion",
|
"login": "Connexion",
|
||||||
|
|||||||
@ -1,4 +1,19 @@
|
|||||||
{
|
{
|
||||||
|
"site": {
|
||||||
|
"made": "Con ❤ fatto",
|
||||||
|
"imprint": "Impressum",
|
||||||
|
"termsAc": "Condizioni d'uso",
|
||||||
|
"privacy": "protezione dei dati",
|
||||||
|
"changelog": "Cambiamenti e storia",
|
||||||
|
"contact": "Contatto",
|
||||||
|
"tribunal": "registro tribunale",
|
||||||
|
"register": "numero di registro",
|
||||||
|
"director": "Direttore Generale",
|
||||||
|
"taxident": "Numero di identificazione dell'imposta sul valore aggiunto ai sensi del § 27 a Legge sull'imposta sul valore aggiunto (Germania)",
|
||||||
|
"responsible": "Responsabile ai sensi del § 55 Abs. 2 RStV (Germania)",
|
||||||
|
"bank": "conto bancario",
|
||||||
|
"germany": "Germania"
|
||||||
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"copy": "Se sei gia registrato su Human Connection, accedi qui.",
|
"copy": "Se sei gia registrato su Human Connection, accedi qui.",
|
||||||
"login": "Accesso",
|
"login": "Accesso",
|
||||||
|
|||||||
@ -1,4 +1,19 @@
|
|||||||
{
|
{
|
||||||
|
"site": {
|
||||||
|
"made": "Met ❤ gemaakt",
|
||||||
|
"imprint": "Afdruk",
|
||||||
|
"termsAc": "Gebruiksvoorwaarden",
|
||||||
|
"privacy": "gegevensbescherming",
|
||||||
|
"changelog": "Veranderingen & Geschiedenis",
|
||||||
|
"contact": "contact",
|
||||||
|
"tribunal": "registerrechtbank",
|
||||||
|
"register": "inschrijfnummer",
|
||||||
|
"director": "Directeur",
|
||||||
|
"taxident": "Identificatienummer voor de belasting over de toegevoegde waarde overeenkomstig § 27 a Wet op de belasting over de toegevoegde waarde (Duitsland).",
|
||||||
|
"responsible": "Verantwoordelijk volgens § 55 Abs. 2 RStV (Duitsland).",
|
||||||
|
"bank": "bankrekening",
|
||||||
|
"germany": "Duitsland"
|
||||||
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"copy": "Als u al een mini-aansluiting account heeft, log dan hier in.",
|
"copy": "Als u al een mini-aansluiting account heeft, log dan hier in.",
|
||||||
"login": "Inloggen",
|
"login": "Inloggen",
|
||||||
|
|||||||
@ -1,4 +1,19 @@
|
|||||||
{
|
{
|
||||||
|
"site": {
|
||||||
|
"made": "Z ❤ zrobiony",
|
||||||
|
"imprint": "Nadruk",
|
||||||
|
"termsAc": "Warunki użytkowania",
|
||||||
|
"privacy": "ochrona danych",
|
||||||
|
"changelog": "Zmiany i historia",
|
||||||
|
"contact": "Kontakt",
|
||||||
|
"tribunal": "sąd rejestrowy",
|
||||||
|
"register": "numer rejestracyjny",
|
||||||
|
"director": "Dyrektor zarządzający",
|
||||||
|
"taxident": "Numer identyfikacyjny podatku od wartości dodanej zgodnie z § 27 a Ustawa o podatku od wartości dodanej (Niemcy)",
|
||||||
|
"responsible": "Odpowiedzialny zgodnie z § 55 Abs. 2 RStV (Niemcy)",
|
||||||
|
"bank": "rachunek bankowy",
|
||||||
|
"germany": "Niemcy"
|
||||||
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"copy": "Jeśli masz już konto Human Connection, zaloguj się tutaj.",
|
"copy": "Jeśli masz już konto Human Connection, zaloguj się tutaj.",
|
||||||
"login": "logowanie",
|
"login": "logowanie",
|
||||||
|
|||||||
@ -1,4 +1,20 @@
|
|||||||
{
|
{
|
||||||
|
"site": {
|
||||||
|
"made": "Com ❤ feito",
|
||||||
|
"imprint": "Impressão",
|
||||||
|
"termsAc": "termos e condições",
|
||||||
|
"privacy": "protecção de dados",
|
||||||
|
"changelog": "Mudanças e Histórico",
|
||||||
|
"contact": "Contato",
|
||||||
|
"tribunal": "tribunal",
|
||||||
|
"tribunal": "tribunal de registo",
|
||||||
|
"register": "número de registo",
|
||||||
|
"director": "Diretor Administrativo",
|
||||||
|
"taxident": "Número de identificação do imposto sobre o valor acrescentado de acordo com o § 27 da Lei do Imposto sobre o Valor Acrescentado (Alemanha)",
|
||||||
|
"responsible": "Responsável segundo § 55 Abs. 2 RStV (Alemanha) ",
|
||||||
|
"bank": "conta bancária",
|
||||||
|
"germany": "Alemanha"
|
||||||
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"copy": "Se você já tem uma conta no Human Connection, entre aqui.",
|
"copy": "Se você já tem uma conta no Human Connection, entre aqui.",
|
||||||
"login": "Entrar",
|
"login": "Entrar",
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
# MIDDLEWARE
|
# MIDDLEWARE
|
||||||
|
|
||||||
**This directory is not required, you can delete it if you don't want to use it.**
|
This directory contains our application middleware. The middleware lets you define custom functions to be ran before rendering a page or a group of pages \(layouts\).
|
||||||
|
|
||||||
This directory contains your application middleware. The middleware lets you define custom function to be ran before rendering a page or a group of pages \(layouts\).
|
|
||||||
|
|
||||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing#middleware).
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing#middleware).
|
||||||
|
|
||||||
|
|||||||
@ -31,10 +31,10 @@ module.exports = {
|
|||||||
'password-reset-request',
|
'password-reset-request',
|
||||||
'password-reset-verify-code',
|
'password-reset-verify-code',
|
||||||
'password-reset-change-password',
|
'password-reset-change-password',
|
||||||
'register',
|
// 'registration-signup', TODO: uncomment to open public registration
|
||||||
'signup',
|
'registration-signup-by-invitation-code',
|
||||||
'reset',
|
'registration-verify-code',
|
||||||
'reset-token',
|
'registration-create-user-account',
|
||||||
'pages-slug',
|
'pages-slug',
|
||||||
],
|
],
|
||||||
// pages to keep alive
|
// pages to keep alive
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user