From 47d55a5eaba6675eb1472bf6950721a028150bc9 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Mon, 18 Sep 2023 06:47:03 +0000 Subject: [PATCH 01/19] feat: add password reset token to schema --- .../migration.sql | 16 +++++++++ packages/prisma/schema.prisma | 36 ++++++++++++------- 2 files changed, 39 insertions(+), 13 deletions(-) create mode 100644 packages/prisma/migrations/20230917190854_password_reset_token/migration.sql diff --git a/packages/prisma/migrations/20230917190854_password_reset_token/migration.sql b/packages/prisma/migrations/20230917190854_password_reset_token/migration.sql new file mode 100644 index 000000000..d22107691 --- /dev/null +++ b/packages/prisma/migrations/20230917190854_password_reset_token/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "PasswordResetToken" ( + "id" SERIAL NOT NULL, + "token" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expiry" TIMESTAMP(3) NOT NULL, + "userId" INTEGER NOT NULL, + + CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "PasswordResetToken_token_key" ON "PasswordResetToken"("token"); + +-- AddForeignKey +ALTER TABLE "PasswordResetToken" ADD CONSTRAINT "PasswordResetToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 1ff3d7a75..96b7db0a3 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -19,19 +19,29 @@ enum Role { } model User { - id Int @id @default(autoincrement()) - name String? - email String @unique - emailVerified DateTime? - password String? - source String? - signature String? - roles Role[] @default([USER]) - identityProvider IdentityProvider @default(DOCUMENSO) - accounts Account[] - sessions Session[] - Document Document[] - Subscription Subscription[] + id Int @id @default(autoincrement()) + name String? + email String @unique + emailVerified DateTime? + password String? + source String? + signature String? + roles Role[] @default([USER]) + identityProvider IdentityProvider @default(DOCUMENSO) + accounts Account[] + sessions Session[] + Document Document[] + Subscription Subscription[] + PasswordResetToken PasswordResetToken[] +} + +model PasswordResetToken { + id Int @id @default(autoincrement()) + token String @unique + createdAt DateTime @default(now()) + expiry DateTime + userId Int + User User @relation(fields: [userId], references: [id]) } enum SubscriptionStatus { From f88e529111716be88bdce7d1211dc94bf58266ef Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Mon, 18 Sep 2023 10:18:33 +0000 Subject: [PATCH 02/19] feat: add forgot passoword page --- .../(unauthenticated)/check-email/page.tsx | 34 ++++++ .../forgot-password/page.tsx | 38 +++++++ .../src/components/forms/forgot-password.tsx | 104 ++++++++++++++++++ apps/web/src/components/forms/signin.tsx | 8 +- 4 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/app/(unauthenticated)/check-email/page.tsx create mode 100644 apps/web/src/app/(unauthenticated)/forgot-password/page.tsx create mode 100644 apps/web/src/components/forms/forgot-password.tsx diff --git a/apps/web/src/app/(unauthenticated)/check-email/page.tsx b/apps/web/src/app/(unauthenticated)/check-email/page.tsx new file mode 100644 index 000000000..26039f4ae --- /dev/null +++ b/apps/web/src/app/(unauthenticated)/check-email/page.tsx @@ -0,0 +1,34 @@ +import Image from 'next/image'; +import Link from 'next/link'; + +import backgroundPattern from '~/assets/background-pattern.png'; + +export default function ForgotPasswordPage() { + return ( +
+
+
+ background pattern +
+ +
+

Reset Pasword

+ +

+ Please check your email for reset instructions +

+ +

+ + Sign in + +

+
+
+
+ ); +} diff --git a/apps/web/src/app/(unauthenticated)/forgot-password/page.tsx b/apps/web/src/app/(unauthenticated)/forgot-password/page.tsx new file mode 100644 index 000000000..73cfd01ca --- /dev/null +++ b/apps/web/src/app/(unauthenticated)/forgot-password/page.tsx @@ -0,0 +1,38 @@ +import Image from 'next/image'; +import Link from 'next/link'; + +import backgroundPattern from '~/assets/background-pattern.png'; +import { ForgotPasswordForm } from '~/components/forms/forgot-password'; + +export default function ForgotPasswordPage() { + return ( +
+
+
+ background pattern +
+ +
+

Forgot Password?

+ +

+ No worries, we'll send you reset instructions. +

+ + + +

+ Don't have an account?{' '} + + Sign up + +

+
+
+
+ ); +} diff --git a/apps/web/src/components/forms/forgot-password.tsx b/apps/web/src/components/forms/forgot-password.tsx new file mode 100644 index 000000000..94b5ea0cd --- /dev/null +++ b/apps/web/src/components/forms/forgot-password.tsx @@ -0,0 +1,104 @@ +'use client'; + +import { useEffect } from 'react'; + +import { useRouter, useSearchParams } from 'next/navigation'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { Loader } from 'lucide-react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { ErrorCode, isErrorCode } from '@documenso/lib/next-auth/error-codes'; +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'; + +const ERROR_MESSAGES = { + [ErrorCode.CREDENTIALS_NOT_FOUND]: 'No account found with that email address.', + [ErrorCode.INCORRECT_EMAIL_PASSWORD]: 'No account found with that email address.', + [ErrorCode.USER_MISSING_PASSWORD]: + 'This account appears to be using a social login method, please sign in using that method', +}; + +export const ZForgotPasswordFormSchema = z.object({ + email: z.string().email().min(1), +}); + +export type TForgotPasswordFormSchema = z.infer; + +export type ForgotPasswordFormProps = { + className?: string; +}; + +export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => { + const searchParams = useSearchParams(); + const router = useRouter(); + + const { toast } = useToast(); + + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ + values: { + email: '', + }, + resolver: zodResolver(ZForgotPasswordFormSchema), + }); + + const errorCode = searchParams?.get('error'); + + useEffect(() => { + let timeout: NodeJS.Timeout | null = null; + + if (isErrorCode(errorCode)) { + timeout = setTimeout(() => { + toast({ + variant: 'destructive', + description: ERROR_MESSAGES[errorCode] ?? 'An unknown error occurred', + }); + }, 0); + } + + return () => { + if (timeout) { + clearTimeout(timeout); + } + }; + }, [errorCode, toast]); + + const onFormSubmit = ({ 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); + router.push('/check-email'); + }; + + return ( +
+
+ + + + + {errors.email && {errors.email.message}} +
+ + +
+ ); +}; diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx index d9d727afc..a06c04c72 100644 --- a/apps/web/src/components/forms/signin.tsx +++ b/apps/web/src/components/forms/signin.tsx @@ -2,6 +2,7 @@ import { useEffect } from 'react'; +import Link from 'next/link'; import { useSearchParams } from 'next/navigation'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -123,8 +124,11 @@ export const SignInForm = ({ className }: SignInFormProps) => {
-