/*********************************************************************************************************************************************************************** * File Name: ForgotPasswordForm.jsx * Type of Program: Frontend Component * Description: Password reset flow — same procedure for every acc_type (admin, * staff, user); only reg_type matters (Google accounts are turned * away, see LoginForm's "Please log in with Google" precedent). * Step 1 — Email (checks reg_type server-side, sends OTP if system) * Step 2 — OTP + new password + confirm, single submit * Module: User Credentials * Author: lash0000 * Date Created: Jul. 4, 2026 ***********************************************************************************************************************************************************************/ import { useState, useEffect } from 'react' import { useNavigate, Link } from 'react-router-dom' import { useForm, Controller } from 'react-hook-form' import { useAuth } from '@/contexts/AuthContext' import { z } from 'zod' import { zodResolver } from '@hookform/resolvers/zod' import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Separator } from '@/components/ui/separator' import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp' import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' import { Eye, EyeOff, LoaderCircle } from 'lucide-react' const emailSchema = z.object({ email: z.string().email('Invalid email address'), }) const resetSchema = z .object({ otp: z.string().length(6, 'Enter all 6 digits').regex(/^\d{6}$/, 'OTP must contain only digits'), new_password: z .string() .min(8, 'Password must be at least 8 characters') .regex(/[A-Z]/, 'Must contain at least one uppercase letter') .regex(/[0-9]/, 'Must contain at least one number'), confirm_password: z.string().min(1, 'Please confirm your password'), }) .refine((d) => d.new_password === d.confirm_password, { message: 'Passwords do not match', path: ['confirm_password'], }) export function ForgotPasswordForm({ className, ...props }) { const navigate = useNavigate() const { forgotPassword, resetPassword } = useAuth() // 0 = email, 1 = otp + new password const [step, setStep] = useState(0) const [pendingEmail, setPendingEmail] = useState('') const [showPassword, setShowPassword] = useState(false) const [showConfirm, setShowConfirm] = useState(false) const [resendCooldown, setResendCooldown] = useState(0) const [errorDialogOpen, setErrorDialogOpen] = useState(false) const [errorMessage, setErrorMessage] = useState('') const [successDialogOpen, setSuccessDialogOpen] = useState(false) useEffect(() => { if (resendCooldown <= 0) return const t = setTimeout(() => setResendCooldown((c) => c - 1), 1000) return () => clearTimeout(t) }, [resendCooldown]) // ── Step 1: Email ────────────────────────────────────────────────────────── const { register: regEmail, handleSubmit: submitEmail, formState: { errors: errEmail, isSubmitting: isRequesting }, } = useForm({ resolver: zodResolver(emailSchema), defaultValues: { email: '' }, }) const onEmailSubmit = async ({ email }) => { const result = await forgotPassword({ email }) if (!result.success) { setErrorMessage(result.message) setErrorDialogOpen(true) return } setPendingEmail(email) setResendCooldown(30) setStep(1) } // ── Step 2: OTP + new password ──────────────────────────────────────────── const { control: resetControl, register: regReset, handleSubmit: submitReset, formState: { errors: errReset, isSubmitting: isResetting }, } = useForm({ resolver: zodResolver(resetSchema), defaultValues: { otp: '', new_password: '', confirm_password: '' }, }) const onResetSubmit = async ({ otp, new_password }) => { const result = await resetPassword({ email: pendingEmail, otp, new_password }) if (!result.success) { setErrorMessage(result.message) setErrorDialogOpen(true) return } setSuccessDialogOpen(true) } const handleResend = async () => { if (resendCooldown > 0) return const result = await forgotPassword({ email: pendingEmail }) if (!result.success) { setErrorMessage(result.message) setErrorDialogOpen(true) return } setResendCooldown(30) } return ( <>