mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 21:51:14 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
316
lib/contexts/AuthContext.tsx
Normal file
316
lib/contexts/AuthContext.tsx
Normal file
@@ -0,0 +1,316 @@
|
||||
h'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;
|
||||
}
|
||||
Reference in New Issue
Block a user