feat(auth): implement forgot-password flow with Firebase Auth

This commit is contained in:
dhinesh-m24
2026-01-28 16:35:18 +05:30
parent 6e81a062ab
commit d07d42ad0b
4 changed files with 268 additions and 3 deletions

View File

@@ -0,0 +1,240 @@
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Loader2, AlertCircle, CheckCircle, ArrowLeft } from "lucide-react";
import loginHero from "../../assets/login-hero.png";
import logo from "../../assets/logo.png";
import { Label } from "@radix-ui/react-label";
import { Input } from "../../common/components/ui/input";
import { Button } from "../../common/components/ui/button";
import { sendPasswordReset } from "../../services/authService";
import { FirebaseError } from "firebase/app";
/**
* ForgotPassword Component
* Allows users to request a password reset by entering their email address.
* Firebase will send a reset link to the provided email.
*/
const ForgotPassword: React.FC = () => {
const [email, setEmail] = useState("");
const [emailError, setEmailError] = useState("");
const [isFormValid, setIsFormValid] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState(false);
const navigate = useNavigate();
useEffect(() => {
setIsFormValid(validateEmail(email));
}, [email]);
// Validate email format
const validateEmail = (value: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value);
};
// Handle email input change
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setEmail(value);
if (value.trim() && !validateEmail(value)) {
setEmailError("Please enter a valid email address");
} else {
setEmailError("");
}
};
// Handle form submission
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
// Validate before submission
if (!isFormValid) {
if (!validateEmail(email)) {
setEmailError("Please enter a valid email address");
}
return;
}
setIsLoading(true);
try {
const result = await sendPasswordReset(email);
if (result.success) {
setSuccess(true);
setEmail("");
// Automatically redirect after 5 seconds
setTimeout(() => {
navigate("/login");
}, 5000);
} else {
setError(result.error || "Failed to send reset email. Please try again.");
}
} catch (err: unknown) {
let message = "Failed to send reset email. Please try again.";
if (err instanceof FirebaseError) {
message = err.message;
} else if (err instanceof Error) {
message = err.message;
}
setError(message);
} finally {
setIsLoading(false);
}
};
return (
<div className="flex min-h-screen bg-slate-50/50">
{/* Left Side: Hero Image (Hidden on Mobile) */}
<div className="hidden lg:flex lg:w-3/5 xl:w-[65%] relative overflow-hidden">
<img
src={loginHero}
alt="Modern workspace"
className="absolute inset-0 w-full h-full object-cover transform scale-105 hover:scale-100 transition-transform duration-10000 ease-in-out"
/>
{/* Cinematic Overlay */}
<div className="absolute inset-0 bg-gradient-to-r from-black/60 via-black/20 to-transparent z-10" />
<div className="absolute inset-0 bg-primary/10 backdrop-blur-[2px] z-0" />
{/* Top Left Logo */}
<div className="absolute top-12 left-12 z-20">
<img
src={logo}
alt="Krow Logo"
className="h-10 w-auto brightness-0 invert"
/>
</div>
<div className="absolute bottom-12 left-12 right-12 text-white z-10">
<h1 className="text-5xl font-bold mb-4 tracking-tight leading-tight">
Streamline your workforce <br /> with{" "}
<span className="text-secondary underline decoration-4 underline-offset-8">
KROW
</span>
</h1>
<p className="text-lg text-slate-100/90 max-w-lg leading-relaxed">
The all-in-one platform for managing staff, orders, and professional
relationships with precision and ease.
</p>
</div>
</div>
{/* Right Side: Forgot Password Form */}
<div className="w-full lg:w-2/5 xl:w-[35%] flex flex-col justify-center items-center p-8 sm:p-12 md:p-16 lg:p-20 bg-white border-l border-slate-200 z-10">
<div className="w-full max-w-md space-y-8">
{/* Header with back button */}
<div className="flex flex-col items-center lg:items-start space-y-4">
<button
onClick={() => navigate("/login")}
className="flex items-center text-sm font-semibold text-primary hover:text-primary/80 transition-colors mb-4"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Login
</button>
<img
src={logo}
alt="Krow Logo"
className="h-10 w-auto lg:hidden mb-2"
/>
<div className="space-y-1">
<h2 className="text-3xl font-bold tracking-tight text-primary-text">
Reset Password
</h2>
<p className="text-secondary-text font-medium">
Enter your email to receive a password reset link
</p>
</div>
</div>
{/* Success Message */}
{success && (
<div className="flex flex-col items-center p-6 text-sm text-green-700 bg-green-50 border border-green-200 rounded-xl transition-all animate-in fade-in slide-in-from-top-2">
<CheckCircle className="w-5 h-5 mb-2 flex-shrink-0" />
<span className="font-semibold mb-1">Check your email</span>
<span className="text-xs text-center text-green-600">
We've sent you a link to reset your password. Please check your email and follow the link.
You'll be redirected to login in a moment...
</span>
</div>
)}
{/* Error Message */}
{error && !success && (
<div className="flex items-center p-4 text-sm text-destructive-foreground bg-destructive/70 border border-destructive/20 rounded-xl transition-all animate-in fade-in slide-in-from-top-2">
<AlertCircle className="w-4 h-4 mr-2 flex-shrink-0" />
<span>{error}</span>
</div>
)}
{/* Form */}
{!success && (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="email" className="text-sm font-semibold">
Work Email
</Label>
<Input
id="email"
type="email"
placeholder="name@company.com"
value={email}
onChange={handleEmailChange}
disabled={isLoading}
required
aria-describedby={emailError ? "email-error" : undefined}
className={
emailError
? "border-destructive focus-visible:ring-destructive"
: "mt-2"
}
/>
{emailError && (
<p
id="email-error"
className="text-xs text-destructive font-medium"
>
{emailError}
</p>
)}
</div>
<Button
type="submit"
className="w-full"
disabled={isLoading || !isFormValid}
size="default"
>
{isLoading ? (
<>
<Loader2 className="w-5 h-5 mr-3 animate-spin" />
Sending Reset Link...
</>
) : (
"Send Reset Link"
)}
</Button>
</form>
)}
{/* Helper Text */}
<div className="p-4 bg-slate-50/80 rounded-xl border border-slate-100 text-xs text-secondary-text">
<p className="font-semibold mb-2">Didn't receive the email?</p>
<ul className="list-disc list-inside space-y-1 text-xs">
<li>Check your spam/junk folder</li>
<li>Make sure the email is correct</li>
<li>Reset links expire after 1 hour</li>
</ul>
</div>
</div>
</div>
</div>
);
};
export default ForgotPassword;

