diff --git a/frontend/app/reset-password/page.tsx b/frontend/app/reset-password/page.tsx new file mode 100644 index 0000000..3991ea0 --- /dev/null +++ b/frontend/app/reset-password/page.tsx @@ -0,0 +1,518 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import Link from "next/link"; +import { useRouter, useSearchParams } from "next/navigation"; + +const passwordSchema = z + .object({ + password: z + .string() + .min(8, "Password must be at least 8 characters") + .regex(/[A-Z]/, "Password must contain at least one uppercase letter") + .regex(/[a-z]/, "Password must contain at least one lowercase letter") + .regex(/[0-9]/, "Password must contain at least one number") + .regex( + /[^A-Za-z0-9]/, + "Password must contain at least one special character", + ), + confirmPassword: z.string(), + }) + .refine((data) => data.password === data.confirmPassword, { + message: "Passwords don't match", + path: ["confirmPassword"], + }); + +type ResetPasswordForm = z.infer; + +export default function ResetPasswordPage() { + const [isLoading, setIsLoading] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [passwordStrength, setPasswordStrength] = useState(0); + const [countdown, setCountdown] = useState(3); + const [tokenError, setTokenError] = useState(""); + const router = useRouter(); + const searchParams = useSearchParams(); + const token = searchParams.get("token"); + + const { + register, + handleSubmit, + watch, + formState: { errors }, + } = useForm({ + resolver: zodResolver(passwordSchema), + }); + + const password = watch("password"); + + useEffect(() => { + if (!token) { + setTokenError( + "No reset token found. Please request a new password reset link.", + ); + } + }, [token]); + + useEffect(() => { + if (password) { + let strength = 0; + if (password.length >= 8) strength += 25; + if (/[A-Z]/.test(password)) strength += 25; + if (/[a-z]/.test(password)) strength += 25; + if (/[0-9]/.test(password) && /[^A-Za-z0-9]/.test(password)) + strength += 25; + setPasswordStrength(strength); + } else { + setPasswordStrength(0); + } + }, [password]); + + const getStrengthLabel = () => { + if (passwordStrength === 0) return ""; + if (passwordStrength <= 25) return "Weak"; + if (passwordStrength <= 50) return "Fair"; + if (passwordStrength <= 75) return "Good"; + return "Strong"; + }; + + const getStrengthColor = () => { + if (passwordStrength <= 25) return "bg-red-500"; + if (passwordStrength <= 50) return "bg-yellow-500"; + if (passwordStrength <= 75) return "bg-blue-500"; + return "bg-green-500"; + }; + + const onSubmit = async (data: ResetPasswordForm) => { + if (!token) { + setTokenError("Invalid or missing reset token."); + return; + } + + setIsLoading(true); + + try { + const response = await fetch("/api/v1/auth/reset-password", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + token, + password: data.password, + }), + }); + + if (!response.ok) { + const error = await response.json(); + if (error.message?.includes("expired")) { + setTokenError( + "This reset link has expired. Please request a new one.", + ); + } else { + throw new Error("Failed to reset password"); + } + return; + } + + setIsSuccess(true); + + // Start countdown and redirect + let timeLeft = 3; + const timer = setInterval(() => { + timeLeft -= 1; + setCountdown(timeLeft); + if (timeLeft === 0) { + clearInterval(timer); + router.push("/login"); + } + }, 1000); + } catch (error) { + alert("Failed to reset password. Please try again."); + } finally { + setIsLoading(false); + } + }; + + if (tokenError) { + return ( +
+
+
+
+ + + +
+

+ Invalid Reset Link +

+

{tokenError}

+
+ +
+ + Request New Reset Link + + + Back to Login + +
+
+
+ ); + } + + if (isSuccess) { + return ( +
+
+
+
+ + + +
+

+ Password Reset Successfully! +

+

+ Your password has been updated. You can now log in with your new + password. +

+
+ +
+ + Go to Login + +

+ Redirecting in {countdown} seconds... +

+
+
+
+ ); + } + + return ( +
+
+
+

+ Create New Password +

+

+ Please enter your new password below +

+
+ +
+
+ {/* New Password */} +
+ +
+ + +
+ + {/* Password Strength Indicator */} + {password && ( +
+
+ Password strength: + + {getStrengthLabel()} + +
+
+
+
+
+ )} + + {errors.password && ( +

+ {errors.password.message} +

+ )} +
+ + {/* Password Requirements */} +
+

+ Password must contain: +

+
    +
  • + = 8 ? "text-green-600" : "text-gray-400" + }`} + > + {password?.length >= 8 ? "✓" : "○"} + + At least 8 characters +
  • +
  • + + {/[A-Z]/.test(password || "") ? "✓" : "○"} + + One uppercase letter +
  • +
  • + + {/[a-z]/.test(password || "") ? "✓" : "○"} + + One lowercase letter +
  • +
  • + + {/[0-9]/.test(password || "") ? "✓" : "○"} + + One number +
  • +
  • + + {/[^A-Za-z0-9]/.test(password || "") ? "✓" : "○"} + + One special character +
  • +
+
+ + {/* Confirm Password */} +
+ +
+ + +
+ {errors.confirmPassword && ( +

+ {errors.confirmPassword.message} +

+ )} +
+
+ +
+ +
+
+
+
+ ); +}