From 36ce6361ec8a8c83d20a6d524b61ad6b5d5ff0b8 Mon Sep 17 00:00:00 2001 From: ogerly Date: Wed, 21 Aug 2019 11:41:00 +0200 Subject: [PATCH] =?UTF-8?q?Alle=20Daten=20=C3=BCbernommen=20die=20f=C3=BCr?= =?UTF-8?q?=20serverseitig=20rendern=20n=C3=B6tig=20sind?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/middleware/userMiddleware.js | 26 +++++ backend/src/models/User.js | 10 ++ .../src/schema/resolvers/user_management.js | 7 +- backend/src/schema/resolvers/users.js | 78 ++++++++++++-- .../users/termsAndConditions.spec.js | 21 ++++ .../src/schema/types/type/EmailAddress.gql | 1 + backend/src/schema/types/type/User.gql | 4 + backend/src/seed/factories/users.js | 2 + webapp/locales/de.json | 6 +- webapp/locales/en.json | 6 +- webapp/middleware/authenticated.js | 16 ++- webapp/pages/terms-and-conditions-confirm.vue | 101 ++++++++++++++++++ webapp/pages/terms-and-conditions.vue | 2 + webapp/store/auth.js | 1 + 14 files changed, 262 insertions(+), 19 deletions(-) create mode 100644 backend/src/schema/resolvers/users/termsAndConditions.spec.js create mode 100644 webapp/pages/terms-and-conditions-confirm.vue diff --git a/backend/src/middleware/userMiddleware.js b/backend/src/middleware/userMiddleware.js index fafbd44e5..f4b81d495 100644 --- a/backend/src/middleware/userMiddleware.js +++ b/backend/src/middleware/userMiddleware.js @@ -8,7 +8,33 @@ export default { return result }, UpdateUser: async (resolve, root, args, context, info) => { + const { currentUser } = context + if ( + !!currentUser && + !!args.termsAndConditionsAgreedVersion && + args.termsAndConditionsAgreedVersion + ) { + const session = context.driver.session() + const cypher = ` + MATCH (user: User { id: $userId}) + SET user.termsAndConditionsAgreedAt = $createdAt + SET user.termsAndConditionsAgreedVersion = $version + RETURN user { .termsAndConditionsAgreedAt, .termsAndConditionsAgreedVersion } + ` + const variable = { + userId: currentUser.id, + createdAt: new Date().toISOString(), + version: args.termsAndConditionsAgreedVersion, + } + await session.run(cypher, variable) + // console.log('Nach dem speichern') + // console.log(transactionResult) + // console.log('-------------------------------------') + session.close() + } + const result = await resolve(root, args, context, info) + await createOrUpdateLocations(args.id, args.locationName, context.driver) return result }, diff --git a/backend/src/models/User.js b/backend/src/models/User.js index fa578f8ad..c082a761e 100644 --- a/backend/src/models/User.js +++ b/backend/src/models/User.js @@ -83,4 +83,14 @@ module.exports = { target: 'Notification', direction: 'in', }, + termsAndConditionsAgreedVersion: { + type: 'string', + allow: [null], + }, + termsAndConditionsAgreedAt: { + type: 'string', + isoDate: true, + allow: [null], + /* required: true, TODO */ + }, } diff --git a/backend/src/schema/resolvers/user_management.js b/backend/src/schema/resolvers/user_management.js index be790ca3a..19c2b524d 100644 --- a/backend/src/schema/resolvers/user_management.js +++ b/backend/src/schema/resolvers/user_management.js @@ -1,7 +1,6 @@ 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() @@ -12,9 +11,9 @@ export default { return Boolean(user && user.id) }, currentUser: async (object, params, ctx, resolveInfo) => { - const { user } = ctx - if (!user) return null - return neo4jgraphql(object, { id: user.id }, ctx, resolveInfo, false) + if (!ctx.user) return null + const user = await instance.find('User', ctx.user.id) + return user.toJson() }, }, Mutation: { diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index 4710942b6..8e8a604f2 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -1,7 +1,9 @@ +import encode from '../../jwt/encode' +import bcrypt from 'bcryptjs' import { neo4jgraphql } from 'neo4j-graphql-js' import fileUpload from './fileUpload' import { neode } from '../../bootstrap/neo4j' -import { UserInputError } from 'apollo-server' +import { AuthenticationError, UserInputError } from 'apollo-server' import Resolver from './helpers/Resolver' const instance = neode() @@ -55,8 +57,66 @@ export default { } return neo4jgraphql(object, args, context, resolveInfo, false) }, + isLoggedIn: (_, args, { driver, user }) => { + return Boolean(user && user.id) + }, + currentUser: async (object, params, ctx, resolveInfo) => { + const { user } = ctx + if (!user) return null + return neo4jgraphql(object, { id: user.id }, ctx, resolveInfo, false) + }, }, Mutation: { + 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)-[: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(record => { + return record.get('user') + }) + + if ( + currentUser && + (await bcrypt.compareSync(password, currentUser.encryptedPassword)) && + !currentUser.disabled + ) { + delete currentUser.encryptedPassword + return encode(currentUser) + } else if (currentUser && currentUser.disabled) { + throw new AuthenticationError('Your account has been disabled.') + } else { + throw new AuthenticationError('Incorrect email address or password.') + } + }, + changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => { + const currentUser = await instance.find('User', user.id) + + const encryptedPassword = currentUser.get('encryptedPassword') + if (!(await bcrypt.compareSync(oldPassword, encryptedPassword))) { + throw new AuthenticationError('Old password is not correct') + } + + if (await bcrypt.compareSync(newPassword, encryptedPassword)) { + throw new AuthenticationError('Old password and new password should be different') + } + + const newEncryptedPassword = await bcrypt.hashSync(newPassword, 10) + await currentUser.update({ + encryptedPassword: newEncryptedPassword, + updatedAt: new Date().toISOString(), + }) + + return encode(await currentUser.toJson()) + }, block: async (object, args, context, resolveInfo) => { const { user: currentUser } = context if (currentUser.id === args.id) return null @@ -122,14 +182,6 @@ export default { }, }, 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 }) - const [{ email }] = result.records.map(r => r.get('e').properties) - return email - }, ...Resolver('User', { undefinedToNull: [ 'actorId', @@ -173,5 +225,13 @@ export default { badges: '<-[:REWARDED]-(related:Badge)', }, }), + 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 }) + const [{ email }] = result.records.map(r => r.get('e').properties) + return email + }, }, } diff --git a/backend/src/schema/resolvers/users/termsAndConditions.spec.js b/backend/src/schema/resolvers/users/termsAndConditions.spec.js new file mode 100644 index 000000000..9b30f0ac8 --- /dev/null +++ b/backend/src/schema/resolvers/users/termsAndConditions.spec.js @@ -0,0 +1,21 @@ +describe('SignupVerification', () => { + describe('given a valid version', () => { + // const version = '1.2.3' + + it.todo('saves the version with the new created user account') + it.todo('saves the current datetime in `termsAndConditionsAgreedAt`') + }) + + describe('given an invalid version string', () => { + // const version = 'this string does not follow semantic versioning' + + it.todo('rejects') + }) +}) + +describe('UpdateUser', () => { + describe('given a new agreed version of terms and conditions', () => { + it.todo('updates `termsAndConditionsAgreedAt`') + it.todo('updates `termsAndConditionsAgreedVersion`') + }) +}) diff --git a/backend/src/schema/types/type/EmailAddress.gql b/backend/src/schema/types/type/EmailAddress.gql index 63b39d457..0516d72c4 100644 --- a/backend/src/schema/types/type/EmailAddress.gql +++ b/backend/src/schema/types/type/EmailAddress.gql @@ -19,5 +19,6 @@ type Mutation { avatarUpload: Upload locationName: String about: String + termsAndConditionsAgreedVersion: String! ): User } diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index 46e699410..c41292652 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -24,6 +24,8 @@ type User { createdAt: String updatedAt: String + termsAndConditionsAgreedVersion: String! + notifications(read: Boolean): [Notification]! @relation(name: "NOTIFIED", direction: "IN") friends: [User]! @relation(name: "FRIENDS", direction: "BOTH") @@ -152,6 +154,7 @@ type Query { ): [User] blockedUsers: [User] + currentUser: User } type Mutation { @@ -165,6 +168,7 @@ type Mutation { avatarUpload: Upload locationName: String about: String + termsAndConditionsAgreedVersion: String ): User DeleteUser(id: ID!, resource: [Deletable]): User diff --git a/backend/src/seed/factories/users.js b/backend/src/seed/factories/users.js index af1699253..dc3d56cb6 100644 --- a/backend/src/seed/factories/users.js +++ b/backend/src/seed/factories/users.js @@ -14,6 +14,8 @@ export default function create() { role: 'user', avatar: faker.internet.avatar(), about: faker.lorem.paragraph(), + termsAndConditionsAgreedAt: new Date().toISOString(), + termsAndConditionsAgreedVersion: '0.0.1', } defaults.slug = slugify(defaults.name, { lower: true }) args = { diff --git a/webapp/locales/de.json b/webapp/locales/de.json index e249ce160..ce7b6647c 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -25,6 +25,7 @@ "imprint": "Impressum", "data-privacy": "Datenschutz", "termsAndConditions": "Nutzungsbedingungen", + "newTermsAndConditions": "Neue Nutzungsbedingungen", "changelog": "Änderungen & Verlauf", "contact": "Kontakt", "tribunal": "Registergericht", @@ -35,7 +36,8 @@ "bank": "Bankverbindung", "germany": "Deutschland", "code-of-conduct": "Verhaltenscodex", - "termsAndConditionsConfirmed": "Ich habe die Nutzungsbedingungen durchgelesen und stimme ihnen zu." + "termsAndConditionsConfirmed": "Ich habe die Nutzungsbedingungen durchgelesen und stimme ihnen zu.", + "termsAndConditionsNewConfirm": "Bestätige bitte die neuen Nutzungsbedingungen" }, "sorting": { "newest": "Neuste", @@ -604,4 +606,4 @@ "have-fun": "Jetzt aber viel Spaß mit der Alpha von Human Connection! Für den ersten Weltfrieden. ♥︎", "closing": "Herzlichst

Euer Human Connection Team" } -} +} \ No newline at end of file diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 7f9e7286b..594101d7a 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -24,6 +24,7 @@ "made": "Made with ❤", "imprint": "Imprint", "termsAndConditions": "Terms and conditions", + "newTermsAndConditions": "New Terms and conditions", "data-privacy": "Data privacy", "changelog": "Changes & History", "contact": "Contact", @@ -35,7 +36,8 @@ "bank": "bank account", "germany": "Germany", "code-of-conduct": "Code of Conduct", - "termsAndConditionsConfirmed": "I have read and confirmed the terms and conditions." + "termsAndConditionsConfirmed": "I have read and confirmed the terms and conditions.", + "termsAndConditionsNewConfirm": "Please confirm the new Terms and Conditions" }, "sorting": { "newest": "Newest", @@ -604,4 +606,4 @@ "have-fun": "Now have fun with the alpha version of Human Connection! For the first universal peace. ♥︎", "closing": "Thank you very much

your Human Connection Team" } -} +} \ No newline at end of file diff --git a/webapp/middleware/authenticated.js b/webapp/middleware/authenticated.js index f2273df58..3d7b0a16b 100644 --- a/webapp/middleware/authenticated.js +++ b/webapp/middleware/authenticated.js @@ -1,4 +1,5 @@ import isEmpty from 'lodash/isEmpty' +import { VERSION } from '~/pages/terms-and-conditions' export default async ({ store, env, route, redirect }) => { let publicPages = env.publicPages @@ -9,7 +10,14 @@ export default async ({ store, env, route, redirect }) => { // await store.dispatch('auth/refreshJWT', 'authenticated middleware') const isAuthenticated = await store.dispatch('auth/check') - if (isAuthenticated === true) { + + // TODO: find a better solution to **reliably** get the user + // having the encrypted JWT does not mean we have access to the user object + const user = await store.getters['auth/user'] + + const upToDate = user.termsAndConditionsAgreedVersion === VERSION + + if (isAuthenticated === true && upToDate) { return true } @@ -22,5 +30,9 @@ export default async ({ store, env, route, redirect }) => { params.path = route.path } - return redirect('/login', params) + if (!upToDate) { + return redirect('/terms-and-conditions-confirm', params) + } else { + return redirect('/login', params) + } } diff --git a/webapp/pages/terms-and-conditions-confirm.vue b/webapp/pages/terms-and-conditions-confirm.vue new file mode 100644 index 000000000..e08c3663f --- /dev/null +++ b/webapp/pages/terms-and-conditions-confirm.vue @@ -0,0 +1,101 @@ + + + diff --git a/webapp/pages/terms-and-conditions.vue b/webapp/pages/terms-and-conditions.vue index 0bd849575..38775e607 100644 --- a/webapp/pages/terms-and-conditions.vue +++ b/webapp/pages/terms-and-conditions.vue @@ -22,6 +22,8 @@