View File

@@ -247,8 +247,8 @@ const Login: React.FC = () => {
</Label>
<button
type="button"
onClick={() => navigate("/forgot-password")}
className="text-xs font-semibold text-primary hover:underline underline-offset-2"
onClick={() => console.log("TODO: Implement forgot password")}
>
Forgot password?
</button>

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import Login from './features/auth/Login';
import ForgotPassword from './features/auth/ForgotPassword';
import Dashboard from './features/dashboard/Dashboard';
@@ -20,6 +21,12 @@ const AppRoutes: React.FC = () => {
<Login />
}
/>
<Route
path="/forgot-password"
element={
<ForgotPassword />
}
/>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="*" element={<Navigate to="/login" replace />} />
</Routes>

View File

@@ -5,6 +5,7 @@ import {
setPersistence,
browserLocalPersistence,
sendPasswordResetEmail,
confirmPasswordReset,
} from "firebase/auth";
import type { User, AuthError } from "firebase/auth";
import { app} from "../features/auth/firebase"
@@ -70,6 +71,20 @@ export const sendPasswordReset = async (email: string) => {
}
};
/**
* Reset password with code and new password
* Used after user clicks the link in the reset email
*/
export const resetPassword = async (code: string, newPassword: string) => {
try {
await confirmPasswordReset(auth, code, newPassword);
return { success: true };
} catch (error) {
const authError = error as AuthError;
return { success: false, error: getAuthErrorMessage(authError.code) };
}
};
/**
* Subscribe to auth state changes
* Returns unsubscribe function
@@ -92,15 +107,18 @@ const getAuthErrorMessage = (errorCode: string): string => {
const errorMessages: Record<string, string> = {
"auth/invalid-email": "Invalid email address format.",
"auth/user-disabled": "This user account has been disabled.",
"auth/user-not-found": "Invalid email or password.",
"auth/user-not-found": "No account found with this email address.",
"auth/wrong-password": "Invalid email or password.",
"auth/invalid-credential": "Invalid email or password.",
"auth/too-many-requests": "Too many login attempts. Please try again later.",
"auth/operation-not-allowed": "Login is currently disabled. Please try again later.",
"auth/network-request-failed": "Network error. Please check your connection.",
"auth/invalid-action-code": "This password reset link is invalid or has expired.",
"auth/expired-action-code": "This password reset link has expired. Please request a new one.",
"auth/weak-password": "Password is too weak. Please choose a stronger password.",
};
return errorMessages[errorCode] || "An error occurred during login. Please try again.";
return errorMessages[errorCode] || "An error occurred. Please try again.";
};
export { app };