diff --git a/src/App.tsx b/src/App.tsx index 2b1094e..6274510 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,6 +16,7 @@ import { Toaster } from '@/components/ui/sonner'; import { toast } from 'sonner'; import { CUSTOM_REQUEST_DATABASE } from '@/utils/customRequestDatabase'; import { CLAIM_MANAGEMENT_DATABASE } from '@/utils/claimManagementDatabase'; +import { AuthCallback } from '@/pages/Auth/AuthCallback'; // Combined Request Database for backward compatibility // This combines both custom and claim management requests @@ -476,6 +477,12 @@ function AppRoutes({ onLogout }: AppProps) { return (
+ {/* Auth Callback - Must be before other routes */} + } + /> + {/* Dashboard */} { - 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); + // Store id_token temporarily if we have it + let tempIdToken: string | null = null; + if (idToken) { + tempIdToken = idToken; + console.log('🚪 Preserving id_token for Okta logout:', tempIdToken.substring(0, 20) + '...'); } + // 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); + + console.log('🚪 Local storage cleared (logout flags preserved)'); + // Final verification BEFORE redirect - console.log('All authentication data cleared. Final verification:'); - console.log(`localStorage.length: ${localStorage.length}`); - console.log(`sessionStorage.length: ${sessionStorage.length}`); + console.log('🚪 Final verification - logout flags preserved:', { + logoutInProgress: sessionStorage.getItem('__logout_in_progress__'), + forceLogout: sessionStorage.getItem('__force_logout__'), + }); - 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'); + console.log('🚪 Clearing local session and redirecting to login...'); + console.log('🚪 Using prompt=login on next auth to force re-authentication'); + console.log('🚪 This will prevent auto-authentication even if Okta session exists'); // Small delay to ensure sessionStorage is written before redirect - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise(resolve => setTimeout(resolve, 100)); - // 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); - } + // 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 - localStorage.clear(); - sessionStorage.clear(); - window.location.replace('/'); + 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()); + } } }; diff --git a/src/pages/Auth/AuthCallback.tsx b/src/pages/Auth/AuthCallback.tsx index 8882bb9..3641259 100644 --- a/src/pages/Auth/AuthCallback.tsx +++ b/src/pages/Auth/AuthCallback.tsx @@ -1,47 +1,186 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { useAuth } from '@/contexts/AuthContext'; -import { Loader } from '@/components/common/Loader'; -import { useNavigate } from 'react-router-dom'; +import { CheckCircle2, AlertCircle, Loader2 } from 'lucide-react'; export function AuthCallback() { const { isAuthenticated, isLoading, error, user } = useAuth(); - const navigate = useNavigate(); + const [authStep, setAuthStep] = useState<'exchanging' | 'fetching' | 'complete' | 'error'>('exchanging'); useEffect(() => { - console.log('AuthCallback Component Mounted'); - console.log('Auth State during callback:', { - isAuthenticated, - isLoading, - error: error?.message, - hasUser: !!user, - timestamp: new Date().toISOString() - }); - - if (user) { - console.log('User authenticated successfully:', { - userId: user.userId, - employeeId: user.employeeId, - email: user.email, - displayName: user.displayName, - userData: user - }); - // Redirect to home after successful authentication - navigate('/'); - } - + // Determine current authentication step based on state if (error) { - console.error('Error during authentication callback:', error); + setAuthStep('error'); + return; } - }, [isAuthenticated, isLoading, error, user, navigate]); + + if (isLoading) { + // If we have code in URL but no user yet, we're exchanging + const urlParams = new URLSearchParams(window.location.search); + const hasCode = urlParams.get('code'); + + if (hasCode && !user) { + setAuthStep('exchanging'); + } else if (user && !isAuthenticated) { + setAuthStep('fetching'); + } else { + setAuthStep('exchanging'); + } + } else if (user && isAuthenticated) { + setAuthStep('complete'); + // Small delay before redirect for better UX + const timer = setTimeout(() => { + // Use window.location instead of navigate since Router context isn't available yet + window.location.href = '/'; + }, 1500); + return () => clearTimeout(timer); + } + }, [isAuthenticated, isLoading, error, user]); - if (error) { - console.error('AuthCallback Error:', error); - } + // Get dynamic loading message + const getLoadingMessage = () => { + switch (authStep) { + case 'exchanging': + return 'Exchanging authorization code...'; + case 'fetching': + return 'Fetching your profile...'; + case 'complete': + return 'Authentication successful!'; + case 'error': + return 'Authentication failed'; + default: + return 'Completing authentication...'; + } + }; return ( -
- +
+
+ +
+ {/* Logo/Brand Section */} +
+
+ + + +
+

Royal Enfield

+

Approval Portal

+
+ + {/* Main Loader Card */} +
+ {/* Status Icon */} +
+ {authStep === 'error' ? ( +
+
+ +
+ +
+ ) : authStep === 'complete' ? ( +
+
+ +
+ +
+ ) : ( +
+ + {/* Outer rotating ring */} +
+
+
+ )} +
+ + {/* Loading Message */} +
+

