mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 16:11:12 -05:00
317 lines
7.8 KiB
TypeScript
317 lines
7.8 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* Authentication Context Provider
|
|
*
|
|
* Provides authentication state and methods to the entire application.
|
|
* Handles token refresh, session management, and user state.
|
|
*/
|
|
|
|
import React, { createContext, useContext, useEffect, useState, useCallback, useRef } from 'react';
|
|
import {
|
|
User,
|
|
AuthContextType,
|
|
LoginCredentials,
|
|
RegisterData,
|
|
UpdateProfileData,
|
|
ChangePasswordData,
|
|
} from '@/lib/types/auth';
|
|
import {
|
|
authService,
|
|
oauthService,
|
|
mfaService,
|
|
tokenStorage,
|
|
getTimeUntilExpiry,
|
|
isRefreshTokenExpired,
|
|
OAuthProvider,
|
|
} from '@/lib/services/auth';
|
|
|
|
// Create the context
|
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
|
|
// Token refresh interval (check every minute)
|
|
const TOKEN_CHECK_INTERVAL = 60 * 1000;
|
|
|
|
// Refresh tokens 5 minutes before expiry
|
|
const REFRESH_BUFFER = 5 * 60 * 1000;
|
|
|
|
interface AuthProviderProps {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
export function AuthProvider({ children }: AuthProviderProps) {
|
|
const [user, setUser] = useState<User | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [isInitialized, setIsInitialized] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const refreshIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
const isRefreshingRef = useRef(false);
|
|
|
|
/**
|
|
* Check if we need to refresh the access token
|
|
*/
|
|
const shouldRefreshToken = useCallback((): boolean => {
|
|
const timeUntilExpiry = getTimeUntilExpiry();
|
|
|
|
if (timeUntilExpiry === null) {
|
|
return false;
|
|
}
|
|
|
|
// Refresh if token expires in less than 5 minutes
|
|
return timeUntilExpiry < REFRESH_BUFFER;
|
|
}, []);
|
|
|
|
/**
|
|
* Refresh the access token
|
|
*/
|
|
const refreshToken = useCallback(async () => {
|
|
// Prevent multiple simultaneous refresh attempts
|
|
if (isRefreshingRef.current) {
|
|
return;
|
|
}
|
|
|
|
// Check if refresh token is still valid
|
|
if (isRefreshTokenExpired()) {
|
|
console.log('Refresh token expired, logging out');
|
|
await logout();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
isRefreshingRef.current = true;
|
|
await authService.refreshAccessToken();
|
|
console.log('Access token refreshed successfully');
|
|
} catch (error) {
|
|
console.error('Failed to refresh token:', error);
|
|
// If refresh fails, log out the user
|
|
await logout();
|
|
} finally {
|
|
isRefreshingRef.current = false;
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Start the token refresh interval
|
|
*/
|
|
const startTokenRefreshInterval = useCallback(() => {
|
|
// Clear existing interval
|
|
if (refreshIntervalRef.current) {
|
|
clearInterval(refreshIntervalRef.current);
|
|
}
|
|
|
|
// Check token every minute
|
|
refreshIntervalRef.current = setInterval(() => {
|
|
if (shouldRefreshToken()) {
|
|
refreshToken();
|
|
}
|
|
}, TOKEN_CHECK_INTERVAL);
|
|
}, [shouldRefreshToken, refreshToken]);
|
|
|
|
/**
|
|
* Stop the token refresh interval
|
|
*/
|
|
const stopTokenRefreshInterval = useCallback(() => {
|
|
if (refreshIntervalRef.current) {
|
|
clearInterval(refreshIntervalRef.current);
|
|
refreshIntervalRef.current = null;
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Check authentication status and load user data
|
|
*/
|
|
const checkAuth = useCallback(async () => {
|
|
try {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
// Check if we have a valid access token
|
|
if (!tokenStorage.hasValidAccessToken()) {
|
|
// Try to refresh if we have a refresh token
|
|
if (!isRefreshTokenExpired()) {
|
|
await refreshToken();
|
|
} else {
|
|
// No valid tokens, user is not authenticated
|
|
setUser(null);
|
|
setIsLoading(false);
|
|
setIsInitialized(true);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Fetch current user data
|
|
const userData = await authService.getCurrentUser();
|
|
setUser(userData);
|
|
|
|
// Start token refresh interval
|
|
startTokenRefreshInterval();
|
|
} catch (error) {
|
|
console.error('Auth check failed:', error);
|
|
setUser(null);
|
|
setError('Failed to verify authentication');
|
|
} finally {
|
|
setIsLoading(false);
|
|
setIsInitialized(true);
|
|
}
|
|
}, [refreshToken, startTokenRefreshInterval]);
|
|
|
|
/**
|
|
* Login with email and password
|
|
*/
|
|
const login = useCallback(async (credentials: LoginCredentials) => {
|
|
try {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
await authService.login(credentials);
|
|
|
|
// Fetch user data after successful login
|
|
const userData = await authService.getCurrentUser();
|
|
setUser(userData);
|
|
|
|
// Start token refresh interval
|
|
startTokenRefreshInterval();
|
|
} catch (error: any) {
|
|
const errorMessage = error.response?.data?.detail || error.message || 'Login failed';
|
|
setError(errorMessage);
|
|
throw error;
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [startTokenRefreshInterval]);
|
|
|
|
/**
|
|
* Register a new user
|
|
*/
|
|
const register = useCallback(async (data: RegisterData) => {
|
|
try {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
await authService.register(data);
|
|
|
|
// Note: Django doesn't auto-login on registration
|
|
// User needs to login after registration
|
|
} catch (error: any) {
|
|
const errorMessage = error.response?.data?.detail || error.message || 'Registration failed';
|
|
setError(errorMessage);
|
|
throw error;
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Logout
|
|
*/
|
|
const logout = useCallback(async () => {
|
|
try {
|
|
setIsLoading(true);
|
|
await authService.logout();
|
|
} catch (error) {
|
|
console.error('Logout error:', error);
|
|
} finally {
|
|
setUser(null);
|
|
stopTokenRefreshInterval();
|
|
setIsLoading(false);
|
|
}
|
|
}, [stopTokenRefreshInterval]);
|
|
|
|
/**
|
|
* Update user profile
|
|
*/
|
|
const updateProfile = useCallback(async (data: UpdateProfileData) => {
|
|
try {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
const updatedUser = await authService.updateProfile(data);
|
|
setUser(updatedUser);
|
|
} catch (error: any) {
|
|
const errorMessage = error.response?.data?.detail || error.message || 'Profile update failed';
|
|
setError(errorMessage);
|
|
throw error;
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Change password
|
|
*/
|
|
const changePassword = useCallback(async (data: ChangePasswordData) => {
|
|
try {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
await authService.changePassword(data);
|
|
} catch (error: any) {
|
|
const errorMessage = error.response?.data?.detail || error.message || 'Password change failed';
|
|
setError(errorMessage);
|
|
throw error;
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Request password reset
|
|
*/
|
|
const requestPasswordReset = useCallback(async (email: string) => {
|
|
try {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
await authService.requestPasswordReset(email);
|
|
} catch (error: any) {
|
|
const errorMessage = error.response?.data?.detail || error.message || 'Password reset request failed';
|
|
setError(errorMessage);
|
|
throw error;
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
// Initialize authentication on mount
|
|
useEffect(() => {
|
|
checkAuth();
|
|
|
|
// Cleanup on unmount
|
|
return () => {
|
|
stopTokenRefreshInterval();
|
|
};
|
|
}, [checkAuth, stopTokenRefreshInterval]);
|
|
|
|
const value: AuthContextType = {
|
|
user,
|
|
isAuthenticated: !!user,
|
|
isLoading,
|
|
isInitialized,
|
|
error,
|
|
login,
|
|
register,
|
|
logout,
|
|
refreshToken,
|
|
updateProfile,
|
|
changePassword,
|
|
requestPasswordReset,
|
|
checkAuth,
|
|
};
|
|
|
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
}
|
|
|
|
/**
|
|
* Hook to use auth context
|
|
*/
|
|
export function useAuth() {
|
|
const context = useContext(AuthContext);
|
|
|
|
if (context === undefined) {
|
|
throw new Error('useAuth must be used within an AuthProvider');
|
|
}
|
|
|
|
return context;
|
|
}
|