Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
369 changes: 310 additions & 59 deletions api-js/src/user/mutations/__tests__/reset-password.test.js

Large diffs are not rendered by default.

47 changes: 34 additions & 13 deletions api-js/src/user/mutations/reset-password.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { GraphQLNonNull, GraphQLString } from 'graphql'
import { mutationWithClientMutationId } from 'graphql-relay'
import { t } from '@lingui/macro'

import { resetPasswordUnion } from '../unions'

export const resetPassword = new mutationWithClientMutationId({
name: 'ResetPassword',
description:
Expand All @@ -22,13 +24,11 @@ export const resetPassword = new mutationWithClientMutationId({
},
}),
outputFields: () => ({
status: {
type: GraphQLString,
result: {
type: resetPasswordUnion,
description:
'Informs the user if the password reset was successful, and to redirect to sign in page.',
resolve: async (payload) => {
return payload.status
},
'`ResetPasswordUnion` returning either a `ResetPasswordResult`, or `ResetPasswordError` object.',
resolve: (payload) => payload,
},
}),
mutateAndGetPayload: async (
Expand Down Expand Up @@ -57,7 +57,11 @@ export const resetPassword = new mutationWithClientMutationId({
console.warn(
`When resetting password user attempted to verify account, but userKey is not located in the token parameters.`,
)
throw new Error(i18n._(t`Unable to reset password. Please try again.`))
return {
_type: 'error',
code: 400,
description: i18n._(t`Unable to reset password. Please try again.`),
}
}

// Check if user exists
Expand All @@ -67,33 +71,49 @@ export const resetPassword = new mutationWithClientMutationId({
console.warn(
`A user attempted to reset the password for ${tokenParameters.userKey}, however there is no associated account.`,
)
throw new Error(i18n._(t`Unable to reset password. Please try again.`))
return {
_type: 'error',
code: 400,
description: i18n._(t`Unable to reset password. Please try again.`),
}
}

// Check if password in token matches token in db
if (tokenParameters.currentPassword !== user.password) {
console.warn(
`User: ${user._key} attempted to reset password, however the current password does not match the current hashed password in the db.`,
)
throw new Error(i18n._(t`Unable to reset password. Please try again.`))
return {
_type: 'error',
code: 400,
description: i18n._(t`Unable to reset password. Please try again.`),
}
}

// Check to see if newly submitted passwords match
if (password !== confirmPassword) {
console.warn(
`User: ${user._key} attempted to reset their password, however the submitted passwords do not match.`,
)
throw new Error(i18n._(t`New passwords do not match. Please try again.`))
return {
_type: 'error',
code: 400,
description: i18n._(t`New passwords do not match. Please try again.`),
}
}

// Check to see if password meets GoC requirements
if (password.length < 12) {
console.warn(
`User: ${user._key} attempted to reset their password, however the submitted password is not long enough.`,
)
throw new Error(
i18n._(t`Password is not strong enough. Please try again.`),
)
return {
_type: 'error',
code: 400,
description: i18n._(
t`Password is not strong enough. Please try again.`,
),
}
}

// Update users password in db
Expand All @@ -114,6 +134,7 @@ export const resetPassword = new mutationWithClientMutationId({
console.info(`User: ${user._key} successfully reset their password.`)

return {
_type: 'regular',
status: i18n._(t`Password was successfully reset.`),
}
},
Expand Down
39 changes: 39 additions & 0 deletions api-js/src/user/objects/__tests__/reset-password-error.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { GraphQLInt, GraphQLString } from 'graphql'

import { resetPasswordErrorType } from '../reset-password-error'

describe('given the resetPasswordErrorType object', () => {
describe('testing the field definitions', () => {
it('has an code field', () => {
const demoType = resetPasswordErrorType.getFields()

expect(demoType).toHaveProperty('code')
expect(demoType.code.type).toMatchObject(GraphQLInt)
})
it('has a description field', () => {
const demoType = resetPasswordErrorType.getFields()

expect(demoType).toHaveProperty('description')
expect(demoType.description.type).toMatchObject(GraphQLString)
})
})

describe('testing the field resolvers', () => {
describe('testing the code resolver', () => {
it('returns the resolved field', () => {
const demoType = resetPasswordErrorType.getFields()

expect(demoType.code.resolve({ code: 400 })).toEqual(400)
})
})
describe('testing the description field', () => {
it('returns the resolved value', () => {
const demoType = resetPasswordErrorType.getFields()

expect(
demoType.description.resolve({ description: 'description' }),
).toEqual('description')
})
})
})
})
24 changes: 24 additions & 0 deletions api-js/src/user/objects/__tests__/reset-password-result.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { GraphQLString } from 'graphql'

import { resetPasswordResultType } from '../reset-password-result'

describe('given the resetPasswordErrorType object', () => {
describe('testing the field definitions', () => {
it('has an status field', () => {
const demoType = resetPasswordResultType.getFields()

expect(demoType).toHaveProperty('status')
expect(demoType.status.type).toMatchObject(GraphQLString)
})
})

describe('testing the field resolvers', () => {
describe('testing the status resolver', () => {
it('returns the resolved field', () => {
const demoType = resetPasswordResultType.getFields()

expect(demoType.status.resolve({ status: 'status' })).toEqual('status')
})
})
})
})
2 changes: 2 additions & 0 deletions api-js/src/user/objects/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export * from './auth-result'
export * from './reset-password-error'
export * from './reset-password-result'
export * from './sign-in-error'
export * from './tfa-sign-in-result'
export * from './user-personal'
Expand Down
19 changes: 19 additions & 0 deletions api-js/src/user/objects/reset-password-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { GraphQLInt, GraphQLObjectType, GraphQLString } from 'graphql'

export const resetPasswordErrorType = new GraphQLObjectType({
name: 'ResetPasswordError',
description:
'This object is used to inform the user if any errors occurred while resetting their password.',
fields: () => ({
code: {
type: GraphQLInt,
description: 'Error code to inform user what the issue is related to.',
resolve: ({ code }) => code,
},
description: {
type: GraphQLString,
description: 'Description of the issue that was encountered.',
resolve: ({ description }) => description,
},
}),
})
15 changes: 15 additions & 0 deletions api-js/src/user/objects/reset-password-result.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { GraphQLObjectType, GraphQLString } from 'graphql'

export const resetPasswordResultType = new GraphQLObjectType({
name: 'ResetPasswordResult',
description:
'This object is used to inform the user that no errors were encountered while resetting their password.',
fields: () => ({
status: {
type: GraphQLString,
description:
'Informs the user if the password reset was successful, and to redirect to sign in page.',
resolve: (payload) => payload.status,
},
}),
})
45 changes: 45 additions & 0 deletions api-js/src/user/unions/__tests__/reset-password-union.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { resetPasswordErrorType, resetPasswordResultType } from '../../objects/index'
import { resetPasswordUnion } from '../reset-password-union'

describe('given the resetPasswordUnion', () => {
describe('testing the field types', () => {
it('contains resetPasswordResultType', () => {
const demoType = resetPasswordUnion.getTypes()

expect(demoType).toContain(resetPasswordResultType)
})
it('contains resetPasswordErrorType', () => {
const demoType = resetPasswordUnion.getTypes()

expect(demoType).toContain(resetPasswordErrorType)
})
})
describe('testing the field selection', () => {
describe('testing the resetPasswordResultType', () => {
it('returns the correct type', () => {
const obj = {
_type: 'regular',
authResult: {},
}

expect(resetPasswordUnion.resolveType(obj)).toMatchObject(
resetPasswordResultType,
)
})
})
describe('testing the resetPasswordErrorType', () => {
it('returns the correct type', () => {
const obj = {
_type: 'error',
error: 'sign-in-error',
code: 401,
description: 'text',
}

expect(resetPasswordUnion.resolveType(obj)).toMatchObject(
resetPasswordErrorType,
)
})
})
})
})
1 change: 1 addition & 0 deletions api-js/src/user/unions/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './reset-password-union'
export * from './sign-in-union'
16 changes: 16 additions & 0 deletions api-js/src/user/unions/reset-password-union.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { GraphQLUnionType } from 'graphql'
import { resetPasswordErrorType, resetPasswordResultType } from '../objects'

export const resetPasswordUnion = new GraphQLUnionType({
name: 'ResetPasswordUnion',
description:
'This union is used with the `ResetPassword` mutation, allowing for users to reset their password, and support any errors that may occur',
types: [resetPasswordErrorType, resetPasswordResultType],
resolveType({ _type }) {
if (_type === 'regular') {
return resetPasswordResultType
} else {
return resetPasswordErrorType
}
},
})
20 changes: 19 additions & 1 deletion frontend/schema.faker.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1402,6 +1402,14 @@ type RequestScanPayload {
clientMutationId: String
}

# This object is used to inform the user if any errors occurred while resetting their password.
type ResetPasswordError {
# Error code to inform user what the issue is related to.
code: Int
# Description of the issue that was encountered.
description: String
}

input ResetPasswordInput {
# The users new password.
password: String!
Expand All @@ -1413,11 +1421,21 @@ input ResetPasswordInput {
}

type ResetPasswordPayload {
# `ResetPasswordUnion` returning either a `ResetPasswordResult`, or `ResetPasswordError` object.
result: ResetPasswordUnion
clientMutationId: String
}

# This object is used to inform the user that no errors were encountered while resetting their password.
type ResetPasswordResult {
# Informs the user if the password reset was successful, and to redirect to sign in page.
status: String
clientMutationId: String
}

# This union is used with the `ResetPassword` mutation, allowing for users to
# reset their password, and support any errors that may occur
union ResetPasswordUnion = ResetPasswordError | ResetPasswordResult

# An enum used to assign, and test users roles.
enum RoleEnums {
# A user who has been given access to view an organization.
Expand Down
41 changes: 31 additions & 10 deletions frontend/src/ResetPasswordPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,37 @@ export default function ResetPasswordPage() {
position: 'top-left',
})
},
onCompleted() {
history.push('/sign-in')
toast({
title: t`Password Updated`,
description: t`You may now sign in with your new password`,
status: 'success',
duration: 9000,
isClosable: true,
position: 'top-left',
})
onCompleted({ resetPassword }) {
if (resetPassword.result.__typename === 'ResetPasswordResult') {
history.push('/sign-in')
toast({
title: t`Password Updated`,
description: t`You may now sign in with your new password`,
status: 'success',
duration: 9000,
isClosable: true,
position: 'top-left',
})
} else if (resetPassword.result.__typename === 'ResetPasswordError') {
toast({
title: t`Unable to reset your password, please try again.`,
description: resetPassword.result.description,
status: 'error',
duration: 9000,
isClosable: true,
position: 'top-left',
})
} else {
toast({
title: t`Incorrect send method received.`,
description: t`Incorrect resetPassword.result typename.`,
status: 'error',
duration: 9000,
isClosable: true,
position: 'top-left',
})
console.log('Incorrect resetPassword.result typename.')
}
},
})

Expand Down
10 changes: 9 additions & 1 deletion frontend/src/graphql/mutations.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,15 @@ export const RESET_PASSWORD = gql`
resetToken: $resetToken
}
) {
status
result {
... on ResetPasswordError {
code
description
}
... on ResetPasswordResult {
status
}
}
}
}
`
Expand Down