diff --git a/apps/web/src/features/auth/ForgotPassword.tsx b/apps/web/src/features/auth/ForgotPassword.tsx new file mode 100644 index 00000000..0a2fd461 --- /dev/null +++ b/apps/web/src/features/auth/ForgotPassword.tsx @@ -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) => { + 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 ( +
+ {/* Left Side: Hero Image (Hidden on Mobile) */} +
+ Modern workspace + {/* Cinematic Overlay */} +
+
+ + {/* Top Left Logo */} +
+ Krow Logo +
+ +
+

+ Streamline your workforce
with{" "} + + KROW + +

+

+ The all-in-one platform for managing staff, orders, and professional + relationships with precision and ease. +

+
+
+ + {/* Right Side: Forgot Password Form */} +
+
+ {/* Header with back button */} +
+ + + Krow Logo +
+

+ Reset Password +

+

+ Enter your email to receive a password reset link +

+
+
+ + {/* Success Message */} + {success && ( +
+ + Check your email + + 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... + +
+ )} + + {/* Error Message */} + {error && !success && ( +
+ + {error} +
+ )} + + {/* Form */} + {!success && ( +
+
+ + + {emailError && ( +

+ {emailError} +

+ )} +
+ + +
+ )} + + {/* Helper Text */} +
+

Didn't receive the email?

+
    +
  • Check your spam/junk folder
  • +
  • Make sure the email is correct
  • +
  • Reset links expire after 1 hour
  • +
+
+
+
+
+ ); +}; + +export default ForgotPassword; diff --git a/apps/web/src/features/auth/Login.tsx b/apps/web/src/features/auth/Login.tsx index e8970239..7b0beeb5 100644 --- a/apps/web/src/features/auth/Login.tsx +++ b/apps/web/src/features/auth/Login.tsx @@ -247,8 +247,8 @@ const Login: React.FC = () => { diff --git a/apps/web/src/routes.tsx b/apps/web/src/routes.tsx index ff41ebd5..25c60910 100644 --- a/apps/web/src/routes.tsx +++ b/apps/web/src/routes.tsx @@ -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 = () => { } /> + + } + /> } /> } /> diff --git a/apps/web/src/services/authService.ts b/apps/web/src/services/authService.ts index db8048dc..d46e372d 100644 --- a/apps/web/src/services/authService.ts +++ b/apps/web/src/services/authService.ts @@ -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 = { "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 };