Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 170 additions & 45 deletions frontend/src/components/auth/ForgotPassword.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,24 @@ export default function ForgotPassword() {
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [cooldown, setCooldown] = useState(0);

const [isEmailValid, setIsEmailValid] = useState(true);

const navigate = useNavigate();

const isPasswordValid = newPassword.length >= 6;
const isOtpValid = otp.trim().length === 6;

const doPasswordsMatch =
confirmPassword.length > 0 && newPassword === confirmPassword;

const validateEmail = (value, validity) => {
return (
value.trim().length > 0 &&
validity.valid &&
value.split("@")[1]?.includes(".")
);
};

useEffect(() => {
let timer;
if (cooldown > 0) {
Expand All @@ -27,10 +42,18 @@ export default function ForgotPassword() {
const handleSendResetCode = async (e) => {
e.preventDefault();
setError('');

const trimmedEmail = email.trim();

if (!trimmedEmail || !isEmailValid) {
setError("Please enter a valid email address");
return;
}

setLoading(true);

try {
await authService.forgotPassword(email);
await authService.forgotPassword(trimmedEmail);
setStep(2);
setCooldown(60);
} catch (err) {
Expand All @@ -44,20 +67,32 @@ export default function ForgotPassword() {
e.preventDefault();
setError('');

if (newPassword !== confirmPassword) {
setError('Passwords do not match');
const normalizedOtp = otp.trim();

if (normalizedOtp.length !== 6) {
setError("OTP must be 6 characters");
return;
}

if (!newPassword.trim() || !confirmPassword.trim()) {
setError("Password cannot be empty or whitespace");
return;
}

if (!isPasswordValid) {
setError("Password must be at least 6 characters");
return;
}

if (newPassword.length < 6) {
setError('Password must be at least 6 characters');
if (!doPasswordsMatch) {
setError("Passwords do not match");
return;
}

setLoading(true);

try {
await authService.resetPassword(email, otp, newPassword);
await authService.resetPassword(email.trim(), normalizedOtp, newPassword);
navigate('/login');
} catch (err) {
setError(err.response?.data?.message || 'Failed to reset password');
Expand All @@ -68,7 +103,7 @@ export default function ForgotPassword() {

const handleResendOtp = async () => {
if (cooldown > 0) return;

setError('');
setLoading(true);

Expand All @@ -88,7 +123,7 @@ export default function ForgotPassword() {
<h2 className="text-3xl sm:text-4xl md:text-5xl font-black uppercase tracking-tighter text-black mb-8 sm:mb-12">
RESET PASSWORD
</h2>

{error && (
<div className="mb-8 border-4 border-red-600 bg-red-50 p-4">
<p className="text-sm font-black uppercase tracking-widest text-red-600">
Expand All @@ -98,108 +133,198 @@ export default function ForgotPassword() {
)}

{step === 1 ? (
<form className="flex flex-col space-y-8" onSubmit={handleSendResetCode}>
<form
noValidate
className="flex flex-col space-y-8"
onSubmit={handleSendResetCode}
>
<p className="text-sm font-black uppercase tracking-widest text-black">
Enter your email address and we'll send you a code to reset your password.
Enter your email address and we'll send you a code to reset your
password.
</p>

<div className="flex flex-col space-y-3">
<label className="text-sm font-black uppercase tracking-widest text-black">
<label
htmlFor="email"
className="text-sm font-black uppercase tracking-widest text-black"
>
Email
</label>
<input
type="email"
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
autoComplete="email"
aria-describedby={
email && !isEmailValid ? "email-error" : undefined
}
aria-invalid={email.length > 0 && !isEmailValid}
onChange={(e) => {
setEmail(e.target.value);
setIsEmailValid(
validateEmail(e.target.value, e.target.validity),
);
}}
className="w-full p-5 border-4 border-black rounded-none text-black font-bold focus:outline-none focus:ring-0 focus:border-gray-500"
placeholder="YOUR@EMAIL.COM"
required
/>
<div className="min-h-[16px]">
{email && !isEmailValid && (
<p
id="email-error"
role="alert"
className="text-xs font-black uppercase tracking-widest text-red-600"
>
Please enter a valid email address
</p>
)}
</div>
</div>
<button
type="submit"
disabled={loading}

<button
type="submit"
disabled={loading || !email.trim() || !isEmailValid}
className="w-full mt-4 py-6 bg-black text-white text-xl font-black uppercase tracking-widest hover:bg-gray-900 transition-colors border-4 border-black rounded-none disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'SENDING...' : 'SEND RESET CODE'}
{loading ? "SENDING..." : "SEND RESET CODE"}
</button>

<div className="flex justify-center">
<Link
to="/login"
<Link
to="/login"
className="text-sm font-black uppercase tracking-widest text-black underline underline-offset-4 decoration-[3px] hover:text-gray-600"
>
Back to Login
</Link>
</div>
</form>
) : (
<form className="flex flex-col space-y-8" onSubmit={handleResetPassword}>
<form
noValidate
className="flex flex-col space-y-8"
onSubmit={handleResetPassword}
>
<p className="text-sm font-black uppercase tracking-widest text-black text-center">
Enter the code sent to<br />
Enter the code sent to
<br />
<span className="text-base">{email}</span>
</p>

<div className="flex flex-col space-y-3">
<label className="text-sm font-black uppercase tracking-widest text-black">
Verification Code
</label>
<input
type="text"
<input
type="text"
value={otp}
onChange={(e) => setOtp(e.target.value.toUpperCase())}
id="otp"
inputMode="text"
aria-invalid={otp.length > 0 && !isOtpValid}
onChange={(e) =>
setOtp(
e.target.value.replace(/[^A-Z0-9]/gi, "").toUpperCase(),
)
}
maxLength={6}
className="w-full p-5 border-4 border-black rounded-none text-black font-black text-2xl tracking-[0.5em] text-center focus:outline-none focus:ring-0 focus:border-gray-500 uppercase"
placeholder="______"
required
/>
<div className="min-h-[16px]">
{otp && !isOtpValid && (
<p
role="alert"
className="text-xs font-black uppercase tracking-widest text-red-600"
>
OTP must be 6 characters
</p>
)}
</div>
</div>

<div className="flex flex-col space-y-3">
<label className="text-sm font-black uppercase tracking-widest text-black">
<label
htmlFor="new-password"
className="text-sm font-black uppercase tracking-widest text-black"
>
New Password
</label>
<input
type="password"
<input
type="password"
autoComplete="new-password"
minLength={6}
aria-invalid={newPassword.length > 0 && !isPasswordValid}
value={newPassword}
id="new-password"
onChange={(e) => setNewPassword(e.target.value)}
className="w-full p-5 border-4 border-black rounded-none text-black font-bold focus:outline-none focus:ring-0 focus:border-gray-500"
placeholder="••••••••"
required
/>
<div className="min-h-[16px]">
{newPassword && !isPasswordValid && (
<p
role="alert"
className="text-xs font-black uppercase tracking-widest text-red-600"
>
Password must be at least 6 characters
</p>
)}
</div>
</div>

<div className="flex flex-col space-y-3">
<label className="text-sm font-black uppercase tracking-widest text-black">
<label
htmlFor="confirm-password"
className="text-sm font-black uppercase tracking-widest text-black">
Confirm Password
</label>
<input
type="password"
<input
type="password"
aria-invalid={confirmPassword.length > 0 && !doPasswordsMatch}
value={confirmPassword}
id="confirm-password"
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full p-5 border-4 border-black rounded-none text-black font-bold focus:outline-none focus:ring-0 focus:border-gray-500"
placeholder="••••••••"
required
/>
<div className="min-h-[16px]">
{confirmPassword && !doPasswordsMatch && (
<p
role="alert"
className="text-xs font-black uppercase tracking-widest text-red-600"
>
Passwords do not match
</p>
)}
</div>
</div>

<button
type="submit"
disabled={loading}

<button
type="submit"
disabled={
loading ||
!isOtpValid ||
!newPassword.trim() ||
!confirmPassword.trim() ||
!isPasswordValid ||
!doPasswordsMatch
}
className="w-full mt-4 py-6 bg-black text-white text-xl font-black uppercase tracking-widest hover:bg-gray-900 transition-colors border-4 border-black rounded-none disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'RESETTING...' : 'RESET PASSWORD'}
{loading ? "RESETTING..." : "RESET PASSWORD"}
</button>

<div className="flex justify-center">
<button
type="button"
onClick={handleResendOtp}
disabled={cooldown > 0 || loading}
className="text-sm font-black uppercase tracking-widest text-black underline underline-offset-4 decoration-[3px] hover:text-gray-600 disabled:opacity-50 disabled:cursor-not-allowed disabled:no-underline"
>
{cooldown > 0 ? `RESEND CODE (${cooldown})` : 'RESEND CODE'}
{cooldown > 0 ? `RESEND CODE (${cooldown})` : "RESEND CODE"}
</button>
</div>
</form>
Expand Down