Alle Daten übernommen die für serverseitig rendern nötig sind

This commit is contained in:
ogerly 2019-08-21 11:41:00 +02:00
parent fb8468dc8b
commit 36ce6361ec
14 changed files with 262 additions and 19 deletions

View File

@ -8,7 +8,33 @@ export default {
return result return result
}, },
UpdateUser: async (resolve, root, args, context, info) => { 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) const result = await resolve(root, args, context, info)
await createOrUpdateLocations(args.id, args.locationName, context.driver) await createOrUpdateLocations(args.id, args.locationName, context.driver)
return result return result
}, },

View File

@ -83,4 +83,14 @@ module.exports = {
target: 'Notification', target: 'Notification',
direction: 'in', direction: 'in',
}, },
termsAndConditionsAgreedVersion: {
type: 'string',
allow: [null],
},
termsAndConditionsAgreedAt: {
type: 'string',
isoDate: true,
allow: [null],
/* required: true, TODO */
},
} }

View File

@ -1,7 +1,6 @@
import encode from '../../jwt/encode' import encode from '../../jwt/encode'
import bcrypt from 'bcryptjs' import bcrypt from 'bcryptjs'
import { AuthenticationError } from 'apollo-server' import { AuthenticationError } from 'apollo-server'
import { neo4jgraphql } from 'neo4j-graphql-js'
import { neode } from '../../bootstrap/neo4j' import { neode } from '../../bootstrap/neo4j'
const instance = neode() const instance = neode()
@ -12,9 +11,9 @@ export default {
return Boolean(user && user.id) return Boolean(user && user.id)
}, },
currentUser: async (object, params, ctx, resolveInfo) => { currentUser: async (object, params, ctx, resolveInfo) => {
const { user } = ctx if (!ctx.user) return null
if (!user) return null const user = await instance.find('User', ctx.user.id)
return neo4jgraphql(object, { id: user.id }, ctx, resolveInfo, false) return user.toJson()
}, },
}, },
Mutation: { Mutation: {

View File

@ -1,7 +1,9 @@
import encode from '../../jwt/encode'
import bcrypt from 'bcryptjs'
import { neo4jgraphql } from 'neo4j-graphql-js' import { neo4jgraphql } from 'neo4j-graphql-js'
import fileUpload from './fileUpload' import fileUpload from './fileUpload'
import { neode } from '../../bootstrap/neo4j' import { neode } from '../../bootstrap/neo4j'
import { UserInputError } from 'apollo-server' import { AuthenticationError, UserInputError } from 'apollo-server'
import Resolver from './helpers/Resolver' import Resolver from './helpers/Resolver'
const instance = neode() const instance = neode()
@ -55,8 +57,66 @@ export default {
} }
return neo4jgraphql(object, args, context, resolveInfo, false) 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: { 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) => { block: async (object, args, context, resolveInfo) => {
const { user: currentUser } = context const { user: currentUser } = context
if (currentUser.id === args.id) return null if (currentUser.id === args.id) return null
@ -122,14 +182,6 @@ export default {
}, },
}, },
User: { 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', { ...Resolver('User', {
undefinedToNull: [ undefinedToNull: [
'actorId', 'actorId',
@ -173,5 +225,13 @@ export default {
badges: '<-[:REWARDED]-(related:Badge)', 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
},
}, },
} }

View File

