feat(auth): implement role-based dashboard redirect

This commit is contained in:
dhinesh-m24
2026-01-29 11:43:19 +05:30
parent d07d42ad0b
commit e214e32c17
12 changed files with 713 additions and 78 deletions

View File

@@ -1,20 +1,21 @@
import React, { useState, useEffect } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { Loader2, AlertCircle } 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 { getAuth, signInWithEmailAndPassword } from "firebase/auth";
import type { User } from "firebase/auth";
import { app as firebaseApp } from "../../services/authService";
import { FirebaseError } from "firebase/app";
import { loginUser } from "../auth/authSlice";
import { getDashboardPath } from "../../services/firestoreService";
import type { RootState, AppDispatch } from "../../store/store";
/**
* Login Page Component.
* Features a modern split-screen layout with an inspirational hero image.
* Handles user authentication via email/password with client-side validation.
* Uses Redux for state management and handles role-based redirection.
*/
type LocationState = {
@@ -28,16 +29,10 @@ const Login: React.FC = () => {
const [password, setPassword] = useState("");
const [emailError, setEmailError] = useState("");
const [passwordError, setPasswordError] = useState("");
const [isFormValid, setIsFormValid] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
const [user, setUser] = useState<User | null>(null);
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
setIsFormValid(validateForm(email, password));
}, [email, password]);
const dispatch = useDispatch<AppDispatch>();
const { status, error: reduxError, user, isAuthenticated } = useSelector((state: RootState) => state.auth);
// Validate email format
const validateEmail = (value: string): boolean => {
@@ -55,6 +50,8 @@ const Login: React.FC = () => {
return validateEmail(emailValue) && validatePassword(passwordValue);
};
const isFormValid = validateForm(email, password);
// Handle email input change
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
@@ -79,26 +76,26 @@ const Login: React.FC = () => {
}
};
// Navigate user after login (customize as needed)
// Navigate user after successful login
useEffect(() => {
if (!user) return;
if (!isAuthenticated || !user?.userRole) return;
const state = location.state as LocationState | null;
const from = state?.from?.pathname;
const dashboardPath = getDashboardPath(user.userRole);
navigate(from ?? "/dashboard", { replace: true });
}, [user, navigate, location.state]);
navigate(from ?? dashboardPath, { replace: true });
}, [isAuthenticated, user?.userRole, navigate, location.state]);
// Clear error message when component unmounts
useEffect(() => {
return () => {
setError("");
// Error will be cleared from Redux state when user navigates away
};
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
// Validate before submission
if (!isFormValid) {
@@ -111,40 +108,8 @@ const Login: React.FC = () => {
return;
}
setIsLoading(true);
try {
const auth = getAuth(firebaseApp);
const userCredential = await signInWithEmailAndPassword(
auth,
email,
password,
);
setUser(userCredential.user);
} catch (err: unknown) {
let message = "Failed to sign in. Please try again.";
if (err instanceof FirebaseError) {
switch (err.code) {
case "auth/user-not-found":
case "auth/wrong-password":
message = "Invalid email or password.";
break;
case "auth/too-many-requests":
message = "Too many failed attempts. Please try again later.";
break;
default:
message = err.message;
}
} else if (err instanceof Error) {
message = err.message;
}
setError(message);
} finally {
setIsLoading(false);
}
// Dispatch Redux action to handle login
dispatch(loginUser({ email, password }));
};
return (
@@ -157,7 +122,7 @@ const Login: React.FC = () => {
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-linear-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 */}
@@ -204,10 +169,10 @@ const Login: React.FC = () => {
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
{reduxError && (
<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>
<AlertCircle className="w-4 h-4 mr-2 shrink-0" />
<span>{reduxError}</span>
</div>
)}
@@ -221,7 +186,7 @@ const Login: React.FC = () => {
placeholder="name@company.com"
value={email}
onChange={handleEmailChange}
disabled={isLoading}
disabled={status === "loading"}
required
aria-describedby={emailError ? "email-error" : undefined}
className={
@@ -259,7 +224,7 @@ const Login: React.FC = () => {
placeholder="••••••••"
value={password}
onChange={handlePasswordChange}
disabled={isLoading}
disabled={status === "loading"}
required
aria-describedby={passwordError ? "password-error" : undefined}
className={
@@ -281,10 +246,10 @@ const Login: React.FC = () => {
<Button
type="submit"
className="w-full"
disabled={isLoading || !isFormValid}
disabled={status === "loading" || !isFormValid}
size="default"
>
{isLoading ? (
{status === "loading" ? (
<>
<Loader2 className="w-5 h-5 mr-3 animate-spin" />
Authenticating...

View File

@@ -1,6 +1,7 @@
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit";
import { loginWithEmail, logout, getCurrentUser } from "../../services/authService";
import { fetchUserData } from "../../services/firestoreService";
import type { User } from "firebase/auth";
export interface AuthUser {
@@ -8,7 +9,7 @@ export interface AuthUser {
email: string | null;
displayName: string | null;
photoURL: string | null;
role?: string;
userRole?: "admin" | "client" | "vendor";
}
interface AuthState {
@@ -27,6 +28,7 @@ const initialState: AuthState = {
/**
* Async thunk for user login
* Fetches user data including role from Firestore after authentication
*/
export const loginUser = createAsyncThunk(
"auth/loginUser",
@@ -38,11 +40,25 @@ export const loginUser = createAsyncThunk(
}
const firebaseUser = result.user as User;
// Fetch user role from Firestore
let userRole: "admin" | "client" | "vendor" = "client";
try {
const userData = await fetchUserData(firebaseUser.uid);
if (userData) {
userRole = userData.userRole;
}
} catch (error) {
console.error("Failed to fetch user role:", error);
// Continue with default role on error
}
return {
uid: firebaseUser.uid,
email: firebaseUser.email,
displayName: firebaseUser.displayName,
photoURL: firebaseUser.photoURL,
userRole: userRole,
};
}
);
@@ -62,16 +78,30 @@ export const logoutUser = createAsyncThunk("auth/logoutUser", async (_, { reject
/**
* Async thunk to check if user is already logged in
* Fetches user role from Firestore on app initialization
*/
export const checkAuthStatus = createAsyncThunk("auth/checkAuthStatus", async () => {
export const checkAuthStatus = createAsyncThunk("auth/checkAuthStatus", async (_, { rejectWithValue }) => {
const currentUser = getCurrentUser();
if (currentUser) {
// Fetch user role from Firestore
let userRole: "admin" | "client" | "vendor" = "client";
try {
const userData = await fetchUserData(currentUser.uid);
if (userData) {
userRole = userData.userRole;
}
} catch (error) {
console.error("Failed to fetch user role:", error);
// Continue with default role on error
}
return {
uid: currentUser.uid,
email: currentUser.email,
displayName: currentUser.displayName,
photoURL: currentUser.photoURL,
userRole: userRole,
};
}
@@ -85,9 +115,9 @@ const authSlice = createSlice({
clearError: (state) => {
state.error = null;
},
setRole: (state, action: PayloadAction<string>) => {
setUserRole: (state, action: PayloadAction<"admin" | "client" | "vendor">) => {
if (state.user) {
state.user.role = action.payload;
state.user.userRole = action.payload;
}
},
},
@@ -137,5 +167,5 @@ const authSlice = createSlice({
},
});
export const { clearError, setRole } = authSlice.actions;
export const { clearError, setUserRole } = authSlice.actions;
export default authSlice.reducer;