+ {authStep === 'complete' ? 'Welcome Back!' : authStep === 'error' ? 'Authentication Error' : 'Authenticating'} +

+

{getLoadingMessage()}

+
+ + {/* Progress Steps */} + {authStep !== 'error' && authStep !== 'complete' && ( +
+
+
+ Validating credentials +
+
+
+ Loading your profile +
+
+
+ Setting up your session +
+
+ )} + + {/* Error Message */} + {authStep === 'error' && error && ( +
+

{error.message || 'An error occurred during authentication'}

+ +
+ )} + + {/* Animated Progress Bar */} + {authStep !== 'error' && authStep !== 'complete' && ( +
+
+
+
+ +
+ )} +
+ + {/* Footer Text */} +

+ {authStep === 'complete' ? 'Redirecting to dashboard...' : 'Please wait while we secure your session'} +

+
+ + {/* Animated Background Elements */} +
+
+
+
); } - diff --git a/src/pages/Auth/AuthenticatedApp.tsx b/src/pages/Auth/AuthenticatedApp.tsx index 1f8f3e8..7418511 100644 --- a/src/pages/Auth/AuthenticatedApp.tsx +++ b/src/pages/Auth/AuthenticatedApp.tsx @@ -9,6 +9,9 @@ export function AuthenticatedApp() { const { isAuthenticated, isLoading, error, user, logout } = useAuth(); const [showDebugInfo, setShowDebugInfo] = useState(false); + // Check if we're on callback route (after all hooks are called) + const isCallbackRoute = typeof window !== 'undefined' && window.location.pathname === '/login/callback'; + const handleLogout = async () => { console.log('🔵 ========================================'); console.log('🔵 AuthenticatedApp.handleLogout - CALLED'); @@ -70,6 +73,11 @@ export function AuthenticatedApp() { } }, [isAuthenticated, isLoading, error, user]); + // Always show callback loader when on callback route (after all hooks) + if (isCallbackRoute) { + return ; + } + // Show loading state while checking authentication if (isLoading) { console.log('Auth0 is still loading...'); diff --git a/src/services/authApi.ts b/src/services/authApi.ts index ca9ebc0..4526ee8 100644 --- a/src/services/authApi.ts +++ b/src/services/authApi.ts @@ -86,6 +86,7 @@ export interface TokenExchangeResponse { }; accessToken: string; refreshToken: string; + idToken?: string; // ID token from Okta for logout } export interface RefreshTokenResponse { @@ -154,6 +155,11 @@ export async function exchangeCodeForTokens( TokenManager.setAccessToken(result.accessToken); TokenManager.setRefreshToken(result.refreshToken); TokenManager.setUserData(result.user); + // Store id_token if available (needed for proper Okta logout) + if (result.idToken) { + TokenManager.setIdToken(result.idToken); + console.log('✅ ID token stored for logout'); + } console.log('✅ Tokens stored successfully'); } else { console.warn('⚠️ Tokens missing in response', { result }); diff --git a/src/utils/tokenManager.ts b/src/utils/tokenManager.ts index 4d5d11d..910b4f7 100644 --- a/src/utils/tokenManager.ts +++ b/src/utils/tokenManager.ts @@ -5,6 +5,7 @@ const ACCESS_TOKEN_KEY = 'accessToken'; const REFRESH_TOKEN_KEY = 'refreshToken'; +const ID_TOKEN_KEY = 'idToken'; const USER_DATA_KEY = 'userData'; /** @@ -49,7 +50,7 @@ export const cookieUtils = { * Attempts to clear cookies with different paths and domains */ clearAll(): void { - const cookieNames = [ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY, USER_DATA_KEY]; + const cookieNames = [ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY, ID_TOKEN_KEY, USER_DATA_KEY]; cookieNames.forEach(name => { // Remove with default path @@ -119,6 +120,27 @@ export class TokenManager { return localStorage.getItem(REFRESH_TOKEN_KEY); } + /** + * Store ID token (from Okta) + */ + static setIdToken(token: string): void { + localStorage.setItem(ID_TOKEN_KEY, token); + if (this.isLocalhost()) { + cookieUtils.set(ID_TOKEN_KEY, token, 1); // 1 day + } + } + + /** + * Get ID token + */ + static getIdToken(): string | null { + if (this.isLocalhost()) { + // Try cookie first, then localStorage + return cookieUtils.get(ID_TOKEN_KEY) || localStorage.getItem(ID_TOKEN_KEY); + } + return localStorage.getItem(ID_TOKEN_KEY); + } + /** * Store user data */ @@ -164,6 +186,7 @@ export class TokenManager { const authKeys = [ ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY, + ID_TOKEN_KEY, USER_DATA_KEY, 'oktaToken', 'authToken', @@ -172,6 +195,7 @@ export class TokenManager { 'access_token', 'refresh_token', 'id_token', + 'idToken', 'token', 'accessToken', 'refreshToken',