diff --git a/backend/src/schema/resolvers/passwordReset.js b/backend/src/schema/resolvers/passwordReset.js index d6d2a6e6c..e2fb1a80f 100644 --- a/backend/src/schema/resolvers/passwordReset.js +++ b/backend/src/schema/resolvers/passwordReset.js @@ -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 }, diff --git a/backend/src/schema/resolvers/passwordReset.spec.js b/backend/src/schema/resolvers/passwordReset.spec.js index 1fbd96b7a..6d0347703 100644 --- a/backend/src/schema/resolvers/passwordReset.spec.js +++ b/backend/src/schema/resolvers/passwordReset.spec.js @@ -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() }) }) }) diff --git a/backend/src/schema/types/schema.gql b/backend/src/schema/types/schema.gql index ae77ef8e8..1ef83bac3 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/schema/types/schema.gql @@ -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 diff --git a/webapp/components/PasswordReset/VerifyCode.vue b/webapp/components/PasswordReset/VerifyCode.vue index 4743a4ed6..4d7c161cf 100644 --- a/webapp/components/PasswordReset/VerifyCode.vue +++ b/webapp/components/PasswordReset/VerifyCode.vue @@ -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 = {