diff --git a/.changeset/poor-chefs-glow.md b/.changeset/poor-chefs-glow.md new file mode 100644 index 00000000..92ea4791 --- /dev/null +++ b/.changeset/poor-chefs-glow.md @@ -0,0 +1,6 @@ +--- +'@hyperdx/api': minor +'@hyperdx/app': minor +--- + +feat(register): password confirmation diff --git a/packages/api/src/routers/api/__tests__/team.test.ts b/packages/api/src/routers/api/__tests__/team.test.ts index 07c1ad06..01c7130c 100644 --- a/packages/api/src/routers/api/__tests__/team.test.ts +++ b/packages/api/src/routers/api/__tests__/team.test.ts @@ -20,7 +20,14 @@ describe('team router', () => { const login = async () => { const agent = getAgent(server); - await agent.post('/register/password').send(MOCK_USER).expect(200); + await agent + .post('/register/password') + .send({ ...MOCK_USER, confirmPassword: 'wrong-password' }) + .expect(400); + await agent + .post('/register/password') + .send({ ...MOCK_USER, confirmPassword: MOCK_USER.password }) + .expect(200); const user = await findUserByEmail(MOCK_USER.email); const team = await getTeam(user?.team as any); diff --git a/packages/api/src/routers/api/root.ts b/packages/api/src/routers/api/root.ts index 16b523b7..2518df2e 100644 --- a/packages/api/src/routers/api/root.ts +++ b/packages/api/src/routers/api/root.ts @@ -15,24 +15,30 @@ import { handleAuthError, } from '../../middleware/auth'; -const registrationSchema = z.object({ - email: z.string().email(), - password: z - .string() - .min(12, 'Password must have at least 12 characters') - .refine( - pass => /[a-z]/.test(pass) && /[A-Z]/.test(pass), - 'Password must include both lower and upper case characters', - ) - .refine( - pass => /\d/.test(pass), - 'Password must include at least one number', - ) - .refine( - pass => /[!@#$%^&*(),.?":{}|<>]/.test(pass), - 'Password must include at least one special character', - ), -}); +const registrationSchema = z + .object({ + email: z.string().email(), + password: z + .string() + .min(12, 'Password must have at least 12 characters') + .refine( + pass => /[a-z]/.test(pass) && /[A-Z]/.test(pass), + 'Password must include both lower and upper case characters', + ) + .refine( + pass => /\d/.test(pass), + 'Password must include at least one number', + ) + .refine( + pass => /[!@#$%^&*(),.?":{}|<>]/.test(pass), + 'Password must include at least one special character', + ), + confirmPassword: z.string(), + }) + .refine(data => data.password === data.confirmPassword, { + message: "Passwords don't match", + path: ['confirmPassword'], + }); const router = express.Router(); diff --git a/packages/app/src/AuthPage.tsx b/packages/app/src/AuthPage.tsx index 62f8c1b3..6e5aec8c 100644 --- a/packages/app/src/AuthPage.tsx +++ b/packages/app/src/AuthPage.tsx @@ -5,6 +5,7 @@ import { API_SERVER_URL } from './config'; import { useRouter } from 'next/router'; import { useEffect } from 'react'; import Link from 'next/link'; +import cx from 'classnames'; import LandingHeader from './LandingHeader'; import * as config from './config'; @@ -13,6 +14,7 @@ import api from './api'; type FormData = { email: string; password: string; + confirmPassword: string; }; export default function AuthPage({ action }: { action: 'register' | 'login' }) { @@ -45,7 +47,11 @@ export default function AuthPage({ action }: { action: 'register' | 'login' }) { const onSubmit: SubmitHandler = data => registerPassword.mutate( - { email: data.email, password: data.password }, + { + email: data.email, + password: data.password, + confirmPassword: data.confirmPassword, + }, { onSuccess: () => router.push('/search'), onError: async error => { @@ -73,6 +79,7 @@ export default function AuthPage({ action }: { action: 'register' | 'login' }) { controller: { onSubmit: handleSubmit(onSubmit) }, email: register('email', { required: true }), password: register('password', { required: true }), + confirmPassword: register('confirmPassword', { required: true }), } : { controller: { @@ -135,9 +142,28 @@ export default function AuthPage({ action }: { action: 'register' | 'login' }) { data-test-id="form-password" id="password" type="password" - className="border-0" + className={cx('border-0', { + 'mb-3': isRegister, + })} {...form.password} /> + {isRegister && ( + <> + + Confirm Password + + + + )} {isRegister && Object.keys(errors).length > 0 && (
{Object.values(errors).map((error, index) => ( diff --git a/packages/app/src/api.ts b/packages/app/src/api.ts index ed78a026..ccd55202 100644 --- a/packages/app/src/api.ts +++ b/packages/app/src/api.ts @@ -545,15 +545,19 @@ const api = { ); }, useRegisterPassword() { - return useMutation( - async ({ email, password }) => - server(`register/password`, { - method: 'POST', - json: { - email, - password, - }, - }).json(), + return useMutation< + any, + HTTPError, + { email: string; password: string; confirmPassword: string } + >(async ({ email, password, confirmPassword }) => + server(`register/password`, { + method: 'POST', + json: { + email, + password, + confirmPassword, + }, + }).json(), ); }, };