diff --git a/frontend-admin-dashboard/src/components/common/layout-container/top-navbar.tsx/navbar.tsx b/frontend-admin-dashboard/src/components/common/layout-container/top-navbar.tsx/navbar.tsx index 9e120d3135..6eb09239a7 100644 --- a/frontend-admin-dashboard/src/components/common/layout-container/top-navbar.tsx/navbar.tsx +++ b/frontend-admin-dashboard/src/components/common/layout-container/top-navbar.tsx/navbar.tsx @@ -27,6 +27,7 @@ import { getTokenFromCookie, getUserRoles, removeCookiesAndLogout, + clearAllClientStorage, } from '@/lib/auth/sessionUtility'; import { useNavigate } from '@tanstack/react-router'; import { useInstituteDetailsStore } from '@/stores/students/students-list/useInstituteDetailsStore'; @@ -215,6 +216,7 @@ export function Navbar({ showMobileBackButton }: { showMobileBackButton?: boolea resetChapterSidebarStore(); removeCookiesAndLogout(); // Ensure logout completes + clearAllClientStorage(); // Wipe localStorage + sessionStorage on explicit user logout navigate({ to: '/login', }); diff --git a/frontend-admin-dashboard/src/lib/auth/sessionUtility.ts b/frontend-admin-dashboard/src/lib/auth/sessionUtility.ts index c52ef2ffd3..6434c332c0 100644 --- a/frontend-admin-dashboard/src/lib/auth/sessionUtility.ts +++ b/frontend-admin-dashboard/src/lib/auth/sessionUtility.ts @@ -280,6 +280,33 @@ const removeCookiesAndLogout = (): void => { Cookies.remove(TokenKey.refreshToken, { domain: SSO_CONFIG.SHARED_DOMAIN }); }; +// Full client-side storage wipe — only call from user-initiated logout flows, +// NOT from axios interceptors or token-refresh failure handlers, since those +// fire on transient 401s during page load and would blank the app on refresh. +// Preserves theme keys so the user's visual preference survives logout/login. +const LOGOUT_PRESERVED_LOCAL_STORAGE_KEYS = ['theme-code', 'theme-custom-color'] as const; + +const clearAllClientStorage = (): void => { + try { + const preserved: Record = {}; + for (const key of LOGOUT_PRESERVED_LOCAL_STORAGE_KEYS) { + const value = localStorage.getItem(key); + if (value !== null) preserved[key] = value; + } + localStorage.clear(); + for (const [key, value] of Object.entries(preserved)) { + localStorage.setItem(key, value); + } + } catch (error) { + console.warn('Failed to clear localStorage during logout:', error); + } + try { + sessionStorage.clear(); + } catch (error) { + console.warn('Failed to clear sessionStorage during logout:', error); + } +}; + // Debug function to check token status const debugTokenStatus = (): void => { const accessToken = getTokenFromCookie(TokenKey.accessToken); @@ -298,6 +325,7 @@ const debugTokenStatus = (): void => { export { refreshTokens, removeCookiesAndLogout, + clearAllClientStorage, setAuthorizationCookie, getTokenFromCookie, isTokenExpired, diff --git a/frontend-learner-dashboard-app/src/lib/auth/sessionUtility.ts b/frontend-learner-dashboard-app/src/lib/auth/sessionUtility.ts index dce2cc30c5..4366607f1d 100644 --- a/frontend-learner-dashboard-app/src/lib/auth/sessionUtility.ts +++ b/frontend-learner-dashboard-app/src/lib/auth/sessionUtility.ts @@ -268,6 +268,41 @@ const removeTokensAndLogout = async (): Promise => { console.log("User logged out."); }; +// Full client-side storage wipe — only call from user-initiated logout flows, +// NOT from axios interceptors or token-refresh failure handlers, since those +// fire on transient 401s during page load and would blank the app on refresh. +// Preserves: +// - InstituteId: needed for comparison in courses (same pattern as Capacitor +// Storage cleanup in removeTokensAndLogout above) +// - theme-code / theme-custom-color: user's visual preference, restored on +// app boot in providers/theme/theme-provider.tsx +const LOGOUT_PRESERVED_LOCAL_STORAGE_KEYS = [ + "InstituteId", + "theme-code", + "theme-custom-color", +] as const; + +const clearAllClientStorage = (): void => { + try { + const preserved: Record = {}; + for (const key of LOGOUT_PRESERVED_LOCAL_STORAGE_KEYS) { + const value = localStorage.getItem(key); + if (value !== null) preserved[key] = value; + } + localStorage.clear(); + for (const [key, value] of Object.entries(preserved)) { + localStorage.setItem(key, value); + } + } catch (error) { + console.warn("Failed to clear localStorage during logout:", error); + } + try { + sessionStorage.clear(); + } catch (error) { + console.warn("Failed to clear sessionStorage during logout:", error); + } +}; + // Get access token from storage export const getAccessToken = async () => { const { value } = await Storage.get({ key: "accessToken" }); @@ -339,6 +374,7 @@ const handleSSOLogin = (): boolean => { export { refreshTokens, removeTokensAndLogout, + clearAllClientStorage, setTokenInStorage, getTokenFromStorage, isTokenExpired, diff --git a/frontend-learner-dashboard-app/src/routes/logout/index.tsx b/frontend-learner-dashboard-app/src/routes/logout/index.tsx index 810ed04d6f..b59b578cbd 100644 --- a/frontend-learner-dashboard-app/src/routes/logout/index.tsx +++ b/frontend-learner-dashboard-app/src/routes/logout/index.tsx @@ -1,6 +1,6 @@ import { createFileRoute } from "@tanstack/react-router"; import { useEffect } from "react"; -import { removeTokensAndLogout } from "@/lib/auth/sessionUtility"; +import { removeTokensAndLogout, clearAllClientStorage } from "@/lib/auth/sessionUtility"; import { pushNotificationService } from "@/services/push-notifications/push-notification-service"; import { useNavigate } from "@tanstack/react-router"; import { NAMING_SETTINGS_KEY } from "@/types/naming-settings"; @@ -29,6 +29,7 @@ function RouteComponent() { clearAll(); // Clear drip conditions on logout pushNotificationService.deactivateToken().catch(() => {}); removeTokensAndLogout(); + clearAllClientStorage(); // Wipe localStorage + sessionStorage on explicit user logout }, [clearAll]); // After logout, navigate once domain routing has resolved (or honor explicit redirect)