Refactor backend

This commit is contained in:
Robert Schäfer 2019-06-18 17:16:56 +02:00
parent 69434cdc7f
commit ba185bcb65
4 changed files with 45 additions and 55 deletions

View File

@ -1,15 +1,16 @@
import uuid from 'uuid/v4'
import bcrypt from 'bcryptjs'
export async function createPasswordReset({ driver, token, email, validUntil }) {
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
CREATE(pr:PasswordReset {token: $token, validUntil: $validUntil, redeemedAt: NULL})
CREATE(pr:PasswordReset {code: $code, issuedAt: datetime($issuedAt), usedAt: NULL})
MERGE (u)-[:REQUESTED]->(pr)
RETURN u,pr
RETURN pr
`
const transactionRes = await session.run(cypher, { token, email, validUntil })
const transactionRes = await session.run(cypher, { issuedAt: issuedAt.toISOString(), code, email })
const resets = transactionRes.records.map(record => record.get('pr'))
session.close()
return resets
@ -18,27 +19,26 @@ export async function createPasswordReset({ driver, token, email, validUntil })
export default {
Mutation: {
requestPasswordReset: async (_, { email }, { driver }) => {
let validUntil = new Date()
validUntil += 3 * 60 * 1000
const token = uuid()
await createPasswordReset({ driver, token, email, validUntil })
const code = uuid().substring(0,6)
await createPasswordReset({ driver, code, email })
return true
},
resetPassword: async (_, { email, token, newPassword }, { driver }) => {
resetPassword: async (_, { email, code, newPassword }, { driver }) => {
const session = driver.session()
const now = new Date().getTime()
const stillValid = new Date()
stillValid.setDate(stillValid.getDate() - 1)
const newHashedPassword = await bcrypt.hashSync(newPassword, 10)
const cypher = `
MATCH (r:PasswordReset {token: $token})
MATCH (r:PasswordReset {code: $code})
MATCH (u:User {email: $email})-[:REQUESTED]->(r)
WHERE r.validUntil > $now AND r.redeemedAt IS NULL
SET r.redeemedAt = $now
WHERE duration.between(r.issuedAt, datetime()).days <= 0 AND r.usedAt IS NULL
SET r.usedAt = datetime()
SET u.password = $newHashedPassword
RETURN r
`
let transactionRes = await session.run(cypher, { now, email, token, newHashedPassword })
let transactionRes = await session.run(cypher, { stillValid, email, code, newHashedPassword })
const [reset] = transactionRes.records.map(record => record.get('r'))
const result = !!(reset && reset.properties.redeemedAt)
const result = !!(reset && reset.properties.usedAt)
session.close()
return result
},

View File

@ -64,23 +64,12 @@ describe('passwordReset', () => {
expect(resets).toHaveLength(1)
})
it('creates a reset token', async () => {
it('creates a reset code', async () => {
await client.request(mutation, variables)
const resets = await getAllPasswordResets()
const [reset] = resets
const { token } = reset.properties
expect(token).toMatch(/^........-....-....-....-............$/)
})
it('created PasswordReset is valid for less than 4 minutes', async () => {
await client.request(mutation, variables)
const resets = await getAllPasswordResets()
const [reset] = resets
let { validUntil } = reset.properties
validUntil = Date.parse(validUntil)
const now = new Date().getTime()
expect(validUntil).toBeGreaterThan(now - 60 * 1000)
expect(validUntil).toBeLessThan(now + 4 * 60 * 1000)
const { code } = reset.properties
expect(code).toHaveLength(6)
})
})
})
@ -89,50 +78,50 @@ describe('passwordReset', () => {
const setup = async (options = {}) => {
const {
email = 'user@example.org',
validUntil = new Date().getTime() + 3 * 60 * 1000,
token = 'abcdefgh-ijkl-mnop-qrst-uvwxyz123456',
issuedAt = new Date(),
code = 'abcdef',
} = options
const session = driver.session()
await createPasswordReset({ driver, email, validUntil, token })
await createPasswordReset({ driver, email, issuedAt, code })
session.close()
}
const mutation = `mutation($token: String!, $email: String!, $newPassword: String!) { resetPassword(token: $token, email: $email, newPassword: $newPassword) }`
const mutation = `mutation($code: String!, $email: String!, $newPassword: String!) { resetPassword(code: $code, email: $email, newPassword: $newPassword) }`
let email = 'user@example.org'
let token = 'abcdefgh-ijkl-mnop-qrst-uvwxyz123456'
let code = 'abcdef'
let newPassword = 'supersecret'
let variables
describe('invalid email', () => {
it('resolves to false', async () => {
await setup()
variables = { newPassword, email: 'non-existent@example.org', token }
variables = { newPassword, email: 'non-existent@example.org', code }
await expect(client.request(mutation, variables)).resolves.toEqual({ resetPassword: false })
})
})
describe('valid email', () => {
describe('but invalid token', () => {
describe('but invalid code', () => {
it('resolves to false', async () => {
await setup()
variables = { newPassword, email, token: 'slkdjfldsjflsdjfsjdfl' }
variables = { newPassword, email, code: 'slkdjf' }
await expect(client.request(mutation, variables)).resolves.toEqual({
resetPassword: false,
})
})
})
describe('and valid token', () => {
describe('and valid code', () => {
beforeEach(() => {
variables = {
newPassword,
email: 'user@example.org',
token: 'abcdefgh-ijkl-mnop-qrst-uvwxyz123456',
code: 'abcdef',
}
})
describe('and token not expired', () => {
describe('and code not expired', () => {
beforeEach(async () => {
await setup()
})
@ -143,12 +132,12 @@ describe('passwordReset', () => {
})
})
it('updates PasswordReset `redeemedAt` property', async () => {
it('updates PasswordReset `usedAt` property', async () => {
await client.request(mutation, variables)
const requests = await getAllPasswordResets()
const [request] = requests
const { redeemedAt } = request.properties
expect(redeemedAt).not.toBeNull()
const { usedAt } = request.properties
expect(usedAt).not.toBeFalsy()
})
it('updates password of the user', async () => {
@ -168,10 +157,11 @@ describe('passwordReset', () => {
})
})
describe('but expired token', () => {
describe('but expired code', () => {
beforeEach(async () => {
const validUntil = new Date().getTime() - 1000
await setup({ validUntil })
const issuedAt = new Date()
issuedAt.setDate(issuedAt.getDate() - 1)
await setup({ issuedAt })
})
it('resolves to false', async () => {
@ -180,12 +170,12 @@ describe('passwordReset', () => {
})
})
it('does not update PasswordReset `redeemedAt` property', async () => {
it('does not update PasswordReset `usedAt` property', async () => {
await client.request(mutation, variables)
const requests = await getAllPasswordResets()
const [request] = requests
const { redeemedAt } = request.properties
expect(redeemedAt).toBeUndefined()
const { usedAt } = request.properties
expect(usedAt).toBeUndefined()
})
})
})

View File

@ -26,7 +26,7 @@ type Mutation {
signup(email: String!, password: String!): Boolean!
changePassword(oldPassword: String!, newPassword: String!): String!
requestPasswordReset(email: String!): Boolean!
resetPassword(email: String!, token: String!, newPassword: String!): Boolean!
resetPassword(email: String!, code: String!, newPassword: String!): Boolean!
report(id: ID!, description: String): Report
disable(id: ID!): ID
enable(id: ID!): ID

View File

@ -150,18 +150,18 @@ export default {
},
async handleSubmitPassword() {
const mutation = gql`
mutation($token: String!, $email: String!, $newPassword: String!) {
resetPassword(token: $token, email: $email, newPassword: $newPassword)
mutation($code: String!, $email: String!, $newPassword: String!) {
resetPassword(code: $code, email: $email, newPassword: $newPassword)
}
`
const { newPassword } = this.password.formData
const { email, code: token } = this.verification.formData
const variables = { newPassword, email, token }
const { email, code } = this.verification.formData
const variables = { newPassword, email, code }
try {
const {
data: { resetPassword },
} = await this.$apollo.mutate({ mutation, variables })
const changePasswordResult = resetPassword ? 'success' : 'failure'
const changePasswordResult = resetPassword ? 'success' : 'error'
this.changePasswordResult = changePasswordResult
this.$emit('change-password-result', changePasswordResult)
this.verification.formData = {