feat: implement change password

This commit is contained in:
Aaron William Po
2023-05-28 20:05:00 -04:00
parent d06415c924
commit f4e6a307f2
10 changed files with 265 additions and 2 deletions

View File

@@ -0,0 +1,77 @@
import { Switch } from '@headlessui/react';
import { FunctionComponent, useState } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { UpdatePasswordSchema } from '@/services/User/schema/CreateUserValidationSchemas';
import sendUpdatePasswordRequest from '@/requests/User/sendUpdatePasswordRequest';
import FormError from '../ui/forms/FormError';
import FormInfo from '../ui/forms/FormInfo';
import FormLabel from '../ui/forms/FormLabel';
import FormTextInput from '../ui/forms/FormTextInput';
const Security: FunctionComponent = () => {
const [editToggled, setEditToggled] = useState(false);
const { register, handleSubmit, formState, reset } = useForm<
z.infer<typeof UpdatePasswordSchema>
>({
resolver: zodResolver(UpdatePasswordSchema),
});
const onSubmit: SubmitHandler<z.infer<typeof UpdatePasswordSchema>> = async (data) => {
await sendUpdatePasswordRequest(data)
reset();
};
return (
<div className="mt-8 w-full space-y-4">
<div className="flex w-full items-center justify-between space-x-5">
<div className="">
<h1 className="text-lg font-bold">Change Your Password</h1>
<p>Update your password to maintain the safety of your account.</p>
</div>
<div>
<Switch
className="toggle"
id="edit-toggle"
checked={editToggled}
onClick={() => setEditToggled((val) => !val)}
/>
</div>
</div>
{editToggled && (
<form className="form-control" noValidate onSubmit={handleSubmit(onSubmit)}>
<FormInfo>
<FormLabel htmlFor="password">New Password</FormLabel>
<FormError>{formState.errors.password?.message}</FormError>
</FormInfo>
<FormTextInput
type="password"
disabled={!editToggled || formState.isSubmitting}
error={!!formState.errors.password}
id="password"
formValidationSchema={register('password')}
/>
<FormInfo>
<FormLabel htmlFor="password">Confirm Password</FormLabel>
<FormError>{formState.errors.confirmPassword?.message}</FormError>
</FormInfo>
<FormTextInput
type="password"
disabled={!editToggled || formState.isSubmitting}
error={!!formState.errors.confirmPassword}
id="password"
formValidationSchema={register('confirmPassword')}
/>
<button className="btn-primary btn mt-5" disabled={!editToggled} type="submit">
Update
</button>
</form>
)}
</div>
);
};
export default Security;

View File

@@ -6,6 +6,7 @@ import Head from 'next/head';
import AccountInfo from '@/components/Account/AccountInfo';
import { useContext } from 'react';
import UserContext from '@/contexts/UserContext';
import Security from '@/components/Account/Security';
const AccountPage: NextPage = () => {
const { user } = useContext(UserContext);
@@ -50,7 +51,10 @@ const AccountPage: NextPage = () => {
<Tab.Panel>
<AccountInfo />
</Tab.Panel>
<Tab.Panel>Content 3</Tab.Panel>
<Tab.Panel>
<Security />
</Tab.Panel>
<Tab.Panel>Your posts!</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>

View File

@@ -0,0 +1,60 @@
import { hashPassword } from '@/config/auth/passwordFns';
import { UserExtendedNextApiRequest } from '@/config/auth/types';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import DBClient from '@/prisma/DBClient';
import { UpdatePasswordSchema } from '@/services/User/schema/CreateUserValidationSchemas';
import GetUserSchema from '@/services/User/schema/GetUserSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
import { z } from 'zod';
interface UpdatePasswordRequest extends UserExtendedNextApiRequest {
body: z.infer<typeof UpdatePasswordSchema>;
}
const updatePassword = async (
req: UpdatePasswordRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const { password } = req.body;
const hash = await hashPassword(password);
const user = req.user!;
const updatedUser: z.infer<typeof GetUserSchema> = await DBClient.instance.user.update({
data: { hash },
where: { id: user.id },
select: {
id: true,
username: true,
createdAt: true,
updatedAt: true,
email: true,
firstName: true,
lastName: true,
dateOfBirth: true,
accountIsVerified: true,
},
});
res.json({
message: 'Updated user password.',
statusCode: 200,
success: true,
payload: updatedUser,
});
};
const router = createRouter<
UpdatePasswordRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.put(
validateRequest({ bodySchema: UpdatePasswordSchema }),
getCurrentUser,
updatePassword,
);
const handler = router.handler(NextConnectOptions);
export default handler;

View File

@@ -0,0 +1,33 @@
import { UpdatePasswordSchema } from '@/services/User/schema/CreateUserValidationSchemas';
import GetUserSchema from '@/services/User/schema/GetUserSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { z } from 'zod';
const sendUpdatePasswordRequest = async (data: z.infer<typeof UpdatePasswordSchema>) => {
const response = await fetch('/api/users/edit-password', {
body: JSON.stringify(data),
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
});
if (!response.ok) {
throw new Error(response.statusText);
}
const json = await response.json();
const parsed = APIResponseValidationSchema.safeParse(json);
if (!parsed.success) {
throw new Error('API response validation failed.');
}
const parsedPayload = GetUserSchema.safeParse(parsed.data.payload);
if (!parsedPayload.success) {
throw new Error('API payload validation failed.');
}
return parsedPayload.data;
};
export default sendUpdatePasswordRequest;

View File

@@ -31,6 +31,7 @@ const createNewUser = async ({
dateOfBirth: true,
createdAt: true,
accountIsVerified: true,
updatedAt: true,
},
});

View File

@@ -69,3 +69,11 @@ export const CreateUserValidationSchemaWithUsernameAndEmailCheck =
message: 'Passwords do not match.',
path: ['confirmPassword'],
});
export const UpdatePasswordSchema = BaseCreateUserSchema.pick({
password: true,
confirmPassword: true,
}).refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match.',
path: ['confirmPassword'],
});

View File

@@ -4,7 +4,7 @@ const GetUserSchema = z.object({
id: z.string().uuid(),
username: z.string(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().nullable(),
email: z.string().email(),
firstName: z.string(),
lastName: z.string(),