feat(register): password confirmation (#85)

Origin: https://github.com/hyperdxio/hyperdx/pull/81#discussion_r1384379205

Add a password confirmation field to the registration form.

- API will require `confirmPassword` field on the registration endpoint
- The `confirmPassword` and `password` fields will be validated by zod on API validation
- UI on the registration form will have a "Confirm Password" field
- Validation errors will be displayed on the UI the same way other validation errors
- API test covers mismatch between `confirmPassword` and `password` (simple check, can be improved)
This commit is contained in:
Mark Omarov 2023-11-10 06:37:15 +09:00 committed by GitHub
parent f66200717e
commit b1a537d88c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 79 additions and 30 deletions

View file

@ -0,0 +1,6 @@
---
'@hyperdx/api': minor
'@hyperdx/app': minor
---
feat(register): password confirmation

View file

@ -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);

View file

@ -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();

View file

@ -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<FormData> = 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 && (
<>
<Form.Label
htmlFor="confirmPassword"
className="text-start text-muted fs-7.5 mb-1"
>
Confirm Password
</Form.Label>
<Form.Control
data-test-id="form-confirm-password"
id="confirmPassword"
type="password"
className="border-0"
{...form.confirmPassword}
/>
</>
)}
{isRegister && Object.keys(errors).length > 0 && (
<div className="text-danger mt-2">
{Object.values(errors).map((error, index) => (

View file

@ -545,15 +545,19 @@ const api = {
);
},
useRegisterPassword() {
return useMutation<any, HTTPError, { email: string; password: string }>(
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(),
);
},
};