'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(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(null); const [isLoading, setIsLoading] = useState(true); const [isInitialized, setIsInitialized] = useState(false); const [error, setError] = useState(null); const refreshIntervalRef = useRef(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 {children}; } /** * 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; }