/** * Authentication Context * Provides unified authentication interface that works with both: * - Backend token exchange (localhost/development) * - Auth0 Provider (production) */ import { createContext, useContext, useEffect, useState, ReactNode, useRef } from 'react'; import { Auth0Provider, useAuth0 as useAuth0Hook } from '@auth0/auth0-react'; import { TokenManager, isTokenExpired } from '../utils/tokenManager'; import { exchangeCodeForTokens, refreshAccessToken, getCurrentUser, logout as logoutApi } from '../services/authApi'; interface User { userId?: string; employeeId?: string; email?: string; firstName?: string; lastName?: string; displayName?: string; department?: string; designation?: string; isAdmin?: boolean; sub?: string; name?: string; picture?: string; nickname?: string; } interface AuthContextType { isAuthenticated: boolean; isLoading: boolean; user: User | null; error: Error | null; login: () => Promise; logout: () => Promise; getAccessTokenSilently: () => Promise; refreshTokenSilently: () => Promise; } const AuthContext = createContext(undefined); interface AuthProviderProps { children: ReactNode; } /** * Check if running on localhost */ const isLocalhost = (): boolean => { return ( window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || window.location.hostname === '' ); }; /** * Backend-based Auth Provider (for localhost) */ function BackendAuthProvider({ children }: { children: ReactNode }) { const [isAuthenticated, setIsAuthenticated] = useState(false); const [isLoading, setIsLoading] = useState(true); const [user, setUser] = useState(null); const [error, setError] = useState(null); const [isLoggingOut, setIsLoggingOut] = useState(false); // Check authentication status on mount // Only check if we're not in the middle of a logout process useEffect(() => { // PRIORITY 1: Check for logout flags in sessionStorage (survives page reload during logout) const logoutFlag = sessionStorage.getItem('__logout_in_progress__'); const forceLogout = sessionStorage.getItem('__force_logout__'); if (logoutFlag === 'true' || forceLogout === 'true') { console.log('🔴 Logout flag detected - PREVENTING auto-authentication'); console.log('🔴 Clearing ALL authentication data and showing login screen'); // Remove flags sessionStorage.removeItem('__logout_in_progress__'); sessionStorage.removeItem('__force_logout__'); // Clear all tokens one more time (aggressive) TokenManager.clearAll(); // Also manually clear everything try { localStorage.clear(); sessionStorage.clear(); } catch (e) { console.error('Error clearing storage:', e); } // Set unauthenticated state setIsAuthenticated(false); setUser(null); setIsLoading(false); setError(null); console.log('🔴 Logout complete - user should see login screen'); return; } // PRIORITY 2: Check if URL has logout parameter (from redirect) const urlParams = new URLSearchParams(window.location.search); if (urlParams.has('logout')) { console.log('🔴 Logout parameter in URL - clearing everything'); TokenManager.clearAll(); localStorage.clear(); sessionStorage.clear(); setIsAuthenticated(false); setUser(null); setIsLoading(false); // Clean URL window.history.replaceState({}, document.title, '/'); return; } // PRIORITY 3: Check if this is a logout redirect by checking if all tokens are cleared const token = TokenManager.getAccessToken(); const refreshToken = TokenManager.getRefreshToken(); const userData = TokenManager.getUserData(); const hasAuthData = token || refreshToken || userData; // If no auth data exists, we're likely after a logout - set unauthenticated state immediately if (!hasAuthData) { console.log('🔴 No auth data found - setting unauthenticated state'); setIsAuthenticated(false); setUser(null); setIsLoading(false); return; } // PRIORITY 4: Only check auth status if we have some auth data AND we're not logging out if (!isLoggingOut) { checkAuthStatus(); } else { console.log('🔴 Skipping checkAuthStatus - logout in progress'); setIsLoading(false); } }, [isLoggingOut]); // Silent refresh interval useEffect(() => { if (!isAuthenticated) return; const checkAndRefresh = async () => { const token = TokenManager.getAccessToken(); if (token && isTokenExpired(token, 5)) { // Token expires in less than 5 minutes, refresh it try { await refreshTokenSilently(); } catch (error) { console.error('Silent refresh failed:', error); } } }; // Check every 5 minutes const interval = setInterval(checkAndRefresh, 5 * 60 * 1000); return () => clearInterval(interval); }, [isAuthenticated]); // Handle callback from OAuth redirect // Use ref to prevent duplicate calls (React StrictMode runs effects twice in dev) const callbackProcessedRef = useRef(false); useEffect(() => { // Skip if already processed or not on callback page if (callbackProcessedRef.current || window.location.pathname !== '/login/callback') { return; } const handleCallback = async () => { // Mark as processed immediately to prevent duplicate calls callbackProcessedRef.current = true; const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get('code'); const errorParam = urlParams.get('error'); // Clean URL immediately to prevent re-running on re-renders window.history.replaceState({}, document.title, '/login/callback'); if (errorParam) { setError(new Error(`Authentication error: ${errorParam}`)); setIsLoading(false); return; } if (!code) { setIsLoading(false); return; } try { setIsLoading(true); setIsAuthenticated(false); setError(null); // IMPORTANT: redirectUri must match the one used in initial Okta authorization request // This is the frontend callback URL, NOT the backend URL // Backend will use this same URI when exchanging code with Okta const redirectUri = `${window.location.origin}/login/callback`; console.log('📥 Authorization Code Received:', { code: code.substring(0, 10) + '...', redirectUri, fullUrl: window.location.href, note: 'redirectUri is frontend URL (not backend) - must match Okta registration', }); const result = await exchangeCodeForTokens(code, redirectUri); setUser(result.user); setIsAuthenticated(true); setError(null); // Clean URL after success window.history.replaceState({}, document.title, '/'); } catch (err: any) { console.error('❌ Token exchange error in AuthContext:', err); setError(err); setIsAuthenticated(false); setUser(null); // Reset ref on error so user can retry if needed callbackProcessedRef.current = false; } finally { setIsLoading(false); } }; handleCallback(); }, []); // Empty deps - only run once on mount const checkAuthStatus = async () => { // Don't check auth status if we're in the middle of logging out if (isLoggingOut) { console.log('🔴 Skipping checkAuthStatus - logout in progress'); setIsLoading(false); return; } try { setIsLoading(true); const token = TokenManager.getAccessToken(); const storedUser = TokenManager.getUserData(); console.log('🔍 Checking auth status:', { hasToken: !!token, hasUser: !!storedUser, isLoggingOut }); // If no token at all, user is not authenticated if (!token) { console.log('🔍 No token found - setting unauthenticated'); setIsAuthenticated(false); setUser(null); setIsLoading(false); return; } // Check if token is expired if (isTokenExpired(token)) { // Token expired, try to refresh try { await refreshTokenSilently(); // Refresh succeeded, check again const newToken = TokenManager.getAccessToken(); if (newToken && !isTokenExpired(newToken)) { const refreshedUser = TokenManager.getUserData(); if (refreshedUser) { setUser(refreshedUser); setIsAuthenticated(true); } else { // Fetch user data try { const userData = await getCurrentUser(); setUser(userData); TokenManager.setUserData(userData); setIsAuthenticated(true); } catch { // Token might be invalid, clear everything TokenManager.clearAll(); setIsAuthenticated(false); setUser(null); } } } else { // Refresh failed, user is logged out TokenManager.clearAll(); setIsAuthenticated(false); setUser(null); } } catch { // Refresh failed, clear everything TokenManager.clearAll(); setIsAuthenticated(false); setUser(null); } } else { // Token is valid if (storedUser) { setUser(storedUser); setIsAuthenticated(true); } else { // Fetch user data to ensure token is still valid try { const userData = await getCurrentUser(); setUser(userData); TokenManager.setUserData(userData); setIsAuthenticated(true); } catch { // Token might be invalid, clear everything TokenManager.clearAll(); setIsAuthenticated(false); setUser(null); } } } } catch (err: any) { console.error('Error checking auth status:', err); setError(err); TokenManager.clearAll(); setIsAuthenticated(false); setUser(null); } finally { setIsLoading(false); } }; const login = async () => { try { setError(null); // Redirect to Okta login const oktaDomain = 'https://dev-830839.oktapreview.com'; const clientId = '0oa2j8slwj5S4bG5k0h8'; const redirectUri = `${window.location.origin}/login/callback`; const responseType = 'code'; const scope = 'openid profile email'; const state = Math.random().toString(36).substring(7); const authUrl = `${oktaDomain}/oauth2/default/v1/authorize?` + `client_id=${clientId}&` + `redirect_uri=${encodeURIComponent(redirectUri)}&` + `response_type=${responseType}&` + `scope=${encodeURIComponent(scope)}&` + `state=${state}`; window.location.href = authUrl; } catch (err: any) { setError(err); throw err; } }; const logout = async () => { console.log('🚪 LOGOUT FUNCTION CALLED - Starting logout process'); console.log('🚪 Current auth state:', { isAuthenticated, hasUser: !!user, isLoading }); try { // Set logout flag to prevent auto-authentication after redirect sessionStorage.setItem('__logout_in_progress__', 'true'); setIsLoggingOut(true); console.log('🚪 Step 1: Resetting auth state...'); // Reset auth state FIRST to prevent any re-authentication setIsAuthenticated(false); setUser(null); setError(null); setIsLoading(true); // Set loading to prevent checkAuthStatus from running console.log('🚪 Step 1: Auth state reset complete'); // Call backend logout API to clear server-side session and httpOnly cookies // IMPORTANT: This MUST be called before clearing local storage to clear httpOnly cookies try { console.log('🚪 Step 2: Calling backend logout API to clear httpOnly cookies...'); await logoutApi(); console.log('🚪 Step 2: Backend logout API completed - httpOnly cookies should be cleared'); } catch (err) { console.error('🚪 Logout API error:', err); console.warn('🚪 Backend logout failed - httpOnly cookies may not be cleared'); // Continue with logout even if API call fails } // Clear all authentication data console.log('========================================'); console.log('LOGOUT - Clearing all authentication data'); console.log('========================================'); // Use TokenManager.clearAll() which does comprehensive cleanup TokenManager.clearAll(); console.log('tryng to clrear all(localStorage and sessionStorage)') // Double-check: Clear everything one more time as backup try { // Get all localStorage keys and remove them const localStorageKeys: string[] = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key) localStorageKeys.push(key); } localStorageKeys.forEach(key => { try { localStorage.removeItem(key); } catch (e) { console.warn(`Failed to remove localStorage key ${key}:`, e); } }); localStorage.clear(); // Get all sessionStorage keys and remove them const sessionStorageKeys: string[] = []; for (let i = 0; i < sessionStorage.length; i++) { const key = sessionStorage.key(i); if (key) sessionStorageKeys.push(key); } sessionStorageKeys.forEach(key => { try { sessionStorage.removeItem(key); } catch (e) { console.warn(`Failed to remove sessionStorage key ${key}:`, e); } }); sessionStorage.clear(); console.log('Final verification - Storage cleared:'); console.log(`localStorage.length: ${localStorage.length}`); console.log(`sessionStorage.length: ${sessionStorage.length}`); if (localStorage.length > 0) { console.error('ERROR: localStorage still has items:', Object.keys(localStorage)); } if (sessionStorage.length > 0) { console.error('ERROR: sessionStorage still has items:', Object.keys(sessionStorage)); } } catch (e) { console.error('Error in final cleanup:', e); } // Final verification BEFORE redirect console.log('All authentication data cleared. Final verification:'); console.log(`localStorage.length: ${localStorage.length}`); console.log(`sessionStorage.length: ${sessionStorage.length}`); if (localStorage.length > 0) { console.error('CRITICAL: localStorage still has items before redirect!', Object.keys(localStorage)); // Force clear one more time const remainingKeys = Object.keys(localStorage); remainingKeys.forEach(key => localStorage.removeItem(key)); localStorage.clear(); } if (sessionStorage.length > 0) { console.error('CRITICAL: sessionStorage still has items before redirect!', Object.keys(sessionStorage)); const remainingKeys = Object.keys(sessionStorage); remainingKeys.forEach(key => sessionStorage.removeItem(key)); sessionStorage.clear(); } console.log('Redirecting to login...'); // Ensure logout flag is set before redirect sessionStorage.setItem('__logout_in_progress__', 'true'); // Small delay to ensure sessionStorage is written before redirect await new Promise(resolve => setTimeout(resolve, 50)); // Use replace instead of href to prevent browser history issues // DON'T use setTimeout - redirect immediately after clearing if (isLocalhost()) { // Direct redirect to login page for localhost - force full page reload // Add timestamp to force fresh page load window.location.replace('/?logout=' + Date.now()); } else { // For production, redirect to Okta logout then to login const oktaDomain = 'https://dev-830839.oktapreview.com'; const loginUrl = `${window.location.origin}/?logout=${Date.now()}`; const logoutUrl = `${oktaDomain}/oauth2/default/v1/logout?post_logout_redirect_uri=${encodeURIComponent(loginUrl)}`; window.location.replace(logoutUrl); } } catch (error) { console.error('Logout error:', error); // Force redirect even on error localStorage.clear(); sessionStorage.clear(); window.location.replace('/'); } }; const getAccessTokenSilently = async (): Promise => { const token = TokenManager.getAccessToken(); if (token && !isTokenExpired(token)) { return token; } // Try to refresh try { await refreshTokenSilently(); return TokenManager.getAccessToken(); } catch { return null; } }; const refreshTokenSilently = async (): Promise => { try { const newToken = await refreshAccessToken(); if (newToken) { // Token refreshed successfully return; } throw new Error('Failed to refresh token'); } catch (err) { TokenManager.clearAll(); setIsAuthenticated(false); setUser(null); throw err; } }; const value: AuthContextType = { isAuthenticated, isLoading, user, error, login, logout, getAccessTokenSilently, refreshTokenSilently, }; return {children}; } /** * Auth0-based Auth Provider (for production) */ function Auth0AuthProvider({ children }: { children: ReactNode }) { return ( { console.log('Auth0 Redirect Callback:', { appState, returnTo: appState?.returnTo || window.location.pathname, }); }} > {children} ); } /** * Wrapper to convert Auth0 hook to our context format */ function Auth0ContextWrapper({ children }: { children: ReactNode }) { const { isAuthenticated: auth0IsAuthenticated, isLoading: auth0IsLoading, user: auth0User, error: auth0Error, loginWithRedirect, logout: logoutAuth0, getAccessTokenSilently: getAuth0Token, } = useAuth0Hook(); const getAccessTokenSilently = async (): Promise => { try { return await getAuth0Token(); } catch { return null; } }; const refreshTokenSilently = async (): Promise => { // Auth0 handles refresh automatically try { await getAuth0Token(); } catch { // Silently fail, Auth0 will handle it } }; const value: AuthContextType = { isAuthenticated: auth0IsAuthenticated, isLoading: auth0IsLoading, user: auth0User as User | null, error: auth0Error as Error | null, login: loginWithRedirect, logout: logoutAuth0, getAccessTokenSilently, refreshTokenSilently, }; return {children}; } /** * Main Auth Provider - conditionally uses backend or Auth0 */ export function AuthProvider({ children }: AuthProviderProps) { if (isLocalhost()) { return {children}; } return {children}; } /** * Hook to use auth context */ export function useAuth(): AuthContextType { const context = useContext(AuthContext); if (context === undefined) { throw new Error('useAuth must be used within an AuthProvider'); } return context; }