diff --git a/a.md b/a.md new file mode 100644 index 000000000..71580a3ef --- /dev/null +++ b/a.md @@ -0,0 +1,65 @@ +// import { NextApiRequest, NextApiResponse } from 'next'; + +// import crypto from 'crypto'; + +// import { sendResetPassword } from '@documenso/lib/mail'; +// import { defaultHandler, defaultResponder } from '@documenso/lib/server'; +// import prisma from '@documenso/prisma'; + +// async function postHandler(req: NextApiRequest, res: NextApiResponse) { +// const { email } = req.body; +// const cleanEmail = email.toLowerCase(); + +// if (!cleanEmail || !/.+@.+/.test(cleanEmail)) { +// res.status(400).json({ message: 'Invalid email' }); +// return; +// } + +// const user = await prisma.user.findFirst({ +// where: { +// email: cleanEmail, +// }, +// }); + +// if (!user) { +// return res.status(200).json({ message: 'A password reset email has been sent.' }); +// } + +// const existingToken = await prisma.passwordResetToken.findFirst({ +// where: { +// userId: user.id, +// createdAt: { +// gte: new Date(Date.now() - 1000 _ 60 _ 60), +// }, +// }, +// }); + +// if (existingToken) { +// return res.status(200).json({ message: 'A password reset email has been sent.' }); +// } + +// const token = crypto.randomBytes(64).toString('hex'); +// const expiry = new Date(); +// expiry.setHours(expiry.getHours() + 24); // Set expiry to one hour from now + +// let passwordResetToken; +// try { +// passwordResetToken = await prisma.passwordResetToken.create({ +// data: { +// token, +// expiry, +// userId: user.id, +// }, +// }); +// } catch (error) { +// return res.status(500).json({ message: 'Something went wrong' }); +// } + +// await sendResetPassword(user, passwordResetToken.token); + +// return res.status(200).json({ message: 'A password reset email has been sent.' }); +// } + +// export default defaultHandler({ +// POST: Promise.resolve({ default: defaultResponder(postHandler) }), +// }); diff --git a/apps/web/src/components/forms/forgot-password.tsx b/apps/web/src/components/forms/forgot-password.tsx index b97d150d0..852ff0301 100644 --- a/apps/web/src/components/forms/forgot-password.tsx +++ b/apps/web/src/components/forms/forgot-password.tsx @@ -7,10 +7,12 @@ import { Loader } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; +import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; import { Input } from '@documenso/ui/primitives/input'; import { Label } from '@documenso/ui/primitives/label'; +import { useToast } from '@documenso/ui/primitives/use-toast'; export const ZForgotPasswordFormSchema = z.object({ email: z.string().email().min(1), @@ -24,10 +26,12 @@ export type ForgotPasswordFormProps = { export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => { const router = useRouter(); + const { toast } = useToast(); const { register, handleSubmit, + reset, formState: { errors, isSubmitting }, } = useForm({ values: { @@ -36,12 +40,29 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => { resolver: zodResolver(ZForgotPasswordFormSchema), }); - const onFormSubmit = ({ email }: TForgotPasswordFormSchema) => { + const { mutateAsync: forgotPassword } = trpc.profile.forgotPassword.useMutation(); + + const onFormSubmit = async ({ email }: TForgotPasswordFormSchema) => { // check if the email is available // if not, throw an error // if the email is available, create a password reset token and send an email - console.log(email); + await forgotPassword({ + email, + }); + + reset(); + + toast({ + title: 'Password updated', + description: 'Your password has been updated successfully.', + duration: 5000, + }); + + await new Promise((resolve) => { + setTimeout(resolve, 2000); + }); + router.push('/check-email'); }; diff --git a/packages/lib/server-only/user/forgot-password.ts b/packages/lib/server-only/user/forgot-password.ts new file mode 100644 index 000000000..8cbb13b82 --- /dev/null +++ b/packages/lib/server-only/user/forgot-password.ts @@ -0,0 +1,52 @@ +import crypto from 'crypto'; + +import { prisma } from '@documenso/prisma'; +import { TForgotPasswordFormSchema } from '@documenso/trpc/server/profile-router/schema'; + +export const forgotPassword = async ({ email }: TForgotPasswordFormSchema) => { + const user = await prisma.user.findFirstOrThrow({ + where: { + email: email.toLowerCase(), + }, + }); + + if (!user) { + throw new Error('A password reset email has been sent.'); + } + + const existingToken = await prisma.passwordResetToken.findFirst({ + where: { + userId: user.id, + createdAt: { + gte: new Date(Date.now() - 1000 * 60 * 60), + }, + }, + }); + + if (existingToken) { + throw new Error('A password reset email has been sent.'); + } + + const token = crypto.randomBytes(64).toString('hex'); + const expiry = new Date(); + expiry.setHours(expiry.getHours() + 24); // Set expiry to one hour from now + + let passwordResetToken; + + try { + passwordResetToken = await prisma.passwordResetToken.create({ + data: { + token, + expiry, + userId: user.id, + }, + }); + } catch (error) { + throw new Error('Something went wrong'); + } + + console.log('Password reset token: ', passwordResetToken); + // send an email to user with password token + + return passwordResetToken; +}; diff --git a/packages/trpc/server/profile-router/router.ts b/packages/trpc/server/profile-router/router.ts index 1ca8a0cf2..d5f60658f 100644 --- a/packages/trpc/server/profile-router/router.ts +++ b/packages/trpc/server/profile-router/router.ts @@ -1,10 +1,15 @@ import { TRPCError } from '@trpc/server'; +import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password'; import { updatePassword } from '@documenso/lib/server-only/user/update-password'; import { updateProfile } from '@documenso/lib/server-only/user/update-profile'; -import { authenticatedProcedure, router } from '../trpc'; -import { ZUpdatePasswordMutationSchema, ZUpdateProfileMutationSchema } from './schema'; +import { authenticatedProcedure, procedure, router } from '../trpc'; +import { + ZForgotPasswordFormSchema, + ZUpdatePasswordMutationSchema, + ZUpdateProfileMutationSchema, +} from './schema'; export const profileRouter = router({ updateProfile: authenticatedProcedure @@ -53,4 +58,22 @@ export const profileRouter = router({ }); } }), + + forgotPassword: procedure.input(ZForgotPasswordFormSchema).mutation(async ({ input }) => { + try { + const { email } = input; + + return await forgotPassword({ + email, + }); + } catch (err) { + console.error(err); + + throw new TRPCError({ + code: 'BAD_REQUEST', + message: + 'We were unable to update your profile. Please review the information you provided and try again.', + }); + } + }), }); diff --git a/packages/trpc/server/profile-router/schema.ts b/packages/trpc/server/profile-router/schema.ts index 0533d40e5..65a296bce 100644 --- a/packages/trpc/server/profile-router/schema.ts +++ b/packages/trpc/server/profile-router/schema.ts @@ -5,10 +5,14 @@ export const ZUpdateProfileMutationSchema = z.object({ signature: z.string(), }); -export type TUpdateProfileMutationSchema = z.infer; - export const ZUpdatePasswordMutationSchema = z.object({ password: z.string().min(6), }); +export const ZForgotPasswordFormSchema = z.object({ + email: z.string().email().min(1), +}); + +export type TUpdateProfileMutationSchema = z.infer; export type TUpdatePasswordMutationSchema = z.infer; +export type TForgotPasswordFormSchema = z.infer;