diff --git a/.codecov.yml b/.codecov.yml index 2767ed675..82b50aba3 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -95,7 +95,7 @@ coverage: # - master #flags: # - integration - paths: + paths: - backend/ # only include coverage in "backend/" folder webapp: # declare a new status context "frontend" against: parent @@ -127,7 +127,7 @@ coverage: # - integration # paths: # - folder - + #changes: # default: # against: parent @@ -150,20 +150,8 @@ coverage: #ignore: # files and folders for processing # - tests/* - + #fixes: # - "old_path::new_path" -comment: - # layout options are quite limited in v4.x - there have been way more options in v1.0 - layout: reach, diff, flags, files # mostly old options: header, diff, uncovered, reach, files, tree, changes, sunburst, flags - behavior: new # default = posts once then update, posts new if delete - # once = post once then updates - # new = delete old, post new - # spammy = post new - require_changes: false # if true: only post the comment if coverage changes - require_base: no # [yes :: must have a base report to post] - require_head: no # [yes :: must have a head report to post] - branches: null # branch names that can post comment - flags: null - paths: null \ No newline at end of file +comment: off diff --git a/.travis.yml b/.travis.yml index f48b0bb36..3c1208496 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,6 @@ addons: before_install: - yarn global add wait-on # Install Codecov - - yarn global add codecov - yarn install - cp cypress.env.template.json cypress.env.json @@ -29,9 +28,10 @@ script: - 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:seed - - docker-compose exec backend yarn run test:cucumber --tags "not @wip" - - docker-compose exec backend yarn run db:reset - - docker-compose exec backend yarn run db:seed + # ActivityPub cucumber testing temporarily disabled because it's too buggy + # - docker-compose exec backend yarn run test:cucumber --tags "not @wip" + # - docker-compose exec backend yarn run db:reset + # - docker-compose exec backend yarn run db:seed # Frontend - docker-compose exec webapp yarn run lint - docker-compose exec webapp yarn run test --ci --verbose=false --coverage @@ -39,7 +39,7 @@ script: # Fullstack - yarn run cypress:run # Coverage - - codecov + - yarn run codecov after_success: - wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 19aaf3301..0eb90a824 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,9 +4,9 @@ Thanks so much for thinking of contributing to the Human Connection project, we ## 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): ![](https://dl.dropbox.com/s/vbmcihkduy9dhko/Screenshot%202019-01-03%2015.50.11.png?dl=0) @@ -17,7 +17,7 @@ Here are some general notes on our development flow: * Currently operating in two week sprints * 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 - * "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 * AgileVentures run open pairing sessions at 10:30am UTC each week on Tuesdays and Thursdays * Core team @@ -51,19 +51,19 @@ But what do we do when waiting for merge into master \(wanting to keep PRs small * solutions * 1\) put 2nd PR into branch that the first PR is hitting - but requires update after merging * 2\) prefer to leave exiting PR until it can be reviewed, and instead go and work on some other part of the codebase that is not impacted by the first PR - + ### Code Review -* Github setting in place - at least one review is required to merge +* Github setting in place - at least one review is required to merge - in principle anyone (who is not the PR owner) can review - but often it will be the core developers (Robert, Ulf, Greg, Wolfgang?) - once there is a review, and presuming no requested changes, PR opener can merge * CI/tests - - the CI needs to pass + - the CI needs to pass - linting <-- autofix? - tests (unit, feature) (backend, frontend) - codecoverage - + ## Notes question: when you want to pick a task - \(find out priority\) - is it in discord? is it in AV slack? --> Robert says you can always ask in discord - group channels are the best @@ -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 - difference between "important" \(creates a lot of value\) and "beginner friendly" \(easy to implement\) - diff --git a/README.md b/README.md index ac7d2a024..411f7d842 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Thank you lokalise for providing us with a premium account :raised_hands:. ## Developer Chat 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! diff --git a/backend/.env.template b/backend/.env.template index 0c80529a1..594c7b28b 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -1,5 +1,5 @@ NEO4J_URI=bolt://localhost:7687 -NEO4J_USER=neo4j +NEO4J_USERNAME=neo4j NEO4J_PASSWORD=letmein GRAPHQL_PORT=4000 GRAPHQL_URI=http://localhost:4000 diff --git a/backend/Dockerfile b/backend/Dockerfile index 2e8667461..935077c98 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -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)" EXPOSE 4000 diff --git a/backend/package.json b/backend/package.json index 599e8eac6..16b0df487 100644 --- a/backend/package.json +++ b/backend/package.json @@ -34,7 +34,6 @@ "!**/src/**/?(*.)+(spec|test).js?(x)" ], "coverageReporters": [ - "text", "lcov" ], "testMatch": [ @@ -42,53 +41,55 @@ ] }, "dependencies": { + "@hapi/joi": "^15.1.0", "activitystrea.ms": "~2.1.3", "apollo-cache-inmemory": "~1.6.2", "apollo-client": "~2.6.3", "apollo-link-context": "~1.0.18", "apollo-link-http": "~1.5.15", - "apollo-server": "~2.6.7", + "apollo-server": "~2.6.9", + "apollo-server-express": "^2.6.9", "bcryptjs": "~2.4.3", "cheerio": "~1.0.0-rc.3", "cors": "~2.8.5", "cross-env": "~5.2.0", - "date-fns": "2.0.0-beta.2", + "date-fns": "2.0.0-beta.1", "debug": "~4.1.1", "dotenv": "~8.0.0", - "express": "~4.17.1", + "express": "^4.17.1", "faker": "Marak/faker.js#master", - "graphql": "~14.4.0", + "graphql": "~14.4.2", "graphql-custom-directives": "~0.2.14", "graphql-iso-date": "~3.6.1", "graphql-middleware": "~3.0.2", - "graphql-shield": "~6.0.2", + "graphql-shield": "~6.0.3", "graphql-tag": "~2.10.1", - "graphql-yoga": "~1.18.0", "helmet": "~3.18.0", "jsonwebtoken": "~8.5.1", "linkifyjs": "~2.1.8", - "lodash": "~4.17.11", + "lodash": "~4.17.14", "merge-graphql-schemas": "^1.5.8", "neo4j-driver": "~1.7.4", "neo4j-graphql-js": "^2.6.3", + "neode": "^0.2.16", "node-fetch": "~2.6.0", - "nodemailer": "^6.2.1", + "nodemailer": "^6.3.0", "npm-run-all": "~4.1.5", "request": "~2.88.0", "sanitize-html": "~1.20.1", "slug": "~1.1.0", "trunc-html": "~1.1.2", "uuid": "~3.3.2", - "wait-on": "~3.2.0" + "wait-on": "~3.3.0" }, "devDependencies": { - "@babel/cli": "~7.4.4", - "@babel/core": "~7.4.5", - "@babel/node": "~7.4.5", + "@babel/cli": "~7.5.0", + "@babel/core": "~7.5.4", + "@babel/node": "~7.5.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", - "apollo-server-testing": "~2.6.7", + "apollo-server-testing": "~2.6.9", "babel-core": "~7.0.0-0", "babel-eslint": "~10.0.2", "babel-jest": "~24.8.0", @@ -98,7 +99,7 @@ "eslint-config-prettier": "~6.0.0", "eslint-config-standard": "~12.0.0", "eslint-plugin-import": "~2.18.0", - "eslint-plugin-jest": "~22.7.1", + "eslint-plugin-jest": "~22.8.0", "eslint-plugin-node": "~9.1.0", "eslint-plugin-prettier": "~3.1.0", "eslint-plugin-promise": "~4.2.1", diff --git a/backend/src/bootstrap/neo4j.js b/backend/src/bootstrap/neo4j.js index bfa68acf3..f9e3a997d 100644 --- a/backend/src/bootstrap/neo4j.js +++ b/backend/src/bootstrap/neo4j.js @@ -1,5 +1,6 @@ import { v1 as neo4j } from 'neo4j-driver' import CONFIG from './../config' +import setupNeode from './neode' let driver @@ -14,3 +15,12 @@ export function getDriver(options = {}) { } 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 +} diff --git a/backend/src/bootstrap/neode.js b/backend/src/bootstrap/neode.js new file mode 100644 index 000000000..65a2074be --- /dev/null +++ b/backend/src/bootstrap/neode.js @@ -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 +} diff --git a/backend/src/helpers/encryptPassword.js b/backend/src/helpers/encryptPassword.js new file mode 100644 index 000000000..ae98af84f --- /dev/null +++ b/backend/src/helpers/encryptPassword.js @@ -0,0 +1,7 @@ +import { hashSync } from 'bcryptjs' + +export default function(args) { + args.encryptedPassword = hashSync(args.password, 10) + delete args.password + return args +} diff --git a/backend/src/index.js b/backend/src/index.js index f28e58947..0e7fc233c 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -1,18 +1,8 @@ import createServer from './server' -import ActivityPub from './activitypub/ActivityPub' import CONFIG from './config' -const serverConfig = { - port: CONFIG.GRAPHQL_PORT, - // cors: { - // credentials: true, - // origin: [CONFIG.CLIENT_URI] // your frontend url. - // } -} - -const server = createServer() -server.start(serverConfig, options => { +const { app } = createServer() +app.listen({ port: CONFIG.GRAPHQL_PORT }, () => { /* eslint-disable-next-line no-console */ console.log(`GraphQLServer ready at ${CONFIG.GRAPHQL_URI} 🚀`) - ActivityPub.init(server) }) diff --git a/backend/src/jest/helpers.js b/backend/src/jest/helpers.js index d07bc9ad1..380aedd16 100644 --- a/backend/src/jest/helpers.js +++ b/backend/src/jest/helpers.js @@ -4,13 +4,20 @@ import { request } from 'graphql-request' // not to be confused with the seeder host export const host = 'http://127.0.0.1:4123' -export async function login({ email, password }) { +export async function login(variables) { const mutation = ` - mutation { - login(email:"${email}", password:"${password}") - }` - const response = await request(host, mutation) + mutation($email: String!, $password: String!) { + login(email: $email, password: $password) + } + ` + const response = await request(host, mutation, variables) return { authorization: `Bearer ${response.login}`, } } + +//* This is a fake ES2015 template string, just to benefit of syntax +// highlighting of `gql` template strings in certain editors. +export function gql(strings) { + return strings.join('') +} diff --git a/backend/src/middleware/activityPubMiddleware.js b/backend/src/middleware/activityPubMiddleware.js index f3ced42f9..e6fb2385c 100644 --- a/backend/src/middleware/activityPubMiddleware.js +++ b/backend/src/middleware/activityPubMiddleware.js @@ -46,7 +46,7 @@ export default { } return post }, - CreateUser: async (resolve, root, args, context, info) => { + SignupVerification: async (resolve, root, args, context, info) => { const keys = generateRsaKeyPair() Object.assign(args, keys) args.actorId = `${activityPub.host}/activitypub/users/${args.slug}` diff --git a/backend/src/middleware/dateTimeMiddleware.js b/backend/src/middleware/dateTimeMiddleware.js index ac6e0ac4a..c8af53a7a 100644 --- a/backend/src/middleware/dateTimeMiddleware.js +++ b/backend/src/middleware/dateTimeMiddleware.js @@ -9,7 +9,6 @@ const setUpdatedAt = (resolve, root, args, context, info) => { export default { Mutation: { - CreateUser: setCreatedAt, CreatePost: setCreatedAt, CreateComment: setCreatedAt, CreateOrganization: setCreatedAt, diff --git a/backend/src/middleware/email/emailMiddleware.js b/backend/src/middleware/email/emailMiddleware.js new file mode 100644 index 000000000..0b7cfd058 --- /dev/null +++ b/backend/src/middleware/email/emailMiddleware.js @@ -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, + }, + } +} diff --git a/backend/src/middleware/email/templates/passwordReset.js b/backend/src/middleware/email/templates/passwordReset.js new file mode 100644 index 000000000..8508adccc --- /dev/null +++ b/backend/src/middleware/email/templates/passwordReset.js @@ -0,0 +1,85 @@ +import CONFIG from '../../../config' + +export const from = '"Human Connection" ' + +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 + `, + } +} diff --git a/backend/src/middleware/email/templates/signup.js b/backend/src/middleware/email/templates/signup.js new file mode 100644 index 000000000..7751f0e67 --- /dev/null +++ b/backend/src/middleware/email/templates/signup.js @@ -0,0 +1,43 @@ +import CONFIG from '../../../config' + +export const from = '"Human Connection" ' + +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) + actionUrl.searchParams.set('email', email) + + 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 + `, + } +} diff --git a/backend/src/middleware/handleHtmlContent/handleContentData.js b/backend/src/middleware/handleHtmlContent/handleContentData.js new file mode 100644 index 000000000..6519ddae7 --- /dev/null +++ b/backend/src/middleware/handleHtmlContent/handleContentData.js @@ -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, + }, +} diff --git a/backend/src/middleware/handleHtmlContent/handleContentData.spec.js b/backend/src/middleware/handleHtmlContent/handleContentData.spec.js new file mode 100644 index 000000000..f2e3c2303 --- /dev/null +++ b/backend/src/middleware/handleHtmlContent/handleContentData.spec.js @@ -0,0 +1,285 @@ +import { GraphQLClient } from 'graphql-request' +import { host, login, gql } 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 @al-capone 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 @al-capone 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 @al-capone` + // 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 @al-capone how do you do? One more mention to @al-capone' + 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 = + '

Hey Dude, #Democracy should work equal for everybody!? That seems to be the only way to have equal #Liberty for everyone.

' + 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 = + '

Hey Dude, #Elections should work equal for everybody!? That seems to be the only way to have equal #Liberty for everyone.

' + 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), + }, + ], + }) + }) + }) + }) + }) +}) diff --git a/backend/src/middleware/handleHtmlContent/hashtags/extractHashtags.js b/backend/src/middleware/handleHtmlContent/hashtags/extractHashtags.js new file mode 100644 index 000000000..fd6613065 --- /dev/null +++ b/backend/src/middleware/handleHtmlContent/hashtags/extractHashtags.js @@ -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 +} diff --git a/backend/src/middleware/handleHtmlContent/hashtags/extractHashtags.spec.js b/backend/src/middleware/handleHtmlContent/hashtags/extractHashtags.spec.js new file mode 100644 index 000000000..eb581d8f5 --- /dev/null +++ b/backend/src/middleware/handleHtmlContent/hashtags/extractHashtags.spec.js @@ -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 = + '

#Elections#Democracy

' + expect(extractHashtags(content)).toEqual(['Elections', 'Democracy']) + }) + + it('ignores mentions', () => { + const content = + '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' + expect(extractHashtags(content)).toEqual([]) + }) + + describe('handles links', () => { + it('ignores links with domains', () => { + const content = + '

#Elections#Democracy

' + expect(extractHashtags(content)).toEqual(['Democracy']) + }) + + it('ignores Hashtag links with not allowed character combinations', () => { + const content = + '

Something inspirational about #AbcDefXyz0123456789!*(),2, #0123456789, #0123456789a and #AbcDefXyz0123456789.

' + expect(extractHashtags(content)).toEqual(['0123456789a', 'AbcDefXyz0123456789']) + }) + }) + + describe('does not crash if', () => { + it('`href` contains no Hashtag name', () => { + const content = + '

Something inspirational about #Democracy and #liberty.

' + expect(extractHashtags(content)).toEqual([]) + }) + + it('`href` contains Hashtag as page anchor', () => { + const content = + '

Something inspirational about #anchor.

