diff --git a/apps/web/src/app/(auth)/reset-password/page.tsx b/apps/web/src/app/(auth)/reset-password/page.tsx new file mode 100644 index 0000000..3d96d7a --- /dev/null +++ b/apps/web/src/app/(auth)/reset-password/page.tsx @@ -0,0 +1,181 @@ +'use client'; + +import { Button } from '@tpmjs/ui/Button/Button'; +import { Input } from '@tpmjs/ui/Input/Input'; +import { Label } from '@tpmjs/ui/Label/Label'; +import Link from 'next/link'; +import { useSearchParams } from 'next/navigation'; +import { Suspense, useState } from 'react'; + +function ResetPasswordForm() { + const searchParams = useSearchParams(); + const token = searchParams.get('token'); + + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(false); + const [loading, setLoading] = useState(false); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(''); + + if (password !== confirmPassword) { + setError('Passwords do not match'); + return; + } + + if (password.length < 8) { + setError('Password must be at least 8 characters'); + return; + } + + if (!token) { + setError('Invalid or missing reset token'); + return; + } + + setLoading(true); + + try { + const response = await fetch('/api/auth/reset-password', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + token, + newPassword: password, + }), + }); + + if (!response.ok) { + const data = await response.json(); + setError(data.message || 'Failed to reset password'); + setLoading(false); + return; + } + + setSuccess(true); + setLoading(false); + } catch (err) { + console.error('Reset password exception:', err); + setError('An unexpected error occurred'); + setLoading(false); + } + } + + if (!token) { + return ( +
+
+

Invalid Link

+

+ This password reset link is invalid or has expired. +

+
+ +
+ Please request a new password reset link. +
+ +

+ + Request new reset link + +

+
+ ); + } + + if (success) { + return ( +
+
+

Password Reset

+

+ Your password has been successfully reset. +

+
+ +
+ You can now sign in with your new password. +
+ + + + +
+ ); + } + + return ( +
+
+

Set New Password

+

Enter your new password below

+
+ +
+ {error && ( +
+ {error} +
+ )} + +
+ + setPassword(e.target.value)} + required + minLength={8} + placeholder="At least 8 characters" + /> +
+ +
+ + setConfirmPassword(e.target.value)} + required + placeholder="Confirm your password" + /> +
+ + +
+ +

+ Remember your password?{' '} + + Sign in + +

+
+ ); +} + +export default function ResetPasswordPage() { + return ( + +
+

Loading...

+
+ + } + > + +
+ ); +} diff --git a/apps/web/src/lib/auth.ts b/apps/web/src/lib/auth.ts index 6ce2f64..dafa2a4 100644 --- a/apps/web/src/lib/auth.ts +++ b/apps/web/src/lib/auth.ts @@ -1,7 +1,7 @@ import { prisma } from '@tpmjs/db'; import { betterAuth } from 'better-auth'; import { prismaAdapter } from 'better-auth/adapters/prisma'; -import { sendVerificationEmail } from './email'; +import { sendResetPasswordEmail, sendVerificationEmail } from './email'; // Determine base URL for auth - MUST match the domain users are browsing on // VERCEL_URL is the deployment URL (e.g., tpmjs-xxx.vercel.app), not the custom domain @@ -26,6 +26,9 @@ export const auth = betterAuth({ emailAndPassword: { enabled: true, requireEmailVerification: true, + sendResetPassword: async ({ user, url }) => { + await sendResetPasswordEmail(user.email, url); + }, }, emailVerification: { sendVerificationEmail: async ({ user, url }) => { diff --git a/apps/web/src/lib/email.ts b/apps/web/src/lib/email.ts index 215a29f..61573a9 100644 --- a/apps/web/src/lib/email.ts +++ b/apps/web/src/lib/email.ts @@ -49,3 +49,37 @@ export async function sendVerificationEmail(to: string, verificationUrl: string) throw new Error('Failed to send verification email'); } } + +export async function sendResetPasswordEmail(to: string, resetUrl: string) { + const { error } = await getResend().emails.send({ + from: 'TPMJS ', + to, + subject: 'Reset your password - TPMJS', + html: ` + + + + + + + +

Reset your password

+

+ Click the button below to reset your password. If you didn't request this, you can safely ignore this email. +

+ + Reset Password + +

+ This link will expire in 1 hour for security reasons. +

+ + + `, + }); + + if (error) { + console.error('Failed to send reset password email:', error); + throw new Error('Failed to send reset password email'); + } +}