888 lines
26 KiB
TypeScript
888 lines
26 KiB
TypeScript
// Replace the existing AuthContext.tsx with these enhancements
|
|
|
|
import React, {
|
|
createContext,
|
|
useContext,
|
|
useState,
|
|
useCallback,
|
|
useEffect,
|
|
useRef,
|
|
JSX,
|
|
} from 'react';
|
|
import * as Types from '../types/types';
|
|
import { ApiClient, CreateEmployerRequest, GuestConversionRequest } from 'services/api-client';
|
|
import { formatApiRequest, toCamelCase } from 'types/conversion';
|
|
|
|
// ============================
|
|
// Enhanced Types and Interfaces
|
|
// ============================
|
|
|
|
interface AuthState {
|
|
user: Types.User | null;
|
|
guest: Types.Guest | null;
|
|
isAuthenticated: boolean;
|
|
isGuest: boolean;
|
|
isLoading: boolean;
|
|
isInitializing: boolean;
|
|
error: string | null;
|
|
mfaResponse: Types.MFARequestResponse | null;
|
|
}
|
|
|
|
interface LoginRequest {
|
|
login: string; // email or username
|
|
password: string;
|
|
}
|
|
|
|
interface EmailVerificationRequest {
|
|
token: string;
|
|
}
|
|
|
|
interface ResendVerificationRequest {
|
|
email: string;
|
|
}
|
|
|
|
interface PasswordResetRequest {
|
|
email: string;
|
|
}
|
|
|
|
// ============================
|
|
// Enhanced Token Storage Constants
|
|
// ============================
|
|
|
|
const TOKEN_STORAGE = {
|
|
ACCESS_TOKEN: 'accessToken',
|
|
REFRESH_TOKEN: 'refreshToken',
|
|
USER_DATA: 'userData',
|
|
TOKEN_EXPIRY: 'tokenExpiry',
|
|
USER_TYPE: 'userType',
|
|
IS_GUEST: 'isGuest',
|
|
PENDING_VERIFICATION_EMAIL: 'pendingVerificationEmail',
|
|
} as const;
|
|
|
|
// ============================
|
|
// JWT Utilities
|
|
// ============================
|
|
|
|
type JwtPayload = {
|
|
exp?: number; // Expiry time in seconds
|
|
};
|
|
|
|
function parseJwtPayload(token: string): JwtPayload | null {
|
|
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;
|
|
}
|
|
|
|
// ============================
|
|
// Enhanced Storage Utilities
|
|
// ============================
|
|
|
|
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);
|
|
localStorage.removeItem(TOKEN_STORAGE.USER_TYPE);
|
|
localStorage.removeItem(TOKEN_STORAGE.IS_GUEST);
|
|
}
|
|
|
|
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<Types.User>(rawUserData);
|
|
|
|
return convertedData;
|
|
} catch (error) {
|
|
console.error('Failed to parse stored user data:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
function storeAuthData(authResponse: Types.AuthResponse, isGuest = false): 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());
|
|
localStorage.setItem(TOKEN_STORAGE.USER_TYPE, authResponse.user.userType);
|
|
localStorage.setItem(TOKEN_STORAGE.IS_GUEST, isGuest.toString());
|
|
}
|
|
|
|
function getStoredAuthData(): {
|
|
accessToken: string | null;
|
|
refreshToken: string | null;
|
|
userData: Types.User | null;
|
|
expiresAt: number | null;
|
|
userType: string | null;
|
|
isGuest: boolean;
|
|
} {
|
|
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);
|
|
const userType = localStorage.getItem(TOKEN_STORAGE.USER_TYPE);
|
|
const isGuestStr = localStorage.getItem(TOKEN_STORAGE.IS_GUEST);
|
|
|
|
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,
|
|
userType,
|
|
isGuest: isGuestStr === 'true',
|
|
};
|
|
}
|
|
|
|
type EmailVerificationResponse = {
|
|
message: string;
|
|
userType: string;
|
|
};
|
|
|
|
interface AuthenticationLogic extends AuthState {
|
|
apiClient: ApiClient;
|
|
login: (loginData: LoginRequest) => Promise<boolean>;
|
|
logout: () => Promise<void>;
|
|
verifyMFA: (mfaData: Types.MFAVerifyRequest) => Promise<boolean>;
|
|
resendMFACode: (email: string, deviceId: string, deviceName: string) => Promise<boolean>;
|
|
clearMFA: () => void;
|
|
verifyEmail: (
|
|
verificationData: EmailVerificationRequest
|
|
) => Promise<EmailVerificationResponse | null>;
|
|
resendEmailVerification: (email: string) => Promise<boolean>;
|
|
setPendingVerificationEmail: (email: string) => void;
|
|
getPendingVerificationEmail: () => string | null;
|
|
createEmployerAccount: (employerData: CreateEmployerRequest) => Promise<boolean>;
|
|
requestPasswordReset: (email: string) => Promise<boolean>;
|
|
refreshAuth: () => Promise<boolean>;
|
|
updateUserData: (updatedUser: Types.User) => void;
|
|
convertGuestToUser: (registrationData: GuestConversionRequest) => Promise<boolean>;
|
|
createGuestSession: () => Promise<boolean>;
|
|
}
|
|
|
|
function useAuthenticationLogic(): AuthenticationLogic {
|
|
const [authState, setAuthState] = useState<AuthState>({
|
|
user: null,
|
|
guest: null,
|
|
isAuthenticated: false,
|
|
isGuest: false,
|
|
isLoading: false,
|
|
isInitializing: true,
|
|
error: null,
|
|
mfaResponse: null,
|
|
});
|
|
|
|
const [apiClient] = useState(() => new ApiClient());
|
|
const initializationCompleted = useRef(false);
|
|
const guestCreationAttempted = useRef(false);
|
|
|
|
// Token refresh function
|
|
const refreshAccessToken = useCallback(
|
|
async (refreshToken: string): Promise<Types.AuthResponse | null> => {
|
|
try {
|
|
const response = await apiClient.refreshToken(refreshToken);
|
|
return response;
|
|
} catch (error) {
|
|
console.error('Token refresh failed:', error);
|
|
return null;
|
|
}
|
|
},
|
|
[apiClient]
|
|
);
|
|
|
|
// Create guest session
|
|
const createGuestSession = useCallback(async (): Promise<boolean> => {
|
|
if (guestCreationAttempted.current) {
|
|
return false;
|
|
}
|
|
|
|
guestCreationAttempted.current = true;
|
|
|
|
try {
|
|
console.log('🔄 Creating guest session...');
|
|
const guestAuth = await apiClient.createGuestSession();
|
|
|
|
if (guestAuth && guestAuth.user && guestAuth.user.userType === 'guest') {
|
|
storeAuthData(guestAuth, true);
|
|
apiClient.setAuthToken(guestAuth.accessToken);
|
|
|
|
setAuthState({
|
|
user: null,
|
|
guest: guestAuth.user as Types.Guest,
|
|
isAuthenticated: true,
|
|
isGuest: true,
|
|
isLoading: false,
|
|
isInitializing: false,
|
|
error: null,
|
|
mfaResponse: null,
|
|
});
|
|
|
|
console.log('👤 Guest session created successfully:', guestAuth.user);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
} catch (error) {
|
|
console.error('❌ Failed to create guest session:', error);
|
|
guestCreationAttempted.current = false;
|
|
|
|
// Set to unauthenticated state if guest creation fails
|
|
setAuthState(prev => ({
|
|
...prev,
|
|
user: null,
|
|
guest: null,
|
|
isAuthenticated: false,
|
|
isGuest: false,
|
|
isLoading: false,
|
|
isInitializing: false,
|
|
error: 'Failed to create guest session',
|
|
}));
|
|
|
|
return false;
|
|
}
|
|
}, [apiClient]);
|
|
|
|
// Initialize authentication state
|
|
const initializeAuth = useCallback(async () => {
|
|
if (initializationCompleted.current) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const stored = getStoredAuthData();
|
|
|
|
// If no stored tokens, create guest session
|
|
if (!stored.accessToken || !stored.refreshToken || !stored.userData) {
|
|
console.log('🔄 No stored auth found, creating guest session...');
|
|
await createGuestSession();
|
|
return;
|
|
}
|
|
|
|
// For guests, always verify the session exists on server
|
|
if (stored.userType === 'guest' && stored.userData) {
|
|
console.log(stored.userData);
|
|
try {
|
|
// Make a quick API call to verify guest still exists
|
|
const response = await fetch(`${apiClient.getBaseUrl()}/users/${stored.userData.id}`, {
|
|
headers: { Authorization: `Bearer ${stored.accessToken}` },
|
|
});
|
|
|
|
if (!response.ok) {
|
|
console.log('🔄 Guest session invalid, creating new guest session...');
|
|
clearStoredAuth();
|
|
await createGuestSession();
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
console.log('🔄 Guest verification failed, creating new guest session...');
|
|
clearStoredAuth();
|
|
await createGuestSession();
|
|
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) {
|
|
const isGuest = stored.userType === 'guest';
|
|
storeAuthData(refreshResult, isGuest);
|
|
apiClient.setAuthToken(refreshResult.accessToken);
|
|
|
|
setAuthState({
|
|
user: isGuest ? null : refreshResult.user,
|
|
guest: isGuest ? (refreshResult.user as Types.Guest) : null,
|
|
isAuthenticated: true,
|
|
isGuest,
|
|
isLoading: false,
|
|
isInitializing: false,
|
|
error: null,
|
|
mfaResponse: null,
|
|
});
|
|
|
|
console.log('✅ Token refreshed successfully');
|
|
} else {
|
|
console.log('❌ Token refresh failed, creating new guest session...');
|
|
clearStoredAuth();
|
|
apiClient.clearAuthToken();
|
|
await createGuestSession();
|
|
}
|
|
} else {
|
|
// Access token is still valid
|
|
apiClient.setAuthToken(stored.accessToken);
|
|
const isGuest = stored.userType === 'guest';
|
|
|
|
setAuthState({
|
|
user: isGuest ? null : stored.userData,
|
|
guest: isGuest ? (stored.userData as Types.Guest) : null,
|
|
isAuthenticated: true,
|
|
isGuest,
|
|
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();
|
|
await createGuestSession();
|
|
} finally {
|
|
initializationCompleted.current = true;
|
|
}
|
|
}, [apiClient, refreshAccessToken, createGuestSession]);
|
|
|
|
// 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((): void => {
|
|
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<boolean> => {
|
|
setAuthState(prev => ({
|
|
...prev,
|
|
isLoading: true,
|
|
error: null,
|
|
mfaResponse: 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 - convert from guest to authenticated user
|
|
const authResponse: Types.AuthResponse = result;
|
|
storeAuthData(authResponse, false);
|
|
apiClient.setAuthToken(authResponse.accessToken);
|
|
|
|
setAuthState(prev => ({
|
|
...prev,
|
|
user: authResponse.user,
|
|
guest: null,
|
|
isAuthenticated: true,
|
|
isGuest: false,
|
|
isLoading: false,
|
|
error: null,
|
|
mfaResponse: null,
|
|
}));
|
|
|
|
console.log('✅ Login successful, converted from guest to authenticated user');
|
|
return true;
|
|
}
|
|
} catch (error) {
|
|
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]
|
|
);
|
|
|
|
// Convert guest to permanent user
|
|
const convertGuestToUser = useCallback(
|
|
async (registrationData: GuestConversionRequest): Promise<boolean> => {
|
|
if (!authState.isGuest || !authState.guest) {
|
|
throw new Error('Not currently a guest user');
|
|
}
|
|
|
|
setAuthState(prev => ({ ...prev, isLoading: true, error: null }));
|
|
|
|
try {
|
|
const result = await apiClient.convertGuestToUser(registrationData);
|
|
|
|
// Store new authentication
|
|
storeAuthData(result.auth, false);
|
|
apiClient.setAuthToken(result.auth.accessToken);
|
|
|
|
setAuthState(prev => ({
|
|
...prev,
|
|
user: result.auth.user,
|
|
guest: null,
|
|
isAuthenticated: true,
|
|
isGuest: false,
|
|
isLoading: false,
|
|
error: null,
|
|
}));
|
|
|
|
console.log('✅ Guest successfully converted to permanent user');
|
|
return true;
|
|
} catch (error) {
|
|
const errorMessage =
|
|
error instanceof Error ? error.message : 'Failed to convert guest account';
|
|
setAuthState(prev => ({
|
|
...prev,
|
|
isLoading: false,
|
|
error: errorMessage,
|
|
}));
|
|
return false;
|
|
}
|
|
},
|
|
[apiClient, authState.isGuest, authState.guest]
|
|
);
|
|
|
|
// MFA verification
|
|
const verifyMFA = useCallback(
|
|
async (mfaData: Types.MFAVerifyRequest): Promise<boolean> => {
|
|
setAuthState(prev => ({ ...prev, isLoading: true, error: null }));
|
|
|
|
try {
|
|
const result = await apiClient.verifyMFA(mfaData);
|
|
|
|
if (result.accessToken) {
|
|
const authResponse: Types.AuthResponse = result;
|
|
storeAuthData(authResponse, false);
|
|
apiClient.setAuthToken(authResponse.accessToken);
|
|
|
|
setAuthState(prev => ({
|
|
...prev,
|
|
user: authResponse.user,
|
|
guest: null,
|
|
isAuthenticated: true,
|
|
isGuest: false,
|
|
isLoading: false,
|
|
error: null,
|
|
mfaResponse: null,
|
|
}));
|
|
|
|
console.log('✅ MFA verification successful, converted from guest');
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : 'MFA verification failed';
|
|
setAuthState(prev => ({
|
|
...prev,
|
|
isLoading: false,
|
|
error: errorMessage,
|
|
}));
|
|
return false;
|
|
}
|
|
},
|
|
[apiClient]
|
|
);
|
|
|
|
// Logout - returns to guest session
|
|
const logout = useCallback(async () => {
|
|
try {
|
|
// If authenticated, try to logout gracefully
|
|
if (authState.isAuthenticated && !authState.isGuest) {
|
|
const stored = getStoredAuthData();
|
|
if (stored.accessToken && stored.refreshToken) {
|
|
try {
|
|
await apiClient.logout(stored.accessToken, stored.refreshToken);
|
|
} catch (error) {
|
|
console.warn('Logout request failed, proceeding with local cleanup');
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn('Error during logout:', error);
|
|
} finally {
|
|
// Always clear stored auth and create new guest session
|
|
clearStoredAuth();
|
|
apiClient.clearAuthToken();
|
|
guestCreationAttempted.current = false;
|
|
|
|
// Create new guest session
|
|
await createGuestSession();
|
|
|
|
console.log('🔄 Logged out, created new guest session');
|
|
}
|
|
}, [apiClient, authState.isAuthenticated, authState.isGuest, createGuestSession]);
|
|
|
|
// Update user data
|
|
const updateUserData = useCallback(
|
|
(updatedUser: Types.User) => {
|
|
updateStoredUserData(updatedUser);
|
|
setAuthState(prev => ({
|
|
...prev,
|
|
user: authState.isGuest ? null : updatedUser,
|
|
guest: authState.isGuest ? (updatedUser as Types.Guest) : prev.guest,
|
|
}));
|
|
console.log('✅ User data updated');
|
|
},
|
|
[authState.isGuest]
|
|
);
|
|
|
|
// Email verification functions (unchanged)
|
|
const verifyEmail = useCallback(
|
|
async (
|
|
verificationData: EmailVerificationRequest
|
|
): Promise<EmailVerificationResponse | 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]
|
|
);
|
|
|
|
// Other existing methods remain the same...
|
|
const resendEmailVerification = useCallback(
|
|
async (email: string): Promise<boolean> => {
|
|
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]
|
|
);
|
|
|
|
const setPendingVerificationEmail = useCallback((email: string) => {
|
|
localStorage.setItem(TOKEN_STORAGE.PENDING_VERIFICATION_EMAIL, email);
|
|
}, []);
|
|
|
|
const getPendingVerificationEmail = useCallback((): string | null => {
|
|
return localStorage.getItem(TOKEN_STORAGE.PENDING_VERIFICATION_EMAIL);
|
|
}, []);
|
|
|
|
const createEmployerAccount = useCallback(
|
|
async (employerData: CreateEmployerRequest): Promise<boolean> => {
|
|
setAuthState(prev => ({ ...prev, isLoading: true, error: null }));
|
|
|
|
try {
|
|
const employer = await apiClient.createEmployer(employerData);
|
|
console.log('✅ Employer created:', employer);
|
|
|
|
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<boolean> => {
|
|
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<boolean> => {
|
|
const stored = getStoredAuthData();
|
|
if (!stored.refreshToken) {
|
|
return false;
|
|
}
|
|
|
|
setAuthState(prev => ({ ...prev, isLoading: true, error: null }));
|
|
|
|
const refreshResult = await refreshAccessToken(stored.refreshToken);
|
|
|
|
if (refreshResult) {
|
|
const isGuest = stored.userType === 'guest';
|
|
storeAuthData(refreshResult, isGuest);
|
|
apiClient.setAuthToken(refreshResult.accessToken);
|
|
|
|
setAuthState(prev => ({
|
|
...prev,
|
|
user: isGuest ? null : refreshResult.user,
|
|
guest: isGuest ? (refreshResult.user as Types.Guest) : null,
|
|
isAuthenticated: true,
|
|
isGuest,
|
|
isLoading: false,
|
|
error: null,
|
|
}));
|
|
|
|
return true;
|
|
} else {
|
|
await logout();
|
|
return false;
|
|
}
|
|
}, [refreshAccessToken, logout, apiClient]);
|
|
|
|
// Resend MFA code
|
|
const resendMFACode = useCallback(
|
|
async (email: string, deviceId: string, deviceName: string): Promise<boolean> => {
|
|
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,
|
|
}));
|
|
}, []);
|
|
|
|
return {
|
|
...authState,
|
|
apiClient,
|
|
login,
|
|
logout,
|
|
verifyMFA,
|
|
resendMFACode,
|
|
clearMFA,
|
|
verifyEmail,
|
|
resendEmailVerification,
|
|
setPendingVerificationEmail,
|
|
getPendingVerificationEmail,
|
|
createEmployerAccount,
|
|
requestPasswordReset,
|
|
refreshAuth,
|
|
updateUserData,
|
|
convertGuestToUser,
|
|
createGuestSession,
|
|
};
|
|
}
|
|
|
|
// ============================
|
|
// Enhanced Context Provider
|
|
// ============================
|
|
|
|
const AuthContext = createContext<ReturnType<typeof useAuthenticationLogic> | null>(null);
|
|
|
|
function AuthProvider({ children }: { children: React.ReactNode }): JSX.Element {
|
|
const auth = useAuthenticationLogic();
|
|
|
|
return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>;
|
|
}
|
|
|
|
function useAuth(): ReturnType<typeof useAuthenticationLogic> {
|
|
const context = useContext(AuthContext);
|
|
if (!context) {
|
|
throw new Error('useAuth must be used within an AuthProvider');
|
|
}
|
|
return context;
|
|
}
|
|
|
|
// ============================
|
|
// Enhanced Protected Route Component
|
|
// ============================
|
|
|
|
interface ProtectedRouteProps {
|
|
children: React.ReactNode;
|
|
fallback?: React.ReactNode;
|
|
requiredUserType?: Types.UserType;
|
|
allowGuests?: boolean;
|
|
}
|
|
|
|
function ProtectedRoute({
|
|
children,
|
|
fallback = <div>Please log in to access this page.</div>,
|
|
requiredUserType,
|
|
allowGuests = false,
|
|
}: ProtectedRouteProps): JSX.Element {
|
|
const { isAuthenticated, isInitializing, user, isGuest } = useAuth();
|
|
|
|
// Show loading while checking stored tokens
|
|
if (isInitializing) {
|
|
return <div>Loading...</div>;
|
|
}
|
|
|
|
// Not authenticated at all (shouldn't happen with guest sessions)
|
|
if (!isAuthenticated) {
|
|
return <>{fallback}</>;
|
|
}
|
|
|
|
// Guest access control
|
|
if (isGuest && !allowGuests) {
|
|
return <div>Please create an account or log in to access this page.</div>;
|
|
}
|
|
|
|
// Check user type if required (only for non-guests)
|
|
if (requiredUserType && !isGuest && user?.userType !== requiredUserType) {
|
|
return <div>Access denied. Required user type: {requiredUserType}</div>;
|
|
}
|
|
|
|
return <>{children}</>;
|
|
}
|
|
|
|
export type {
|
|
AuthState,
|
|
LoginRequest,
|
|
EmailVerificationRequest,
|
|
ResendVerificationRequest,
|
|
PasswordResetRequest,
|
|
GuestConversionRequest,
|
|
};
|
|
|
|
export type { CreateCandidateRequest, CreateEmployerRequest } from '../services/api-client';
|
|
|
|
export { useAuthenticationLogic, AuthProvider, useAuth, ProtectedRoute };
|