/** * 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; role?: 'USER' | 'MANAGEMENT' | 'ADMIN'; 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 * Note: Function reserved for future use * @internal - Reserved for future use */ export 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') { // 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); return; } // PRIORITY 2: Check if URL has logout parameter (from redirect) const urlParams = new URLSearchParams(window.location.search); if (urlParams.has('logout') || urlParams.has('okta_logged_out')) { TokenManager.clearAll(); localStorage.clear(); sessionStorage.clear(); setIsAuthenticated(false); setUser(null); setIsLoading(false); // Clean URL but preserve okta_logged_out flag if it exists (for prompt=login) const cleanParams = new URLSearchParams(); if (urlParams.has('okta_logged_out')) { cleanParams.set('okta_logged_out', 'true'); } const newUrl = cleanParams.toString() ? `/?${cleanParams.toString()}` : '/'; window.history.replaceState({}, document.title, newUrl); return; } // PRIORITY 3: Skip auth check if on callback page - let callback handler process first // This is critical for production mode where we need to exchange code for tokens // before we can verify session with server if (window.location.pathname === '/login/callback') { // Don't check auth status here - let the callback handler do its job // The callback handler will set isAuthenticated after successful token exchange return; } // PRIORITY 4: Check authentication status const token = TokenManager.getAccessToken(); const refreshToken = TokenManager.getRefreshToken(); const userData = TokenManager.getUserData(); const hasAuthData = token || refreshToken || userData; // Check if we're in production mode (tokens in httpOnly cookies, not accessible to JS) const isProductionMode = import.meta.env.PROD || import.meta.env.MODE === 'production'; // In production: Always verify with server (cookies are sent automatically) // In development: Check local auth data first if (isProductionMode) { // Production: Verify session with server via httpOnly cookie if (!isLoggingOut) { checkAuthStatus(); } else { setIsLoading(false); } } else { // Development: If no auth data exists, user is not authenticated if (!hasAuthData) { setIsAuthenticated(false); setUser(null); setIsLoading(false); return; } // PRIORITY 5: Only check auth status if we have some auth data AND we're not logging out if (!isLoggingOut) { checkAuthStatus(); } else { setIsLoading(false); } } }, [isLoggingOut]); // Silent refresh interval useEffect(() => { if (!isAuthenticated) return; const isProductionMode = import.meta.env.PROD || import.meta.env.MODE === 'production'; const checkAndRefresh = async () => { if (isProductionMode) { // In production, proactively refresh the session every 10 minutes // The httpOnly cookie will be sent automatically try { await refreshTokenSilently(); } catch (error) { console.error('Silent refresh failed:', error); } } else { // In development, check token expiration 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 10 minutes in production, 5 minutes in development const intervalMs = isProductionMode ? 10 * 60 * 1000 : 5 * 60 * 1000; const interval = setInterval(checkAndRefresh, intervalMs); 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`; 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) { setIsLoading(false); return; } const isProductionMode = import.meta.env.PROD || import.meta.env.MODE === 'production'; try { setIsLoading(true); // PRODUCTION MODE: Verify session via httpOnly cookie // The cookie is sent automatically with the request (withCredentials: true) if (isProductionMode) { const storedUser = TokenManager.getUserData(); // Try to get current user from server - this validates the httpOnly cookie try { const userData = await getCurrentUser(); setUser(userData); TokenManager.setUserData(userData); setIsAuthenticated(true); } catch (error: any) { // If 401, try to refresh the token (refresh token is also in httpOnly cookie) if (error?.response?.status === 401) { try { await refreshTokenSilently(); // Retry getting user after refresh const userData = await getCurrentUser(); setUser(userData); TokenManager.setUserData(userData); setIsAuthenticated(true); } catch { // Refresh failed - clear user data and show login TokenManager.clearAll(); setIsAuthenticated(false); setUser(null); } } else if (error?.isConnectionError) { // Backend not reachable - use stored user data if available if (storedUser) { setUser(storedUser); setIsAuthenticated(true); } else { setIsAuthenticated(false); setUser(null); } } else { // Other error - clear and show login TokenManager.clearAll(); setIsAuthenticated(false); setUser(null); } } return; } // DEVELOPMENT MODE: Check local token const token = TokenManager.getAccessToken(); const storedUser = TokenManager.getUserData(); // If no token at all, user is not authenticated if (!token) { 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 = import.meta.env.VITE_OKTA_DOMAIN || 'https://dev-830839.oktapreview.com'; const clientId = import.meta.env.VITE_OKTA_CLIENT_ID || '0oa2jgzvrpdwx2iqd0h8'; const redirectUri = `${window.location.origin}/login/callback`; const responseType = 'code'; const scope = 'openid profile email'; const state = Math.random().toString(36).substring(7); // Check if we're coming from a logout - if so, add prompt=login to force re-authentication const urlParams = new URLSearchParams(window.location.search); const isAfterLogout = urlParams.has('logout') || urlParams.has('okta_logged_out'); let authUrl = `${oktaDomain}/oauth2/default/v1/authorize?` + `client_id=${clientId}&` + `redirect_uri=${encodeURIComponent(redirectUri)}&` + `response_type=${responseType}&` + `scope=${encodeURIComponent(scope)}&` + `state=${state}`; // Add prompt=login if coming from logout to force re-authentication // This ensures Okta requires login even if a session still exists if (isAfterLogout) { authUrl += `&prompt=login`; } window.location.href = authUrl; } catch (err: any) { setError(err); throw err; } }; const logout = async () => { try { // CRITICAL: Get id_token from TokenManager before clearing anything // Okta logout endpoint works better with id_token_hint to properly end the session // Note: Currently not used but kept for future Okta integration void TokenManager.getIdToken(); // Set logout flag to prevent auto-authentication after redirect // This must be set BEFORE clearing storage so it survives sessionStorage.setItem('__logout_in_progress__', 'true'); sessionStorage.setItem('__force_logout__', 'true'); setIsLoggingOut(true); // Reset auth state FIRST to prevent any re-authentication setIsAuthenticated(false); setUser(null); setError(null); setIsLoading(true); // Set loading to prevent checkAuthStatus from running // 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 { await logoutApi(); } 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 EXCEPT the logout flags and id_token (we need it for Okta logout) // Clear tokens but preserve logout flags const logoutInProgress = sessionStorage.getItem('__logout_in_progress__'); const forceLogout = sessionStorage.getItem('__force_logout__'); // Use TokenManager.clearAll() but then restore logout flags TokenManager.clearAll(); // Restore logout flags immediately after clearAll if (logoutInProgress) sessionStorage.setItem('__logout_in_progress__', logoutInProgress); if (forceLogout) sessionStorage.setItem('__force_logout__', forceLogout); // Small delay to ensure sessionStorage is written before redirect await new Promise(resolve => setTimeout(resolve, 100)); // Redirect directly to login page with flags // The okta_logged_out flag will trigger prompt=login in the login() function // This forces re-authentication even if Okta session still exists const loginUrl = `${window.location.origin}/?okta_logged_out=true&logout=${Date.now()}`; window.location.replace(loginUrl); } catch (error) { console.error('🚪 Logout error:', error); // Force redirect even on error - clear everything and redirect to login try { localStorage.clear(); sessionStorage.clear(); sessionStorage.setItem('__logout_in_progress__', 'true'); const loginUrl = `${window.location.origin}/?okta_logged_out=true&logout=${Date.now()}`; window.location.replace(loginUrl); } catch { // Last resort - redirect to home window.location.replace('/?logout=' + Date.now()); } } }; const getAccessTokenSilently = async (): Promise => { const isProductionMode = import.meta.env.PROD || import.meta.env.MODE === 'production'; // In production mode, tokens are in httpOnly cookies // We can't access them directly, but API calls will include them automatically if (isProductionMode) { // If user is authenticated, return a placeholder indicating cookies are used // The actual token is in httpOnly cookie and sent automatically with requests if (isAuthenticated) { return 'cookie-based-auth'; // Placeholder - actual auth via cookies } // Try to refresh the session try { await refreshTokenSilently(); return isAuthenticated ? 'cookie-based-auth' : null; } catch { return null; } } // Development mode: tokens in localStorage 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 => { const isProductionMode = import.meta.env.PROD || import.meta.env.MODE === 'production'; try { const newToken = await refreshAccessToken(); // In production, refresh might not return token (it's in httpOnly cookie) // but if the call succeeded, the session is valid if (isProductionMode) { // Session refreshed via cookies return; } if (newToken) { // Token refreshed successfully (development mode) 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) * Note: Reserved for future use when Auth0 integration is needed * @internal - Reserved for future use */ export function _Auth0AuthProvider({ children }: { children: ReactNode }) { return ( { // Auth0 redirect callback handled }} > {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 - uses backend for token exchange in all environments * Backend handles Okta token exchange securely with client secret */ export function AuthProvider({ children }: AuthProviderProps) { // Always use BackendAuthProvider so backend handles token exchange // This keeps the client secret secure on the server 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; } /** * Helper function to check if user is admin */ export function isAdmin(user: User | null): boolean { return user?.role === 'ADMIN'; } /** * Helper function to check if user is management */ export function isManagement(user: User | null): boolean { return user?.role === 'MANAGEMENT'; } /** * Helper function to check if user has management access (MANAGEMENT or ADMIN) */ export function hasManagementAccess(user: User | null): boolean { return user?.role === 'MANAGEMENT' || user?.role === 'ADMIN'; } /** * Helper function to check if user has admin access (ADMIN only) */ export function hasAdminAccess(user: User | null): boolean { return user?.role === 'ADMIN'; }