From 7133e59e577da43ee913b4cc0ce6e18179967514 Mon Sep 17 00:00:00 2001 From: dhinesh-m24 Date: Thu, 29 Jan 2026 12:39:45 +0530 Subject: [PATCH] feat(auth): Implemented Session Persistence --- apps/web/src/common/components/PageHeader.tsx | 53 ++++++++ apps/web/src/features/auth/Login.tsx | 21 +++ .../dashboard/RoleDashboardRedirect.tsx | 27 ++++ apps/web/src/features/layouts/AppLayout.tsx | 9 +- .../src/features/layouts/DashboardLayout.tsx | 48 +++++++ .../web/src/features/layouts/PublicLayout.tsx | 23 ++++ apps/web/src/features/layouts/Sidebar.tsx | 17 ++- apps/web/src/hooks/useSessionPersistence.ts | 127 ++++++++++++++++++ apps/web/src/routes.tsx | 35 +---- apps/web/src/services/authService.ts | 89 +++++++++++- 10 files changed, 417 insertions(+), 32 deletions(-) create mode 100644 apps/web/src/common/components/PageHeader.tsx create mode 100644 apps/web/src/features/dashboard/RoleDashboardRedirect.tsx create mode 100644 apps/web/src/features/layouts/DashboardLayout.tsx create mode 100644 apps/web/src/features/layouts/PublicLayout.tsx create mode 100644 apps/web/src/hooks/useSessionPersistence.ts diff --git a/apps/web/src/common/components/PageHeader.tsx b/apps/web/src/common/components/PageHeader.tsx new file mode 100644 index 00000000..3994b4cb --- /dev/null +++ b/apps/web/src/common/components/PageHeader.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import { ArrowLeft } from "lucide-react"; +import { Button } from "./ui/button"; + +interface PageHeaderProps { + title: string; + subtitle?: string; + actions?: React.ReactNode; + backTo?: string | null; + backButtonLabel?: string; +} + +export default function PageHeader({ + title, + subtitle, + actions = null, + backTo = null, + backButtonLabel = "Back" +}: PageHeaderProps) { + return ( +
+ {/* Back Button */} + {backTo && ( + + + + )} + + {/* Main Header */} +
+
+

+ {title} +

+ {subtitle && ( +

{subtitle}

+ )} +
+ + {/* Custom Actions (if provided) */} + {actions && ( +
+ {actions} +
+ )} +
+
+ ); +} diff --git a/apps/web/src/features/auth/Login.tsx b/apps/web/src/features/auth/Login.tsx index 4ceeae21..12544c06 100644 --- a/apps/web/src/features/auth/Login.tsx +++ b/apps/web/src/features/auth/Login.tsx @@ -22,6 +22,7 @@ type LocationState = { from?: { pathname: string; }; + message?: string; }; const Login: React.FC = () => { @@ -29,6 +30,7 @@ const Login: React.FC = () => { const [password, setPassword] = useState(""); const [emailError, setEmailError] = useState(""); const [passwordError, setPasswordError] = useState(""); + const [sessionMessage, setSessionMessage] = useState(null); const navigate = useNavigate(); const location = useLocation(); const dispatch = useDispatch(); @@ -87,6 +89,18 @@ const Login: React.FC = () => { navigate(from ?? dashboardPath, { replace: true }); }, [isAuthenticated, user?.userRole, navigate, location.state]); + // Check for session expiration message from redirect + useEffect(() => { + const state = location.state as LocationState | null; + if (state?.message) { + // Auto-clear session message after 5 seconds + const timer = setTimeout(() => setSessionMessage(null), 5000); + // Use a microtask to avoid cascading renders + Promise.resolve().then(() => setSessionMessage(state.message || null)); + return () => clearTimeout(timer); + } + }, [location.state]); + // Clear error message when component unmounts useEffect(() => { return () => { @@ -169,6 +183,13 @@ const Login: React.FC = () => {
+ {sessionMessage && ( +
+ + {sessionMessage} +
+ )} + {reduxError && (
diff --git a/apps/web/src/features/dashboard/RoleDashboardRedirect.tsx b/apps/web/src/features/dashboard/RoleDashboardRedirect.tsx new file mode 100644 index 00000000..5392f2b3 --- /dev/null +++ b/apps/web/src/features/dashboard/RoleDashboardRedirect.tsx @@ -0,0 +1,27 @@ +import React, {useEffect} from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useSelector } from 'react-redux'; +import { getDashboardPath } from '../../services/firestoreService' +import type { RootState } from '../../store/store'; +/** + * RoleDashboardRedirect Component + * Dynamically redirects users to their appropriate dashboard based on their role + */ +export const RoleDashboardRedirect: React.FC = () => { + const { isAuthenticated, user } = useSelector((state: RootState) => state.auth); + const navigate = useNavigate(); + + useEffect(() => { + if (!isAuthenticated) { + navigate('/login', { replace: true }); + return; + } + + if (user?.userRole) { + const dashboardPath = getDashboardPath(user.userRole); + navigate(dashboardPath, { replace: true }); + } + }, [isAuthenticated, user?.userRole, navigate]); + + return null; +}; \ No newline at end of file diff --git a/apps/web/src/features/layouts/AppLayout.tsx b/apps/web/src/features/layouts/AppLayout.tsx index 8ce252f7..3db7da5e 100644 --- a/apps/web/src/features/layouts/AppLayout.tsx +++ b/apps/web/src/features/layouts/AppLayout.tsx @@ -10,6 +10,7 @@ import { } from 'lucide-react'; import Sidebar from './Sidebar'; import { getDashboardPath } from '../../services/firestoreService'; +import { useSessionPersistence } from '../../hooks/useSessionPersistence'; /** * Main Application Layout for Authenticated Users. @@ -17,6 +18,9 @@ import { getDashboardPath } from '../../services/firestoreService'; * Handles role-based navigation rendering and validates user access to dashboard routes. */ const AppLayout: React.FC = () => { + // Initialize session persistence and token refresh + useSessionPersistence(); + // Typed selectors const { isAuthenticated, user } = useSelector((state: RootState) => state.auth); const dispatch = useDispatch(); @@ -47,7 +51,10 @@ const AppLayout: React.FC = () => { } const handleLogout = () => { - dispatch(logoutUser()); + dispatch(logoutUser()).then(() => { + // Navigate to login page after logout is complete + navigate('/login', { replace: true }); + }); }; return ( diff --git a/apps/web/src/features/layouts/DashboardLayout.tsx b/apps/web/src/features/layouts/DashboardLayout.tsx new file mode 100644 index 00000000..219deff4 --- /dev/null +++ b/apps/web/src/features/layouts/DashboardLayout.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import PageHeader from '../../common/components/PageHeader'; +import { motion } from 'framer-motion'; + +interface DashboardLayoutProps { + title: string; + subtitle?: string; + actions?: React.ReactNode; + children: React.ReactNode; + maxWidth?: string; + backAction?: React.ReactNode; +} + +const DashboardLayout: React.FC = ({ + title, + subtitle, + actions, + children, + maxWidth = 'max-w-7xl', + backAction +}) => { + return ( +
+
+ {backAction && ( + + {backAction} + + )} + + + + {children} +
+
+ ); +}; + +export default DashboardLayout; diff --git a/apps/web/src/features/layouts/PublicLayout.tsx b/apps/web/src/features/layouts/PublicLayout.tsx new file mode 100644 index 00000000..ea4d1045 --- /dev/null +++ b/apps/web/src/features/layouts/PublicLayout.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +interface PublicLayoutProps { + children: React.ReactNode; +} + +/** + * Layout for public, unauthenticated pages (e.g. Login, Forgot Password). + * Provides a centered container with a gradient background. + */ +const PublicLayout: React.FC = ({ children }) => { + return ( +
+ {/* Background decoration */} +
+
+ {children} +
+
+ ); +}; + +export default PublicLayout; \ No newline at end of file diff --git a/apps/web/src/features/layouts/Sidebar.tsx b/apps/web/src/features/layouts/Sidebar.tsx index d230f205..401b4043 100644 --- a/apps/web/src/features/layouts/Sidebar.tsx +++ b/apps/web/src/features/layouts/Sidebar.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react'; -import { Link, useLocation } from 'react-router-dom'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; import { LogOut, Menu, X } from 'lucide-react'; import { Button } from '../../common/components/ui/button'; import { NAV_CONFIG } from "../../common/config/navigation"; @@ -22,6 +22,19 @@ const Sidebar: React.FC = ({ onLogout }) => { const location = useLocation(); + const navigate = useNavigate(); + + /** + * Handle logout with navigation + * Ensures user is redirected to login page after logout + */ + const handleLogoutClick = async () => { + onLogout(); + // Small delay to allow logout to complete before navigation + setTimeout(() => { + navigate('/login', { replace: true }); + }, 100); + }; // Filter navigation based on user role const filteredNav = useMemo(() => { @@ -107,7 +120,7 @@ const Sidebar: React.FC = ({