@ -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`')
})
})

View File

@ -19,5 +19,6 @@ type Mutation {
avatarUpload: Upload avatarUpload: Upload
locationName: String locationName: String
about: String about: String
termsAndConditionsAgreedVersion: String!
): User ): User
} }

View File

@ -24,6 +24,8 @@ type User {
createdAt: String createdAt: String
updatedAt: String updatedAt: String
termsAndConditionsAgreedVersion: String!
notifications(read: Boolean): [Notification]! @relation(name: "NOTIFIED", direction: "IN") notifications(read: Boolean): [Notification]! @relation(name: "NOTIFIED", direction: "IN")
friends: [User]! @relation(name: "FRIENDS", direction: "BOTH") friends: [User]! @relation(name: "FRIENDS", direction: "BOTH")
@ -152,6 +154,7 @@ type Query {
): [User] ): [User]
blockedUsers: [User] blockedUsers: [User]
currentUser: User
} }
type Mutation { type Mutation {
@ -165,6 +168,7 @@ type Mutation {
avatarUpload: Upload avatarUpload: Upload
locationName: String locationName: String
about: String about: String
termsAndConditionsAgreedVersion: String
): User ): User
DeleteUser(id: ID!, resource: [Deletable]): User DeleteUser(id: ID!, resource: [Deletable]): User

View File

@ -14,6 +14,8 @@ export default function create() {
role: 'user', role: 'user',
avatar: faker.internet.avatar(), avatar: faker.internet.avatar(),
about: faker.lorem.paragraph(), about: faker.lorem.paragraph(),
termsAndConditionsAgreedAt: new Date().toISOString(),
termsAndConditionsAgreedVersion: '0.0.1',
} }
defaults.slug = slugify(defaults.name, { lower: true }) defaults.slug = slugify(defaults.name, { lower: true })
args = { args = {

View File

@ -25,6 +25,7 @@
"imprint": "Impressum", "imprint": "Impressum",
"data-privacy": "Datenschutz", "data-privacy": "Datenschutz",
"termsAndConditions": "Nutzungsbedingungen", "termsAndConditions": "Nutzungsbedingungen",
"newTermsAndConditions": "Neue Nutzungsbedingungen",
"changelog": "Änderungen & Verlauf", "changelog": "Änderungen & Verlauf",
"contact": "Kontakt", "contact": "Kontakt",
"tribunal": "Registergericht", "tribunal": "Registergericht",
@ -35,7 +36,8 @@
"bank": "Bankverbindung", "bank": "Bankverbindung",
"germany": "Deutschland", "germany": "Deutschland",
"code-of-conduct": "Verhaltenscodex", "code-of-conduct": "Verhaltenscodex",
"termsAndConditionsConfirmed": "Ich habe die <a href=\"/terms-and-conditions\" target=\"_blank\">Nutzungsbedingungen</a> durchgelesen und stimme ihnen zu." "termsAndConditionsConfirmed": "Ich habe die <a href=\"/terms-and-conditions\" target=\"_blank\">Nutzungsbedingungen</a> durchgelesen und stimme ihnen zu.",
"termsAndConditionsNewConfirm": "Bestätige bitte die neuen Nutzungsbedingungen"
}, },
"sorting": { "sorting": {
"newest": "Neuste", "newest": "Neuste",
@ -604,4 +606,4 @@
"have-fun": "Jetzt aber viel Spaß mit der Alpha von Human Connection! Für den ersten Weltfrieden. ♥︎", "have-fun": "Jetzt aber viel Spaß mit der Alpha von Human Connection! Für den ersten Weltfrieden. ♥︎",
"closing": "Herzlichst <br><br> Euer Human Connection Team" "closing": "Herzlichst <br><br> Euer Human Connection Team"
} }
} }

View File

@ -24,6 +24,7 @@
"made": "Made with &#10084;", "made": "Made with &#10084;",
"imprint": "Imprint", "imprint": "Imprint",
"termsAndConditions": "Terms and conditions", "termsAndConditions": "Terms and conditions",
"newTermsAndConditions": "New Terms and conditions",
"data-privacy": "Data privacy", "data-privacy": "Data privacy",
"changelog": "Changes & History", "changelog": "Changes & History",
"contact": "Contact", "contact": "Contact",
@ -35,7 +36,8 @@
"bank": "bank account", "bank": "bank account",
"germany": "Germany", "germany": "Germany",
"code-of-conduct": "Code of Conduct", "code-of-conduct": "Code of Conduct",
"termsAndConditionsConfirmed": "I have read and confirmed the <a href=\"/terms-and-conditions\" target=\"_blank\">terms and conditions</a>." "termsAndConditionsConfirmed": "I have read and confirmed the <a href=\"/terms-and-conditions\" target=\"_blank\">terms and conditions</a>.",
"termsAndConditionsNewConfirm": "Please confirm the new Terms and Conditions"
}, },
"sorting": { "sorting": {
"newest": "Newest", "newest": "Newest",
@ -604,4 +606,4 @@
"have-fun": "Now have fun with the alpha version of Human Connection! For the first universal peace. ♥︎", "have-fun": "Now have fun with the alpha version of Human Connection! For the first universal peace. ♥︎",
"closing": "Thank you very much <br> <br> your Human Connection Team" "closing": "Thank you very much <br> <br> your Human Connection Team"
} }
} }

View File

@ -1,4 +1,5 @@
import isEmpty from 'lodash/isEmpty' import isEmpty from 'lodash/isEmpty'
import { VERSION } from '~/pages/terms-and-conditions'
export default async ({ store, env, route, redirect }) => { export default async ({ store, env, route, redirect }) => {
let publicPages = env.publicPages let publicPages = env.publicPages
@ -9,7 +10,14 @@ export default async ({ store, env, route, redirect }) => {
// await store.dispatch('auth/refreshJWT', 'authenticated middleware') // await store.dispatch('auth/refreshJWT', 'authenticated middleware')
const isAuthenticated = await store.dispatch('auth/check') 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 return true
} }
@ -22,5 +30,9 @@ export default async ({ store, env, route, redirect }) => {
params.path = route.path params.path = route.path
} }
return redirect('/login', params) if (!upToDate) {
return redirect('/terms-and-conditions-confirm', params)
} else {
return redirect('/login', params)
}
} }

View File

@ -0,0 +1,101 @@
<template>
<div>
<ds-space>
<ds-heading tag="h2">{{ $t(`site.newTermsAndConditions`) }}</ds-heading>
</ds-space>
<ds-container>
<div>
<ds-button secondary class="display:none" @click="submit">
{{ $t(`site.termsAndConditionsNewConfirm`) }}
</ds-button>
</div>
<div>
<ol>
<li v-for="section in sections" :key="section">
<strong>{{ $t(`termsAndConditions.${section}.title`) }}:</strong>
<p v-html="$t(`termsAndConditions.${section}.description`)" />
</li>
</ol>
<p>{{ $t(`termsAndConditions.have-fun`) }}</p>
<br />
<p>
<strong v-html="$t(`termsAndConditions.closing`)" />
</p>
</div>
<div>
<ds-button secondary class="display:none" @click="submit">
{{ $t(`site.termsAndConditionsNewConfirm`) }}
</ds-button>
</div>
</ds-container>
</div>
</template>
<script>
import gql from 'graphql-tag'
import { mapGetters, mapMutations } from 'vuex'
import { VERSION } from '~/pages/terms-and-conditions'
const mutation = gql`
mutation($id: ID!, $termsAndConditionsAgreedVersion: String) {
UpdateUser(id: $id, termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion) {
id
termsAndConditionsAgreedVersion
}
}
`
export default {
layout: 'default',
head() {
return {
title: this.$t('site.newTermsAndConditions'),
}
},
computed: {
...mapGetters({
currentUser: 'auth/user',
}),
...mapMutations({
setCurrentUser: 'auth/SET_USER',
}),
},
data() {
return {
isOpen: false,
sections: [
'risk',
'data-privacy',
'work-in-progress',
'code-of-conduct',
'moderation',
'fairness',
'questions',
'human-connection',
],
}
},
methods: {
async submit() {
try {
await this.$apollo.mutate({
mutation,
variables: {
id: this.currentUser.id,
termsAndConditionsAgreedVersion: VERSION,
},
update: (store, { data: { UpdateUser } }) => {
const { termsAndConditionsAgreedVersion } = UpdateUser
this.setCurrentUser({
...this.currentUser,
termsAndConditionsAgreedVersion,
})
},
})
this.$toast.success(this.$t('DANKE'))
this.$router.push('/')
} catch (err) {
this.$toast.error(err.message)
}
},
},
}
</script>

View File

@ -22,6 +22,8 @@
</template> </template>
<script> <script>
export const VERSION = '0.0.2'
export default { export default {
layout: 'default', layout: 'default',
head() { head() {

View File

@ -82,6 +82,7 @@ export const actions = {
locationName locationName
contributionsCount contributionsCount
commentedCount commentedCount
termsAndConditionsAgreedVersion
socialMedia { socialMedia {
id id
url url