Skip to content

Commit a58e0e9

Browse files
authoredJun 19, 2024··
BA-1424-fe-refactor-password-expiration-flow-library (#96)
* BA-1424-fe-refactor-password-expiration-flow-library * rebase * remove usePreAuth * rebase * Rename IUseChangeExpiredPassword to UseChangeExpiredPassword

File tree

19 files changed

+315
-12
lines changed

19 files changed

+315
-12
lines changed
 

‎packages/authentication/CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# @baseapp-frontend/authentication
22

3+
## 3.1.0
4+
5+
### Minor Changes
6+
7+
- Add change expired password logic in useLogin and add change expired password api
8+
39
## 3.0.1
410

511
### Patch Changes

‎packages/authentication/modules/access/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@ export type * from './useResetPassword/types'
1515

1616
export { default as useSignUp } from './useSignUp'
1717
export type * from './useSignUp/types'
18+
19+
export { default as useChangeExpiredPassword } from './useChangeExpiredPassword'
20+
export type * from './useChangeExpiredPassword/types'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { ComponentWithProviders, MockAdapter, renderHook } from '@baseapp-frontend/test'
2+
import { axios } from '@baseapp-frontend/utils'
3+
4+
import { z } from 'zod'
5+
6+
import useChangeExpiredPassword from '../index'
7+
8+
// @ts-ignore TODO: (BA-1081) investigate AxiosRequestHeaders error
9+
export const axiosMock = new MockAdapter(axios)
10+
11+
describe('useChangeExpiredPassword', () => {
12+
const currentPassword = '1234'
13+
const password = '123456'
14+
const token = 'fake-token'
15+
16+
test('should run onSuccess', async () => {
17+
axiosMock.onPost('/change-expired-password').reply(200, {
18+
currentPassword,
19+
newPassword: password,
20+
token,
21+
})
22+
23+
let hasOnSuccessRan = false
24+
25+
const { result } = renderHook(
26+
() =>
27+
useChangeExpiredPassword({
28+
token,
29+
defaultValues: {
30+
currentPassword,
31+
newPassword: password,
32+
confirmNewPassword: password,
33+
},
34+
options: {
35+
onSuccess: () => {
36+
hasOnSuccessRan = true
37+
},
38+
},
39+
}),
40+
{
41+
wrapper: ComponentWithProviders,
42+
},
43+
)
44+
45+
await result.current.form.handleSubmit()
46+
47+
expect(hasOnSuccessRan).toBe(true)
48+
})
49+
50+
test('should run onError', async () => {
51+
axiosMock.onPost('/change-expired-password').reply(500, {
52+
error: 'any',
53+
})
54+
55+
let hasOnErrorRan = false
56+
57+
const { result } = renderHook(
58+
() =>
59+
useChangeExpiredPassword({
60+
token,
61+
defaultValues: {
62+
currentPassword,
63+
newPassword: password,
64+
confirmNewPassword: password,
65+
},
66+
options: {
67+
onError: () => {
68+
hasOnErrorRan = true
69+
},
70+
},
71+
}),
72+
{
73+
wrapper: ComponentWithProviders,
74+
},
75+
)
76+
77+
await result.current.form.handleSubmit()
78+
79+
expect(hasOnErrorRan).toBe(true)
80+
})
81+
82+
test('should run onError when newPassword and confirmNewPassword are different', async () => {
83+
axiosMock.onPost('/change-expired-password').reply(200, {})
84+
85+
let hasOnSuccessRan = false
86+
87+
const { result } = renderHook(
88+
() =>
89+
useChangeExpiredPassword({
90+
token,
91+
defaultValues: {
92+
currentPassword,
93+
newPassword: password,
94+
confirmNewPassword: `${password}different`,
95+
},
96+
options: {
97+
onSuccess: () => {
98+
hasOnSuccessRan = true
99+
},
100+
},
101+
}),
102+
{
103+
wrapper: ComponentWithProviders,
104+
},
105+
)
106+
107+
await result.current.form.handleSubmit()
108+
109+
expect(hasOnSuccessRan).toBe(false)
110+
})
111+
112+
test('should allow custom defaultValues and validationSchema', async () => {
113+
axiosMock.onPost('/change-expired-password').reply(200, {})
114+
115+
const customDefaultValues = {
116+
currentPassword: '1234',
117+
newPassword: '12345',
118+
confirmNewPassword: '123456', // that would pass since the schema is not the default one, and doesnt check for password equality
119+
}
120+
const customValidationSchema = z.object({
121+
currentPassword: z.string().nonempty(),
122+
newPassword: z.string().nonempty(),
123+
confirmNewPassword: z.string().nonempty(),
124+
})
125+
126+
let hasOnSuccessRan = false
127+
128+
const { result } = renderHook(
129+
() =>
130+
useChangeExpiredPassword({
131+
token,
132+
defaultValues: customDefaultValues,
133+
validationSchema: customValidationSchema,
134+
options: {
135+
onSuccess: () => {
136+
hasOnSuccessRan = true
137+
},
138+
},
139+
}),
140+
{
141+
wrapper: ComponentWithProviders,
142+
},
143+
)
144+
145+
await result.current.form.handleSubmit()
146+
147+
expect(hasOnSuccessRan).toBe(true)
148+
})
149+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { ZOD_MESSAGE } from '@baseapp-frontend/utils'
2+
3+
import { z } from 'zod'
4+
5+
import { ChangeExpiredPasswordForm } from './types'
6+
7+
export const DEFAULT_VALIDATION_SCHEMA = z
8+
.object({
9+
currentPassword: z.string().nonempty(ZOD_MESSAGE.required),
10+
newPassword: z.string().nonempty(ZOD_MESSAGE.required),
11+
confirmNewPassword: z.string().nonempty(ZOD_MESSAGE.required),
12+
})
13+
.refine(({ confirmNewPassword, newPassword }) => newPassword === confirmNewPassword, {
14+
message: ZOD_MESSAGE.passwordDoNotMatch,
15+
path: ['confirmNewPassword'],
16+
})
17+
18+
export const DEFAULT_INITIAL_VALUES: ChangeExpiredPasswordForm = {
19+
currentPassword: '',
20+
newPassword: '',
21+
confirmNewPassword: '',
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { setFormApiErrors } from '@baseapp-frontend/utils'
2+
3+
import { zodResolver } from '@hookform/resolvers/zod'
4+
import { useMutation } from '@tanstack/react-query'
5+
import { SubmitHandler, useForm } from 'react-hook-form'
6+
7+
import AuthApi from '../../../services/auth'
8+
import { DEFAULT_INITIAL_VALUES, DEFAULT_VALIDATION_SCHEMA } from './constants'
9+
import { ChangeExpiredPasswordForm, UseChangeExpiredPassword } from './types'
10+
11+
const useChangeExpiredPassword = ({
12+
token,
13+
validationSchema = DEFAULT_VALIDATION_SCHEMA,
14+
defaultValues = DEFAULT_INITIAL_VALUES,
15+
ApiClass = AuthApi,
16+
enableFormApiErrors = true,
17+
options = {},
18+
}: UseChangeExpiredPassword) => {
19+
const form = useForm({
20+
defaultValues,
21+
resolver: zodResolver(validationSchema),
22+
mode: 'onChange',
23+
})
24+
25+
const mutation = useMutation({
26+
mutationFn: ({ currentPassword, newPassword }) =>
27+
ApiClass.changeExpiredPassword({ currentPassword, newPassword, token }),
28+
...options, // needs to be placed below all overridable options
29+
onError: (err, variables, context) => {
30+
options?.onError?.(err, variables, context)
31+
if (enableFormApiErrors) {
32+
setFormApiErrors(form, err)
33+
}
34+
},
35+
onSuccess: (response, variables, context) => {
36+
options?.onSuccess?.(response, variables, context)
37+
},
38+
})
39+
40+
const handleSubmit: SubmitHandler<ChangeExpiredPasswordForm> = async (values) => {
41+
try {
42+
await mutation.mutateAsync(values)
43+
} catch (error) {
44+
// mutateAsync will raise an error if there's an API error
45+
}
46+
}
47+
48+
return {
49+
form: {
50+
...form,
51+
// TODO: improve types
52+
handleSubmit: form.handleSubmit(handleSubmit) as any,
53+
},
54+
mutation,
55+
}
56+
}
57+
58+
export default useChangeExpiredPassword
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { UseMutationOptions } from '@tanstack/react-query'
2+
import { z } from 'zod'
3+
4+
import AuthApi from '../../../services/auth'
5+
6+
type ApiClass = Pick<typeof AuthApi, 'changeExpiredPassword'>
7+
8+
export type ChangeExpiredPasswordForm = {
9+
currentPassword: string
10+
newPassword: string
11+
confirmNewPassword: string
12+
}
13+
14+
export interface UseChangeExpiredPassword {
15+
token: string
16+
validationSchema?: z.ZodObject<z.ZodRawShape> | z.ZodEffects<z.ZodObject<z.ZodRawShape>>
17+
defaultValues?: ChangeExpiredPasswordForm
18+
options?: UseMutationOptions<void, unknown, ChangeExpiredPasswordForm, any>
19+
ApiClass?: ApiClass
20+
enableFormApiErrors?: boolean
21+
}

‎packages/authentication/modules/access/useLogin/index.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,13 @@ import {
2222
LoginMfaRequest,
2323
LoginRequest,
2424
LoginSimpleTokenResponse,
25+
LoginChangeExpiredPasswordRedirectResponse
2526
} from '../../../types/auth'
26-
import { isJWTResponse, isLoginMfaResponse } from '../../../utils/login'
27+
import {
28+
isJWTResponse,
29+
isLoginChangeExpiredPasswordRedirectResponse,
30+
isLoginMfaResponse,
31+
} from '../../../utils/login'
2732
import { CODE_VALIDATION_INITIAL_VALUES, CODE_VALIDATION_SCHEMA } from '../../mfa/constants'
2833
import { useSimpleTokenUser } from '../../user'
2934
import { DEFAULT_INITIAL_VALUES, DEFAULT_VALIDATION_SCHEMA } from './constants'
@@ -72,7 +77,15 @@ const useLogin = ({
7277
/*
7378
* Handles login success with the auth token in response
7479
*/
75-
async function handleLoginSuccess(response: LoginJWTResponse | LoginSimpleTokenResponse) {
80+
async function handleLoginSuccess(
81+
response:
82+
| LoginJWTResponse
83+
| LoginSimpleTokenResponse
84+
| LoginChangeExpiredPasswordRedirectResponse,
85+
) {
86+
if (isLoginChangeExpiredPasswordRedirectResponse(response)) {
87+
return
88+
}
7689
if (isJWTResponse(tokenType, response)) {
7790
jwtSuccessHandler(response, cookieName, refreshCookieName)
7891
} else {

‎packages/authentication/modules/access/useRecoverPassword/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const useRecoverPassword = ({
2626

2727
const mutation = useMutation({
2828
mutationFn: ({ email }) => ApiClass.recoverPassword({ email }),
29-
...options, // needs to be placed bellow all overridable options
29+
...options, // needs to be placed below all overridable options
3030
onError: (err, variables, context) => {
3131
options?.onError?.(err, variables, context)
3232
if (enableFormApiErrors) {

‎packages/authentication/modules/access/useResetPassword/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const useResetPassword = ({
2626

2727
const mutation = useMutation({
2828
mutationFn: ({ newPassword }) => ApiClass.resetPassword({ newPassword, token }),
29-
...options, // needs to be placed bellow all overridable options
29+
...options, // needs to be placed below all overridable options
3030
onError: (err, variables, context) => {
3131
options?.onError?.(err, variables, context)
3232
if (enableFormApiErrors) {

‎packages/authentication/modules/access/useSignUp/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const useSignUp = <TRegisterRequest extends RegisterRequest, TRegisterResponse =
2727

2828
const mutation = useMutation({
2929
mutationFn: (values) => ApiClass.register<TRegisterResponse>(values),
30-
...options, // needs to be placed bellow all overridable options
30+
...options, // needs to be placed below all overridable options
3131
onError: (err, variables, context) => {
3232
options?.onError?.(err, variables, context)
3333
if (enableFormApiErrors) {

‎packages/authentication/modules/mfa/useMfaActivate/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { UseMfaActivateOptions } from './types'
66
const useMfaActivate = ({ options, ApiClass = MfaApi }: UseMfaActivateOptions = {}) =>
77
useMutation({
88
mutationFn: ({ method }) => ApiClass.activate({ method }),
9-
...options, // needs to be placed bellow all overridable options
9+
...options, // needs to be placed below all overridable options
1010
onError: (err, variables, context) => {
1111
options?.onError?.(err, variables, context)
1212
},

‎packages/authentication/modules/mfa/useMfaActivateConfirm/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const useMfaActivateConfirm = ({
2626

2727
const mutation = useMutation({
2828
mutationFn: (data) => ApiClass.confirmActivation(data),
29-
...options, // needs to be placed bellow all overridable options
29+
...options, // needs to be placed below all overridable options
3030
onError: (err, variables, context) => {
3131
options?.onError?.(err, variables, context)
3232
if (enableFormApiErrors) {

‎packages/authentication/modules/mfa/useMfaActiveMethods/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const useMfaActiveMethods = ({ options, ApiClass = MfaApi }: UseMfaActiveMethods
99
const { data, ...rest } = useQuery({
1010
queryFn: () => ApiClass.getActiveMethods(),
1111
queryKey: MFA_API_KEY.getActiveMethods(),
12-
...restOptions, // needs to be placed bellow all overridable options
12+
...restOptions, // needs to be placed below all overridable options
1313
enabled,
1414
})
1515

‎packages/authentication/modules/mfa/useMfaConfiguration/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const useMfaConfiguration = ({ options, ApiClass = MfaApi }: UseMfaConfiguration
88
const { data: configuration, ...rest } = useQuery({
99
queryFn: () => ApiClass.getConfiguration(),
1010
queryKey: MFA_API_KEY.getConfiguration(),
11-
...options, // needs to be placed bellow all overridable options
11+
...options, // needs to be placed below all overridable options
1212
enabled,
1313
})
1414

‎packages/authentication/modules/mfa/useMfaDeactivate/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const useMfaDeactivate = ({ options, ApiClass = MfaApi }: UseMfaDeactivateOption
88

99
return useMutation({
1010
mutationFn: ({ method, code }) => ApiClass.deactivate({ method, code }),
11-
...options, // needs to be placed bellow all overridable options
11+
...options, // needs to be placed below all overridable options
1212
onError: (err, variables, context) => {
1313
options?.onError?.(err, variables, context)
1414
},

‎packages/authentication/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@baseapp-frontend/authentication",
33
"description": "Authentication modules.",
4-
"version": "3.0.1",
4+
"version": "3.1.0",
55
"main": "./dist/index.ts",
66
"module": "./dist/index.mjs",
77
"scripts": {

0 commit comments

Comments
 (0)
Please sign in to comment.