' + expect(extractHashtags(content)).toEqual([]) + }) + + it('`href` is empty or invalid', () => { + const content = + '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' + expect(extractHashtags(content)).toEqual([]) + }) + }) + }) +}) diff --git a/backend/src/middleware/notifications/extractIds/index.js b/backend/src/middleware/handleHtmlContent/notifications/extractMentionedUsers.js similarity index 100% rename from backend/src/middleware/notifications/extractIds/index.js rename to backend/src/middleware/handleHtmlContent/notifications/extractMentionedUsers.js diff --git a/backend/src/middleware/notifications/extractIds/spec.js b/backend/src/middleware/handleHtmlContent/notifications/extractMentionedUsers.spec.js similarity index 79% rename from backend/src/middleware/notifications/extractIds/spec.js rename to backend/src/middleware/handleHtmlContent/notifications/extractMentionedUsers.spec.js index 341c39cec..f39fbc859 100644 --- a/backend/src/middleware/notifications/extractIds/spec.js +++ b/backend/src/middleware/handleHtmlContent/notifications/extractMentionedUsers.spec.js @@ -1,9 +1,9 @@ -import extractIds from '.' +import extractMentionedUsers from './extractMentionedUsers' -describe('extractIds', () => { +describe('extractMentionedUsers', () => { describe('content undefined', () => { it('returns empty array', () => { - expect(extractIds()).toEqual([]) + expect(extractMentionedUsers()).toEqual([]) }) }) @@ -11,33 +11,33 @@ describe('extractIds', () => { it('ignores links without .mention class', () => { const content = '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' - expect(extractIds(content)).toEqual([]) + expect(extractMentionedUsers(content)).toEqual([]) }) describe('given a link with .mention class', () => { it('extracts ids', () => { const content = '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' - expect(extractIds(content)).toEqual(['u2', 'u3']) + expect(extractMentionedUsers(content)).toEqual(['u2', 'u3']) }) describe('handles links', () => { it('with slug and id', () => { const content = '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' - expect(extractIds(content)).toEqual(['u2', 'u3']) + expect(extractMentionedUsers(content)).toEqual(['u2', 'u3']) }) it('with domains', () => { const content = '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' - expect(extractIds(content)).toEqual(['u2', 'u3']) + expect(extractMentionedUsers(content)).toEqual(['u2', 'u3']) }) it('special characters', () => { const content = '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' - 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', () => { const content = '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' - expect(extractIds(content)).toEqual([]) + expect(extractMentionedUsers(content)).toEqual([]) }) it('`href` is empty or invalid', () => { const content = '

Something inspirational about @bob-der-baumeister and @jenny-rostock.

' - expect(extractIds(content)).toEqual([]) + expect(extractMentionedUsers(content)).toEqual([]) }) }) }) diff --git a/backend/src/middleware/index.js b/backend/src/middleware/index.js index 9b85bd340..19c72c19f 100644 --- a/backend/src/middleware/index.js +++ b/backend/src/middleware/index.js @@ -1,6 +1,7 @@ +import { applyMiddleware } from 'graphql-middleware' import CONFIG from './../config' + import activityPub from './activityPubMiddleware' -import password from './passwordMiddleware' import softDelete from './softDeleteMiddleware' import sluggify from './sluggifyMiddleware' import excerpt from './excerptMiddleware' @@ -10,35 +11,36 @@ import permissions from './permissionsMiddleware' import user from './userMiddleware' import includedFields from './includedFieldsMiddleware' import orderBy from './orderByMiddleware' -import validation from './validation' -import notifications from './notifications' +import validation from './validation/validationMiddleware' +import handleContentData from './handleHtmlContent/handleContentData' +import email from './email/emailMiddleware' export default schema => { const middlewares = { permissions: permissions, activityPub: activityPub, - password: password, dateTime: dateTime, validation: validation, sluggify: sluggify, excerpt: excerpt, - notifications: notifications, + handleContentData: handleContentData, xss: xss, softDelete: softDelete, user: user, includedFields: includedFields, orderBy: orderBy, + email: email({ isEnabled: CONFIG.SMTP_HOST && CONFIG.SMTP_PORT }), } let order = [ 'permissions', - 'activityPub', - 'password', + // 'activityPub', disabled temporarily 'dateTime', 'validation', 'sluggify', 'excerpt', - 'notifications', + 'email', + 'handleContentData', 'xss', 'softDelete', 'user', @@ -56,5 +58,6 @@ export default schema => { console.log(`Warning: "${disabledMiddlewares}" middlewares have been disabled.`) } - return order.map(key => middlewares[key]) + const appliedMiddlewares = order.map(key => middlewares[key]) + return applyMiddleware(schema, ...appliedMiddlewares) } diff --git a/backend/src/middleware/notifications/index.js b/backend/src/middleware/notifications/index.js deleted file mode 100644 index ca460a512..000000000 --- a/backend/src/middleware/notifications/index.js +++ /dev/null @@ -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, - }, -} diff --git a/backend/src/middleware/notifications/spec.js b/backend/src/middleware/notifications/spec.js deleted file mode 100644 index d214a5571..000000000 --- a/backend/src/middleware/notifications/spec.js +++ /dev/null @@ -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 @al-capone 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 @al-capone 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 @al-capone` - 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 @al-capone how do you do? One more mention to @al-capone' - const expected = { - currentUser: { - notifications: [ - { read: false, post: { content: expectedContent } }, - { read: false, post: { content: expectedContent } }, - ], - }, - } - await expect(client.request(query, { read: false })).resolves.toEqual(expected) - }) - }) - }) - }) - }) -}) diff --git a/backend/src/middleware/passwordMiddleware.js b/backend/src/middleware/passwordMiddleware.js deleted file mode 100644 index 1078e5529..000000000 --- a/backend/src/middleware/passwordMiddleware.js +++ /dev/null @@ -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 - }, -} diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index af4a46d81..31c373fc7 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -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 @@ -70,6 +70,29 @@ const onlyEnabledContent = rule({ 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({ cache: 'no_cache', })(async (parent, args, { user, driver }) => { @@ -101,6 +124,12 @@ const isDeletingOwnAccount = rule({ return context.user.id === args.id }) +const noEmailFilter = rule({ + cache: 'no_cache', +})(async (_, args) => { + return !('email' in args) +}) + // Permissions const permissions = shield( { @@ -108,30 +137,30 @@ const permissions = shield( '*': deny, findPosts: allow, Category: allow, - Tag: isAdmin, + Tag: allow, Report: isModerator, Notification: isAdmin, statistics: allow, currentUser: allow, Post: or(onlyEnabledContent, isModerator), Comment: allow, - User: allow, + User: or(noEmailFilter, isAdmin), isLoggedIn: allow, + Badge: allow, }, Mutation: { '*': deny, login: allow, + SignupByInvitation: allow, + Signup: isAdmin, + SignupVerification: allow, + CreateInvitationCode: and(isAuthenticated, or(not(invitationLimitReached), isAdmin)), UpdateNotification: belongsToMe, - CreateUser: isAdmin, UpdateUser: onlyYourself, CreatePost: isAuthenticated, UpdatePost: isAuthor, DeletePost: isAuthor, report: isAuthenticated, - CreateBadge: isAdmin, - UpdateBadge: isAdmin, - DeleteBadge: isAdmin, - AddUserBadges: isAdmin, CreateSocialMedia: isAuthenticated, DeleteSocialMedia: isAuthenticated, // AddBadgeRewarded: isAdmin, @@ -154,8 +183,6 @@ const permissions = shield( }, User: { email: isMyOwn, - password: isMyOwn, - privateKey: isMyOwn, }, }, { diff --git a/backend/src/middleware/sluggifyMiddleware.js b/backend/src/middleware/sluggifyMiddleware.js index 226bef8e5..6133a3c14 100644 --- a/backend/src/middleware/sluggifyMiddleware.js +++ b/backend/src/middleware/sluggifyMiddleware.js @@ -13,6 +13,10 @@ const isUniqueFor = (context, type) => { export default { 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) => { args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post'))) return resolve(root, args, context, info) @@ -21,10 +25,6 @@ export default { args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post'))) 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) => { args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Organization'))) return resolve(root, args, context, info) diff --git a/backend/src/middleware/slugifyMiddleware.spec.js b/backend/src/middleware/slugifyMiddleware.spec.js index 4e060dc90..5ee4faa3c 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.js +++ b/backend/src/middleware/slugifyMiddleware.spec.js @@ -1,10 +1,12 @@ import { GraphQLClient } from 'graphql-request' import Factory from '../seed/factories' import { host, login } from '../jest/helpers' +import { neode } from '../bootstrap/neo4j' let authenticatedClient let headers const factory = Factory() +const instance = neode() beforeEach(async () => { const adminParams = { role: 'admin', email: 'admin@example.org', password: '1234' } @@ -76,33 +78,41 @@ describe('slugify', () => { }) }) - describe('CreateUser', () => { - const action = async (mutation, params) => { - return authenticatedClient.request(`mutation { - ${mutation}(password: "yo", email: "123@123.de", ${params}) { slug } - }`) + describe('SignupVerification', () => { + const mutation = `mutation($password: String!, $email: String!, $name: String!, $slug: String, $nonce: String!) { + SignupVerification(email: $email, password: $password, name: $name, slug: $slug, nonce: $nonce) { 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 () => { - await expect(action('CreateUser', 'name: "I am a user"')).resolves.toEqual({ - CreateUser: { slug: 'i-am-a-user' }, + await expect(action({ name: 'I am a user' })).resolves.toEqual({ + SignupVerification: { slug: 'i-am-a-user' }, }) }) describe('if slug exists', () => { 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 () => { - await expect(action('CreateUser', 'name: "pre-existing-user"')).resolves.toEqual({ - CreateUser: { slug: 'pre-existing-user-1' }, + await expect(action({ name: 'pre-existing-user' })).resolves.toEqual({ + SignupVerification: { slug: 'pre-existing-user-1' }, }) }) describe('but if the client specifies a slug', () => { - it('rejects CreateUser', async () => { + it('rejects SignupVerification', async () => { 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') }) }) diff --git a/backend/src/middleware/userMiddleware.js b/backend/src/middleware/userMiddleware.js index 29e512ebd..fafbd44e5 100644 --- a/backend/src/middleware/userMiddleware.js +++ b/backend/src/middleware/userMiddleware.js @@ -2,7 +2,7 @@ import createOrUpdateLocations from './nodes/locations' export default { Mutation: { - CreateUser: async (resolve, root, args, context, info) => { + SignupVerification: async (resolve, root, args, context, info) => { const result = await resolve(root, args, context, info) await createOrUpdateLocations(args.id, args.locationName, context.driver) return result diff --git a/backend/src/middleware/validation/index.js b/backend/src/middleware/validation/index.js index cfc852dcb..ca7a6b338 100644 --- a/backend/src/middleware/validation/index.js +++ b/backend/src/middleware/validation/index.js @@ -1,16 +1,5 @@ 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 { url } = args const isValid = url.match(/^(?:https?:\/\/)(?:[^@\n])?(?:www\.)?([^:/\n?]+)/g) @@ -24,8 +13,6 @@ const validateUrl = async (resolve, root, args, context, info) => { export default { Mutation: { - CreateUser: validateUsername, - UpdateUser: validateUsername, CreateSocialMedia: validateUrl, }, } diff --git a/backend/src/middleware/validation/validationMiddleware.js b/backend/src/middleware/validation/validationMiddleware.js new file mode 100644 index 000000000..77282b6b5 --- /dev/null +++ b/backend/src/middleware/validation/validationMiddleware.js @@ -0,0 +1,53 @@ +import { UserInputError } from 'apollo-server' +import Joi from '@hapi/joi' + +const COMMENT_MIN_LENGTH = 1 +const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!' + +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(), +}) + +const validateCommentCreation = async (resolve, root, args, context, info) => { + const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim() + const { postId } = args + + if (!args.content || content.length < COMMENT_MIN_LENGTH) { + throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`) + } + const session = context.driver.session() + const postQueryRes = await session.run( + ` + MATCH (post:Post {id: $postId}) + RETURN post`, + { + postId, + }, + ) + const [post] = postQueryRes.records.map(record => { + return record.get('post') + }) + + if (!post) { + throw new UserInputError(NO_POST_ERR_MESSAGE) + } else { + return resolve(root, args, context, info) + } +} + +export default { + Mutation: { + CreateSocialMedia: validate(socialMediaSchema), + CreateComment: validateCommentCreation, + }, +} diff --git a/backend/src/models/Badge.js b/backend/src/models/Badge.js new file mode 100644 index 000000000..6968a056b --- /dev/null +++ b/backend/src/models/Badge.js @@ -0,0 +1,7 @@ +module.exports = { + id: { type: 'string', primary: true, lowercase: true }, + status: { type: 'string', valid: ['permanent', 'temporary'] }, + type: { type: 'string', valid: ['role', 'crowdfunding'] }, + icon: { type: 'string', required: true }, + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, +} diff --git a/backend/src/models/EmailAddress.js b/backend/src/models/EmailAddress.js new file mode 100644 index 000000000..6afccd1ed --- /dev/null +++ b/backend/src/models/EmailAddress.js @@ -0,0 +1,13 @@ +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', + eager: true, + }, +} diff --git a/backend/src/models/InvitationCode.js b/backend/src/models/InvitationCode.js new file mode 100644 index 000000000..f137f6c15 --- /dev/null +++ b/backend/src/models/InvitationCode.js @@ -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', + }, +} diff --git a/backend/src/models/User.js b/backend/src/models/User.js new file mode 100644 index 000000000..cac8fd7a6 --- /dev/null +++ b/backend/src/models/User.js @@ -0,0 +1,59 @@ +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 }, + 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', + }, + rewarded: { + type: 'relationship', + relationship: 'REWARDED', + target: 'Badge', + 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(), + }, +} diff --git a/backend/src/models/User.spec.js b/backend/src/models/User.spec.js new file mode 100644 index 000000000..e00136970 --- /dev/null +++ b/backend/src/models/User.spec.js @@ -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', + }), + ) + }) +}) diff --git a/backend/src/models/index.js b/backend/src/models/index.js new file mode 100644 index 000000000..09d1dbbeb --- /dev/null +++ b/backend/src/models/index.js @@ -0,0 +1,8 @@ +// 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 { + Badge: require('./Badge.js'), + User: require('./User.js'), + InvitationCode: require('./InvitationCode.js'), + EmailAddress: require('./EmailAddress.js'), +} diff --git a/backend/src/schema/index.js b/backend/src/schema/index.js index d294d8aba..0f724d9b5 100644 --- a/backend/src/schema/index.js +++ b/backend/src/schema/index.js @@ -12,10 +12,26 @@ export default applyScalars( resolvers, config: { query: { - exclude: ['Notfication', 'Statistics', 'LoggedInUser'], + exclude: [ + 'Badge', + 'InvitationCode', + 'EmailAddress', + 'Notfication', + 'Statistics', + 'LoggedInUser', + ], + // add 'User' here as soon as possible }, mutation: { - exclude: ['Notfication', 'Statistics', 'LoggedInUser'], + exclude: [ + 'Badge', + 'InvitationCode', + 'EmailAddress', + 'Notfication', + 'Statistics', + 'LoggedInUser', + ], + // add 'User' here as soon as possible }, debug: CONFIG.DEBUG, }, diff --git a/backend/src/schema/resolvers/badges.js b/backend/src/schema/resolvers/badges.js new file mode 100644 index 000000000..19bc24fd6 --- /dev/null +++ b/backend/src/schema/resolvers/badges.js @@ -0,0 +1,9 @@ +import { neo4jgraphql } from 'neo4j-graphql-js' + +export default { + Query: { + Badge: async (object, args, context, resolveInfo) => { + return neo4jgraphql(object, args, context, resolveInfo, false) + }, + }, +} diff --git a/backend/src/schema/resolvers/badges.spec.js b/backend/src/schema/resolvers/badges.spec.js deleted file mode 100644 index a0dbafe00..000000000 --- a/backend/src/schema/resolvers/badges.spec.js +++ /dev/null @@ -1,200 +0,0 @@ -import { GraphQLClient } from 'graphql-request' -import Factory from '../../seed/factories' -import { host, login } from '../../jest/helpers' - -const factory = Factory() -let client - -describe('badges', () => { - beforeEach(async () => { - await factory.create('User', { - email: 'user@example.org', - role: 'user', - password: '1234', - }) - await factory.create('User', { - id: 'u2', - role: 'moderator', - email: 'moderator@example.org', - }) - await factory.create('User', { - id: 'u3', - role: 'admin', - email: 'admin@example.org', - }) - }) - - afterEach(async () => { - await factory.cleanDatabase() - }) - - describe('CreateBadge', () => { - const variables = { - id: 'b1', - key: 'indiegogo_en_racoon', - type: 'crowdfunding', - status: 'permanent', - icon: '/img/badges/indiegogo_en_racoon.svg', - } - - const mutation = ` - mutation( - $id: ID - $key: String! - $type: BadgeType! - $status: BadgeStatus! - $icon: String! - ) { - CreateBadge(id: $id, key: $key, type: $type, status: $status, icon: $icon) { - id, - key, - type, - status, - icon - } - } - ` - - describe('unauthenticated', () => { - it('throws authorization error', async () => { - client = new GraphQLClient(host) - await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') - }) - }) - - describe('authenticated admin', () => { - beforeEach(async () => { - const headers = await login({ email: 'admin@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - }) - it('creates a badge', async () => { - const expected = { - CreateBadge: { - icon: '/img/badges/indiegogo_en_racoon.svg', - id: 'b1', - key: 'indiegogo_en_racoon', - status: 'permanent', - type: 'crowdfunding', - }, - } - await expect(client.request(mutation, variables)).resolves.toEqual(expected) - }) - }) - - describe('authenticated moderator', () => { - beforeEach(async () => { - const headers = await login({ email: 'moderator@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - }) - - it('throws authorization error', async () => { - await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') - }) - }) - }) - - describe('UpdateBadge', () => { - beforeEach(async () => { - await factory.authenticateAs({ email: 'admin@example.org', password: '1234' }) - await factory.create('Badge', { id: 'b1' }) - }) - const variables = { - id: 'b1', - key: 'whatever', - } - - const mutation = ` - mutation($id: ID!, $key: String!) { - UpdateBadge(id: $id, key: $key) { - id - key - } - } - ` - - describe('unauthenticated', () => { - it('throws authorization error', async () => { - client = new GraphQLClient(host) - await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') - }) - }) - - describe('authenticated moderator', () => { - beforeEach(async () => { - const headers = await login({ email: 'moderator@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - }) - - it('throws authorization error', async () => { - await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') - }) - }) - - describe('authenticated admin', () => { - beforeEach(async () => { - const headers = await login({ email: 'admin@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - }) - it('updates a badge', async () => { - const expected = { - UpdateBadge: { - id: 'b1', - key: 'whatever', - }, - } - await expect(client.request(mutation, variables)).resolves.toEqual(expected) - }) - }) - }) - - describe('DeleteBadge', () => { - beforeEach(async () => { - await factory.authenticateAs({ email: 'admin@example.org', password: '1234' }) - await factory.create('Badge', { id: 'b1' }) - }) - const variables = { - id: 'b1', - } - - const mutation = ` - mutation($id: ID!) { - DeleteBadge(id: $id) { - id - } - } - ` - - describe('unauthenticated', () => { - it('throws authorization error', async () => { - client = new GraphQLClient(host) - await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') - }) - }) - - describe('authenticated moderator', () => { - beforeEach(async () => { - const headers = await login({ email: 'moderator@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - }) - - it('throws authorization error', async () => { - await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') - }) - }) - - describe('authenticated admin', () => { - beforeEach(async () => { - const headers = await login({ email: 'admin@example.org', password: '1234' }) - client = new GraphQLClient(host, { headers }) - }) - it('deletes a badge', async () => { - const expected = { - DeleteBadge: { - id: 'b1', - }, - } - await expect(client.request(mutation, variables)).resolves.toEqual(expected) - }) - }) - }) -}) diff --git a/backend/src/schema/resolvers/comments.js b/backend/src/schema/resolvers/comments.js index 7aef63c59..31219d6d9 100644 --- a/backend/src/schema/resolvers/comments.js +++ b/backend/src/schema/resolvers/comments.js @@ -1,40 +1,15 @@ import { neo4jgraphql } from 'neo4j-graphql-js' -import { UserInputError } from 'apollo-server' - -const COMMENT_MIN_LENGTH = 1 -const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!' export default { Mutation: { CreateComment: async (object, params, context, resolveInfo) => { - const content = params.content.replace(/<(?:.|\n)*?>/gm, '').trim() const { postId } = params // Adding relationship from comment to post by passing in the postId, // but we do not want to create the comment with postId as an attribute // because we use relationships for this. So, we are deleting it from params // before comment creation. delete params.postId - - if (!params.content || content.length < COMMENT_MIN_LENGTH) { - throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`) - } - const session = context.driver.session() - const postQueryRes = await session.run( - ` - MATCH (post:Post {id: $postId}) - RETURN post`, - { - postId, - }, - ) - const [post] = postQueryRes.records.map(record => { - return record.get('post') - }) - - if (!post) { - throw new UserInputError(NO_POST_ERR_MESSAGE) - } const commentWithoutRelationships = await neo4jgraphql( object, params, diff --git a/backend/src/schema/resolvers/comments.spec.js b/backend/src/schema/resolvers/comments.spec.js index 07462ed49..a12e2dfcc 100644 --- a/backend/src/schema/resolvers/comments.spec.js +++ b/backend/src/schema/resolvers/comments.spec.js @@ -1,7 +1,6 @@ -import gql from 'graphql-tag' import { GraphQLClient } from 'graphql-request' import Factory from '../../seed/factories' -import { host, login } from '../../jest/helpers' +import { host, login, gql } from '../../jest/helpers' const factory = Factory() let client @@ -9,12 +8,16 @@ let createCommentVariables let createPostVariables let createCommentVariablesSansPostId let createCommentVariablesWithNonExistentPost +let userParams +let authorParams beforeEach(async () => { - await factory.create('User', { + userParams = { + name: 'TestUser', email: 'test@example.org', password: '1234', - }) + } + await factory.create('User', userParams) }) afterEach(async () => { @@ -53,10 +56,7 @@ describe('CreateComment', () => { describe('authenticated', () => { let headers beforeEach(async () => { - headers = await login({ - email: 'test@example.org', - password: '1234', - }) + headers = await login(userParams) client = new GraphQLClient(host, { headers, }) @@ -89,7 +89,7 @@ describe('CreateComment', () => { const { User } = await client.request(gql` { - User(email: "test@example.org") { + User(name: "TestUser") { comments { content } @@ -201,15 +201,13 @@ describe('DeleteComment', () => { } beforeEach(async () => { + authorParams = { + email: 'author@example.org', + password: '1234', + } const asAuthor = Factory() - await asAuthor.create('User', { - email: 'author@example.org', - password: '1234', - }) - await asAuthor.authenticateAs({ - email: 'author@example.org', - password: '1234', - }) + await asAuthor.create('User', authorParams) + await asAuthor.authenticateAs(authorParams) await asAuthor.create('Post', { id: 'p1', content: 'Post to be commented', @@ -233,13 +231,8 @@ describe('DeleteComment', () => { describe('authenticated but not the author', () => { beforeEach(async () => { let headers - headers = await login({ - email: 'test@example.org', - password: '1234', - }) - client = new GraphQLClient(host, { - headers, - }) + headers = await login(userParams) + client = new GraphQLClient(host, { headers }) }) it('throws authorization error', async () => { @@ -252,13 +245,8 @@ describe('DeleteComment', () => { describe('authenticated as author', () => { beforeEach(async () => { let headers - headers = await login({ - email: 'author@example.org', - password: '1234', - }) - client = new GraphQLClient(host, { - headers, - }) + headers = await login(authorParams) + client = new GraphQLClient(host, { headers }) }) it('deletes the comment', async () => { diff --git a/backend/src/schema/resolvers/moderation.spec.js b/backend/src/schema/resolvers/moderation.spec.js index b1dec603b..db679f522 100644 --- a/backend/src/schema/resolvers/moderation.spec.js +++ b/backend/src/schema/resolvers/moderation.spec.js @@ -254,7 +254,7 @@ describe('enable', () => { beforeEach(async () => { authenticateClient = setupAuthenticateClient({ role: 'moderator', - email: 'someUser@example.org', + email: 'someuser@example.org', password: '1234', }) }) diff --git a/backend/src/schema/resolvers/passwordReset.js b/backend/src/schema/resolvers/passwordReset.js index 13789662b..88d82846a 100644 --- a/backend/src/schema/resolvers/passwordReset.js +++ b/backend/src/schema/resolvers/passwordReset.js @@ -1,28 +1,11 @@ import uuid from 'uuid/v4' 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) { const { driver, code, email, issuedAt = new Date() } = options const session = driver.session() const cypher = ` - MATCH (u:User) WHERE u.email = $email + MATCH (u:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email:$email}) CREATE(pr:PasswordReset {code: $code, issuedAt: datetime($issuedAt), usedAt: NULL}) MERGE (u)-[:REQUESTED]->(pr) RETURN u @@ -42,27 +25,28 @@ export default { requestPasswordReset: async (_, { email }, { driver }) => { const code = uuid().substring(0, 6) const [user] = await createPasswordReset({ driver, code, email }) - if (CONFIG.SMTP_HOST && CONFIG.SMTP_PORT) { - const name = (user && user.name) || '' - const mailTemplate = user ? resetPasswordMail : wrongAccountMail - await transporter().sendMail(mailTemplate({ email, code, name })) - } - return true + const name = (user && user.name) || '' + return { user, code, name, response: true } }, resetPassword: async (_, { email, code, newPassword }, { driver }) => { const session = driver.session() const stillValid = new Date() stillValid.setDate(stillValid.getDate() - 1) - const newHashedPassword = await bcrypt.hashSync(newPassword, 10) + const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10) const cypher = ` MATCH (pr:PasswordReset {code: $code}) - MATCH (u:User {email: $email})-[:REQUESTED]->(pr) + MATCH (e:EmailAddress {email: $email})<-[:PRIMARY_EMAIL]-(u:User)-[:REQUESTED]->(pr) WHERE duration.between(pr.issuedAt, datetime()).days <= 0 AND pr.usedAt IS NULL SET pr.usedAt = datetime() - SET u.password = $newHashedPassword + SET u.encryptedPassword = $encryptedNewPassword 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 result = !!(reset && reset.properties.usedAt) session.close() diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index 763945527..233be450a 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -4,6 +4,9 @@ import { host, login } from '../../jest/helpers' const factory = Factory() let client +let userParams +let authorParams + const postTitle = 'I am a title' const postContent = 'Some content' const oldTitle = 'Old title' @@ -15,10 +18,11 @@ const createPostWithCategoriesMutation = ` mutation($title: String!, $content: String!, $categoryIds: [ID]) { CreatePost(title: $title, content: $content, categoryIds: $categoryIds) { id + title } } ` -const creatPostWithCategoriesVariables = { +const createPostWithCategoriesVariables = { title: postTitle, content: postContent, categoryIds: ['cat9', 'cat4', 'cat15'], @@ -32,11 +36,37 @@ const postQueryWithCategories = ` } } ` +const createPostWithoutCategoriesVariables = { + title: 'This is a post without categories', + content: 'I should be able to filter it out', + categoryIds: null, +} +const postQueryFilteredByCategory = ` +query Post($filter: _PostFilter) { + Post(filter: $filter) { + title + id + categories { + id + } + } + } +` +const postCategoriesFilterParam = { categories_some: { id_in: ['cat4'] } } +const postQueryFilteredByCategoryVariables = { + filter: postCategoriesFilterParam, +} beforeEach(async () => { - await factory.create('User', { + userParams = { + name: 'TestUser', email: 'test@example.org', password: '1234', - }) + } + authorParams = { + email: 'author@example.org', + password: '1234', + } + await factory.create('User', userParams) }) afterEach(async () => { @@ -66,7 +96,7 @@ describe('CreatePost', () => { describe('authenticated', () => { let headers beforeEach(async () => { - headers = await login({ email: 'test@example.org', password: '1234' }) + headers = await login(userParams) client = new GraphQLClient(host, { headers }) }) @@ -84,7 +114,7 @@ describe('CreatePost', () => { await client.request(mutation, createPostVariables) const { User } = await client.request( `{ - User(email:"test@example.org") { + User(name: "TestUser") { contributions { title } @@ -124,7 +154,8 @@ describe('CreatePost', () => { }) describe('categories', () => { - it('allows a user to set the categories of the post', async () => { + let postWithCategories + beforeEach(async () => { await Promise.all([ factory.create('Category', { id: 'cat9', @@ -142,18 +173,39 @@ describe('CreatePost', () => { icon: 'shopping-cart', }), ]) - const expected = [{ id: 'cat9' }, { id: 'cat4' }, { id: 'cat15' }] - const postWithCategories = await client.request( + postWithCategories = await client.request( createPostWithCategoriesMutation, - creatPostWithCategoriesVariables, + createPostWithCategoriesVariables, ) + }) + + it('allows a user to set the categories of the post', async () => { + const expected = [{ id: 'cat9' }, { id: 'cat4' }, { id: 'cat15' }] const postQueryWithCategoriesVariables = { id: postWithCategories.CreatePost.id, } + await expect( client.request(postQueryWithCategories, postQueryWithCategoriesVariables), ).resolves.toEqual({ Post: [{ categories: expect.arrayContaining(expected) }] }) }) + + it('allows a user to filter for posts by category', async () => { + await client.request(createPostWithCategoriesMutation, createPostWithoutCategoriesVariables) + const categoryIds = [{ id: 'cat4' }, { id: 'cat15' }, { id: 'cat9' }] + const expected = { + Post: [ + { + title: postTitle, + id: postWithCategories.CreatePost.id, + categories: expect.arrayContaining(categoryIds), + }, + ], + } + await expect( + client.request(postQueryFilteredByCategory, postQueryFilteredByCategoryVariables), + ).resolves.toEqual(expected) + }) }) }) }) @@ -163,14 +215,8 @@ describe('UpdatePost', () => { let updatePostVariables beforeEach(async () => { const asAuthor = Factory() - await asAuthor.create('User', { - email: 'author@example.org', - password: '1234', - }) - await asAuthor.authenticateAs({ - email: 'author@example.org', - password: '1234', - }) + await asAuthor.create('User', authorParams) + await asAuthor.authenticateAs(authorParams) await asAuthor.create('Post', { id: 'p1', title: oldTitle, @@ -205,7 +251,7 @@ describe('UpdatePost', () => { describe('authenticated but not the author', () => { let headers beforeEach(async () => { - headers = await login({ email: 'test@example.org', password: '1234' }) + headers = await login(userParams) client = new GraphQLClient(host, { headers }) }) @@ -219,7 +265,7 @@ describe('UpdatePost', () => { describe('authenticated as author', () => { let headers beforeEach(async () => { - headers = await login({ email: 'author@example.org', password: '1234' }) + headers = await login(authorParams) client = new GraphQLClient(host, { headers }) }) @@ -257,7 +303,7 @@ describe('UpdatePost', () => { ]) postWithCategories = await client.request( createPostWithCategoriesMutation, - creatPostWithCategoriesVariables, + createPostWithCategoriesVariables, ) updatePostVariables = { id: postWithCategories.CreatePost.id, @@ -297,14 +343,8 @@ describe('DeletePost', () => { beforeEach(async () => { const asAuthor = Factory() - await asAuthor.create('User', { - email: 'author@example.org', - password: '1234', - }) - await asAuthor.authenticateAs({ - email: 'author@example.org', - password: '1234', - }) + await asAuthor.create('User', authorParams) + await asAuthor.authenticateAs(authorParams) await asAuthor.create('Post', { id: 'p1', content: 'To be deleted', @@ -321,7 +361,7 @@ describe('DeletePost', () => { describe('authenticated but not the author', () => { let headers beforeEach(async () => { - headers = await login({ email: 'test@example.org', password: '1234' }) + headers = await login(userParams) client = new GraphQLClient(host, { headers }) }) @@ -333,7 +373,7 @@ describe('DeletePost', () => { describe('authenticated as author', () => { let headers beforeEach(async () => { - headers = await login({ email: 'author@example.org', password: '1234' }) + headers = await login(authorParams) client = new GraphQLClient(host, { headers }) }) diff --git a/backend/src/schema/resolvers/registration.js b/backend/src/schema/resolvers/registration.js new file mode 100644 index 000000000..20c54a49b --- /dev/null +++ b/backend/src/schema/resolvers/registration.js @@ -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 emails = await instance.all('EmailAddress', { email }) + if (emails.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) + } + }, + }, +} diff --git a/backend/src/schema/resolvers/registration.spec.js b/backend/src/schema/resolvers/registration.spec.js new file mode 100644 index 000000000..dc2e96348 --- /dev/null +++ b/backend/src/schema/resolvers/registration.spec.js @@ -0,0 +1,404 @@ +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 additional EmailAddress node', async done => { + try { + await action() + } catch (e) { + let emailAddresses = await instance.all('EmailAddress') + emailAddresses = await emailAddresses.toJson + 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() + let emailAddress = await instance.first('EmailAddress', { email: 'someuser@example.org' }) + emailAddress = await emailAddress.toJson() + expect(emailAddress.createdAt).toBeTruthy() + expect(Date.parse(emailAddress.createdAt)).toEqual(expect.any(Number)) + }) + + it('with a cryptographic `nonce`', async () => { + await action() + let emailAddress = await instance.first('EmailAddress', { email: 'someuser@example.org' }) + emailAddress = await emailAddress.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 { + variables.email = 'yetanotheremail@example.org' + 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() + let emailAddress = await instance.first('EmailAddress', { email: 'someuser@example.org' }) + emailAddress = await emailAddress.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', + ) + }) + }) + }) + }) + }) +}) diff --git a/backend/src/schema/resolvers/rewards.js b/backend/src/schema/resolvers/rewards.js index ec5043da3..f7a759aa4 100644 --- a/backend/src/schema/resolvers/rewards.js +++ b/backend/src/schema/resolvers/rewards.js @@ -1,47 +1,47 @@ +import { neode } from '../../bootstrap/neo4j' +import { UserInputError } from 'apollo-server' + +const instance = neode() + +const getUserAndBadge = async ({ badgeKey, userId }) => { + let user = await instance.first('User', 'id', userId) + const badge = await instance.first('Badge', 'id', badgeKey) + if (!user) throw new UserInputError("Couldn't find a user with that id") + if (!badge) throw new UserInputError("Couldn't find a badge with that id") + return { user, badge } +} + export default { Mutation: { reward: async (_object, params, context, _resolveInfo) => { - const { fromBadgeId, toUserId } = params - const session = context.driver.session() - - let transactionRes = await session.run( - `MATCH (badge:Badge {id: $badgeId}), (rewardedUser:User {id: $rewardedUserId}) - MERGE (badge)-[:REWARDED]->(rewardedUser) - RETURN rewardedUser {.id}`, - { - badgeId: fromBadgeId, - rewardedUserId: toUserId, - }, - ) - - const [rewardedUser] = transactionRes.records.map(record => { - return record.get('rewardedUser') - }) - - session.close() - - return rewardedUser.id + const { user, badge } = await getUserAndBadge(params) + await user.relateTo(badge, 'rewarded') + return user.toJson() }, unreward: async (_object, params, context, _resolveInfo) => { - const { fromBadgeId, toUserId } = params + const { badgeKey, userId } = params + const { user } = await getUserAndBadge(params) const session = context.driver.session() - - let transactionRes = await session.run( - `MATCH (badge:Badge {id: $badgeId})-[reward:REWARDED]->(rewardedUser:User {id: $rewardedUserId}) - DELETE reward - RETURN rewardedUser {.id}`, - { - badgeId: fromBadgeId, - rewardedUserId: toUserId, - }, - ) - const [rewardedUser] = transactionRes.records.map(record => { - return record.get('rewardedUser') - }) - session.close() - - return rewardedUser.id + try { + // silly neode cannot remove relationships + await session.run( + ` + MATCH (badge:Badge {id: $badgeKey})-[reward:REWARDED]->(rewardedUser:User {id: $userId}) + DELETE reward + RETURN rewardedUser + `, + { + badgeKey, + userId, + }, + ) + } catch (err) { + throw err + } finally { + session.close() + } + return user.toJson() }, }, } diff --git a/backend/src/schema/resolvers/rewards.spec.js b/backend/src/schema/resolvers/rewards.spec.js index 2bdd9a39b..3b94e93aa 100644 --- a/backend/src/schema/resolvers/rewards.spec.js +++ b/backend/src/schema/resolvers/rewards.spec.js @@ -1,12 +1,19 @@ import { GraphQLClient } from 'graphql-request' import Factory from '../../seed/factories' -import { host, login } from '../../jest/helpers' +import { host, login, gql } from '../../jest/helpers' const factory = Factory() +let user +let badge describe('rewards', () => { + const variables = { + from: 'indiegogo_en_rhino', + to: 'u1', + } + beforeEach(async () => { - await factory.create('User', { + user = await factory.create('User', { id: 'u1', role: 'user', email: 'user@example.org', @@ -22,9 +29,8 @@ describe('rewards', () => { role: 'admin', email: 'admin@example.org', }) - await factory.create('Badge', { - id: 'b6', - key: 'indiegogo_en_rhino', + badge = await factory.create('Badge', { + id: 'indiegogo_en_rhino', type: 'crowdfunding', status: 'permanent', icon: '/img/badges/indiegogo_en_rhino.svg', @@ -35,21 +41,19 @@ describe('rewards', () => { await factory.cleanDatabase() }) - describe('RewardBadge', () => { - const mutation = ` - mutation( - $from: ID! - $to: ID! - ) { - reward(fromBadgeId: $from, toUserId: $to) + describe('reward', () => { + const mutation = gql` + mutation($from: ID!, $to: ID!) { + reward(badgeKey: $from, userId: $to) { + id + badges { + id + } + } } ` describe('unauthenticated', () => { - const variables = { - from: 'b6', - to: 'u1', - } let client it('throws authorization error', async () => { @@ -65,74 +69,95 @@ describe('rewards', () => { client = new GraphQLClient(host, { headers }) }) + describe('badge for id does not exist', () => { + it('rejects with a telling error message', async () => { + await expect( + client.request(mutation, { + ...variables, + from: 'bullshit', + }), + ).rejects.toThrow("Couldn't find a badge with that id") + }) + }) + + describe('user for id does not exist', () => { + it('rejects with a telling error message', async () => { + await expect( + client.request(mutation, { + ...variables, + to: 'bullshit', + }), + ).rejects.toThrow("Couldn't find a user with that id") + }) + }) + it('rewards a badge to user', async () => { - const variables = { - from: 'b6', - to: 'u1', - } const expected = { - reward: 'u1', + reward: { + id: 'u1', + badges: [{ id: 'indiegogo_en_rhino' }], + }, } await expect(client.request(mutation, variables)).resolves.toEqual(expected) }) + it('rewards a second different badge to same user', async () => { await factory.create('Badge', { - id: 'b1', - key: 'indiegogo_en_racoon', - type: 'crowdfunding', - status: 'permanent', + id: 'indiegogo_en_racoon', icon: '/img/badges/indiegogo_en_racoon.svg', }) - const variables = { - from: 'b1', - to: 'u1', - } + const badges = [{ id: 'indiegogo_en_racoon' }, { id: 'indiegogo_en_rhino' }] const expected = { - reward: 'u1', + reward: { + id: 'u1', + badges: expect.arrayContaining(badges), + }, } - await expect(client.request(mutation, variables)).resolves.toEqual(expected) + await client.request(mutation, variables) + await expect( + client.request(mutation, { + ...variables, + from: 'indiegogo_en_racoon', + }), + ).resolves.toEqual(expected) }) + it('rewards the same badge as well to another user', async () => { - const variables1 = { - from: 'b6', - to: 'u1', - } - await client.request(mutation, variables1) - - const variables2 = { - from: 'b6', - to: 'u2', - } const expected = { - reward: 'u2', + reward: { + id: 'u2', + badges: [{ id: 'indiegogo_en_rhino' }], + }, } - await expect(client.request(mutation, variables2)).resolves.toEqual(expected) + await expect( + client.request(mutation, { + ...variables, + to: 'u2', + }), + ).resolves.toEqual(expected) }) - it('returns the original reward if a reward is attempted a second time', async () => { - const variables = { - from: 'b6', - to: 'u1', - } + + it('creates no duplicate reward relationships', async () => { await client.request(mutation, variables) await client.request(mutation, variables) - const query = `{ - User( id: "u1" ) { - badgesCount + const query = gql` + { + User(id: "u1") { + badgesCount + badges { + id + } + } } - } ` - const expected = { User: [{ badgesCount: 1 }] } + const expected = { User: [{ badgesCount: 1, badges: [{ id: 'indiegogo_en_rhino' }] }] } await expect(client.request(query)).resolves.toEqual(expected) }) }) describe('authenticated moderator', () => { - const variables = { - from: 'b6', - to: 'u1', - } let client beforeEach(async () => { const headers = await login({ email: 'moderator@example.org', password: '1234' }) @@ -147,27 +172,41 @@ describe('rewards', () => { }) }) - describe('RemoveReward', () => { + describe('unreward', () => { beforeEach(async () => { - await factory.relate('User', 'Badges', { from: 'b6', to: 'u1' }) + await user.relateTo(badge, 'rewarded') }) - const variables = { - from: 'b6', - to: 'u1', - } - const expected = { - unreward: 'u1', - } + const expected = { unreward: { id: 'u1', badges: [] } } - const mutation = ` - mutation( - $from: ID! - $to: ID! - ) { - unreward(fromBadgeId: $from, toUserId: $to) + const mutation = gql` + mutation($from: ID!, $to: ID!) { + unreward(badgeKey: $from, userId: $to) { + id + badges { + id + } + } } ` + describe('check test setup', () => { + it('user has one badge', async () => { + const query = gql` + { + User(id: "u1") { + badgesCount + badges { + id + } + } + } + ` + const expected = { User: [{ badgesCount: 1, badges: [{ id: 'indiegogo_en_rhino' }] }] } + const client = new GraphQLClient(host) + await expect(client.request(query)).resolves.toEqual(expected) + }) + }) + describe('unauthenticated', () => { let client @@ -188,12 +227,9 @@ describe('rewards', () => { await expect(client.request(mutation, variables)).resolves.toEqual(expected) }) - it('fails to remove a not existing badge from user', async () => { + it('does not crash when unrewarding multiple times', async () => { await client.request(mutation, variables) - - await expect(client.request(mutation, variables)).rejects.toThrow( - "Cannot read property 'id' of undefined", - ) + await expect(client.request(mutation, variables)).resolves.toEqual(expected) }) }) diff --git a/backend/src/schema/resolvers/socialMedia.spec.js b/backend/src/schema/resolvers/socialMedia.spec.js index 38850761c..7ec35a08f 100644 --- a/backend/src/schema/resolvers/socialMedia.spec.js +++ b/backend/src/schema/resolvers/socialMedia.spec.js @@ -1,7 +1,6 @@ -import gql from 'graphql-tag' import { GraphQLClient } from 'graphql-request' import Factory from '../../seed/factories' -import { host, login } from '../../jest/helpers' +import { host, login, gql } from '../../jest/helpers' const factory = Factory() @@ -98,14 +97,19 @@ describe('SocialMedia', () => { const variables = { 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 () => { const variables = { 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', + ) }) }) }) diff --git a/backend/src/schema/resolvers/user_management.js b/backend/src/schema/resolvers/user_management.js index e33314f7e..7ed84586b 100644 --- a/backend/src/schema/resolvers/user_management.js +++ b/backend/src/schema/resolvers/user_management.js @@ -2,10 +2,13 @@ import encode from '../../jwt/encode' import bcrypt from 'bcryptjs' import { AuthenticationError } from 'apollo-server' import { neo4jgraphql } from 'neo4j-graphql-js' +import { neode } from '../../bootstrap/neo4j' + +const instance = neode() export default { Query: { - isLoggedIn: (parent, args, { driver, user }) => { + isLoggedIn: (_, args, { driver, user }) => { return Boolean(user && user.id) }, currentUser: async (object, params, ctx, resolveInfo) => { @@ -15,40 +18,29 @@ export default { }, }, Mutation: { - signup: async (parent, { email, password }, { req }) => { - // 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 }) => { + login: async (_, { email, password }, { driver, req, user }) => { // if (user && user.id) { // throw new Error('Already logged in.') // } const session = driver.session() const result = await session.run( - 'MATCH (user:User {email: $userEmail}) ' + - 'RETURN user {.id, .slug, .name, .avatar, .email, .password, .role, .disabled} as user LIMIT 1', + 'MATCH (user:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $userEmail})' + + 'RETURN user {.id, .slug, .name, .avatar, .encryptedPassword, .role, .disabled, email:e.email} as user LIMIT 1', { userEmail: email, }, ) - session.close() - const [currentUser] = await result.records.map(function(record) { + const [currentUser] = await result.records.map(record => { return record.get('user') }) if ( currentUser && - (await bcrypt.compareSync(password, currentUser.password)) && + (await bcrypt.compareSync(password, currentUser.encryptedPassword)) && !currentUser.disabled ) { - delete currentUser.password + delete currentUser.encryptedPassword return encode(currentUser) } else if (currentUser && currentUser.disabled) { throw new AuthenticationError('Your account has been disabled.') @@ -57,41 +49,24 @@ export default { } }, changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => { - const session = driver.session() - let result = await session.run( - `MATCH (user:User {email: $userEmail}) - RETURN user {.id, .email, .password}`, - { - userEmail: user.email, - }, - ) + let currentUser = await instance.find('User', user.id) - const [currentUser] = result.records.map(function(record) { - return record.get('user') - }) - - if (!(await bcrypt.compareSync(oldPassword, currentUser.password))) { + const encryptedPassword = currentUser.get('encryptedPassword') + if (!(await bcrypt.compareSync(oldPassword, encryptedPassword))) { throw new AuthenticationError('Old password is not correct') } - if (await bcrypt.compareSync(newPassword, currentUser.password)) { + if (await bcrypt.compareSync(newPassword, encryptedPassword)) { throw new AuthenticationError('Old password and new password should be different') - } else { - const newHashedPassword = await bcrypt.hashSync(newPassword, 10) - session.run( - `MATCH (user:User {email: $userEmail}) - SET user.password = $newHashedPassword - RETURN user - `, - { - userEmail: user.email, - newHashedPassword, - }, - ) - session.close() - - return encode(currentUser) } + + const newEncryptedPassword = await bcrypt.hashSync(newPassword, 10) + await currentUser.update({ + encryptedPassword: newEncryptedPassword, + updatedAt: new Date().toISOString(), + }) + + return encode(await currentUser.toJson()) }, }, } diff --git a/backend/src/schema/resolvers/user_management.spec.js b/backend/src/schema/resolvers/user_management.spec.js index 463c5ea6d..50b5896b3 100644 --- a/backend/src/schema/resolvers/user_management.spec.js +++ b/backend/src/schema/resolvers/user_management.spec.js @@ -1,4 +1,3 @@ -import gql from 'graphql-tag' import { GraphQLClient, request } from 'graphql-request' import jwt from 'jsonwebtoken' 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') - }) - }) -}) diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index c5c3701b5..820688a1a 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -1,15 +1,91 @@ import { neo4jgraphql } from 'neo4j-graphql-js' 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 { - Mutation: { - UpdateUser: async (object, params, context, resolveInfo) => { - params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar' }) - return neo4jgraphql(object, params, context, resolveInfo, false) + Query: { + User: async (object, args, context, resolveInfo) => { + const { email } = args + if (email) { + const e = await instance.first('EmailAddress', { email }) + let user = e.get('belongsTo') + user = await user.toJson() + return [user.node] + } + return neo4jgraphql(object, args, context, resolveInfo, false) }, - CreateUser: async (object, params, context, resolveInfo) => { - params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar' }) - return neo4jgraphql(object, params, context, resolveInfo, false) + }, + Mutation: { + 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) => { const { resource } = params @@ -34,4 +110,51 @@ export default { return neo4jgraphql(object, params, context, resolveInfo, false) }, }, + User: { + email: async (parent, params, context, resolveInfo) => { + if (typeof parent.email !== 'undefined') return parent.email + const { id } = parent + const statement = `MATCH(u:User {id: {id}})-[:PRIMARY_EMAIL]->(e:EmailAddress) RETURN e` + const result = await instance.cypher(statement, { id }) + let [{ email }] = result.records.map(r => r.get('e').properties) + return email + }, + ...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)', + }), + }, } diff --git a/backend/src/schema/resolvers/users.spec.js b/backend/src/schema/resolvers/users.spec.js index 9df5473bf..fff6acadb 100644 --- a/backend/src/schema/resolvers/users.spec.js +++ b/backend/src/schema/resolvers/users.spec.js @@ -1,7 +1,6 @@ import { GraphQLClient } from 'graphql-request' -import { login, host } from '../../jest/helpers' import Factory from '../../seed/factories' -import gql from 'graphql-tag' +import { host, login, gql } from '../../jest/helpers' const factory = Factory() let client @@ -11,50 +10,39 @@ afterEach(async () => { }) describe('users', () => { - describe('CreateUser', () => { - const mutation = ` - 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 () => { - client = new GraphQLClient(host) - }) - - it('is not allowed to create users', async () => { - await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') - }) + describe('User', () => { + describe('query by email address', () => { + beforeEach(async () => { + await factory.create('User', { name: 'Johnny', email: 'any-email-address@example.org' }) }) - describe('authenticated admin', () => { + const query = `query($email: String) { User(email: $email) { name } }` + const variables = { email: 'any-email-address@example.org' } + beforeEach(() => { + client = new GraphQLClient(host) + }) + + it('is forbidden', async () => { + await expect(client.request(query, variables)).rejects.toThrow('Not Authorised') + }) + + describe('as admin', () => { beforeEach(async () => { - const adminParams = { + const userParams = { role: 'admin', email: 'admin@example.org', password: '1234', } - await factory.create('User', adminParams) - const headers = await login(adminParams) + const factory = Factory() + await factory.create('User', userParams) + const headers = await login(userParams) client = new GraphQLClient(host, { headers }) }) - it('is allowed to create new users', async () => { - const expected = { - CreateUser: { - id: expect.any(String), - }, - } - await expect(client.request(mutation, variables)).resolves.toEqual(expected) + it('is permitted', async () => { + await expect(client.request(query, variables)).resolves.toEqual({ + User: [{ name: 'Johnny' }], + }) }) }) }) @@ -88,7 +76,7 @@ describe('users', () => { describe('as another user', () => { beforeEach(async () => { const someoneElseParams = { - email: 'someoneElse@example.org', + email: 'someone-else@example.org', password: '1234', name: 'James Doe', } @@ -119,12 +107,12 @@ describe('users', () => { await expect(client.request(mutation, variables)).resolves.toEqual(expected) }) - it('with no name', async () => { + it('with `null` as name', async () => { const variables = { id: 'u47', 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) }) @@ -133,7 +121,7 @@ describe('users', () => { id: 'u47', 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) }) }) @@ -158,13 +146,13 @@ describe('users', () => { } ` beforeEach(async () => { - asAuthor = await factory.create('User', { + await factory.create('User', { email: 'test@example.org', password: '1234', id: 'u343', }) await factory.create('User', { - email: 'friendsAccount@example.org', + email: 'friends-account@example.org', password: '1234', id: 'u565', }) @@ -202,6 +190,7 @@ describe('users', () => { describe('attempting to delete my own account', () => { let expectedResponse beforeEach(async () => { + asAuthor = Factory() await asAuthor.authenticateAs({ email: 'test@example.org', password: '1234', diff --git a/backend/src/schema/types/enum/BadgeStatus.gql b/backend/src/schema/types/enum/BadgeStatus.gql deleted file mode 100644 index b109663b3..000000000 --- a/backend/src/schema/types/enum/BadgeStatus.gql +++ /dev/null @@ -1,4 +0,0 @@ -enum BadgeStatus { - permanent - temporary -} \ No newline at end of file diff --git a/backend/src/schema/types/enum/BadgeType.gql b/backend/src/schema/types/enum/BadgeType.gql deleted file mode 100644 index eccf2e661..000000000 --- a/backend/src/schema/types/enum/BadgeType.gql +++ /dev/null @@ -1,4 +0,0 @@ -enum BadgeType { - role - crowdfunding -} \ No newline at end of file diff --git a/backend/src/schema/types/enum/Emotion.gql b/backend/src/schema/types/enum/Emotion.gql new file mode 100644 index 000000000..88a436f98 --- /dev/null +++ b/backend/src/schema/types/enum/Emotion.gql @@ -0,0 +1,7 @@ +enum Emotion { + surprised + cry + happy + angry + funny +} \ No newline at end of file diff --git a/backend/src/schema/types/schema.gql b/backend/src/schema/types/schema.gql index 8b0f422c8..492dd3966 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/schema/types/schema.gql @@ -17,21 +17,17 @@ type Query { LIMIT $limit """ ) - CommentByPost(postId: ID!): [Comment]! } type Mutation { # Get a JWT Token for the given Email and password login(email: String!, password: String!): String! - signup(email: String!, password: String!): Boolean! changePassword(oldPassword: String!, newPassword: String!): String! requestPasswordReset(email: String!): Boolean! resetPassword(email: String!, code: String!, newPassword: String!): Boolean! report(id: ID!, description: String): Report disable(id: ID!): ID enable(id: ID!): ID - reward(fromBadgeId: ID!, toUserId: ID!): ID - unreward(fromBadgeId: ID!, toUserId: ID!): ID # Shout the given Type and ID shout(id: ID!, type: ShoutTypeEnum): Boolean! # Unshout the given Type and ID @@ -40,7 +36,6 @@ type Mutation { follow(id: ID!, type: FollowTypeEnum): Boolean! # Unfollow the given Type and ID unfollow(id: ID!, type: FollowTypeEnum): Boolean! - DeleteUser(id: ID!, resource: [Deletable]): User } type Statistics { diff --git a/backend/src/schema/types/schema_full.gql_ b/backend/src/schema/types/schema_full.gql_ deleted file mode 100644 index a581d287c..000000000 --- a/backend/src/schema/types/schema_full.gql_ +++ /dev/null @@ -1,324 +0,0 @@ -scalar Upload - -type Query { - isLoggedIn: Boolean! - # Get the currently logged in User based on the given JWT Token - currentUser: User - # Get the latest Network Statistics - statistics: Statistics! - findPosts(filter: String!, limit: Int = 10): [Post]! @cypher( - statement: """ - CALL db.index.fulltext.queryNodes('full_text_search', $filter) - YIELD node as post, score - MATCH (post)<-[:WROTE]-(user:User) - WHERE score >= 0.2 - AND NOT user.deleted = true AND NOT user.disabled = true - AND NOT post.deleted = true AND NOT post.disabled = true - RETURN post - LIMIT $limit - """ - ) - CommentByPost(postId: ID!): [Comment]! -} - -type Mutation { - # Get a JWT Token for the given Email and password - login(email: String!, password: String!): String! - signup(email: String!, password: String!): Boolean! - changePassword(oldPassword:String!, newPassword: String!): String! - report(id: ID!, description: String): Report - disable(id: ID!): ID - enable(id: ID!): ID - reward(fromBadgeId: ID!, toUserId: ID!): ID - unreward(fromBadgeId: ID!, toUserId: ID!): ID - # Shout the given Type and ID - shout(id: ID!, type: ShoutTypeEnum): Boolean! - # Unshout the given Type and ID - unshout(id: ID!, type: ShoutTypeEnum): Boolean! - # Follow the given Type and ID - follow(id: ID!, type: FollowTypeEnum): Boolean! - # Unfollow the given Type and ID - unfollow(id: ID!, type: FollowTypeEnum): Boolean! -} - -type Statistics { - countUsers: Int! - countPosts: Int! - countComments: Int! - countNotifications: Int! - countOrganizations: Int! - countProjects: Int! - countInvites: Int! - countFollows: Int! - countShouts: Int! -} - -type Notification { - id: ID! - read: Boolean, - user: User @relation(name: "NOTIFIED", direction: "OUT") - post: Post @relation(name: "NOTIFIED", direction: "IN") - createdAt: String -} - -scalar Date -scalar Time -scalar DateTime - -enum VisibilityEnum { - public - friends - private -} - -enum UserGroupEnum { - admin - moderator - user -} - -type Location { - id: ID! - name: String! - nameEN: String - nameDE: String - nameFR: String - nameNL: String - nameIT: String - nameES: String - namePT: String - namePL: String - type: String! - lat: Float - lng: Float - parent: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") -} - -type User { - id: ID! - actorId: String - name: String - email: String! - slug: String - password: String! - avatar: String - avatarUpload: Upload - deleted: Boolean - disabled: Boolean - disabledBy: User @relation(name: "DISABLED", direction: "IN") - role: UserGroupEnum - publicKey: String - privateKey: String - - location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") - locationName: String - about: String - socialMedia: [SocialMedia]! @relation(name: "OWNED", direction: "OUT") - - createdAt: String - updatedAt: String - - notifications(read: Boolean): [Notification]! @relation(name: "NOTIFIED", direction: "IN") - - friends: [User]! @relation(name: "FRIENDS", direction: "BOTH") - friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)") - - following: [User]! @relation(name: "FOLLOWS", direction: "OUT") - followingCount: Int! @cypher(statement: "MATCH (this)-[:FOLLOWS]->(r:User) RETURN COUNT(DISTINCT r)") - - followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN") - followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)") - - # Is the currently logged in user following that user? - followedByCurrentUser: Boolean! @cypher( - statement: """ - MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId}) - RETURN COUNT(u) >= 1 - """ - ) - - #contributions: [WrittenPost]! - #contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]! - # @cypher( - # statement: "MATCH (this)-[w:WROTE]->(p:Post) RETURN p as Post, w.timestamp as timestamp" - # ) - contributions: [Post]! @relation(name: "WROTE", direction: "OUT") - contributionsCount: Int! @cypher( - statement: """ - MATCH (this)-[:WROTE]->(r:Post) - WHERE (NOT exists(r.deleted) OR r.deleted = false) - AND (NOT exists(r.disabled) OR r.disabled = false) - RETURN COUNT(r) - """ - ) - - comments: [Comment]! @relation(name: "WROTE", direction: "OUT") - commentsCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(r:Comment) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)") - - shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT") - shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)") - - organizationsCreated: [Organization] @relation(name: "CREATED_ORGA", direction: "OUT") - organizationsOwned: [Organization] @relation(name: "OWNING_ORGA", direction: "OUT") - - blacklisted: [User]! @relation(name: "BLACKLISTED", direction: "OUT") - - categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT") - - badges: [Badge]! @relation(name: "REWARDED", direction: "IN") - badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)") -} - -type Post { - id: ID! - activityId: String - objectId: String - author: User @relation(name: "WROTE", direction: "IN") - title: String! - slug: String - content: String! - contentExcerpt: String - image: String - imageUpload: Upload - visibility: VisibilityEnum - deleted: Boolean - disabled: Boolean - disabledBy: User @relation(name: "DISABLED", direction: "IN") - createdAt: String - updatedAt: String - - relatedContributions: [Post]! @cypher( - statement: """ - MATCH (this)-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post) - RETURN DISTINCT post - LIMIT 10 - """ - ) - - tags: [Tag]! @relation(name: "TAGGED", direction: "OUT") - categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT") - - comments: [Comment]! @relation(name: "COMMENTS", direction: "IN") - commentsCount: Int! @cypher(statement: "MATCH (this)<-[:COMMENTS]-(r:Comment) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)") - - shoutedBy: [User]! @relation(name: "SHOUTED", direction: "IN") - shoutedCount: Int! @cypher(statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)") - - # Has the currently logged in user shouted that post? - shoutedByCurrentUser: Boolean! @cypher( - statement: """ - MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId}) - RETURN COUNT(u) >= 1 - """ - ) -} - -type Comment { - id: ID! - activityId: String - postId: ID - author: User @relation(name: "WROTE", direction: "IN") - content: String! - contentExcerpt: String - post: Post @relation(name: "COMMENTS", direction: "OUT") - createdAt: String - updatedAt: String - deleted: Boolean - disabled: Boolean - disabledBy: User @relation(name: "DISABLED", direction: "IN") -} - -type Report { - id: ID! - submitter: User @relation(name: "REPORTED", direction: "IN") - description: String - type: String! @cypher(statement: "MATCH (resource)<-[:REPORTED]-(this) RETURN labels(resource)[0]") - createdAt: String - comment: Comment @relation(name: "REPORTED", direction: "OUT") - post: Post @relation(name: "REPORTED", direction: "OUT") - user: User @relation(name: "REPORTED", direction: "OUT") -} - -type Category { - id: ID! - name: String! - slug: String - icon: String! - posts: [Post]! @relation(name: "CATEGORIZED", direction: "IN") - postCount: Int! @cypher(statement: "MATCH (this)<-[:CATEGORIZED]-(r:Post) RETURN COUNT(r)") -} - -type Badge { - id: ID! - key: String! - type: BadgeTypeEnum! - status: BadgeStatusEnum! - icon: String! - - rewarded: [User]! @relation(name: "REWARDED", direction: "OUT") -} - -enum BadgeTypeEnum { - role - crowdfunding -} -enum BadgeStatusEnum { - permanent - temporary -} -enum ShoutTypeEnum { - Post - Organization - Project -} -enum FollowTypeEnum { - User - Organization - Project -} - -type Reward { - id: ID! - user: User @relation(name: "REWARDED", direction: "IN") - rewarderId: ID - createdAt: String - badge: Badge @relation(name: "REWARDED", direction: "OUT") -} - -type Organization { - id: ID! - createdBy: User @relation(name: "CREATED_ORGA", direction: "IN") - ownedBy: [User] @relation(name: "OWNING_ORGA", direction: "IN") - name: String! - slug: String - description: String! - descriptionExcerpt: String - deleted: Boolean - disabled: Boolean - - tags: [Tag]! @relation(name: "TAGGED", direction: "OUT") - categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT") -} - -type Tag { - id: ID! - name: String! - taggedPosts: [Post]! @relation(name: "TAGGED", direction: "IN") - taggedOrganizations: [Organization]! @relation(name: "TAGGED", direction: "IN") - taggedCount: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p) RETURN COUNT(DISTINCT p)") - taggedCountUnique: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p)<-[:WROTE]-(u:User) RETURN COUNT(DISTINCT u)") - deleted: Boolean - disabled: Boolean -} - -type SharedInboxEndpoint { - id: ID! - uri: String -} - -type SocialMedia { - id: ID! - url: String - ownedBy: [User]! @relation(name: "OWNED", direction: "IN") -} - diff --git a/backend/src/schema/types/type/Badge.gql b/backend/src/schema/types/type/Badge.gql index 68c5d5707..99015a518 100644 --- a/backend/src/schema/types/type/Badge.gql +++ b/backend/src/schema/types/type/Badge.gql @@ -1,6 +1,5 @@ type Badge { id: ID! - key: String! type: BadgeType! status: BadgeStatus! icon: String! @@ -10,4 +9,23 @@ type Badge { updatedAt: String rewarded: [User]! @relation(name: "REWARDED", direction: "OUT") -} \ No newline at end of file +} + +enum BadgeStatus { + permanent + temporary +} + +enum BadgeType { + role + crowdfunding +} + +type Query { + Badge: [Badge] +} + +type Mutation { + reward(badgeKey: ID!, userId: ID!): User + unreward(badgeKey: ID!, userId: ID!): User +} diff --git a/backend/src/schema/types/type/EMOTED.gql b/backend/src/schema/types/type/EMOTED.gql new file mode 100644 index 000000000..80d655b5c --- /dev/null +++ b/backend/src/schema/types/type/EMOTED.gql @@ -0,0 +1,10 @@ +type EMOTED @relation(name: "EMOTED") { + from: User + to: Post + + emotion: Emotion + #createdAt: DateTime + #updatedAt: DateTime + createdAt: String + updatedAt: String +} \ No newline at end of file diff --git a/backend/src/schema/types/type/EmailAddress.gql b/backend/src/schema/types/type/EmailAddress.gql new file mode 100644 index 000000000..63b39d457 --- /dev/null +++ b/backend/src/schema/types/type/EmailAddress.gql @@ -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 +} diff --git a/backend/src/schema/types/type/InvitationCode.gql b/backend/src/schema/types/type/InvitationCode.gql new file mode 100644 index 000000000..044967286 --- /dev/null +++ b/backend/src/schema/types/type/InvitationCode.gql @@ -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 +} diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index deb1d8f85..d254a9a9c 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -48,6 +48,8 @@ type Post { RETURN COUNT(u) >= 1 """ ) + + emotions: [EMOTED] } type Mutation { diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index 6836f16fe..81bf3e782 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -2,21 +2,17 @@ type User { id: ID! actorId: String name: String - email: String! - slug: String - password: String! + email: String! @cypher(statement: "MATCH (this)-[:PRIMARY_EMAIL]->(e:EmailAddress) RETURN e.email") + slug: String! avatar: String coverImg: String - avatarUpload: Upload deleted: Boolean disabled: Boolean disabledBy: User @relation(name: "DISABLED", direction: "IN") - role: UserGroup + role: UserGroup! publicKey: String - privateKey: String - - wasInvited: Boolean - wasSeeded: Boolean + invitedBy: User @relation(name: "INVITED", direction: "IN") + invited: [User] @relation(name: "INVITED", direction: "OUT") location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") locationName: String @@ -77,4 +73,95 @@ type User { badges: [Badge]! @relation(name: "REWARDED", direction: "IN") badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)") + + emotions: [EMOTED] +} + + +input _UserFilter { + AND: [_UserFilter!] + OR: [_UserFilter!] + name_contains: String + about_contains: String + slug_contains: String + 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 } diff --git a/backend/src/seed/factories/badges.js b/backend/src/seed/factories/badges.js index 6414e9f36..5f0482460 100644 --- a/backend/src/seed/factories/badges.js +++ b/backend/src/seed/factories/badges.js @@ -1,28 +1,15 @@ -import uuid from 'uuid/v4' - -export default function(params) { - const { - id = uuid(), - key = '', - type = 'crowdfunding', - status = 'permanent', - icon = '/img/badges/indiegogo_en_panda.svg', - } = params - +export default function create() { return { - mutation: ` - mutation( - $id: ID - $key: String! - $type: BadgeType! - $status: BadgeStatus! - $icon: String! - ) { - CreateBadge(id: $id, key: $key, type: $type, status: $status, icon: $icon) { - id - } + factory: async ({ args, neodeInstance }) => { + const defaults = { + type: 'crowdfunding', + status: 'permanent', } - `, - variables: { id, key, type, status, icon }, + args = { + ...defaults, + ...args, + } + return neodeInstance.create('Badge', args) + }, } } diff --git a/backend/src/seed/factories/index.js b/backend/src/seed/factories/index.js index 211edf87e..e841b0beb 100644 --- a/backend/src/seed/factories/index.js +++ b/backend/src/seed/factories/index.js @@ -1,5 +1,5 @@ import { GraphQLClient, request } from 'graphql-request' -import { getDriver } from '../../bootstrap/neo4j' +import { getDriver, neode } from '../../bootstrap/neo4j' import createBadge from './badges.js' import createUser from './users.js' import createOrganization from './organizations.js' @@ -48,7 +48,11 @@ export const cleanDatabase = async (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) @@ -58,19 +62,24 @@ export default function Factory(options = {}) { graphQLClient, factories, lastResponse: null, + neodeInstance, async authenticateAs({ email, password }) { const headers = await authenticatedHeaders({ email, password }, seedServerHost) this.lastResponse = headers this.graphQLClient = new GraphQLClient(seedServerHost, { headers }) return this }, - async create(node, properties) { - const { mutation, variables } = this.factories[node](properties) - this.lastResponse = await this.graphQLClient.request(mutation, variables) + async create(node, args = {}) { + const { factory, mutation, variables } = this.factories[node](args) + if (factory) { + this.lastResponse = await factory({ args, neodeInstance }) + return this.lastResponse + } else { + this.lastResponse = await this.graphQLClient.request(mutation, variables) + } return this }, - async relate(node, relationship, properties) { - const { from, to } = properties + async relate(node, relationship, { from, to }) { const mutation = ` mutation { Add${node}${relationship}( @@ -112,6 +121,11 @@ export default function Factory(options = {}) { this.lastResponse = await this.graphQLClient.request(mutation) 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() { this.lastResponse = await cleanDatabase({ driver: this.neo4jDriver }) return this @@ -121,6 +135,9 @@ export default function Factory(options = {}) { result.create.bind(result) result.relate.bind(result) result.mutate.bind(result) + result.shout.bind(result) + result.follow.bind(result) + result.invite.bind(result) result.cleanDatabase.bind(result) return result } diff --git a/backend/src/seed/factories/users.js b/backend/src/seed/factories/users.js index ca17d1721..af1699253 100644 --- a/backend/src/seed/factories/users.js +++ b/backend/src/seed/factories/users.js @@ -1,51 +1,31 @@ import faker from 'faker' import uuid from 'uuid/v4' +import encryptPassword from '../../helpers/encryptPassword' +import slugify from 'slug' -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 - +export default function create() { return { - mutation: ` - mutation( - $id: ID! - $name: String - $slug: String - $password: String! - $email: String! - $avatar: String - $about: String - $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 - } + factory: async ({ args, neodeInstance }) => { + const defaults = { + id: uuid(), + name: faker.name.findName(), + email: faker.internet.email(), + password: '1234', + role: 'user', + avatar: faker.internet.avatar(), + about: faker.lorem.paragraph(), } - `, - variables: { id, name, slug, password, email, avatar, about, role }, + defaults.slug = slugify(defaults.name, { lower: true }) + args = { + ...defaults, + ...args, + } + args = await encryptPassword(args) + const user = await neodeInstance.create('User', args) + const email = await neodeInstance.create('EmailAddress', { email: args.email }) + await user.relateTo(email, 'primaryEmail') + await email.relateTo(user, 'belongsTo') + return user + }, } } diff --git a/backend/src/seed/seed-db.js b/backend/src/seed/seed-db.js index 27c07868d..8f693cfd3 100644 --- a/backend/src/seed/seed-db.js +++ b/backend/src/seed/seed-db.js @@ -5,52 +5,42 @@ import Factory from './factories' ;(async function() { try { const f = Factory() - await Promise.all([ + const [racoon, rabbit, wolf, bear, turtle, rhino] = await Promise.all([ f.create('Badge', { - id: 'b1', - key: 'indiegogo_en_racoon', - type: 'crowdfunding', - status: 'permanent', + id: 'indiegogo_en_racoon', icon: '/img/badges/indiegogo_en_racoon.svg', }), f.create('Badge', { - id: 'b2', - key: 'indiegogo_en_rabbit', - type: 'crowdfunding', - status: 'permanent', + id: 'indiegogo_en_rabbit', icon: '/img/badges/indiegogo_en_rabbit.svg', }), f.create('Badge', { - id: 'b3', - key: 'indiegogo_en_wolf', - type: 'crowdfunding', - status: 'permanent', + id: 'indiegogo_en_wolf', icon: '/img/badges/indiegogo_en_wolf.svg', }), f.create('Badge', { - id: 'b4', - key: 'indiegogo_en_bear', - type: 'crowdfunding', - status: 'permanent', + id: 'indiegogo_en_bear', icon: '/img/badges/indiegogo_en_bear.svg', }), f.create('Badge', { - id: 'b5', - key: 'indiegogo_en_turtle', - type: 'crowdfunding', - status: 'permanent', + id: 'indiegogo_en_turtle', icon: '/img/badges/indiegogo_en_turtle.svg', }), f.create('Badge', { - id: 'b6', - key: 'indiegogo_en_rhino', - type: 'crowdfunding', - status: 'permanent', + id: 'indiegogo_en_rhino', icon: '/img/badges/indiegogo_en_rhino.svg', }), ]) - await Promise.all([ + const [ + peterLustig, + bobDerBaumeister, + jennyRostock, + tick, // eslint-disable-line no-unused-vars + trick, // eslint-disable-line no-unused-vars + track, // eslint-disable-line no-unused-vars + dagobert, + ] = await Promise.all([ f.create('User', { id: 'u1', name: 'Peter Lustig', @@ -69,47 +59,130 @@ import Factory from './factories' role: 'user', email: 'user@example.org', }), - f.create('User', { id: 'u4', name: 'Tick', 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' }), + f.create('User', { + id: 'u4', + name: 'Tick', + 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([ - Factory().authenticateAs({ email: 'admin@example.org', password: '1234' }), - Factory().authenticateAs({ 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' }), + Factory().authenticateAs({ + email: 'admin@example.org', + password: '1234', + }), + Factory().authenticateAs({ + 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([ - f.relate('User', 'Badges', { from: 'b6', to: 'u1' }), - f.relate('User', 'Badges', { from: 'b5', to: 'u2' }), - f.relate('User', 'Badges', { from: 'b4', to: 'u3' }), - 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' }), + peterLustig.relateTo(racoon, 'rewarded'), + peterLustig.relateTo(rhino, 'rewarded'), + peterLustig.relateTo(wolf, 'rewarded'), + bobDerBaumeister.relateTo(racoon, 'rewarded'), + bobDerBaumeister.relateTo(turtle, 'rewarded'), + jennyRostock.relateTo(bear, 'rewarded'), + dagobert.relateTo(rabbit, 'rewarded'), ]) await Promise.all([ - asAdmin.follow({ id: 'u3', type: 'User' }), - asModerator.follow({ 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' }), + 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([ - f.create('Category', { id: 'cat1', name: 'Just For Fun', slug: 'justforfun', icon: 'smile' }), + asAdmin.follow({ + id: 'u3', + type: 'User', + }), + asModerator.follow({ + 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([ + f.create('Category', { + id: 'cat1', + name: 'Just For Fun', + slug: 'justforfun', + icon: 'smile', + }), f.create('Category', { id: 'cat2', name: 'Happyness & Values', @@ -203,10 +276,22 @@ import Factory from './factories' ]) await Promise.all([ - f.create('Tag', { id: 't1', name: 'Umwelt' }), - f.create('Tag', { id: 't2', name: 'Naturschutz' }), - f.create('Tag', { id: 't3', name: 'Demokratie' }), - f.create('Tag', { id: 't4', name: 'Freiheit' }), + f.create('Tag', { + id: 'Umwelt', + name: 'Umwelt', + }), + f.create('Tag', { + id: 'Naturschutz', + name: 'Naturschutz', + }), + f.create('Tag', { + id: 'Demokratie', + name: 'Demokratie', + }), + f.create('Tag', { + id: 'Freiheit', + name: 'Freiheit', + }), ]) const mention1 = 'Hey @jenny-rostock, what\'s up?' @@ -214,108 +299,347 @@ import Factory from './factories' 'Hey @jenny-rostock, here is another notification for you!' await Promise.all([ - asAdmin.create('Post', { id: 'p0', image: faker.image.unsplash.food() }), - asModerator.create('Post', { id: 'p1', image: faker.image.unsplash.technology() }), - asUser.create('Post', { id: 'p2' }), - asTick.create('Post', { id: 'p3' }), - asTrick.create('Post', { 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' }), + asAdmin.create('Post', { + id: 'p0', + image: faker.image.unsplash.food(), + }), + asModerator.create('Post', { + id: 'p1', + image: faker.image.unsplash.technology(), + }), + asUser.create('Post', { + id: 'p2', + }), + asTick.create('Post', { + id: 'p3', + }), + asTrick.create('Post', { + 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([ - f.relate('Post', 'Categories', { from: 'p0', to: 'cat16' }), - f.relate('Post', 'Categories', { from: 'p1', to: 'cat1' }), - f.relate('Post', 'Categories', { from: 'p2', to: 'cat2' }), - f.relate('Post', 'Categories', { from: 'p3', to: 'cat3' }), - 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', 'Categories', { + from: 'p0', + to: 'cat16', + }), + f.relate('Post', 'Categories', { + from: 'p1', + to: 'cat1', + }), + f.relate('Post', 'Categories', { + from: 'p2', + to: 'cat2', + }), + f.relate('Post', 'Categories', { + from: 'p3', + to: 'cat3', + }), + 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', { from: 'p1', to: 't1' }), - f.relate('Post', 'Tags', { from: 'p2', to: 't2' }), - f.relate('Post', 'Tags', { from: 'p3', to: 't3' }), - f.relate('Post', 'Tags', { from: 'p4', to: 't4' }), - f.relate('Post', 'Tags', { from: 'p5', to: 't1' }), - f.relate('Post', 'Tags', { from: 'p6', to: 't2' }), - f.relate('Post', 'Tags', { from: 'p7', to: 't3' }), - f.relate('Post', 'Tags', { from: 'p8', to: 't4' }), - f.relate('Post', 'Tags', { from: 'p9', to: 't1' }), - f.relate('Post', 'Tags', { from: 'p10', to: 't2' }), - f.relate('Post', 'Tags', { from: 'p11', to: 't3' }), - f.relate('Post', 'Tags', { from: 'p12', to: 't4' }), - f.relate('Post', 'Tags', { from: 'p13', to: 't1' }), - f.relate('Post', 'Tags', { from: 'p14', to: 't2' }), - f.relate('Post', 'Tags', { from: 'p15', to: 't3' }), + f.relate('Post', 'Tags', { + from: 'p0', + to: 'Freiheit', + }), + f.relate('Post', 'Tags', { + from: 'p1', + to: 'Umwelt', + }), + f.relate('Post', 'Tags', { + from: 'p2', + to: 'Naturschutz', + }), + f.relate('Post', 'Tags', { + from: 'p3', + to: 'Demokratie', + }), + 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([ - asAdmin.shout({ id: 'p2', type: 'Post' }), - asAdmin.shout({ id: 'p6', 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' }), + asAdmin.shout({ + id: 'p2', + type: 'Post', + }), + asAdmin.shout({ + id: 'p6', + 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([ - asAdmin.shout({ id: 'p2', type: 'Post' }), - asAdmin.shout({ id: 'p6', 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' }), + asAdmin.shout({ + id: 'p2', + type: 'Post', + }), + asAdmin.shout({ + id: 'p6', + 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([ - asUser.create('Comment', { id: 'c1', postId: 'p1' }), - asTick.create('Comment', { id: 'c2', postId: 'p1' }), - asTrack.create('Comment', { id: 'c3', postId: 'p3' }), - 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' }), + asUser.create('Comment', { + id: 'c1', + postId: 'p1', + }), + asTick.create('Comment', { + id: 'c2', + postId: 'p1', + }), + asTrack.create('Comment', { + id: 'c3', + postId: 'p3', + }), + 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) }' await Promise.all([ - asModerator.mutate(disableMutation, { id: 'p11' }), - asModerator.mutate(disableMutation, { id: 'c5' }), + asModerator.mutate(disableMutation, { + id: 'p11', + }), + asModerator.mutate(disableMutation, { + id: 'c5', + }), ]) await Promise.all([ - asTick.create('Report', { description: "I don't like this comment", 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' }), + asTick.create('Report', { + description: "I don't like this comment", + 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([ @@ -342,13 +666,33 @@ import Factory from './factories' ]) await Promise.all([ - f.relate('Organization', 'CreatedBy', { from: 'u1', to: 'o1' }), - f.relate('Organization', 'CreatedBy', { from: 'u1', to: 'o2' }), - f.relate('Organization', 'OwnedBy', { from: 'u2', to: 'o2' }), - f.relate('Organization', 'OwnedBy', { from: 'u2', to: 'o3' }), + f.relate('Organization', 'CreatedBy', { + from: 'u1', + to: 'o1', + }), + f.relate('Organization', 'CreatedBy', { + from: 'u1', + to: 'o2', + }), + f.relate('Organization', 'OwnedBy', { + from: 'u2', + to: 'o2', + }), + f.relate('Organization', 'OwnedBy', { + from: 'u2', + to: 'o3', + }), ]) + + await Promise.all( + [...Array(30).keys()].map(i => { + return f.create('User') + }), + ) + /* eslint-disable-next-line no-console */ console.log('Seeded Data...') + process.exit(0) } catch (err) { /* eslint-disable-next-line no-console */ console.error(err) diff --git a/backend/src/server.js b/backend/src/server.js index 7692f0d2c..d58ecd277 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -1,6 +1,6 @@ import express from 'express' import helmet from 'helmet' -import { GraphQLServer } from 'graphql-yoga' +import { ApolloServer } from 'apollo-server-express' import CONFIG, { requiredConfigs } from './config' import mocks from './mocks' import middleware from './middleware' @@ -20,28 +20,30 @@ const driver = getDriver() const createServer = options => { const defaults = { - context: async ({ request }) => { - const user = await decode(driver, request.headers.authorization) + context: async ({ req }) => { + const user = await decode(driver, req.headers.authorization) return { driver, user, - req: request, + req, cypherParams: { currentUserId: user ? user.id : null, }, } }, - schema, + schema: middleware(schema), debug: CONFIG.DEBUG, tracing: CONFIG.DEBUG, - middlewares: middleware(schema), mocks: CONFIG.MOCKS ? mocks : false, } - const server = new GraphQLServer(Object.assign({}, defaults, options)) + const server = new ApolloServer(Object.assign({}, defaults, options)) - server.express.use(helmet()) - server.express.use(express.static('public')) - return server + const app = express() + app.use(helmet()) + app.use(express.static('public')) + server.applyMiddleware({ app, path: '/' }) + + return { server, app } } export default createServer diff --git a/backend/src/server.spec.js b/backend/src/server.spec.js new file mode 100644 index 000000000..6d4ef546d --- /dev/null +++ b/backend/src/server.spec.js @@ -0,0 +1,43 @@ +import { createTestClient } from 'apollo-server-testing' +import createServer from './server' + +/** + * This file is for demonstration purposes. It does not really test the + * `isLoggedIn` query but demonstrates how we can use `apollo-server-testing`. + * All we need to do is to get an instance of `ApolloServer` and maybe we want + * stub out `context` as shown below. + * + */ + +let user +let action +describe('isLoggedIn', () => { + beforeEach(() => { + action = async () => { + const { server } = createServer({ + context: () => { + return { + user, + } + }, + }) + const { query } = createTestClient(server) + + const isLoggedIn = `{ isLoggedIn }` + return query({ query: isLoggedIn }) + } + }) + + it('returns false', async () => { + const expected = expect.objectContaining({ data: { isLoggedIn: false } }) + await expect(action()).resolves.toEqual(expected) + }) + + describe('when authenticated', () => { + it('returns true', async () => { + user = { id: '123' } + const expected = expect.objectContaining({ data: { isLoggedIn: true } }) + await expect(action()).resolves.toEqual(expected) + }) + }) +}) diff --git a/backend/yarn.lock b/backend/yarn.lock index 53075537f..23ebc2de3 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -9,15 +9,15 @@ dependencies: apollo-env "0.5.1" -"@apollographql/graphql-playground-html@1.6.20": - version "1.6.20" - resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.20.tgz#bf9f2acdf319c0959fad8ec1239741dd2ead4e8d" - integrity sha512-3LWZa80HcP70Pl+H4KhLDJ7S0px+9/c8GTXdl6SpunRecUaB27g/oOQnAjNHLHdbWuGE0uyqcuGiTfbKB3ilaQ== +"@apollographql/graphql-playground-html@1.6.24": + version "1.6.24" + resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.24.tgz#3ce939cb127fb8aaa3ffc1e90dff9b8af9f2e3dc" + integrity sha512-8GqG48m1XqyXh4mIZrtB5xOhUwSsh1WsrrsaZQOEYYql3YN9DEu9OOSg0ILzXHZo/h2Q74777YE4YzlArQzQEQ== -"@babel/cli@~7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.4.4.tgz#5454bb7112f29026a4069d8e6f0e1794e651966c" - integrity sha512-XGr5YjQSjgTa6OzQZY57FAJsdeVSAKR/u/KA5exWIz66IKtv/zXtHy+fIZcMry/EgYegwuHE7vzGnrFhjdIAsQ== +"@babel/cli@~7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.5.0.tgz#f403c930692e28ecfa3bf02a9e7562b474f38271" + integrity sha512-qNH55fWbKrEsCwID+Qc/3JDPnsSGpIIiMDbppnR8Z6PxLAqMQCFNqBctkIkBrMH49Nx+qqVTrHRWUR+ho2k+qQ== dependencies: commander "^2.8.1" convert-source-map "^1.1.0" @@ -38,18 +38,18 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/core@^7.1.0", "@babel/core@~7.4.5": - version "7.4.5" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.5.tgz#081f97e8ffca65a9b4b0fdc7e274e703f000c06a" - integrity sha512-OvjIh6aqXtlsA8ujtGKfC7LYWksYSX8yQcM8Ay3LuvVeQ63lcOKgoZWVqcpFwkd29aYU9rVx7jxhfhiEDV9MZA== +"@babel/core@^7.1.0", "@babel/core@~7.5.4": + version "7.5.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.4.tgz#4c32df7ad5a58e9ea27ad025c11276324e0b4ddd" + integrity sha512-+DaeBEpYq6b2+ZmHx3tHspC+ZRflrvLqwfv8E3hNr5LVQoyBnL8RPKSBCg+rK2W2My9PWlujBiqd0ZPsR9Q6zQ== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.4.4" - "@babel/helpers" "^7.4.4" - "@babel/parser" "^7.4.5" + "@babel/generator" "^7.5.0" + "@babel/helpers" "^7.5.4" + "@babel/parser" "^7.5.0" "@babel/template" "^7.4.4" - "@babel/traverse" "^7.4.5" - "@babel/types" "^7.4.4" + "@babel/traverse" "^7.5.0" + "@babel/types" "^7.5.0" convert-source-map "^1.1.0" debug "^4.1.0" json5 "^2.1.0" @@ -58,12 +58,12 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.0.0", "@babel/generator@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.4.4.tgz#174a215eb843fc392c7edcaabeaa873de6e8f041" - integrity sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ== +"@babel/generator@^7.0.0", "@babel/generator@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.5.0.tgz#f20e4b7a91750ee8b63656073d843d2a736dca4a" + integrity sha512-1TTVrt7J9rcG5PMjvO7VEG3FrEoEJNHxumRq66GemPmzboLWtIjjcJgk8rokuAS7IiRSpgVSu5Vb9lc99iJkOA== dependencies: - "@babel/types" "^7.4.4" + "@babel/types" "^7.5.0" jsesc "^2.5.1" lodash "^4.17.11" source-map "^0.5.0" @@ -260,14 +260,14 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.2.0" -"@babel/helpers@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.4.4.tgz#868b0ef59c1dd4e78744562d5ce1b59c89f2f2a5" - integrity sha512-igczbR/0SeuPR8RFfC7tGrbdTbFL3QTvH6D+Z6zNxnTe//GyqmtHmDkzrqDmyZ3eSwPqB/LhyKoU5DXsp+Vp2A== +"@babel/helpers@^7.5.4": + version "7.5.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.5.4.tgz#2f00608aa10d460bde0ccf665d6dcf8477357cf0" + integrity sha512-6LJ6xwUEJP51w0sIgKyfvFMJvIb9mWAfohJp0+m6eHJigkFdcH8duZ1sfhn0ltJRzwUIT/yqqhdSfRpCpL7oow== dependencies: "@babel/template" "^7.4.4" - "@babel/traverse" "^7.4.4" - "@babel/types" "^7.4.4" + "@babel/traverse" "^7.5.0" + "@babel/types" "^7.5.0" "@babel/highlight@^7.0.0": version "7.0.0" @@ -278,10 +278,10 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/node@~7.4.5": - version "7.4.5" - resolved "https://registry.yarnpkg.com/@babel/node/-/node-7.4.5.tgz#bce71bb44d902bfdd4da0b9c839a8a90fc084056" - integrity sha512-nDXPT0KwYMycDHhFG9wKlkipCR+iXzzoX9bD2aF2UABLhQ13AKhNi5Y61W8ASGPPll/7p9GrHesmlOgTUJVcfw== +"@babel/node@~7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/node/-/node-7.5.0.tgz#bcc5a286317ad771703889fb658e1f768c0a2a2e" + integrity sha512-VBlCrbJp7HDrKt4HRbtfq4Rs/XjBokvkfxXRQs4qA1C6eV3JycSOMELx4BFTPFRd9QnNA4PsIRfnvJqe/3tHow== dependencies: "@babel/polyfill" "^7.0.0" "@babel/register" "^7.0.0" @@ -290,10 +290,10 @@ node-environment-flags "^1.0.5" v8flags "^3.1.1" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.4.4", "@babel/parser@^7.4.5": - version "7.4.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.5.tgz#04af8d5d5a2b044a2a1bffacc1e5e6673544e872" - integrity sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew== +"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.4.4", "@babel/parser@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.0.tgz#3e0713dff89ad6ae37faec3b29dcfc5c979770b7" + integrity sha512-I5nW8AhGpOXGCCNYGc+p7ExQIBxRFnS2fd/d862bNOKvmoEPjYPcfIjsfdy0ujagYOIYPczKgD9l3FsgTkAzKA== "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.2.0" @@ -304,6 +304,14 @@ "@babel/helper-remap-async-to-generator" "^7.1.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": version "7.2.0" 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/plugin-syntax-json-strings" "^7.2.0" -"@babel/plugin-proposal-object-rest-spread@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.4.4.tgz#1ef173fcf24b3e2df92a678f027673b55e7e3005" - integrity sha512-dMBG6cSPBbHeEBdFXeQ2QLc5gUpg4Vkaz8octD4aoW/ISO+jBOcsuxYL7bsb5WSu8RLP6boxrBIALEHgoHtO9g== +"@babel/plugin-proposal-object-rest-spread@^7.5.4": + version "7.5.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.5.4.tgz#250de35d867ce8260a31b1fdac6c4fc1baa99331" + integrity sha512-KCx0z3y7y8ipZUMAEEJOyNi11lMb/FOPUjjB113tfowgw0c16EGYos7worCKBcUAh2oG+OBnoUhsnTSoLpV9uA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-object-rest-spread" "^7.2.0" @@ -352,6 +360,13 @@ dependencies: "@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": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz#72bd13f6ffe1d25938129d2a186b11fd62951470" @@ -387,10 +402,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-async-to-generator@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.4.4.tgz#a3f1d01f2f21cadab20b33a82133116f14fb5894" - integrity sha512-YiqW2Li8TXmzgbXw+STsSqPBPFnGviiaSp6CYOq55X8GQ2SGVLrXB6pNid8HkqkZAzOH6knbai3snhP7v0fNwA== +"@babel/plugin-transform-async-to-generator@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.5.0.tgz#89a3848a0166623b5bc481164b5936ab947e887e" + integrity sha512-mqvkzwIGkq0bEF1zLRRiTdjfomZJDV33AH3oQzHVGkI2VzEmXLpKKOBvEVaFZBJdN0XTyH38s9j/Kiqr68dggg== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -432,10 +447,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-destructuring@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.4.4.tgz#9d964717829cc9e4b601fc82a26a71a4d8faf20f" - integrity sha512-/aOx+nW0w8eHiEHm+BTERB2oJn5D127iye/SUQl7NjHy0lf+j7h4MKMMSOwdazGq9OxgiNADncE+SRJkCxjZpQ== +"@babel/plugin-transform-destructuring@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.5.0.tgz#f6c09fdfe3f94516ff074fe877db7bc9ef05855a" + integrity sha512-YbYgbd3TryYYLGyC7ZR+Tq8H/+bCmwoaxHfJHupom5ECstzbRLTch6gOQbhEY9Z4hiCNHEURgq06ykFv9JZ/QQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" @@ -448,10 +463,10 @@ "@babel/helper-regex" "^7.4.4" regexpu-core "^4.5.4" -"@babel/plugin-transform-duplicate-keys@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.2.0.tgz#d952c4930f312a4dbfff18f0b2914e60c35530b3" - integrity sha512-q+yuxW4DsTjNceUiTzK0L+AfQ0zD9rWaTLiUqHA8p0gxx7lu1EylenfzjeIWNkPy6e/0VG/Wjw9uf9LueQwLOw== +"@babel/plugin-transform-duplicate-keys@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.5.0.tgz#c5dbf5106bf84cdf691222c0974c12b1df931853" + integrity sha512-igcziksHizyQPlX9gfSjHkE2wmoCH3evvD2qR5w29/Dk0SMKE/eOI7f1HhBdNhR/zxJDqrgpoDTq5YSLH/XMsQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" @@ -492,30 +507,33 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-modules-amd@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.2.0.tgz#82a9bce45b95441f617a24011dc89d12da7f4ee6" - integrity sha512-mK2A8ucqz1qhrdqjS9VMIDfIvvT2thrEsIQzbaTdc5QFzhDjQv2CkJJ5f6BXIkgbmaoax3zBr2RyvV/8zeoUZw== +"@babel/plugin-transform-modules-amd@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.5.0.tgz#ef00435d46da0a5961aa728a1d2ecff063e4fb91" + integrity sha512-n20UsQMKnWrltocZZm24cRURxQnWIvsABPJlw/fvoy9c6AgHZzoelAIzajDHAQrDpuKFFPPcFGd7ChsYuIUMpg== dependencies: "@babel/helper-module-transforms" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" + babel-plugin-dynamic-import-node "^2.3.0" -"@babel/plugin-transform-modules-commonjs@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.4.4.tgz#0bef4713d30f1d78c2e59b3d6db40e60192cac1e" - integrity sha512-4sfBOJt58sEo9a2BQXnZq+Q3ZTSAUXyK3E30o36BOGnJ+tvJ6YSxF0PG6kERvbeISgProodWuI9UVG3/FMY6iw== +"@babel/plugin-transform-modules-commonjs@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.5.0.tgz#425127e6045231360858eeaa47a71d75eded7a74" + integrity sha512-xmHq0B+ytyrWJvQTc5OWAC4ii6Dhr0s22STOoydokG51JjWhyYo5mRPXoi+ZmtHQhZZwuXNN+GG5jy5UZZJxIQ== dependencies: "@babel/helper-module-transforms" "^7.4.4" "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-simple-access" "^7.1.0" + babel-plugin-dynamic-import-node "^2.3.0" -"@babel/plugin-transform-modules-systemjs@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.4.4.tgz#dc83c5665b07d6c2a7b224c00ac63659ea36a405" - integrity sha512-MSiModfILQc3/oqnG7NrP1jHaSPryO6tA2kOMmAQApz5dayPxWiHqmq4sWH2xF5LcQK56LlbKByCd8Aah/OIkQ== +"@babel/plugin-transform-modules-systemjs@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.5.0.tgz#e75266a13ef94202db2a0620977756f51d52d249" + integrity sha512-Q2m56tyoQWmuNGxEtUyeEkm6qJYFqs4c+XyXH5RAuYxObRNz9Zgj/1g2GMnjYp2EUyEy7YTrxliGCXzecl/vJg== dependencies: "@babel/helper-hoist-variables" "^7.4.4" "@babel/helper-plugin-utils" "^7.0.0" + babel-plugin-dynamic-import-node "^2.3.0" "@babel/plugin-transform-modules-umd@^7.2.0": version "7.2.0" @@ -631,39 +649,41 @@ core-js "^2.5.7" regenerator-runtime "^0.12.0" -"@babel/preset-env@~7.4.5": - version "7.4.5" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.4.5.tgz#2fad7f62983d5af563b5f3139242755884998a58" - integrity sha512-f2yNVXM+FsR5V8UwcFeIHzHWgnhXg3NpRmy0ADvALpnhB0SLbCvrCRr4BLOUYbQNLS+Z0Yer46x9dJXpXewI7w== +"@babel/preset-env@~7.5.4": + version "7.5.4" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.5.4.tgz#64bc15041a3cbb0798930319917e70fcca57713d" + integrity sha512-hFnFnouyRNiH1rL8YkX1ANCNAUVC8Djwdqfev8i1415tnAG+7hlA5zhZ0Q/3Q5gkop4HioIPbCEWAalqcbxRoQ== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.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-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-unicode-property-regex" "^7.4.4" "@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-object-rest-spread" "^7.2.0" "@babel/plugin-syntax-optional-catch-binding" "^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-scoping" "^7.4.4" "@babel/plugin-transform-classes" "^7.4.4" "@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-duplicate-keys" "^7.2.0" + "@babel/plugin-transform-duplicate-keys" "^7.5.0" "@babel/plugin-transform-exponentiation-operator" "^7.2.0" "@babel/plugin-transform-for-of" "^7.4.4" "@babel/plugin-transform-function-name" "^7.4.4" "@babel/plugin-transform-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-commonjs" "^7.4.4" - "@babel/plugin-transform-modules-systemjs" "^7.4.4" + "@babel/plugin-transform-modules-amd" "^7.5.0" + "@babel/plugin-transform-modules-commonjs" "^7.5.0" + "@babel/plugin-transform-modules-systemjs" "^7.5.0" "@babel/plugin-transform-modules-umd" "^7.2.0" "@babel/plugin-transform-named-capturing-groups-regex" "^7.4.5" "@babel/plugin-transform-new-target" "^7.4.4" @@ -678,7 +698,7 @@ "@babel/plugin-transform-template-literals" "^7.4.4" "@babel/plugin-transform-typeof-symbol" "^7.2.0" "@babel/plugin-transform-unicode-regex" "^7.4.4" - "@babel/types" "^7.4.4" + "@babel/types" "^7.5.0" browserslist "^4.6.0" core-js-compat "^3.1.1" invariant "^2.2.2" @@ -704,6 +724,13 @@ dependencies: 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": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" @@ -713,25 +740,25 @@ "@babel/parser" "^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": - version "7.4.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.5.tgz#4e92d1728fd2f1897dafdd321efbff92156c3216" - integrity sha512-Vc+qjynwkjRmIFGxy0KYoPj4FdVDxLej89kMHFsWScq999uX+pwcX4v9mWRjW0KcAYTPAuVQl2LKP1wEVLsp+A== +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.4", "@babel/traverse@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.5.0.tgz#4216d6586854ef5c3c4592dab56ec7eb78485485" + integrity sha512-SnA9aLbyOCcnnbQEGwdfBggnc142h/rbqqsXcaATj2hZcegCl903pUD/lfpsNBlBSuWow/YDfRyJuWi2EPR5cg== dependencies: "@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-split-export-declaration" "^7.4.4" - "@babel/parser" "^7.4.5" - "@babel/types" "^7.4.4" + "@babel/parser" "^7.5.0" + "@babel/types" "^7.5.0" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.11" -"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.4.tgz#8db9e9a629bb7c29370009b4b779ed93fe57d5f0" - integrity sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ== +"@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.5.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.5.0.tgz#e47d43840c2e7f9105bc4d3a2c371b4d0c7832ab" + integrity sha512-UFpDVqRABKsW01bvw7/wSUe56uy6RXM5+VJibVVAybDGxEW25jdwiFJEf7ASvSaC7sN7rbE/l3cLp2izav+CtQ== dependencies: esutils "^2.0.2" lodash "^4.17.11" @@ -745,6 +772,43 @@ exec-sh "^0.3.2" 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.0.3", "@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": version "24.7.1" resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.7.1.tgz#32a9e42535a97aedfe037e725bd67e954b459545" @@ -1020,7 +1084,7 @@ "@types/node" "*" "@types/range-parser" "*" -"@types/express@*", "@types/express@4.17.0", "@types/express@^4.11.1": +"@types/express@*", "@types/express@4.17.0": version "4.17.0" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.0.tgz#49eaedb209582a86f12ed9b725160f12d04ef287" integrity sha512-CjaMu57cjgjuZbh9DpkloeGxV45CnMGlVd+XpG7Gm9QgVrd7KFq+X4HY0vM+2v0bczS48Wg7bvnMY5TN+Xmcfw== @@ -1029,16 +1093,6 @@ "@types/express-serve-static-core" "*" "@types/serve-static" "*" -"@types/graphql-deduplicator@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/graphql-deduplicator/-/graphql-deduplicator-2.0.0.tgz#9e577b8f3feb3d067b0ca756f4a1fb356d533922" - integrity sha512-swUwj5hWF1yFzbUXStLJrUa0ksAt11B8+SwhsAjQAX0LYJ1LLioAyuDcJ9bovWbsNzIXJYXLvljSPQw8nR728w== - -"@types/graphql@^14.0.0": - version "14.0.3" - resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-14.0.3.tgz#389e2e5b83ecdb376d9f98fae2094297bc112c1c" - integrity sha512-TcFkpEjcQK7w8OcrQcd7iIBPjU0rdyi3ldj6d0iJ4PPSzbWqPBvXj9KSwO14hTOX2dm9RoiH7VuxksJLNYdXUQ== - "@types/istanbul-lib-coverage@*": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" @@ -1110,15 +1164,10 @@ resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.9.tgz#693e76a52f61a2f1e7fb48c0eef167b95ea4ffd0" integrity sha512-sCZy4SxP9rN2w30Hlmg5dtdRwgYQfYRiLo9usw8X9cxlf+H4FqM1xX7+sNH7NNKVdbXMJWqva7iyy+fxh/V7fA== -"@types/yup@0.26.20": - version "0.26.20" - resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.20.tgz#3b85a05f5dd76e2e8475abb6a8aeae7777627143" - integrity sha512-LpCsA6NG7vIU7Umv1k4w3YGIBH5ZLZRPEKo8vJLHVbBUqRy2WaJ002kbsRqcwODpkICAOMuyGOqLQJa5isZ8+g== - -"@types/zen-observable@^0.5.3": - version "0.5.4" - resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.5.4.tgz#b863a4191e525206819e008097ebf0fb2e3a1cdc" - integrity sha512-sW6xN96wUak4tgc89d0tbTg7QDGYhGv5hvQIS6h4mRCd8h2btiZ80loPU8cyLwsBbA4ZeQt0FjvUhJ4rNhdsGg== +"@types/yup@0.26.21": + version "0.26.21" + resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.21.tgz#bfca27a02a0631495bfd25b6c63647a125e6944e" + integrity sha512-1C45M7hZrVsl8bXxYV01bitRp0r35djt+eX5HWFH3NdH+8ejqta3KM7rmQLRLrupkhF7jRkAtXl2EgDsriIqwA== "@types/zen-observable@^0.8.0": version "0.8.0" @@ -1279,20 +1328,13 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -apollo-cache-control@0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.7.4.tgz#0cb5c7be0e0dd0c44b1257144cd7f9f2a3c374e6" - integrity sha512-TVACHwcEF4wfHo5H9FLnoNjo0SLDo2jPW+bXs9aw0Y4Z2UisskSAPnIYOqUPnU8SoeNvs7zWgbLizq11SRTJtg== +apollo-cache-control@0.7.5: + version "0.7.5" + resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.7.5.tgz#5d8b949bd9b4f03ca32c7d7e429f509c6881eefc" + integrity sha512-zCPwHjbo/VlmXl0sclZfBq/MlVVeGUAg02Q259OIXSgHBvn9BbExyz+EkO/DJvZfGMquxqS1X1BFO3VKuLUTdw== dependencies: apollo-server-env "2.4.0" - graphql-extensions "0.7.4" - -apollo-cache-control@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.1.1.tgz#173d14ceb3eb9e7cb53de7eb8b61bee6159d4171" - integrity sha512-XJQs167e9u+e5ybSi51nGYr70NPBbswdvTEHtbtXbwkZ+n9t0SLPvUcoqceayOSwjK1XYOdU/EKPawNdb3rLQA== - dependencies: - graphql-extensions "^0.0.x" + graphql-extensions "0.7.7" apollo-cache-inmemory@~1.6.2: version "1.6.2" @@ -1342,17 +1384,17 @@ apollo-engine-reporting-protobuf@0.3.1: dependencies: protobufjs "^6.8.6" -apollo-engine-reporting@1.3.5: - version "1.3.5" - resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.3.5.tgz#075424d39dfe77a20f96e8e33b7ae52d58c38e1e" - integrity sha512-pSwjPgXK/elFsR22LXALtT3jI4fpEpeTNTHgNwLVLohaolusMYgBc/9FnVyFWFfMFS9k+3RmfeQdHhZ6T7WKFQ== +apollo-engine-reporting@1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.3.6.tgz#579ba2da85ff848bd92be1b0f1ad61f0c57e3585" + integrity sha512-oCoFAUBGveg1i1Sao/2gNsf1kirJBT6vw6Zan9BCNUkyh68ewDts+xRg32VnD9lDhaHpXVJ3tVtuaV44HmdSEw== dependencies: apollo-engine-reporting-protobuf "0.3.1" apollo-graphql "^0.3.3" - apollo-server-core "2.6.7" + apollo-server-core "2.6.9" apollo-server-env "2.4.0" async-retry "^1.2.1" - graphql-extensions "0.7.6" + graphql-extensions "0.7.7" apollo-env@0.5.1: version "0.5.1" @@ -1422,24 +1464,24 @@ apollo-server-caching@0.4.0: dependencies: lru-cache "^5.0.0" -apollo-server-core@2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.7.tgz#85b0310f40cfec43a702569c73af16d88776a6f0" - integrity sha512-HfOGLvEwPgDWTvd3ZKRPEkEnICKb7xadn1Mci4+auMTsL/NVkfpjPa8cdzubi/kS2/MvioIn7Bg74gmiSLghGQ== +apollo-server-core@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.9.tgz#75542ad206782e5c31a023b54962e9fdc6404a91" + integrity sha512-r2/Kjm1UmxoTViUt5EcExWXkWl0riXsuGyS1q5LpHKKnA+6b+t4LQKECkRU4EWNpuuzJQn7aF7MmMdvURxoEig== dependencies: "@apollographql/apollo-tools" "^0.3.6" - "@apollographql/graphql-playground-html" "1.6.20" + "@apollographql/graphql-playground-html" "1.6.24" "@types/ws" "^6.0.0" - apollo-cache-control "0.7.4" + apollo-cache-control "0.7.5" apollo-datasource "0.5.0" - apollo-engine-reporting "1.3.5" + apollo-engine-reporting "1.3.6" apollo-server-caching "0.4.0" apollo-server-env "2.4.0" - apollo-server-errors "2.3.0" - apollo-server-plugin-base "0.5.6" - apollo-tracing "0.7.3" + apollo-server-errors "2.3.1" + apollo-server-plugin-base "0.5.8" + apollo-tracing "0.7.4" fast-json-stable-stringify "^2.0.0" - graphql-extensions "0.7.6" + graphql-extensions "0.7.7" graphql-subscriptions "^1.0.0" graphql-tag "^2.9.2" graphql-tools "^4.0.0" @@ -1448,15 +1490,6 @@ apollo-server-core@2.6.7: subscriptions-transport-ws "^0.9.11" ws "^6.0.0" -apollo-server-core@^1.3.6, apollo-server-core@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-1.4.0.tgz#4faff7f110bfdd6c3f47008302ae24140f94c592" - integrity sha512-BP1Vh39krgEjkQxbjTdBURUjLHbFq1zeOChDJgaRsMxGtlhzuLWwwC6lLdPatN8jEPbeHq8Tndp9QZ3iQZOKKA== - dependencies: - apollo-cache-control "^0.1.0" - apollo-tracing "^0.1.0" - graphql-extensions "^0.0.x" - apollo-server-env@2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.0.tgz#6611556c6b627a1636eed31317d4f7ea30705872" @@ -1465,97 +1498,59 @@ apollo-server-env@2.4.0: node-fetch "^2.1.2" util.promisify "^1.0.0" -apollo-server-errors@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.0.tgz#700622b66a16dffcad3b017e4796749814edc061" - integrity sha512-rUvzwMo2ZQgzzPh2kcJyfbRSfVKRMhfIlhY7BzUfM4x6ZT0aijlgsf714Ll3Mbf5Fxii32kD0A/DmKsTecpccw== +apollo-server-errors@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.1.tgz#033cf331463ebb99a563f8354180b41ac6714eb6" + integrity sha512-errZvnh0vUQChecT7M4A/h94dnBSRL213dNxpM5ueMypaLYgnp4hiCTWIEaooo9E4yMGd1qA6WaNbLDG2+bjcg== -apollo-server-express@2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.6.7.tgz#22307e08b75be1553f4099d00028abe52597767d" - integrity sha512-qbCQM+8LxXpwPNN5Sdvcb+Sne8zuCORFt25HJtPJRkHlyBUzOd7JA7SEnUn5e2geTiiGoVIU5leh+++C51udTw== +apollo-server-express@2.6.9, apollo-server-express@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.6.9.tgz#176dab7f2cd5a99655c8eb382ad9b10797422a7b" + integrity sha512-iTkdIdX7m9EAlmL/ZPkKR+x/xuFk1HYZWuJIJG57hHUhcOxj50u7F1E5+5fDwl5RFIdepQ61azF31hhNZuNi4g== dependencies: - "@apollographql/graphql-playground-html" "1.6.20" + "@apollographql/graphql-playground-html" "1.6.24" "@types/accepts" "^1.3.5" "@types/body-parser" "1.17.0" "@types/cors" "^2.8.4" "@types/express" "4.17.0" accepts "^1.3.5" - apollo-server-core "2.6.7" + apollo-server-core "2.6.9" body-parser "^1.18.3" cors "^2.8.4" graphql-subscriptions "^1.0.0" graphql-tools "^4.0.0" type-is "^1.6.16" -apollo-server-express@^1.3.6: - version "1.4.0" - resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-1.4.0.tgz#7d7c58d6d6f9892b83fe575669093bb66738b125" - integrity sha512-zkH00nxhLnJfO0HgnNPBTfZw8qI5ILaPZ5TecMCI9+Y9Ssr2b0bFr9pBRsXy9eudPhI+/O4yqegSUsnLdF/CPw== +apollo-server-plugin-base@0.5.8: + version "0.5.8" + resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.8.tgz#77b4127aff4e3514a9d49e3cc61256aee4d9422e" + integrity sha512-ICbaXr0ycQZL5llbtZhg8zyHbxuZ4khdAJsJgiZaUXXP6+F47XfDQ5uwnl/4Sq9fvkpwS0ctvfZ1D+Ks4NvUzA== + +apollo-server-testing@~2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.6.9.tgz#6c1d20a89c0676bf32714405d729c302d62adfb1" + integrity sha512-MQfXAjNsI63O9sY60tQnGy102sqJSr++Yzm+IR44WrK3Z7FHUDisoh6UATly04EDGtO034xtqukzdUNQCK7+rw== dependencies: - apollo-server-core "^1.4.0" - apollo-server-module-graphiql "^1.4.0" + apollo-server-core "2.6.9" -apollo-server-lambda@1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/apollo-server-lambda/-/apollo-server-lambda-1.3.6.tgz#bdaac37f143c6798e40b8ae75580ba673cea260e" - integrity sha1-varDfxQ8Z5jkC4rnVYC6ZzzqJg4= +apollo-server@~2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.6.9.tgz#10e70488b35bf5171612dfd3f030e4ef94c75295" + integrity sha512-thZxUHVM1CLl3503gMCVirxN9J/33s5C1R+hHMEfLaUSoDlXSMA81Y9LCOi9+6d0C9l5DwiZCFXeZI/fKic2RA== dependencies: - apollo-server-core "^1.3.6" - apollo-server-module-graphiql "^1.3.4" - -apollo-server-module-graphiql@^1.3.4, apollo-server-module-graphiql@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.4.0.tgz#c559efa285578820709f1769bb85d3b3eed3d8ec" - integrity sha512-GmkOcb5he2x5gat+TuiTvabnBf1m4jzdecal3XbXBh/Jg+kx4hcvO3TTDFQ9CuTprtzdcVyA11iqG7iOMOt7vA== - -apollo-server-plugin-base@0.5.6: - version "0.5.6" - resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.6.tgz#3a7128437a0f845e7d873fa43ef091ff7bf27975" - integrity sha512-wJvcPqfm/kiBwY5JZT85t2A4pcHv24xdQIpWMNt1zsnx77lIZqJmhsc22eSUSrlnYqUMXC4XMVgSUfAO4oI9wg== - -apollo-server-testing@~2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.6.7.tgz#cfc6366921eb99fd0cbc5d02552a8a5b268787d5" - integrity sha512-lqgZuSqBd5hkRILeVEleo2ScJjukR/E71Mv67vPBUs01s0gEHNnjSRnuOJJOM3cAFBQOdKPc42cHGANzf2ZZTw== - dependencies: - apollo-server-core "2.6.7" - -apollo-server@~2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.6.7.tgz#b707ede529b4d45f2f00a74f3b457658b0e62e83" - integrity sha512-4wk9JykURLed6CnNIj9jhU6ueeTVmGBTyAnnvnlhRrOf50JAFszUErZIKg6lw5vVr5riaByrGFIkMBTySCHgPQ== - dependencies: - apollo-server-core "2.6.7" - apollo-server-express "2.6.7" + apollo-server-core "2.6.9" + apollo-server-express "2.6.9" express "^4.0.0" graphql-subscriptions "^1.0.0" graphql-tools "^4.0.0" -apollo-tracing@0.7.3: - version "0.7.3" - resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.7.3.tgz#8533e3e2dca2d5a25e8439ce498ea33ff4d159ee" - integrity sha512-H6fSC+awQGnfDyYdGIB0UQUhcUC3n5Vy+ujacJ0bY6R+vwWeZOQvu7wRHNjk/rbOSTLCo9A0OcVX7huRyu9SZg== +apollo-tracing@0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.7.4.tgz#f24d1065100b6d8bf581202859ea0e85ba7bf30d" + integrity sha512-vA0FJCBkFpwdWyVF5UtCqN+enShejyiqSGqq8NxXHU1+GEYTngWa56x9OGsyhX+z4aoDIa3HPKPnP3pjzA0qpg== dependencies: apollo-server-env "2.4.0" - graphql-extensions "0.7.4" - -apollo-tracing@^0.1.0: - version "0.1.4" - resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.1.4.tgz#5b8ae1b01526b160ee6e552a7f131923a9aedcc7" - integrity sha512-Uv+1nh5AsNmC3m130i2u3IqbS+nrxyVV3KYimH5QKsdPjxxIQB3JAT+jJmpeDxBel8gDVstNmCh82QSLxLSIdQ== - dependencies: - graphql-extensions "~0.0.9" - -apollo-upload-server@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/apollo-upload-server/-/apollo-upload-server-7.1.0.tgz#21e07b52252b3749b913468599813e13cfca805f" - integrity sha512-cD9ReCeyurYwZyEDqJYb5TOc9dt8yhPzS+MtrY3iJdqw+pqiiyPngAvVXHjN+Ca7Lajvom4/AT/PBrYVDMM3Kw== - dependencies: - busboy "^0.2.14" - fs-capacitor "^1.0.0" - http-errors "^1.7.0" - object-path "^0.11.4" + graphql-extensions "0.7.7" apollo-utilities@1.3.2, apollo-utilities@^1.0.1, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2: version "1.3.2" @@ -1742,30 +1737,6 @@ atob@^2.1.1: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== -aws-lambda@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/aws-lambda/-/aws-lambda-0.1.2.tgz#19b1585075df31679597b976a5f1def61f12ccee" - integrity sha1-GbFYUHXfMWeVl7l2pfHe9h8SzO4= - dependencies: - aws-sdk "^*" - commander "^2.5.0" - dotenv "^0.4.0" - -aws-sdk@^*: - version "2.373.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.373.0.tgz#fcc5606634b3b11d80810ad252d1b52b3733d780" - integrity sha512-NZYXwXGtFt9jxaKXc+PJsLPnpbD03t0MAZRxh93g36kbFMuRXtY8CDqHYNQ0ZcrgQpXbCQiz1fxT5/wu5Cu70g== - dependencies: - buffer "4.9.1" - events "1.1.1" - ieee754 "1.1.8" - jmespath "0.15.0" - querystring "0.2.0" - sax "1.2.1" - url "0.10.3" - uuid "3.1.0" - xml2js "0.4.19" - aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -1806,6 +1777,13 @@ babel-jest@^24.8.0, babel-jest@~24.8.0: chalk "^2.4.2" 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: version "5.1.0" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.1.0.tgz#6892f529eff65a3e2d33d87dc5888ffa2ecd4a30" @@ -1848,11 +1826,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= -base64-js@^1.0.2: - version "1.3.0" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" - integrity sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw== - base@^0.11.1: version "0.11.2" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" @@ -1921,14 +1894,7 @@ bn.js@^2.0.0: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-2.2.0.tgz#12162bc2ae71fc40a5626c33438f3a875cd37625" integrity sha1-EhYrwq5x/EClYmwzQ486h1zTdiU= -body-parser-graphql@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/body-parser-graphql/-/body-parser-graphql-1.1.0.tgz#80a80353c7cb623562fd375750dfe018d75f0f7c" - integrity sha512-bOBF4n1AnUjcY1SzLeibeIx4XOuYqEkjn/Lm4yKhnN6KedoXMv4hVqgcKHGRnxOMJP64tErqrQU+4cihhpbJXg== - dependencies: - body-parser "^1.18.2" - -body-parser@1.19.0, body-parser@^1.18.2, body-parser@^1.18.3: +body-parser@1.19.0, body-parser@^1.18.3: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== @@ -2039,28 +2005,11 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== -buffer@4.9.1: - version "4.9.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" - integrity sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg= - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - builtin-modules@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= -busboy@^0.2.14: - version "0.2.14" - resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" - integrity sha1-bCpiLvz0fFe7vh4qnDetNseSVFM= - dependencies: - dicer "0.2.5" - readable-stream "1.1.x" - busboy@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b" @@ -2294,7 +2243,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@^2.5.0, commander@^2.8.1, commander@^2.9.0: +commander@^2.8.1, commander@^2.9.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== @@ -2406,17 +2355,12 @@ core-js-pure@3.1.2: resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.1.2.tgz#62fc435f35b7374b9b782013cdcb2f97e9f6dffa" integrity sha512-5ckIdBF26B3ldK9PM177y2ZcATP2oweam9RskHSoqfZCrJ2As6wVg8zJ1zTriFsZf6clj/N1ThDFRGaomMsh9w== -core-js@^2.4.0, core-js@^2.5.3, core-js@^2.5.7: - version "2.6.2" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.2.tgz#267988d7268323b349e20b4588211655f0e83944" - integrity sha512-NdBPF/RVwPW6jr0NCILuyN9RiqLo2b1mddWHkUL+VnvcB7dzlnBJ1bXYntjpTGOgkZiiLWj2JxmOr7eGE3qK6g== +core-js@^2.4.0, core-js@^2.5.7, core-js@^2.6.5: + version "2.6.9" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" + integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== -core-js@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.0.0.tgz#a8dbfa978d29bfc263bfb66c556d0ca924c28957" - integrity sha512-WBmxlgH2122EzEJ6GH8o9L/FeoUKxxxZ6q6VUxoTlsE4EvbTWKJb447eyVxTEuq0LpXjlq/kCB2qgBvsYRkLvQ== - -core-js@^3.0.1: +core-js@^3.0.0, core-js@^3.0.1: version "3.1.3" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.1.3.tgz#95700bca5f248f5f78c0ec63e784eca663ec4138" integrity sha512-PWZ+ZfuaKf178BIAg+CRsljwjIMRV8MY00CbZczkR6Zk5LfkSkjGoaab3+bqRQWVITNZxQB7TFYz+CFcyuamvA== @@ -2584,10 +2528,10 @@ data-urls@^1.0.0: whatwg-mimetype "^2.2.0" whatwg-url "^7.0.0" -date-fns@2.0.0-beta.2: - version "2.0.0-beta.2" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-beta.2.tgz#ccd556df832ef761baa88c600f53d2e829245999" - integrity sha512-4cicZF707RNerr3/Q3CcdLo+3OHMCfrRXE7h5iFgn7AMvX07sqKLxSf8Yp+WJW5bvKr2cy9/PkctXLv4iFtOaA== +date-fns@2.0.0-beta.1: + version "2.0.0-beta.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-beta.1.tgz#6f3209ea8be559211be5160e0a6379a7eade227b" + 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: version "2.6.9" @@ -2711,14 +2655,6 @@ detect-newline@^2.1.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= -dicer@0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" - integrity sha1-WZbAhrszIYyBLAkL3cCc0S+stw8= - dependencies: - readable-stream "1.1.x" - streamsearch "0.1.2" - dicer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872" @@ -2811,10 +2747,10 @@ dot-prop@^4.1.0: dependencies: is-obj "^1.0.0" -dotenv@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-0.4.0.tgz#f6fb351363c2d92207245c737802c9ab5ae1495a" - 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: version "8.0.0" @@ -3053,10 +2989,10 @@ eslint-plugin-import@~2.18.0: read-pkg-up "^2.0.0" resolve "^1.11.0" -eslint-plugin-jest@~22.7.1: - version "22.7.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.7.1.tgz#5dcdf8f7a285f98040378220d6beca581f0ab2a1" - integrity sha512-CrT3AzA738neimv8G8iK2HCkrCwHnAJeeo7k5TEHK86VMItKl6zdJT/tHBDImfnVVAYsVs4Y6BUdBZQCCgfiyw== +eslint-plugin-jest@~22.8.0: + version "22.8.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.8.0.tgz#242ef5459e8da25d2c41438e95eb546e03d7fae1" + integrity sha512-2VftZMfILmlhL3VMq5ptHRIuyyXb3ShDEDb1J1UjvWNzm4l+UK/YmwNuTuJcM0gv8pJuOfiR/8ZptJ8Ou68pFw== eslint-plugin-node@~9.1.0: version "9.1.0" @@ -3208,11 +3144,6 @@ eventemitter3@^3.1.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA== -events@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" - integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= - exec-sh@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b" @@ -3279,7 +3210,7 @@ expect@^24.8.0: jest-message-util "^24.8.0" jest-regex-util "^24.3.0" -express@^4.0.0, express@^4.16.3, express@~4.17.1: +express@^4.0.0, express@^4.17.1: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== @@ -3534,11 +3465,6 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= -fs-capacitor@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-1.0.1.tgz#ff9dbfa14dfaf4472537720f19c3088ed9278df0" - integrity sha512-XdZK0Q78WP29Vm3FGgJRhRhrBm51PagovzWtW2kJ3Q6cYJbGtZqWSGTSPwvtEkyjIirFd7b8Yes/dpOYjt4RRQ== - fs-capacitor@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-2.0.4.tgz#5a22e72d40ae5078b4fe64fe4d08c0d3fc88ad3c" @@ -3715,72 +3641,26 @@ graphql-custom-directives@~0.2.14: moment "^2.22.2" numeral "^2.0.6" -graphql-deduplicator@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/graphql-deduplicator/-/graphql-deduplicator-2.0.2.tgz#d8608161cf6be97725e178df0c41f6a1f9f778f3" - integrity sha512-0CGmTmQh4UvJfsaTPppJAcHwHln8Ayat7yXXxdnuWT+Mb1dBzkbErabCWzjXyKh/RefqlGTTA7EQOZHofMaKJA== - -graphql-extensions@0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.4.tgz#78327712822281d5778b9210a55dc59c93a9c184" - integrity sha512-Ly+DiTDU+UtlfPGQkqmBX2SWMr9OT3JxMRwpB9K86rDNDBTJtG6AE2kliQKKE+hg1+945KAimO7Ep+YAvS7ywg== +graphql-extensions@0.7.7: + version "0.7.7" + resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.7.tgz#19f4dea35391065de72b25def98f8396887bdf43" + integrity sha512-xiTbVGPUpLbF86Bc+zxI/v/axRkwZx3s+y2/kUb2c2MxNZeNhMZEw1dSutuhY2f2JkRkYFJii0ucjIVqPAQ/Lg== dependencies: "@apollographql/apollo-tools" "^0.3.6" - -graphql-extensions@0.7.6: - version "0.7.6" - resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.6.tgz#80cdddf08b0af12525529d1922ee2ea0d0cc8ecf" - integrity sha512-RV00O3YFD1diehvdja180BlKOGWgeigr/8/Wzr6lXwLcFtk6FecQC/7nf6oW1qhuXczHyNjt/uCr0WWbWq6mYg== - dependencies: - "@apollographql/apollo-tools" "^0.3.6" - -graphql-extensions@^0.0.x, graphql-extensions@~0.0.9: - version "0.0.10" - resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.0.10.tgz#34bdb2546d43f6a5bc89ab23c295ec0466c6843d" - integrity sha512-TnQueqUDCYzOSrpQb3q1ngDSP2otJSF+9yNLrQGPzkMsvnQ+v6e2d5tl+B35D4y+XpmvVnAn4T3ZK28mkILveA== - dependencies: - core-js "^2.5.3" - source-map-support "^0.5.1" - -graphql-import@^0.7.0: - version "0.7.1" - resolved "https://registry.yarnpkg.com/graphql-import/-/graphql-import-0.7.1.tgz#4add8d91a5f752d764b0a4a7a461fcd93136f223" - integrity sha512-YpwpaPjRUVlw2SN3OPljpWbVRWAhMAyfSba5U47qGMOSsPLi2gYeJtngGpymjm9nk57RFWEpjqwh4+dpYuFAPw== - dependencies: - lodash "^4.17.4" - resolve-from "^4.0.0" + apollo-server-env "2.4.0" graphql-iso-date@~3.6.1: version "3.6.1" resolved "https://registry.yarnpkg.com/graphql-iso-date/-/graphql-iso-date-3.6.1.tgz#bd2d0dc886e0f954cbbbc496bbf1d480b57ffa96" integrity sha512-AwFGIuYMJQXOEAgRlJlFL4H1ncFM8n8XmoVDTNypNOZyQ8LFDG2ppMFlsS862BSTCDcSUfHp8PD3/uJhv7t59Q== -graphql-middleware@3.0.2, graphql-middleware@~3.0.2: +graphql-middleware@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/graphql-middleware/-/graphql-middleware-3.0.2.tgz#c8cdb67615eec02aec237b455e679f5fc973ddc4" integrity sha512-sRqu1sF+77z42z1OVM1QDHKQWnWY5K3nAgqWiZwx3U4tqNZprrDuXxSChPMliV343IrVkpYdejUYq9w24Ot3FA== dependencies: graphql-tools "^4.0.4" -graphql-playground-html@1.6.12: - version "1.6.12" - resolved "https://registry.yarnpkg.com/graphql-playground-html/-/graphql-playground-html-1.6.12.tgz#8b3b34ab6013e2c877f0ceaae478fafc8ca91b85" - integrity sha512-yOYFwwSMBL0MwufeL8bkrNDgRE7eF/kTHiwrqn9FiR9KLcNIl1xw9l9a+6yIRZM56JReQOHpbQFXTZn1IuSKRg== - -graphql-playground-middleware-express@1.7.11: - version "1.7.11" - resolved "https://registry.yarnpkg.com/graphql-playground-middleware-express/-/graphql-playground-middleware-express-1.7.11.tgz#bbffd784a37133bfa7165bdd8f429081dbf4bcf6" - integrity sha512-sKItB4s3FxqlwCgXdMfwRAfssSoo31bcFsGAAg/HzaZLicY6CDlofKXP8G5iPDerB6NaoAcAaBLutLzl9sd4fQ== - dependencies: - graphql-playground-html "1.6.12" - -graphql-playground-middleware-lambda@1.7.12: - version "1.7.12" - resolved "https://registry.yarnpkg.com/graphql-playground-middleware-lambda/-/graphql-playground-middleware-lambda-1.7.12.tgz#1b06440a288dbcd53f935b43e5b9ca2738a06305" - integrity sha512-fJ1Y0Ck5ctmfaQFoWv7vNnVP7We19P3miVmOT85YPrjpzbMYv0wPfxm4Zjt8nnqXr0KU9nGW53tz3K7/Lvzxtw== - dependencies: - graphql-playground-html "1.6.12" - graphql-request@~1.8.2: version "1.8.2" resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-1.8.2.tgz#398d10ae15c585676741bde3fc01d5ca948f8fbe" @@ -3788,23 +3668,16 @@ graphql-request@~1.8.2: dependencies: cross-fetch "2.2.2" -graphql-shield@~6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-6.0.2.tgz#3ebad8faacbada91b8e576029732e91b5a041c7f" - integrity sha512-3qV2qjeNZla1Fyg6Q2NR5J9AsMaNePLbUboOwhRXB7IcMnTnrxSiVn2R//8VnjnmBjF9rcvgAIAvETZ8AKGfsg== +graphql-shield@~6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-6.0.3.tgz#a79ca8b2fe58fb9558ffc0e64fa8aa19f63af1b3" + integrity sha512-+yVT/dRWsRqeJOTHcEElJVfvIRPrhBqPlg5FHLmSkWNdGMR4AFqAQGrJteuZuNDvJ3bt380CZ96Bvf4J9hUpKA== dependencies: - "@types/yup" "0.26.20" + "@types/yup" "0.26.21" lightercollective "^0.3.0" object-hash "^1.3.1" yup "^0.27.0" -graphql-subscriptions@^0.5.8: - version "0.5.8" - resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-0.5.8.tgz#13a6143c546bce390404657dc73ca501def30aa7" - integrity sha512-0CaZnXKBw2pwnIbvmVckby5Ge5e2ecmjofhYCdyeACbCly2j3WXDP/pl+s+Dqd2GQFC7y99NB+53jrt55CKxYQ== - dependencies: - iterall "^1.2.1" - graphql-subscriptions@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-1.0.0.tgz#475267694b3bd465af6477dbab4263a3f62702b8" @@ -3828,7 +3701,7 @@ graphql-tools@^4.0.0, graphql-tools@^4.0.4: iterall "^1.1.3" uuid "^3.1.0" -graphql-upload@^8.0.0, graphql-upload@^8.0.2: +graphql-upload@^8.0.2: version "8.0.7" resolved "https://registry.yarnpkg.com/graphql-upload/-/graphql-upload-8.0.7.tgz#8644264e241529552ea4b3797e7ee15809cf01a3" integrity sha512-gi2yygbDPXbHPC7H0PNPqP++VKSoNoJO4UrXWq4T0Bi4IhyUd3Ycop/FSxhx2svWIK3jdXR/i0vi91yR1aAF0g== @@ -3838,38 +3711,10 @@ graphql-upload@^8.0.0, graphql-upload@^8.0.2: http-errors "^1.7.2" object-path "^0.11.4" -graphql-yoga@~1.18.0: - version "1.18.0" - resolved "https://registry.yarnpkg.com/graphql-yoga/-/graphql-yoga-1.18.0.tgz#2668278e94a0bd1b2ff8c60f928c4e18d62e381a" - integrity sha512-WEibitQA2oFTmD7XBO8/ps8DWeVpkzOzgbB3EvtM2oIpyGhPCzRZYrC7OS9MmijvRwLRXsgHImHWUm82ZrIOWA== - dependencies: - "@types/cors" "^2.8.4" - "@types/express" "^4.11.1" - "@types/graphql" "^14.0.0" - "@types/graphql-deduplicator" "^2.0.0" - "@types/zen-observable" "^0.5.3" - apollo-server-express "^1.3.6" - apollo-server-lambda "1.3.6" - apollo-upload-server "^7.0.0" - aws-lambda "^0.1.2" - body-parser-graphql "1.1.0" - cors "^2.8.4" - express "^4.16.3" - graphql "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0" - graphql-deduplicator "^2.0.1" - graphql-import "^0.7.0" - graphql-middleware "3.0.2" - graphql-playground-middleware-express "1.7.11" - graphql-playground-middleware-lambda "1.7.12" - graphql-subscriptions "^0.5.8" - graphql-tools "^4.0.0" - graphql-upload "^8.0.0" - 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: - version "14.4.0" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.4.0.tgz#e97086acfc0338e4fdc8f7dba519c6b8a6badfd9" - integrity sha512-E55z1oK6e4cGxCqlSsRWytYDPcIUxky3XkbuQUf6TIjCmn6C7CuBJpmkMF1066q95yPAGOZVPTVT7jABKbRFSA== +graphql@^14.2.1, graphql@~14.4.2: + version "14.4.2" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.4.2.tgz#553a7d546d524663eda49ed6df77577be3203ae3" + integrity sha512-6uQadiRgnpnSS56hdZUSvFrVcQ6OF9y6wkxJfKquFtHlnl7+KSuWwSJsdwiK1vybm1HgcdbpGkCpvhvsVQ0UZQ== dependencies: iterall "^1.2.2" @@ -4073,17 +3918,6 @@ http-errors@1.7.2, http-errors@^1.7.2, http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -http-errors@^1.7.0: - version "1.7.1" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.1.tgz#6a4ffe5d35188e1c39f872534690585852e1f027" - integrity sha512-jWEUgtZWGSMba9I1N3gc1HmvpBUaNC9vDdA46yScAdp+C5rdEuKWUBLWTQpW9FwSWSbYYs++b6SDCxf9UEJzfw== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -4100,16 +3934,6 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: dependencies: safer-buffer ">= 2.1.2 < 3" -ieee754@1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" - integrity sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q= - -ieee754@^1.1.4: - version "1.1.12" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b" - integrity sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA== - ienoopen@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ienoopen/-/ienoopen-1.1.0.tgz#411e5d530c982287dbdc3bb31e7a9c9e32630974" @@ -4176,7 +4000,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= @@ -4470,11 +4294,6 @@ is-windows@^1.0.0, is-windows@^1.0.2: resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= - isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -4911,12 +4730,7 @@ jest@~24.8.0: import-local "^2.0.0" jest-cli "^24.8.0" -jmespath@0.15.0: - version "0.15.0" - resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" - integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc= - -joi@^13.0.0: +joi@^13.7.0: version "13.7.0" resolved "https://registry.yarnpkg.com/joi/-/joi-13.7.0.tgz#cfd85ebfe67e8a1900432400b4d03bbd93fb879f" integrity sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q== @@ -5275,9 +5089,9 @@ lodash.isstring@^4.0.1: integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= lodash.mergewith@^4.6.1: - version "4.6.1" - resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" - integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ== + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" + integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== lodash.once@^4.0.0: version "4.1.1" @@ -5294,10 +5108,10 @@ lodash@=3.10.1: resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" 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: - version "4.17.11" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" - integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== +lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.5, lodash@~4.17.14: + version "4.17.14" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba" + integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw== long@^4.0.0: version "4.0.0" @@ -5604,6 +5418,15 @@ negotiator@0.6.2: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" 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: version "1.7.4" resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.4.tgz#9661cf643b63818bff85e82c4691918e75098c1e" @@ -5623,6 +5446,16 @@ neo4j-graphql-js@^2.6.3: lodash "^4.17.11" 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: version "1.0.0" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" @@ -5711,10 +5544,10 @@ node-releases@^1.1.19: dependencies: semver "^5.3.0" -nodemailer@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.2.1.tgz#20d773925eb8f7a06166a0b62c751dc8290429f3" - integrity sha512-TagB7iuIi9uyNgHExo8lUDq3VK5/B0BpbkcjIgNvxbtVrjNqq0DwAOTuzALPVkK76kMhTSzIgHqg8X1uklVs6g== +nodemailer@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.3.0.tgz#a89b0c62d3937bdcdeecbf55687bd7911b627e12" + integrity sha512-TEHBNBPHv7Ie/0o3HXnb7xrPSSQmH1dXwQKRaMKDBGt/ZN54lvDVujP6hKkO/vjkIYL9rK8kHSG11+G42Nhxuw== nodemon@~1.19.1: version "1.19.1" @@ -5860,6 +5693,11 @@ object-hash@^1.3.1: resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df" 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: version "1.0.12" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2" @@ -5877,6 +5715,16 @@ object-visit@^1.0.0: dependencies: 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: version "2.0.3" resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" @@ -6359,11 +6207,6 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - punycode@2.x.x, punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -6384,11 +6227,6 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -6473,16 +6311,6 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" -readable-stream@1.1.x: - version "1.1.14" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" - integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.2.3, readable-stream@^2.3.5: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" @@ -6832,11 +6660,6 @@ sanitize-html@~1.20.1: srcset "^1.0.0" xtend "^4.0.1" -sax@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" - integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= - sax@>=0.6.0, sax@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -6936,11 +6759,6 @@ set-value@^2.0.0: is-plain-object "^2.0.3" split-string "^3.0.1" -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" - integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== - setprototypeof@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" @@ -7053,7 +6871,7 @@ source-map-resolve@^0.5.0: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@^0.5.1, source-map-support@^0.5.6, source-map-support@^0.5.9: +source-map-support@^0.5.6, source-map-support@^0.5.9: version "0.5.9" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" integrity sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA== @@ -7259,11 +7077,6 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.1.0" -string_decoder@~0.10.x: - version "0.10.31" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" - integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= - string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -7307,7 +7120,7 @@ strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= -subscriptions-transport-ws@^0.9.11, subscriptions-transport-ws@^0.9.8: +subscriptions-transport-ws@^0.9.11: version "0.9.15" resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.15.tgz#68a8b7ba0037d8c489fb2f5a102d1494db297d0d" integrity sha512-f9eBfWdHsePQV67QIX+VRhf++dn1adyC/PZHP6XI5AfKnZ4n0FW+v5omxwdHVpd4xq2ZijaHEcmlQrhBY79ZWQ== @@ -7411,6 +7224,11 @@ test-exclude@^5.0.0: read-pkg-up "^4.0.0" 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: version "0.6.4" resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19" @@ -7733,14 +7551,6 @@ url-parse-lax@^1.0.0: dependencies: prepend-http "^1.0.1" -url@0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" - integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ= - dependencies: - punycode "1.3.2" - querystring "0.2.0" - use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" @@ -7776,11 +7586,6 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" - integrity sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g== - uuid@^3.1.0, uuid@^3.3.2, uuid@~3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" @@ -7891,13 +7696,13 @@ w3c-hr-time@^1.0.1: dependencies: browser-process-hrtime "^0.1.2" -wait-on@~3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-3.2.0.tgz#c83924df0fc42a675c678324c49c769d378bcb85" - integrity sha512-QUGNKlKLDyY6W/qHdxaRlXUAgLPe+3mLL/tRByHpRNcHs/c7dZXbu+OnJWGNux6tU1WFh/Z8aEwvbuzSAu79Zg== +wait-on@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-3.3.0.tgz#9940981d047a72a9544a97b8b5fca45b2170a082" + integrity sha512-97dEuUapx4+Y12aknWZn7D25kkjMk16PbWoYzpSdA8bYpVfS6hpl2a2pOWZ3c+Tyt3/i4/pglyZctG3J4V1hWQ== dependencies: - core-js "^2.5.7" - joi "^13.0.0" + "@hapi/joi" "^15.0.3" + core-js "^2.6.5" minimist "^1.2.0" request "^2.88.0" rx "^4.1.0" @@ -8052,7 +7857,7 @@ xml-name-validator@^3.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== -xml2js@0.4.19, xml2js@^0.4.17: +xml2js@^0.4.17: version "0.4.19" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== diff --git a/cypress/features.md b/cypress/features.md index eb8292c3b..3adfd8771 100644 --- a/cypress/features.md +++ b/cypress/features.md @@ -16,7 +16,7 @@ The following features will be implemented. This gets done in three steps: ### User Account -[Cucumber Features](./integration/user_account) +[Cucumber Features](https://github.com/Human-Connection/Human-Connection/tree/master/cypress/integration/user_account) * Sign-up * Agree to Data Privacy Statement @@ -34,7 +34,7 @@ The following features will be implemented. This gets done in three steps: ### 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 Profile Picture @@ -59,7 +59,7 @@ The following features will be implemented. This gets done in three steps: ### Posts -[Cucumber Features](./integration/post/) +[Cucumber Features](https://github.com/Human-Connection/Human-Connection/tree/master/cypress/integration/post) * Creating Posts * Persistent Links @@ -78,13 +78,13 @@ The following features will be implemented. This gets done in three steps: ### Comments -* Creating Comments +* Creating Comments * Deleting Comments * Editing Comments * Upvote comments of others ### Notifications -[Cucumber features](./integration/notifications) +[Cucumber features](https://github.com/Human-Connection/Human-Connection/tree/master/cypress/integration/notifications) * User @-mentionings * Notify authors for comments @@ -94,12 +94,12 @@ The following features will be implemented. This gets done in three steps: * Show Posts by Tiles * Show Posts as List -* Filter by Category \(Health and Wellbeing, Global Peace & Non-Violence, ...\) +* Filter by Category \(Health and Wellbeing, Global Peace & Non-Violence, ...\) * Filter by Mood \(Funny, Happy, Surprised, Cry, Angry, ...\) * Filter by Source \(Connections, Following, Individuals, Non-Profits, ...\) * Filter by Posts & Tools \(Post, Events, CanDos, ...\) * Filter by Format Type \(Text, Pictures, Video, ...\) -* Extended Filter \(Continent, Country, Language, ...\) +* Extended Filter \(Continent, Country, Language, ...\) * Sort Posts by Date * Sort Posts by Shouts * Sort Posts by most Comments @@ -116,7 +116,7 @@ The following features will be implemented. This gets done in three steps: ### Search -[Cucumber Features](./integration/search) +[Cucumber Features](https://github.com/Human-Connection/Human-Connection/tree/master/cypress/integration/search) * Search for Categories * Search for Tags @@ -186,13 +186,13 @@ The following features will be implemented. This gets done in three steps: ### More Info -Shows autmatically releated information for existing post. +Shows automatically related information for existing post. * Show related Posts * Show Pros and Cons * Show Bestlist * Show Votes -* Link to corresponding Chatroom +* Link to corresponding Chatroom ### Take Action @@ -237,7 +237,7 @@ Shows automatically related actions for existing post. ### 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 * Moderator Panel @@ -262,7 +262,7 @@ Shows automatically related actions for existing post. ### Internationalization -[Cucumber Features](./integration/internationalization) +[Cucumber Features](https://github.com/Human-Connection/Human-Connection/tree/master/cypress/integration/internationalization) * Frontend UI * Backend Error Messages @@ -276,4 +276,3 @@ Shows automatically related actions for existing post. * Receiving Undo and Delete Activities for Articles and Notes * Serving Webfinger records and Actor Objects * Serving Followers, Following and Outbox collections - diff --git a/cypress/integration/administration/TagsAndCategories.feature b/cypress/integration/administration/TagsAndCategories.feature index e55535ea9..96196be01 100644 --- a/cypress/integration/administration/TagsAndCategories.feature +++ b/cypress/integration/administration/TagsAndCategories.feature @@ -22,16 +22,16 @@ Feature: Tags and Categories When I navigate to the administration dashboard And I click on the menu item "Categories" Then I can see the following table: - | | Name | Posts | - | | Just For Fun | 2 | - | | Happyness & Values | 1 | - | | Health & Wellbeing | 0 | + | | Name | Posts | + | | Just For Fun | 2 | + | | Happyness & Values | 1 | + | | Health & Wellbeing | 0 | Scenario: See an overview of tags When I navigate to the administration dashboard And I click on the menu item "Tags" Then I can see the following table: - | | Name | Users | Posts | - | 1 | Democracy | 3 | 4 | - | 2 | Nature | 2 | 3 | - | 3 | Ecology | 1 | 1 | + | | Name | Users | Posts | + | 1 | Democracy | 3 | 4 | + | 2 | Nature | 2 | 3 | + | 3 | Ecology | 1 | 1 | diff --git a/cypress/integration/common/profile.js b/cypress/integration/common/profile.js index 1df1e2652..b1bf9e4e0 100644 --- a/cypress/integration/common/profile.js +++ b/cypress/integration/common/profile.js @@ -1,36 +1,36 @@ -import { When, Then } from 'cypress-cucumber-preprocessor/steps' +import { When, Then } from "cypress-cucumber-preprocessor/steps"; /* global cy */ -When('I visit my profile page', () => { - cy.openPage('profile/peter-pan') -}) +When("I visit my profile page", () => { + cy.openPage("profile/peter-pan"); +}); -Then('I should be able to change my profile picture', () => { - const avatarUpload = 'onourjourney.png' +Then("I should be able to change my profile picture", () => { + const avatarUpload = "onourjourney.png"; - cy.fixture(avatarUpload, 'base64').then(fileContent => { - cy.get('#customdropzone').upload( - { fileContent, fileName: avatarUpload, mimeType: 'image/png' }, - { subjectType: 'drag-n-drop' } - ) - }) - cy.get('.profile-avatar img') - .should('have.attr', 'src') - .and('contains', 'onourjourney') - cy.contains('.iziToast-message', 'Upload successful').should( - 'have.length', + cy.fixture(avatarUpload, "base64").then(fileContent => { + cy.get("#customdropzone").upload( + { fileContent, fileName: avatarUpload, mimeType: "image/png" }, + { subjectType: "drag-n-drop", force: true } + ); + }); + cy.get(".profile-avatar img") + .should("have.attr", "src") + .and("contains", "onourjourney"); + cy.contains(".iziToast-message", "Upload successful").should( + "have.length", 1 - ) -}) + ); +}); When("I visit another user's profile page", () => { - cy.openPage('profile/peter-pan') -}) + cy.openPage("profile/peter-pan"); +}); -Then('I cannot upload a picture', () => { - cy.get('.ds-card-content') +Then("I cannot upload a picture", () => { + cy.get(".ds-card-content") .children() - .should('not.have.id', 'customdropzone') - .should('have.class', 'ds-avatar') -}) + .should("not.have.id", "customdropzone") + .should("have.class", "ds-avatar"); +}); diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index 73313d331..f996db992 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -1,5 +1,6 @@ import { Given, When, Then } from "cypress-cucumber-preprocessor/steps"; import { getLangByName } from "../../support/helpers"; +import slugify from 'slug' /* global cy */ @@ -11,6 +12,7 @@ let loginCredentials = { }; const narratorParams = { name: "Peter Pan", + slug: 'peter-pan', avatar: "https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg", ...loginCredentials }; @@ -171,10 +173,11 @@ When("I press {string}", label => { }); 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 = { name: Author, - email: `${Author}@example.org`, + email: `${slugify(Author, {lower: true})}@example.org`, password: "1234" }; postAttributes.deleted = Boolean(postAttributes.deleted); @@ -273,9 +276,9 @@ When("I fill the password form with:", table => { table = table.rowsHash(); cy.get("input[id=oldPassword]") .type(table["Your old password"]) - .get("input[id=newPassword]") + .get("input[id=password]") .type(table["Your new passsword"]) - .get("input[id=confirmPassword]") + .get("input[id=passwordConfirmation]") .type(table["Confirm new password"]); }); diff --git a/cypress/integration/search/Search.feature b/cypress/integration/search/Search.feature index 71aee608a..c1afc5b97 100644 --- a/cypress/integration/search/Search.feature +++ b/cypress/integration/search/Search.feature @@ -6,9 +6,9 @@ Feature: Search Background: Given I have a user account And we have the following posts in our database: - | Author | id | title | content | - | Brianna Wiest | 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 | + | id | title | content | + | p1 | 101 Essays that will change the way you think | 101 Essays, of course! | + | p2 | No searched for content | will be found in this post, I guarantee | Given I am logged in Scenario: Search for specific words diff --git a/cypress/support/factories.js b/cypress/support/factories.js index 3bdb86800..825f65b83 100644 --- a/cypress/support/factories.js +++ b/cypress/support/factories.js @@ -1,12 +1,15 @@ import Factory from '../../backend/src/seed/factories' 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'), username: Cypress.env('NEO4J_USERNAME'), 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') beforeEach(async () => { @@ -14,30 +17,33 @@ beforeEach(async () => { }) Cypress.Commands.add('factory', () => { - return Factory({ seedServerHost }) + return Factory({ seedServerHost, neo4jDriver, neodeInstance: setupNeode(neo4jConfigs) }) }) Cypress.Commands.add( 'create', { prevSubject: true }, - (factory, node, properties) => { - return factory.create(node, properties) + async (factory, node, properties) => { + await factory.create(node, properties) + return factory } ) Cypress.Commands.add( 'relate', { prevSubject: true }, - (factory, node, relationship, properties) => { - return factory.relate(node, relationship, properties) + async (factory, node, relationship, properties) => { + await factory.relate(node, relationship, properties) + return factory } ) Cypress.Commands.add( 'mutate', { prevSubject: true }, - (factory, mutation, variables) => { - return factory.mutate(mutation, variables) + async (factory, mutation, variables) => { + await factory.mutate(mutation, variables) + return factory } ) diff --git a/deployment/human-connection/deployment-neo4j.yaml b/deployment/human-connection/deployment-neo4j.yaml index 3c4887194..afc03ca0d 100644 --- a/deployment/human-connection/deployment-neo4j.yaml +++ b/deployment/human-connection/deployment-neo4j.yaml @@ -32,21 +32,9 @@ value: 1G - name: NEO4J_dbms_memory_heap_max__size value: 1G - - name: NEO4J_URI - valueFrom: - configMapKeyRef: - 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 + envFrom: + - configMapRef: + name: configmap ports: - containerPort: 7687 - containerPort: 7474 diff --git a/deployment/human-connection/templates/configmap.template.yaml b/deployment/human-connection/templates/configmap.template.yaml index 762901ae8..1e8b37b06 100644 --- a/deployment/human-connection/templates/configmap.template.yaml +++ b/deployment/human-connection/templates/configmap.template.yaml @@ -4,13 +4,10 @@ data: SMTP_HOST: "mailserver.human-connection" SMTP_PORT: "25" - SMTP_USERNAME: "" - SMTP_PASSWORD: "" GRAPHQL_PORT: "4000" GRAPHQL_URI: "http://nitro-backend.human-connection:4000" MOCKS: "false" NEO4J_URI: "bolt://nitro-neo4j.human-connection:7687" - NEO4J_USER: "neo4j" NEO4J_AUTH: "none" CLIENT_URI: "https://nitro-staging.human-connection.org" metadata: diff --git a/deployment/human-connection/templates/secrets.template.yaml b/deployment/human-connection/templates/secrets.template.yaml index 9f59b948a..6a6206189 100644 --- a/deployment/human-connection/templates/secrets.template.yaml +++ b/deployment/human-connection/templates/secrets.template.yaml @@ -5,11 +5,10 @@ data: MONGODB_PASSWORD: "TU9OR09EQl9QQVNTV09SRA==" PRIVATE_KEY_PASSPHRASE: "YTdkc2Y3OHNhZGc4N2FkODdzZmFnc2FkZzc4" MAPBOX_TOKEN: "cGsuZXlKMUlqb2lhSFZ0WVc0dFkyOXVibVZqZEdsdmJpSXNJbUVpT2lKamFqbDBjbkJ1Ykdvd2VUVmxNM1Z3WjJsek5UTnVkM1p0SW4wLktaOEtLOWw3MG9talhiRWtrYkhHc1EK" - SMTP_HOST: - SMTP_PORT: 587 SMTP_USERNAME: SMTP_PASSWORD: - SMTP_IGNORE_TLS: + NEO4J_USERNAME: + NEO4J_PASSWORD: metadata: name: human-connection namespace: human-connection diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges/badges.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges/badges.cql index 027cea019..adf63dc1f 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges/badges.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges/badges.cql @@ -25,7 +25,7 @@ [?] type: String, // in nitro this is a defined enum - seems good for now [X] required: true }, -[X] key: { +[X] id: { [X] type: String, [X] required: true }, @@ -43,7 +43,7 @@ CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as badge MERGE(b:Badge {id: badge._id["$oid"]}) ON CREATE SET -b.key = badge.key, +b.id = badge.key, b.type = badge.type, b.icon = replace(badge.image.path, 'https://api-alpha.human-connection.org', ''), b.status = badge.status, diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/delete.cql index e69de29bb..18fb6699f 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/delete.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/delete.cql @@ -0,0 +1 @@ +MATCH (u:User)-[e:EMOTED]->(c:Post) DETACH DELETE e; \ No newline at end of file diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/emotions.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/emotions.cql index 8aad9e923..06341f277 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/emotions.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/emotions/emotions.cql @@ -5,31 +5,54 @@ // [-] Omitted in Nitro // [?] Unclear / has work to be done for Nitro { -[ ] userId: { -[ ] type: String, -[ ] required: true, +[X] userId: { +[X] type: String, +[X] required: true, [-] index: true }, -[ ] contributionId: { -[ ] type: String, -[ ] required: true, +[X] contributionId: { +[X] type: String, +[X] required: true, [-] index: true }, -[ ] rated: { -[ ] type: String, +[?] rated: { +[X] type: String, [ ] required: true, -[ ] enum: ['funny', 'happy', 'surprised', 'cry', 'angry'] +[?] enum: ['funny', 'happy', 'surprised', 'cry', 'angry'] }, -[ ] createdAt: { -[ ] type: Date, -[ ] default: Date.now +[X] createdAt: { +[X] type: Date, +[X] default: Date.now }, -[ ] updatedAt: { -[ ] type: Date, -[ ] default: Date.now +[X] updatedAt: { +[X] type: Date, +[X] default: Date.now }, -[ ] wasSeeded: { type: Boolean } +[-] wasSeeded: { type: Boolean } } */ -CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as emotion; +CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as emotion +MATCH (u:User {id: emotion.userId}), + (c:Post {id: emotion.contributionId}) +MERGE (u)-[e:EMOTED { + id: emotion._id["$oid"], + emotion: emotion.rated, + createdAt: datetime(emotion.createdAt.`$date`), + updatedAt: datetime(emotion.updatedAt.`$date`) + }]->(c) +RETURN e; +/* + // Queries + // user sets an emotion emotion: + // MERGE (u)-[e:EMOTED {id: ..., emotion: "funny", createdAt: ..., updatedAt: ...}]->(c) + // user removes emotion + // MATCH (u)-[e:EMOTED]->(c) DELETE e + // contribution distributions over every `emotion` property value for one post + // MATCH (u:User)-[e:EMOTED]->(c:Post {id: "5a70bbc8508f5b000b443b1a"}) RETURN e.emotion,COUNT(e.emotion) + // contribution distributions over every `emotion` property value for one user (advanced - "whats the primary emotion used by the user?") + // MATCH (u:User{id:"5a663b1ac64291000bf302a1"})-[e:EMOTED]->(c:Post) RETURN e.emotion,COUNT(e.emotion) + // contribution distributions over every `emotion` property value for all posts created by one user (advanced - "how do others react to my contributions?") + // MATCH (u:User)-[e:EMOTED]->(c:Post)<-[w:WROTE]-(a:User{id:"5a663b1ac64291000bf302a1"}) RETURN e.emotion,COUNT(e.emotion) + // if we can filter the above an a variable timescale that would be great (should be possible on createdAt and updatedAt fields) +*/ \ No newline at end of file diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows/delete.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows/delete.cql index 3624448c3..3de01f8ea 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows/delete.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/follows/delete.cql @@ -1 +1 @@ -// this is just a relation between users(?) - no need to delete \ No newline at end of file +MATCH (u1:User)-[f:FOLLOWS]->(u2:User) DETACH DELETE f; \ No newline at end of file diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh b/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh index 8eef68c92..cef2846a7 100755 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/import.sh @@ -60,8 +60,8 @@ delete_collection "contributions" "contributions_post" delete_collection "contributions" "contributions_cando" delete_collection "shouts" "shouts" delete_collection "comments" "comments" +delete_collection "emotions" "emotions" -#delete_collection "emotions" #delete_collection "invites" #delete_collection "notifications" #delete_collection "organizations" @@ -82,12 +82,12 @@ import_collection "users" "users/users.cql" import_collection "follows_users" "follows/follows.cql" #import_collection "follows_organizations" "follows/follows.cql" import_collection "contributions_post" "contributions/contributions.cql" -import_collection "contributions_cando" "contributions/contributions.cql" +#import_collection "contributions_cando" "contributions/contributions.cql" #import_collection "contributions_DELETED" "contributions/contributions.cql" import_collection "shouts" "shouts/shouts.cql" import_collection "comments" "comments/comments.cql" +import_collection "emotions" "emotions/emotions.cql" -# import_collection "emotions" # import_collection "invites" # import_collection "notifications" # import_collection "organizations" diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/users/users.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/users/users.cql index 4d7c9aa9f..7574fd3b2 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/users/users.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/users/users.cql @@ -101,7 +101,7 @@ ON CREATE SET u.name = user.name, u.slug = user.slug, u.email = user.email, -u.password = user.password, +u.encryptedPassword = user.password, u.avatar = replace(user.avatar, 'https://api-alpha.human-connection.org', ''), u.coverImg = replace(user.coverImg, 'https://api-alpha.human-connection.org', ''), u.wasInvited = user.wasInvited, @@ -111,6 +111,13 @@ u.createdAt = user.createdAt.`$date`, u.updatedAt = user.updatedAt.`$date`, u.deleted = user.deletedAt IS NOT NULL, u.disabled = false +MERGE (e:EmailAddress { + email: user.email, + createdAt: toString(datetime()), + verifiedAt: toString(datetime()) +}) +MERGE (e)-[:BELONGS_TO]->(u) +MERGE (u)<-[:PRIMARY_EMAIL]-(e) WITH u, user, user.badgeIds AS badgeIds UNWIND badgeIds AS badgeId MATCH (b:Badge {id: badgeId}) diff --git a/docker-compose.travis.yml b/docker-compose.travis.yml index bc627a67a..0c6576ca7 100644 --- a/docker-compose.travis.yml +++ b/docker-compose.travis.yml @@ -26,9 +26,4 @@ services: ports: - 4001:4001 - 4123:4123 - neo4j: - environment: - - NEO4J_AUTH=none - ports: - - 7687:7687 - - 7474:7474 + diff --git a/docker-compose.yml b/docker-compose.yml index b1a0f4d3f..47d87f9d9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: networks: - hc-network environment: + - NUXT_BUILD=.nuxt-dist - HOST=0.0.0.0 - GRAPHQL_URI=http://backend:4000 - MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.bZ8KK9l70omjXbEkkbHGsQ" diff --git a/edit-this-documentation.md b/edit-this-documentation.md index b01ace78f..cd83ac7a6 100644 --- a/edit-this-documentation.md +++ b/edit-this-documentation.md @@ -1,12 +1,6 @@ # Edit this Documentation -Go to the section and theme you want to change: On the left navigator. - -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. +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 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. -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 @@ -117,4 +111,3 @@ TODO: How to modify screenshots in Linux ... {% endhint %} {% endtab %} {% endtabs %} - diff --git a/neo4j/README.md b/neo4j/README.md index 379a89eec..78c4bc62e 100644 --- a/neo4j/README.md +++ b/neo4j/README.md @@ -20,7 +20,7 @@ for an interactive cypher shell and a visualization of the graph. ## 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. 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. 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 @@ -59,6 +65,6 @@ $ cp .env.template .env $ ./db_setup.sh ``` -Otherwise if you don't have `cypher-shell` available, simply copy the cypher -statements [from the script](./neo4j/db_setup.sh) and paste the scripts into your -database [browser frontend](http://localhost:7474). +Otherwise, if you don't have `cypher-shell` available, copy the cypher +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). diff --git a/neo4j/db_setup.sh b/neo4j/db_setup.sh index 21ed54571..d4c7b9af8 100755 --- a/neo4j/db_setup.sh +++ b/neo4j/db_setup.sh @@ -34,6 +34,8 @@ CREATE CONSTRAINT ON (p:Post) ASSERT p.slug IS UNIQUE; CREATE CONSTRAINT ON (c:Category) ASSERT c.slug IS UNIQUE; CREATE CONSTRAINT ON (u:User) ASSERT u.slug IS UNIQUE; CREATE CONSTRAINT ON (o:Organization) ASSERT o.slug IS UNIQUE; + +CREATE CONSTRAINT ON (e:EmailAddress) ASSERT e.email IS UNIQUE; ' | cypher-shell echo ' diff --git a/package.json b/package.json index 1446f0009..b4447bdd2 100644 --- a/package.json +++ b/package.json @@ -19,16 +19,19 @@ "test:jest": "cd webapp && yarn test && cd ../backend && yarn test:jest && codecov" }, "devDependencies": { + "bcryptjs": "^2.4.3", "codecov": "^3.5.0", "cross-env": "^5.2.0", - "cypress": "^3.3.2", + "cypress": "^3.4.0", "cypress-cucumber-preprocessor": "^1.12.0", - "cypress-file-upload": "^3.2.0", + "cypress-file-upload": "^3.3.1", "cypress-plugin-retries": "^1.2.2", "dotenv": "^8.0.0", "faker": "Marak/faker.js#master", "graphql-request": "^1.8.2", "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" } } diff --git a/webapp/.gitignore b/webapp/.gitignore index f8c980f7c..bc179d78a 100644 --- a/webapp/.gitignore +++ b/webapp/.gitignore @@ -61,6 +61,8 @@ typings/ # nuxt.js build output .nuxt +# also the build output in docker container +.nuxt-dist # Nuxt generate dist diff --git a/webapp/Dockerfile b/webapp/Dockerfile index 9b7f1329c..d3a17a3aa 100644 --- a/webapp/Dockerfile +++ b/webapp/Dockerfile @@ -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)" EXPOSE 3000 diff --git a/webapp/README.md b/webapp/README.md index ce27eca2f..604c7e6ba 100644 --- a/webapp/README.md +++ b/webapp/README.md @@ -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) -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! diff --git a/webapp/assets.md b/webapp/assets.md index 06786539d..6ac7dc388 100644 --- a/webapp/assets.md +++ b/webapp/assets.md @@ -1,8 +1,5 @@ # 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. +This directory contains your un-compiled assets such as LESS, SASS, or JavaScript – in our case SCSS styles. More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked). - diff --git a/webapp/assets/styles/main.scss b/webapp/assets/styles/main.scss index 560249b4a..11652fad0 100644 --- a/webapp/assets/styles/main.scss +++ b/webapp/assets/styles/main.scss @@ -66,7 +66,8 @@ blockquote { border-left: 3px dotted $color-neutral-70; &::before { - content: '\201C'; /*Unicode for Left Double Quote*/ + content: '\201C'; + /*Unicode for Left Double Quote*/ /*Font*/ font-size: $font-size-xxxx-large; diff --git a/webapp/components.md b/webapp/components.md index be43ae454..92b3dd1fb 100644 --- a/webapp/components.md +++ b/webapp/components.md @@ -1,8 +1,5 @@ # 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. _Nuxt.js doesn't supercharge these components._ - diff --git a/webapp/components/Badges.vue b/webapp/components/Badges.vue index 42ac23e4d..936d13adb 100644 --- a/webapp/components/Badges.vue +++ b/webapp/components/Badges.vue @@ -1,6 +1,6 @@ diff --git a/webapp/components/Editor/spec.js b/webapp/components/Editor/Editor.spec.js similarity index 94% rename from webapp/components/Editor/spec.js rename to webapp/components/Editor/Editor.spec.js index b982d941d..d457609bd 100644 --- a/webapp/components/Editor/spec.js +++ b/webapp/components/Editor/Editor.spec.js @@ -1,5 +1,5 @@ import { mount, createLocalVue } from '@vue/test-utils' -import Editor from './' +import Editor from './Editor' import Vuex from 'vuex' import Styleguide from '@human-connection/styleguide' @@ -36,7 +36,9 @@ describe('Editor.vue', () => { propsData, localVue, sync: false, - stubs: { transition: false }, + stubs: { + transition: false, + }, store, })) } diff --git a/webapp/components/Editor/index.vue b/webapp/components/Editor/Editor.vue similarity index 67% rename from webapp/components/Editor/index.vue rename to webapp/components/Editor/Editor.vue index 84649f436..4413bfa0d 100644 --- a/webapp/components/Editor/index.vue +++ b/webapp/components/Editor/Editor.vue @@ -1,18 +1,51 @@