feat: create change password frontend form
This commit is contained in:
parent
6477164493
commit
96427705e6
7 changed files with 167 additions and 14 deletions
|
@ -35,7 +35,7 @@ describe('ResetPasswordContainer', () => {
|
|||
|
||||
const newPassword = 'new_password';
|
||||
const response = { email };
|
||||
server.use(getTRPCMock({ path: ['auth', 'resetPassword'], type: 'mutation', response, delay: 100 }));
|
||||
server.use(getTRPCMock({ path: ['auth', 'changeOperatorPassword'], type: 'mutation', response, delay: 100 }));
|
||||
|
||||
const passwordInput = screen.getByLabelText('Password');
|
||||
const confirmPasswordInput = screen.getByLabelText('Confirm password');
|
||||
|
@ -62,7 +62,7 @@ describe('ResetPasswordContainer', () => {
|
|||
|
||||
const newPassword = 'new_password';
|
||||
const error = { message: 'Something went wrong' };
|
||||
server.use(getTRPCMockError({ path: ['auth', 'resetPassword'], type: 'mutation', message: error.message }));
|
||||
server.use(getTRPCMockError({ path: ['auth', 'changeOperatorPassword'], type: 'mutation', message: error.message }));
|
||||
|
||||
const passwordInput = screen.getByLabelText('Password');
|
||||
const confirmPasswordInput = screen.getByLabelText('Confirm password');
|
||||
|
@ -102,7 +102,7 @@ describe('ResetPasswordContainer', () => {
|
|||
it('should redirect to login page when Back to login button is clicked', async () => {
|
||||
// Arrange
|
||||
render(<ResetPasswordContainer isRequested />);
|
||||
server.use(getTRPCMock({ path: ['auth', 'resetPassword'], type: 'mutation', response: { email: 'goofy@test.com' } }));
|
||||
server.use(getTRPCMock({ path: ['auth', 'changeOperatorPassword'], type: 'mutation', response: { email: 'goofy@test.com' } }));
|
||||
const resetPasswordForm = screen.getByRole('button', { name: 'Reset password' });
|
||||
const passwordInput = screen.getByLabelText('Password');
|
||||
const confirmPasswordInput = screen.getByLabelText('Confirm password');
|
||||
|
|
|
@ -16,7 +16,7 @@ export const ResetPasswordContainer: React.FC<Props> = ({ isRequested }) => {
|
|||
const { addToast } = useToastStore();
|
||||
const router = useRouter();
|
||||
const utils = trpc.useContext();
|
||||
const resetPassword = trpc.auth.resetPassword.useMutation({
|
||||
const resetPassword = trpc.auth.changeOperatorPassword.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.auth.checkPasswordChangeRequest.invalidate();
|
||||
},
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
import React from 'react';
|
||||
import { server } from '@/client/mocks/server';
|
||||
import { getTRPCMock, getTRPCMockError } from '@/client/mocks/getTrpcMock';
|
||||
import { useToastStore } from '@/client/state/toastStore';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { render, screen, waitFor, fireEvent } from '../../../../../../tests/test-utils';
|
||||
import { ChangePasswordForm } from './ChangePasswordForm';
|
||||
|
||||
describe('<ChangePasswordForm />', () => {
|
||||
it('should show success toast upon password change', async () => {
|
||||
// arrange
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
server.use(getTRPCMock({ path: ['auth', 'changePassword'], type: 'mutation', response: true }));
|
||||
render(<ChangePasswordForm />);
|
||||
const currentPasswordInput = screen.getByRole('textbox', { name: 'currentPassword' });
|
||||
const newPasswordInput = screen.getByRole('textbox', { name: 'newPassword' });
|
||||
const confirmPasswordInput = screen.getByRole('textbox', { name: 'newPasswordConfirm' });
|
||||
const newPassword = faker.random.alphaNumeric(8);
|
||||
|
||||
// act
|
||||
fireEvent.change(currentPasswordInput, { target: { value: 'test' } });
|
||||
fireEvent.change(newPasswordInput, { target: { value: newPassword } });
|
||||
fireEvent.change(confirmPasswordInput, { target: { value: newPassword } });
|
||||
const submitButton = screen.getByRole('button', { name: /Change password/i });
|
||||
submitButton.click();
|
||||
|
||||
// assert
|
||||
await waitFor(() => {
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0]?.status).toEqual('success');
|
||||
expect(result.current.toasts[0]?.description).toEqual('Password successfully changed');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error toast if change password failed', async () => {
|
||||
// arrange
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
server.use(getTRPCMockError({ path: ['auth', 'changePassword'], type: 'mutation', message: 'Invalid password' }));
|
||||
render(<ChangePasswordForm />);
|
||||
const currentPasswordInput = screen.getByRole('textbox', { name: 'currentPassword' });
|
||||
const newPasswordInput = screen.getByRole('textbox', { name: 'newPassword' });
|
||||
const confirmPasswordInput = screen.getByRole('textbox', { name: 'newPasswordConfirm' });
|
||||
const newPassword = faker.random.alphaNumeric(8);
|
||||
|
||||
// act
|
||||
fireEvent.change(currentPasswordInput, { target: { value: faker.random.alphaNumeric(8) } });
|
||||
fireEvent.change(newPasswordInput, { target: { value: newPassword } });
|
||||
fireEvent.change(confirmPasswordInput, { target: { value: newPassword } });
|
||||
const submitButton = screen.getByRole('button', { name: /Change password/i });
|
||||
submitButton.click();
|
||||
|
||||
// assert
|
||||
await waitFor(() => {
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0]?.status).toEqual('error');
|
||||
expect(result.current.toasts[0]?.title).toEqual('Error');
|
||||
expect(result.current.toasts[0]?.description).toEqual('Invalid password');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error in the form if passwords do not match', async () => {
|
||||
// arrange
|
||||
render(<ChangePasswordForm />);
|
||||
const currentPasswordInput = screen.getByRole('textbox', { name: 'currentPassword' });
|
||||
const newPasswordInput = screen.getByRole('textbox', { name: 'newPassword' });
|
||||
const confirmPasswordInput = screen.getByRole('textbox', { name: 'newPasswordConfirm' });
|
||||
|
||||
// act
|
||||
fireEvent.change(currentPasswordInput, { target: { value: 'test' } });
|
||||
fireEvent.change(newPasswordInput, { target: { value: faker.random.alphaNumeric(8) } });
|
||||
fireEvent.change(confirmPasswordInput, { target: { value: faker.random.alphaNumeric(8) } });
|
||||
const submitButton = screen.getByRole('button', { name: /Change password/i });
|
||||
submitButton.click();
|
||||
|
||||
// assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Passwords do not match/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,64 @@
|
|||
import React from 'react';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { trpc } from '@/utils/trpc';
|
||||
import { useToastStore } from '@/client/state/toastStore';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
currentPassword: z.string().min(1),
|
||||
newPassword: z.string().min(8, 'Password must be at least 8 characters'),
|
||||
newPasswordConfirm: z.string().min(8, 'Password must be at least 8 characters'),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.newPassword !== data.newPasswordConfirm) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Passwords do not match',
|
||||
path: ['newPasswordConfirm'],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof schema>;
|
||||
|
||||
export const ChangePasswordForm = () => {
|
||||
const router = useRouter();
|
||||
const { addToast } = useToastStore();
|
||||
const changePassword = trpc.auth.changePassword.useMutation({
|
||||
onError: (e) => {
|
||||
addToast({ title: 'Error', description: e.message, status: 'error' });
|
||||
},
|
||||
onSuccess: () => {
|
||||
addToast({ title: 'Success', description: 'Password successfully changed', status: 'success' });
|
||||
router.push('/');
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<FormValues>({
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
|
||||
const onSubmit = (values: FormValues) => {
|
||||
changePassword.mutate(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="mb-4 w-100 ">
|
||||
<Input disabled={changePassword.isLoading} {...register('currentPassword')} error={errors.currentPassword?.message} type="password" placeholder="Current password" />
|
||||
<Input disabled={changePassword.isLoading} {...register('newPassword')} error={errors.newPassword?.message} className="mt-2" type="password" placeholder="New password" />
|
||||
<Input disabled={changePassword.isLoading} {...register('newPasswordConfirm')} error={errors.newPasswordConfirm?.message} className="mt-2" type="password" placeholder="Confirm new password" />
|
||||
<Button disabled={changePassword.isLoading} className="mt-3" type="submit">
|
||||
Change password
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export { ChangePasswordForm } from './ChangePasswordForm';
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import { trpc } from '@/utils/trpc';
|
||||
import { Switch } from '@/components/ui/Switch';
|
||||
import { IconLock } from '@tabler/icons-react';
|
||||
import { useToastStore } from '@/client/state/toastStore';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
|
||||
|
@ -99,15 +98,6 @@ export const OtpForm = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="d-flex">
|
||||
<IconLock className="me-2" />
|
||||
<h2>Two-Factor Authentication</h2>
|
||||
</div>
|
||||
<p className="text-muted">
|
||||
Two-factor authentication (2FA) adds an additional layer of security to your account.
|
||||
<br />
|
||||
When enabled, you will be prompted to enter a code from your authenticator app when you log in.
|
||||
</p>
|
||||
{!key && <Switch disabled={!me.isSuccess} onCheckedChange={handleTotp} checked={me.data?.totp_enabled} label="Enable two-factor authentication" />}
|
||||
{getTotpUri.isLoading && (
|
||||
<div className="progress w-50">
|
||||
|
|
|
@ -1,9 +1,26 @@
|
|||
import React from 'react';
|
||||
import { IconLock, IconKey } from '@tabler/icons-react';
|
||||
import { OtpForm } from '../../components/OtpForm';
|
||||
import { ChangePasswordForm } from '../../components/ChangePasswordForm';
|
||||
|
||||
export const SecurityContainer = () => {
|
||||
return (
|
||||
<div className="card-body">
|
||||
<div className="d-flex">
|
||||
<IconKey className="me-2" />
|
||||
<h2>Change password</h2>
|
||||
</div>
|
||||
<p className="text-muted">Changing your password will log you out of all devices.</p>
|
||||
<ChangePasswordForm />
|
||||
<div className="d-flex">
|
||||
<IconLock className="me-2" />
|
||||
<h2>Two-Factor Authentication</h2>
|
||||
</div>
|
||||
<p className="text-muted">
|
||||
Two-factor authentication (2FA) adds an additional layer of security to your account.
|
||||
<br />
|
||||
When enabled, you will be prompted to enter a code from your authenticator app when you log in.
|
||||
</p>
|
||||
<OtpForm />
|
||||
</div>
|
||||
);
|
||||
|
|
Loading…
Add table
Reference in a new issue