diff --git a/apps/web/src/app/(unauthenticated)/reset-password/[token]/page.tsx b/apps/web/src/app/(unauthenticated)/reset-password/[token]/page.tsx
new file mode 100644
index 000000000..15b7ebb17
--- /dev/null
+++ b/apps/web/src/app/(unauthenticated)/reset-password/[token]/page.tsx
@@ -0,0 +1,36 @@
+import Image from 'next/image';
+import Link from 'next/link';
+
+import backgroundPattern from '~/assets/background-pattern.png';
+import { ResetPasswordForm } from '~/components/forms/reset-password';
+
+export default function ResetPasswordPage({ params }: { params: { token: string } }) {
+ return (
+
+
+
+
+
+
+
+
Reset Password
+
+
Please choose your new password
+
+
+
+
+ Don't have an account?{' '}
+
+ Sign up
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/(unauthenticated)/reset-password/page.tsx b/apps/web/src/app/(unauthenticated)/reset-password/page.tsx
index d782e5039..49071b581 100644
--- a/apps/web/src/app/(unauthenticated)/reset-password/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/reset-password/page.tsx
@@ -1,36 +1,5 @@
-import Image from 'next/image';
-import Link from 'next/link';
+import React from 'react';
-import backgroundPattern from '~/assets/background-pattern.png';
-import { ResetPasswordForm } from '~/components/forms/reset-password';
-
-export default function ResetPasswordPage() {
- return (
-
-
-
-
-
-
-
-
Reset Password
-
-
Please choose your new password
-
-
-
-
- Don't have an account?{' '}
-
- Sign up
-
-
-
-
-
- );
+export default function ResetPassword() {
+ return
ResetPassword
;
}
diff --git a/apps/web/src/components/forms/reset-password.tsx b/apps/web/src/components/forms/reset-password.tsx
index c07b45d2b..f4e83d203 100644
--- a/apps/web/src/components/forms/reset-password.tsx
+++ b/apps/web/src/components/forms/reset-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 ZResetPasswordFormSchema = z
.object({
@@ -26,13 +28,17 @@ export type TResetPasswordFormSchema = z.infer;
export type ResetPasswordFormProps = {
className?: string;
+ token: string;
};
-export const ResetPasswordForm = ({ className }: ResetPasswordFormProps) => {
+export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps) => {
const router = useRouter();
+ const { toast } = useToast();
+
const {
register,
+ reset,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm({
@@ -43,8 +49,24 @@ export const ResetPasswordForm = ({ className }: ResetPasswordFormProps) => {
resolver: zodResolver(ZResetPasswordFormSchema),
});
- const onFormSubmit = ({ password, repeatedPassword }: TResetPasswordFormSchema) => {
- console.log(password, repeatedPassword);
+ const { mutateAsync: resetPassword } = trpc.profile.resetPassword.useMutation();
+
+ const onFormSubmit = async ({ password, repeatedPassword }: TResetPasswordFormSchema) => {
+ // TODO: Handle error with try/catch
+ console.log(password, repeatedPassword, token);
+
+ await resetPassword({
+ password,
+ token,
+ });
+
+ reset();
+
+ toast({
+ title: 'Password updated',
+ description: 'Your password has been updated successfully.',
+ duration: 5000,
+ });
router.push('/signin');
};
diff --git a/packages/lib/server-only/auth/send-forgot-password.ts b/packages/lib/server-only/auth/send-forgot-password.ts
index 88de43270..e0f74d4ff 100644
--- a/packages/lib/server-only/auth/send-forgot-password.ts
+++ b/packages/lib/server-only/auth/send-forgot-password.ts
@@ -30,7 +30,7 @@ export const sendForgotPassword = async ({ userId }: SendForgotPasswordOptions)
const token = user.PasswordResetToken[0].token;
const assetBaseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
- const resetPasswordLink = `${process.env.NEXT_PUBLIC_SITE_URL}/reset/${token}`;
+ const resetPasswordLink = `${process.env.NEXT_PUBLIC_SITE_URL}/reset-password/${token}`;
const template = createElement(ForgotPasswordTemplate, {
assetBaseUrl,
diff --git a/packages/lib/server-only/user/reset-password.ts b/packages/lib/server-only/user/reset-password.ts
new file mode 100644
index 000000000..7ce1bcb20
--- /dev/null
+++ b/packages/lib/server-only/user/reset-password.ts
@@ -0,0 +1,66 @@
+import { compare, hash } from 'bcrypt';
+
+import { prisma } from '@documenso/prisma';
+
+import { SALT_ROUNDS } from '../../constants/auth';
+
+export type ResetPasswordOptions = {
+ token: string;
+ password: string;
+};
+
+export const resetPassword = async ({ token, password }: ResetPasswordOptions) => {
+ if (!token) {
+ throw new Error('Invalid Token');
+ }
+
+ const foundToken = await prisma.passwordResetToken.findFirstOrThrow({
+ where: {
+ token,
+ },
+ include: {
+ User: true,
+ },
+ });
+
+ if (!foundToken) {
+ throw new Error('Invalid Token');
+ }
+
+ const now = new Date();
+
+ if (now > foundToken.expiry) {
+ throw new Error('Token has expired');
+ }
+
+ const isSamePassword = await compare(password, foundToken.User.password!);
+
+ if (isSamePassword) {
+ throw new Error('Your new password cannot be the same as your old password.');
+ }
+
+ const hashedPassword = await hash(password, SALT_ROUNDS);
+
+ const transactions = await prisma.$transaction([
+ prisma.user.update({
+ where: {
+ id: foundToken.userId,
+ },
+ data: {
+ password: hashedPassword,
+ },
+ }),
+ prisma.passwordResetToken.deleteMany({
+ where: {
+ userId: foundToken.userId,
+ },
+ }),
+ ]);
+
+ if (!transactions) {
+ throw new Error('Unable to update password');
+ }
+
+ // await sendResetPasswordSuccessMail(foundToken.User);
+ return transactions;
+};
diff --git a/packages/trpc/server/profile-router/router.ts b/packages/trpc/server/profile-router/router.ts
index d5f60658f..de61f64f4 100644
--- a/packages/trpc/server/profile-router/router.ts
+++ b/packages/trpc/server/profile-router/router.ts
@@ -1,12 +1,14 @@
import { TRPCError } from '@trpc/server';
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
+import { resetPassword } from '@documenso/lib/server-only/user/reset-password';
import { updatePassword } from '@documenso/lib/server-only/user/update-password';
import { updateProfile } from '@documenso/lib/server-only/user/update-profile';
import { authenticatedProcedure, procedure, router } from '../trpc';
import {
ZForgotPasswordFormSchema,
+ ZResetPasswordFormSchema,
ZUpdatePasswordMutationSchema,
ZUpdateProfileMutationSchema,
} from './schema';
@@ -69,6 +71,7 @@ export const profileRouter = router({
} catch (err) {
console.error(err);
+ // TODO: Handle this error better
throw new TRPCError({
code: 'BAD_REQUEST',
message:
@@ -76,4 +79,27 @@ export const profileRouter = router({
});
}
}),
+
+ resetPassword: procedure.input(ZResetPasswordFormSchema).mutation(async ({ input }) => {
+ try {
+ const { password, token } = input;
+
+ return await resetPassword({
+ token,
+ password,
+ });
+ } catch (err) {
+ let message =
+ 'We were unable to update your profile. Please review the information you provided and try again.';
+
+ if (err instanceof Error) {
+ message = err.message;
+ }
+
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message,
+ });
+ }
+ }),
});
diff --git a/packages/trpc/server/profile-router/schema.ts b/packages/trpc/server/profile-router/schema.ts
index 65a296bce..641227684 100644
--- a/packages/trpc/server/profile-router/schema.ts
+++ b/packages/trpc/server/profile-router/schema.ts
@@ -13,6 +13,12 @@ export const ZForgotPasswordFormSchema = z.object({
email: z.string().email().min(1),
});
+export const ZResetPasswordFormSchema = z.object({
+ password: z.string().min(6),
+ token: z.string().min(1),
+});
+
export type TUpdateProfileMutationSchema = z.infer;
export type TUpdatePasswordMutationSchema = z.infer;
export type TForgotPasswordFormSchema = z.infer;
+export type TResetPasswordFormSchema = z.infer;