From c68b8dbfd1aea4e89a89f67a8988b870ecf8c525 Mon Sep 17 00:00:00 2001 From: PunGrumpy <108584943+PunGrumpy@users.noreply.github.com> Date: Wed, 9 Apr 2025 02:09:52 +0700 Subject: [PATCH 1/5] feat(email): implement email functionality with password reset and verification templates - Added email package with templates for password reset and email verification. - Integrated email sending functionality in the auth module for password reset and verification emails. - Updated environment configuration to include email-related keys. - Created new Forgot Password and Reset Password pages in the web app with corresponding forms and validation. - Enhanced existing signup and login forms to include password confirmation validation. --- apps/web/.env.example | 2 + .../forgot-password/forgot-password.tsx | 102 +++++ .../forgot-password/page.tsx | 9 + .../forgot-password/schema.ts | 5 + apps/web/app/(unauthenticated)/layout.tsx | 4 +- .../login/email/login-email.tsx | 120 +++--- .../web/app/(unauthenticated)/login/login.tsx | 2 +- .../(unauthenticated)/reset-password/page.tsx | 9 + .../reset-password/reset-password.tsx | 136 ++++++ .../reset-password/schema.ts | 14 + .../app/(unauthenticated)/signup/schema.ts | 20 +- .../app/(unauthenticated)/signup/signup.tsx | 18 +- apps/web/lib/env.ts | 3 +- bun.lock | 391 ++++++++++++++++++ packages/auth/client.ts | 9 +- packages/auth/keys.ts | 3 +- packages/auth/server.ts | 45 +- packages/email/index.ts | 4 + packages/email/keys.ts | 14 + packages/email/package.json | 31 ++ packages/email/templates/contact.tsx | 56 +++ packages/email/templates/password-reset.tsx | 102 +++++ packages/email/templates/verification.tsx | 98 +++++ packages/email/tsconfig.json | 8 + packages/ui/components/password-input.tsx | 4 + 25 files changed, 1142 insertions(+), 67 deletions(-) create mode 100644 apps/web/app/(unauthenticated)/forgot-password/forgot-password.tsx create mode 100644 apps/web/app/(unauthenticated)/forgot-password/page.tsx create mode 100644 apps/web/app/(unauthenticated)/forgot-password/schema.ts create mode 100644 apps/web/app/(unauthenticated)/reset-password/page.tsx create mode 100644 apps/web/app/(unauthenticated)/reset-password/reset-password.tsx create mode 100644 apps/web/app/(unauthenticated)/reset-password/schema.ts create mode 100644 packages/email/index.ts create mode 100644 packages/email/keys.ts create mode 100644 packages/email/package.json create mode 100644 packages/email/templates/contact.tsx create mode 100644 packages/email/templates/password-reset.tsx create mode 100644 packages/email/templates/verification.tsx create mode 100644 packages/email/tsconfig.json diff --git a/apps/web/.env.example b/apps/web/.env.example index a57d297..397dd57 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -3,6 +3,8 @@ BETTER_AUTH_SECRET="" DATABASE_URL="" GOOGLE_CLIENT_ID="" GOOGLE_CLIENT_SECRET="" +RESEND_FROM="" +RESEND_TOKEN="" BETTERSTACK_API_KEY="" BETTERSTACK_URL="" diff --git a/apps/web/app/(unauthenticated)/forgot-password/forgot-password.tsx b/apps/web/app/(unauthenticated)/forgot-password/forgot-password.tsx new file mode 100644 index 0000000..64b4301 --- /dev/null +++ b/apps/web/app/(unauthenticated)/forgot-password/forgot-password.tsx @@ -0,0 +1,102 @@ +'use client' + +import { zodResolver } from '@hookform/resolvers/zod' +import { forgetPassword } from '@repo/auth/client' +import { Alert } from '@repo/ui/components/ui/alert' +import { Button } from '@repo/ui/components/ui/button' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@repo/ui/components/ui/form' +import { Input } from '@repo/ui/components/ui/input' +import { AlertCircle, PartyPopper } from 'lucide-react' +import { useState } from 'react' +import { useForm } from 'react-hook-form' +import type { z } from 'zod' +import { formSchema } from './schema' + +export const ForgotPassword = () => { + const [status, setStatus] = useState(false) + const [generalError, setGeneralError] = useState(null) + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: '' + } + }) + + const onSubmit = async ({ email }: z.infer) => { + try { + const { data, error } = await forgetPassword({ + email, + redirectTo: '/reset-password' + }) + + if (error) { + setGeneralError(error.message || 'Failed to send reset password email.') + } + + if (data && !error) { + setStatus(true) + } + } catch { + setGeneralError( + 'Could not connect to the authentication service. Please try again later or contact support if the issue persists.' + ) + } + } + + return ( +
+
+

Forgot Password?

+
+
+ {generalError && ( + + + {generalError} + + )} + {status && ( + + + Check your email for a link to reset your password. + + )} +
+ + ( + + Email + + + + + + )} + /> + + + +
+
+ ) +} diff --git a/apps/web/app/(unauthenticated)/forgot-password/page.tsx b/apps/web/app/(unauthenticated)/forgot-password/page.tsx new file mode 100644 index 0000000..313b306 --- /dev/null +++ b/apps/web/app/(unauthenticated)/forgot-password/page.tsx @@ -0,0 +1,9 @@ +import dynamic from 'next/dynamic' + +const ForgotPassword = dynamic(() => + import('./forgot-password').then(mod => mod.ForgotPassword) +) + +export default function ForgotPasswordPage() { + return +} diff --git a/apps/web/app/(unauthenticated)/forgot-password/schema.ts b/apps/web/app/(unauthenticated)/forgot-password/schema.ts new file mode 100644 index 0000000..1c3784f --- /dev/null +++ b/apps/web/app/(unauthenticated)/forgot-password/schema.ts @@ -0,0 +1,5 @@ +import { z } from 'zod' + +export const formSchema = z.object({ + email: z.string().email() +}) diff --git a/apps/web/app/(unauthenticated)/layout.tsx b/apps/web/app/(unauthenticated)/layout.tsx index 02cae1b..4f95e14 100644 --- a/apps/web/app/(unauthenticated)/layout.tsx +++ b/apps/web/app/(unauthenticated)/layout.tsx @@ -22,7 +22,9 @@ export default async function AuthLayout({ children }: AuthLayoutProps) { return ( <>
- {children} +
+ {children} +