From 0440110da98d10d5b9be5ee186f1145ad03a5a87 Mon Sep 17 00:00:00 2001 From: keshav-005 Date: Wed, 22 Apr 2026 23:07:48 +0530 Subject: [PATCH] Address password reset review feedback --- app/(auth)/forgot-password/page.tsx | 22 ++-- .../reset-password/ResetPasswordForm.tsx | 117 ++++++++++++++++++ app/(auth)/reset-password/page.tsx | 107 +--------------- lib/actions/auth.actions.ts | 13 +- lib/nodemailer/reset-password.ts | 16 ++- 5 files changed, 161 insertions(+), 114 deletions(-) create mode 100644 app/(auth)/reset-password/ResetPasswordForm.tsx diff --git a/app/(auth)/forgot-password/page.tsx b/app/(auth)/forgot-password/page.tsx index b3af01f..1c0f747 100644 --- a/app/(auth)/forgot-password/page.tsx +++ b/app/(auth)/forgot-password/page.tsx @@ -26,16 +26,22 @@ const ForgotPasswordPage = () => { }); const onSubmit = async (data: ForgotPasswordFormData) => { - const result = await requestPasswordResetEmail(data); + try { + const result = await requestPasswordResetEmail(data); - if (result.success) { - toast.success('If an account exists for that email, a reset link has been sent.'); - return; + if (result.success) { + toast.success('If an account exists for that email, a reset link has been sent.'); + return; + } + + toast.error('Password reset unavailable', { + description: result.error ?? 'Unable to start password reset.', + }); + } catch (error) { + toast.error('Password reset unavailable', { + description: error instanceof Error ? error.message : 'Unable to start password reset.', + }); } - - toast.error('Password reset unavailable', { - description: result.error ?? 'Unable to start password reset.', - }); }; return ( diff --git a/app/(auth)/reset-password/ResetPasswordForm.tsx b/app/(auth)/reset-password/ResetPasswordForm.tsx new file mode 100644 index 0000000..8cb0979 --- /dev/null +++ b/app/(auth)/reset-password/ResetPasswordForm.tsx @@ -0,0 +1,117 @@ +'use client'; + +import React, { useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { toast } from 'sonner'; + +import FooterLink from '@/components/forms/FooterLink'; +import InputField from '@/components/forms/InputField'; +import OpenDevSocietyBranding from '@/components/OpenDevSocietyBranding'; +import { Button } from '@/components/ui/button'; +import { resetPasswordWithToken } from '@/lib/actions/auth.actions'; + +type ResetPasswordFormData = { + newPassword: string; + confirmPassword: string; +}; + +const ResetPasswordForm = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + const token = searchParams.get('token') ?? ''; + const error = searchParams.get('error'); + + const { + register, + watch, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ + defaultValues: { + newPassword: '', + confirmPassword: '', + }, + mode: 'onBlur', + }); + + const newPassword = watch('newPassword'); + + useEffect(() => { + if (error === 'INVALID_TOKEN') { + toast.error('Reset link is invalid or expired.'); + } + }, [error]); + + const onSubmit = async (data: ResetPasswordFormData) => { + if (!token) { + toast.error('Reset link is invalid or expired.'); + return; + } + + try { + const result = await resetPasswordWithToken({ + token, + newPassword: data.newPassword, + }); + + if (result.success) { + toast.success('Password updated. You can sign in now.'); + router.push('/sign-in'); + return; + } + + toast.error('Password reset failed', { + description: result.error ?? 'Unable to reset your password.', + }); + } catch (error) { + toast.error('Password reset failed', { + description: error instanceof Error ? error.message : 'Unable to reset your password.', + }); + } + }; + + return ( + <> +

Choose a new password

+

+ Enter a new password for your account. +

+ +
+ + + + value === newPassword || 'Passwords do not match', + }} + /> + + + + + + + + ); +}; + +export default ResetPasswordForm; diff --git a/app/(auth)/reset-password/page.tsx b/app/(auth)/reset-password/page.tsx index 4a95672..5951267 100644 --- a/app/(auth)/reset-password/page.tsx +++ b/app/(auth)/reset-password/page.tsx @@ -1,109 +1,12 @@ -'use client'; +import { Suspense } from 'react'; -import React, { useEffect } from 'react'; -import { useForm } from 'react-hook-form'; -import { useRouter, useSearchParams } from 'next/navigation'; -import { toast } from 'sonner'; -import { Button } from '@/components/ui/button'; -import InputField from '@/components/forms/InputField'; -import FooterLink from '@/components/forms/FooterLink'; -import OpenDevSocietyBranding from '@/components/OpenDevSocietyBranding'; -import { resetPasswordWithToken } from '@/lib/actions/auth.actions'; - -type ResetPasswordFormData = { - newPassword: string; - confirmPassword: string; -}; +import ResetPasswordForm from './ResetPasswordForm'; const ResetPasswordPage = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - const token = searchParams.get('token') ?? ''; - const error = searchParams.get('error'); - - const { - register, - watch, - handleSubmit, - formState: { errors, isSubmitting }, - } = useForm({ - defaultValues: { - newPassword: '', - confirmPassword: '', - }, - mode: 'onBlur', - }); - - const newPassword = watch('newPassword'); - - useEffect(() => { - if (error === 'INVALID_TOKEN') { - toast.error('Reset link is invalid or expired.'); - } - }, [error]); - - const onSubmit = async (data: ResetPasswordFormData) => { - if (!token) { - toast.error('Reset link is invalid or expired.'); - return; - } - - const result = await resetPasswordWithToken({ - token, - newPassword: data.newPassword, - }); - - if (result.success) { - toast.success('Password updated. You can sign in now.'); - router.push('/sign-in'); - return; - } - - toast.error('Password reset failed', { - description: result.error ?? 'Unable to reset your password.', - }); - }; - return ( - <> -

Choose a new password

-

- Enter a new password for your account. -

- -
- - - - value === newPassword || 'Passwords do not match', - }} - /> - - - - - - - + Loading reset form...}> + + ); }; diff --git a/lib/actions/auth.actions.ts b/lib/actions/auth.actions.ts index 4750a1b..220c831 100644 --- a/lib/actions/auth.actions.ts +++ b/lib/actions/auth.actions.ts @@ -64,7 +64,18 @@ export const requestPasswordResetEmail = async ({ email }: { email: string }) => } try { - const baseUrl = process.env.BETTER_AUTH_URL ?? 'http://localhost:3000'; + const configuredBaseUrl = process.env.BETTER_AUTH_URL; + const baseUrl = configuredBaseUrl || ( + process.env.NODE_ENV !== 'production' ? 'http://localhost:3000' : null + ); + + if (!baseUrl) { + return { + success: false, + error: 'BETTER_AUTH_URL must be configured before password reset emails can be sent.', + } + } + await auth.api.requestPasswordReset({ body: { email, diff --git a/lib/nodemailer/reset-password.ts b/lib/nodemailer/reset-password.ts index 5255e22..309cdb1 100644 --- a/lib/nodemailer/reset-password.ts +++ b/lib/nodemailer/reset-password.ts @@ -1,5 +1,13 @@ import { transporter } from "@/lib/nodemailer"; +const escapeHtml = (value: string) => + value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + export const sendPasswordResetEmail = async ( { email, name, resetUrl }: { email: string; name?: string | null; resetUrl: string } ) => { @@ -9,16 +17,18 @@ export const sendPasswordResetEmail = async ( } const firstName = name?.trim().split(' ')[0] || 'there'; + const escapedFirstName = escapeHtml(firstName); + const escapedResetUrl = escapeHtml(encodeURI(resetUrl)); const html = `