Files
thrilltrack-explorer/lib/contexts/AuthContext.tsx

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;
}