Re_Figma_Code/src/contexts/AuthContext.tsx
2025-10-29 19:40:16 +05:30

619 lines
20 KiB
TypeScript

/**
* 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<void>;
logout: () => Promise<void>;
getAccessTokenSilently: () => Promise<string | null>;
refreshTokenSilently: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(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<User | null>(null);
const [error, setError] = useState<Error | null>(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<string | null> => {
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<void> => {
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 <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
/**
* Auth0-based Auth Provider (for production)
*/
function Auth0AuthProvider({ children }: { children: ReactNode }) {
return (
<Auth0Provider
domain="https://dev-830839.oktapreview.com/oauth2/default/v1"
clientId="0oa2j8slwj5S4bG5k0h8"
authorizationParams={{
redirect_uri: window.location.origin + '/login/callback',
}}
onRedirectCallback={(appState) => {
console.log('Auth0 Redirect Callback:', {
appState,
returnTo: appState?.returnTo || window.location.pathname,
});
}}
>
<Auth0ContextWrapper>{children}</Auth0ContextWrapper>
</Auth0Provider>
);
}
/**
* 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<string | null> => {
try {
return await getAuth0Token();
} catch {
return null;
}
};
const refreshTokenSilently = async (): Promise<void> => {
// 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 <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
/**
* Main Auth Provider - conditionally uses backend or Auth0
*/
export function AuthProvider({ children }: AuthProviderProps) {
if (isLocalhost()) {
return <BackendAuthProvider>{children}</BackendAuthProvider>;
}
return <Auth0AuthProvider>{children}</Auth0AuthProvider>;
}
/**
* 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;
}