import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react'; import * as Types from '../types/types'; import { ApiClient, CreateCandidateRequest, CreateEmployerRequest } from '../services/api-client'; import { formatApiRequest, toCamelCase } from '../types/conversion'; // ============================ // Types and Interfaces // ============================ interface AuthState { user: Types.User | null; guest: Types.Guest | null; isAuthenticated: boolean; isLoading: boolean; isInitializing: boolean; error: string | null; mfaResponse: Types.MFARequestResponse | null; } interface LoginRequest { login: string; // email or username password: string; } interface MFAVerificationRequest { email: string; code: string; deviceId: string; rememberDevice?: boolean; } interface EmailVerificationRequest { token: string; } interface ResendVerificationRequest { email: string; } interface PasswordResetRequest { email: string; } // ============================ // Token Storage Constants // ============================ const TOKEN_STORAGE = { ACCESS_TOKEN: 'accessToken', REFRESH_TOKEN: 'refreshToken', USER_DATA: 'userData', TOKEN_EXPIRY: 'tokenExpiry', GUEST_DATA: 'guestData', PENDING_VERIFICATION_EMAIL: 'pendingVerificationEmail' } as const; // ============================ // JWT Utilities // ============================ function parseJwtPayload(token: string): any { try { const base64Url = token.split('.')[1]; const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); const jsonPayload = decodeURIComponent( atob(base64) .split('') .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) .join('') ); return JSON.parse(jsonPayload); } catch (error) { console.error('Failed to parse JWT token:', error); return null; } } function isTokenExpired(token: string): boolean { const payload = parseJwtPayload(token); if (!payload || !payload.exp) { return true; } // Check if token expires within the next 5 minutes (buffer time) const expiryTime = payload.exp * 1000; // Convert to milliseconds const bufferTime = 5 * 60 * 1000; // 5 minutes const currentTime = Date.now(); return currentTime >= (expiryTime - bufferTime); } // ============================ // Storage Utilities with Date Conversion // ============================ function clearStoredAuth(): void { localStorage.removeItem(TOKEN_STORAGE.ACCESS_TOKEN); localStorage.removeItem(TOKEN_STORAGE.REFRESH_TOKEN); localStorage.removeItem(TOKEN_STORAGE.USER_DATA); localStorage.removeItem(TOKEN_STORAGE.TOKEN_EXPIRY); } function prepareUserDataForStorage(user: Types.User): string { try { // Convert dates to ISO strings for storage const userForStorage = formatApiRequest(user); return JSON.stringify(userForStorage); } catch (error) { console.error('Failed to prepare user data for storage:', error); return JSON.stringify(user); // Fallback to direct serialization } } function parseStoredUserData(userDataStr: string): Types.User | null { try { const rawUserData = JSON.parse(userDataStr); // Convert the data using toCamelCase which handles date conversion const convertedData = toCamelCase(rawUserData); return convertedData; } catch (error) { console.error('Failed to parse stored user data:', error); return null; } } function storeAuthData(authResponse: Types.AuthResponse): void { localStorage.setItem(TOKEN_STORAGE.ACCESS_TOKEN, authResponse.accessToken); localStorage.setItem(TOKEN_STORAGE.REFRESH_TOKEN, authResponse.refreshToken); localStorage.setItem(TOKEN_STORAGE.USER_DATA, prepareUserDataForStorage(authResponse.user)); localStorage.setItem(TOKEN_STORAGE.TOKEN_EXPIRY, authResponse.expiresAt.toString()); } function getStoredAuthData(): { accessToken: string | null; refreshToken: string | null; userData: Types.User | null; expiresAt: number | null; } { const accessToken = localStorage.getItem(TOKEN_STORAGE.ACCESS_TOKEN); const refreshToken = localStorage.getItem(TOKEN_STORAGE.REFRESH_TOKEN); const userDataStr = localStorage.getItem(TOKEN_STORAGE.USER_DATA); const expiryStr = localStorage.getItem(TOKEN_STORAGE.TOKEN_EXPIRY); let userData: Types.User | null = null; let expiresAt: number | null = null; try { if (userDataStr) { userData = parseStoredUserData(userDataStr); } if (expiryStr) { expiresAt = parseInt(expiryStr, 10); } } catch (error) { console.error('Failed to parse stored auth data:', error); clearStoredAuth(); } return { accessToken, refreshToken, userData, expiresAt }; } function updateStoredUserData(user: Types.User): void { try { localStorage.setItem(TOKEN_STORAGE.USER_DATA, prepareUserDataForStorage(user)); } catch (error) { console.error('Failed to update stored user data:', error); } } // ============================ // Guest Session Utilities // ============================ function createGuestSession(): Types.Guest { const sessionId = `guest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const guest: Types.Guest = { sessionId, createdAt: new Date(), lastActivity: new Date(), ipAddress: 'unknown', userAgent: navigator.userAgent }; try { localStorage.setItem(TOKEN_STORAGE.GUEST_DATA, JSON.stringify(formatApiRequest(guest))); } catch (error) { console.error('Failed to store guest session:', error); } return guest; } function getStoredGuestData(): Types.Guest | null { try { const guestDataStr = localStorage.getItem(TOKEN_STORAGE.GUEST_DATA); if (guestDataStr) { return toCamelCase(JSON.parse(guestDataStr)); } } catch (error) { console.error('Failed to parse stored guest data:', error); localStorage.removeItem(TOKEN_STORAGE.GUEST_DATA); } return null; } // ============================ // Main Authentication Hook // ============================ function useAuthenticationLogic() { const [authState, setAuthState] = useState({ user: null, guest: null, isAuthenticated: false, isLoading: false, isInitializing: true, error: null, mfaResponse: null, }); const [apiClient] = useState(() => new ApiClient()); const initializationCompleted = useRef(false); // Token refresh function const refreshAccessToken = useCallback(async (refreshToken: string): Promise => { try { const response = await apiClient.refreshToken(refreshToken); return response; } catch (error) { console.error('Token refresh failed:', error); return null; } }, [apiClient]); // Initialize authentication state const initializeAuth = useCallback(async () => { if (initializationCompleted.current) { return; } try { // Initialize guest session first let guest = getStoredGuestData(); if (!guest) { guest = createGuestSession(); } const stored = getStoredAuthData(); // If no stored tokens, user is not authenticated but has guest session if (!stored.accessToken || !stored.refreshToken || !stored.userData) { setAuthState({ user: null, guest, isAuthenticated: false, isLoading: false, isInitializing: false, error: null, mfaResponse: null, }); return; } // Check if access token is expired if (isTokenExpired(stored.accessToken)) { console.log('Access token expired, attempting refresh...'); const refreshResult = await refreshAccessToken(stored.refreshToken); if (refreshResult) { storeAuthData(refreshResult); apiClient.setAuthToken(refreshResult.accessToken); setAuthState({ user: refreshResult.user, guest, isAuthenticated: true, isLoading: false, isInitializing: false, error: null, mfaResponse: null }); console.log('Token refreshed successfully'); } else { console.log('Token refresh failed, clearing stored auth'); clearStoredAuth(); apiClient.clearAuthToken(); setAuthState({ user: null, guest, isAuthenticated: false, isLoading: false, isInitializing: false, error: null, mfaResponse: null }); } } else { // Access token is still valid apiClient.setAuthToken(stored.accessToken); setAuthState({ user: stored.userData, guest, isAuthenticated: true, isLoading: false, isInitializing: false, error: null, mfaResponse: null }); console.log('Restored authentication from stored tokens'); } } catch (error) { console.error('Error initializing auth:', error); clearStoredAuth(); apiClient.clearAuthToken(); const guest = createGuestSession(); setAuthState({ user: null, guest, isAuthenticated: false, isLoading: false, isInitializing: false, error: null, mfaResponse: null }); } finally { initializationCompleted.current = true; } }, [apiClient, refreshAccessToken]); // Run initialization on mount useEffect(() => { initializeAuth(); }, [initializeAuth]); // Set up automatic token refresh useEffect(() => { if (!authState.isAuthenticated) { return; } const stored = getStoredAuthData(); if (!stored.expiresAt) { return; } const expiryTime = stored.expiresAt * 1000; const currentTime = Date.now(); const timeUntilExpiry = expiryTime - currentTime - (5 * 60 * 1000); // 5 minute buffer if (timeUntilExpiry <= 0) { initializeAuth(); return; } const refreshTimer = setTimeout(() => { console.log('Auto-refreshing token before expiry...'); initializeAuth(); }, timeUntilExpiry); return () => clearTimeout(refreshTimer); }, [authState.isAuthenticated, initializeAuth]); // Enhanced login with MFA support const login = useCallback(async (loginData: LoginRequest): Promise => { setAuthState(prev => ({ ...prev, isLoading: true, error: null, mfaResponse: null, mfaData: null })); try { const result = await apiClient.login({ login: loginData.login, password: loginData.password, }); if ('mfaRequired' in result) { // MFA required for new device setAuthState(prev => ({ ...prev, isLoading: false, mfaResponse: result, })); return false; // Login not complete yet } else { // Normal login success const authResponse: Types.AuthResponse = result; storeAuthData(authResponse); apiClient.setAuthToken(authResponse.accessToken); setAuthState(prev => ({ ...prev, user: authResponse.user, isAuthenticated: true, isLoading: false, error: null, mfaResponse: null, })); console.log('Login successful'); return true; } } catch (error: any) { const errorMessage = error instanceof Error ? error.message : 'Network error occurred. Please try again.'; setAuthState(prev => ({ ...prev, isLoading: false, error: errorMessage, mfaResponse: null, })); return false; } }, [apiClient]); // MFA verification const verifyMFA = useCallback(async (mfaData: MFAVerificationRequest): Promise => { setAuthState(prev => ({ ...prev, isLoading: true, error: null })); try { const result = await apiClient.verifyMFA(mfaData); if (result.accessToken) { const authResponse: Types.AuthResponse = result; storeAuthData(authResponse); apiClient.setAuthToken(authResponse.accessToken); setAuthState(prev => ({ ...prev, user: authResponse.user, isAuthenticated: true, isLoading: false, error: null, mfaResponse: null, })); console.log('MFA verification successful'); return true; } return false; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'MFA verification failed'; console.log(errorMessage); setAuthState(prev => ({ ...prev, isLoading: false, error: errorMessage })); return false; } }, [apiClient]); // Resend MFA code const resendMFACode = useCallback(async (email: string, deviceId: string, deviceName: string): Promise => { setAuthState(prev => ({ ...prev, isLoading: true, error: null })); try { await apiClient.requestMFA({ email, password: '', // This would need to be stored securely or re-entered deviceId, deviceName, }); setAuthState(prev => ({ ...prev, isLoading: false })); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Failed to resend MFA code'; setAuthState(prev => ({ ...prev, isLoading: false, error: errorMessage })); return false; } }, [apiClient]); // Clear MFA state const clearMFA = useCallback(() => { setAuthState(prev => ({ ...prev, mfaResponse: null, error: null })); }, []); // Email verification const verifyEmail = useCallback(async (verificationData: EmailVerificationRequest): Promise<{ message: string; userType: string } | null> => { setAuthState(prev => ({ ...prev, isLoading: true, error: null })); try { const result = await apiClient.verifyEmail(verificationData); setAuthState(prev => ({ ...prev, isLoading: false })); return { message: result.message || 'Email verified successfully', userType: result.userType || 'user' }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Email verification failed'; setAuthState(prev => ({ ...prev, isLoading: false, error: errorMessage })); return null; } }, [apiClient]); // Resend email verification const resendEmailVerification = useCallback(async (email: string): Promise => { setAuthState(prev => ({ ...prev, isLoading: true, error: null })); try { await apiClient.resendVerificationEmail({ email }); setAuthState(prev => ({ ...prev, isLoading: false })); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Failed to resend verification email'; setAuthState(prev => ({ ...prev, isLoading: false, error: errorMessage })); return false; } }, [apiClient]); // Store pending verification email const setPendingVerificationEmail = useCallback((email: string) => { localStorage.setItem(TOKEN_STORAGE.PENDING_VERIFICATION_EMAIL, email); }, []); // Get pending verification email const getPendingVerificationEmail = useCallback((): string | null => { return localStorage.getItem(TOKEN_STORAGE.PENDING_VERIFICATION_EMAIL); }, []); const logout = useCallback(() => { clearStoredAuth(); apiClient.clearAuthToken(); // Create new guest session after logout const guest = createGuestSession(); setAuthState(prev => ({ ...prev, user: null, guest, isAuthenticated: false, isLoading: false, error: null, mfaResponse: null, })); console.log('User logged out'); }, [apiClient]); const updateUserData = useCallback((updatedUser: Types.User) => { updateStoredUserData(updatedUser); setAuthState(prev => ({ ...prev, user: updatedUser })); console.log('User data updated'); }, []); const createCandidateAccount = useCallback(async (candidateData: CreateCandidateRequest): Promise => { setAuthState(prev => ({ ...prev, isLoading: true, error: null })); try { const candidate = await apiClient.createCandidate(candidateData); console.log('Candidate created:', candidate); // Store email for potential verification resend setPendingVerificationEmail(candidateData.email); setAuthState(prev => ({ ...prev, isLoading: false })); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Account creation failed'; setAuthState(prev => ({ ...prev, isLoading: false, error: errorMessage })); return false; } }, [apiClient, setPendingVerificationEmail]); const createEmployerAccount = useCallback(async (employerData: CreateEmployerRequest): Promise => { setAuthState(prev => ({ ...prev, isLoading: true, error: null })); try { const employer = await apiClient.createEmployer(employerData); console.log('Employer created:', employer); // Store email for potential verification resend setPendingVerificationEmail(employerData.email); setAuthState(prev => ({ ...prev, isLoading: false })); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Account creation failed'; setAuthState(prev => ({ ...prev, isLoading: false, error: errorMessage })); return false; } }, [apiClient, setPendingVerificationEmail]); const requestPasswordReset = useCallback(async (email: string): Promise => { setAuthState(prev => ({ ...prev, isLoading: true, error: null })); try { await apiClient.requestPasswordReset({ email }); setAuthState(prev => ({ ...prev, isLoading: false })); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Password reset request failed'; setAuthState(prev => ({ ...prev, isLoading: false, error: errorMessage })); return false; } }, [apiClient]); const refreshAuth = useCallback(async (): Promise => { const stored = getStoredAuthData(); if (!stored.refreshToken) { return false; } setAuthState(prev => ({ ...prev, isLoading: true, error: null })); const refreshResult = await refreshAccessToken(stored.refreshToken); if (refreshResult) { storeAuthData(refreshResult); apiClient.setAuthToken(refreshResult.accessToken); setAuthState(prev => ({ ...prev, user: refreshResult.user, isAuthenticated: true, isLoading: false, error: null })); return true; } else { logout(); return false; } }, [refreshAccessToken, logout]); return { ...authState, apiClient, login, logout, verifyMFA, resendMFACode, clearMFA, verifyEmail, resendEmailVerification, setPendingVerificationEmail, getPendingVerificationEmail, createCandidateAccount, createEmployerAccount, requestPasswordReset, refreshAuth, updateUserData }; } // ============================ // Context Provider // ============================ const AuthContext = createContext | null>(null); function AuthProvider({ children }: { children: React.ReactNode }) { const auth = useAuthenticationLogic(); return ( {children} ); } function useAuth() { const context = useContext(AuthContext); if (!context) { throw new Error('useAuth must be used within an AuthProvider'); } return context; } // ============================ // Protected Route Component // ============================ interface ProtectedRouteProps { children: React.ReactNode; fallback?: React.ReactNode; requiredUserType?: Types.UserType; } function ProtectedRoute({ children, fallback =
Please log in to access this page.
, requiredUserType }: ProtectedRouteProps) { const { isAuthenticated, isInitializing, user } = useAuth(); // Show loading while checking stored tokens if (isInitializing) { return
Loading...
; } // Not authenticated if (!isAuthenticated) { return <>{fallback}; } // Check user type if required if (requiredUserType && user?.userType !== requiredUserType) { return
Access denied. Required user type: {requiredUserType}
; } return <>{children}; } export type { AuthState, LoginRequest, MFAVerificationRequest, EmailVerificationRequest, ResendVerificationRequest, PasswordResetRequest } export type { CreateCandidateRequest, CreateEmployerRequest } from '../services/api-client'; export { useAuthenticationLogic, AuthProvider, useAuth, ProtectedRoute }