'use client'; import React, { createContext, useContext, useEffect, useState, useRef, ReactNode } from 'react'; import type { RefreshToken } from '../types/api'; import { API_PATHS } from '../types/api'; interface TokenContextType { accessToken: string | null; refreshToken: string | null; isRefreshing: boolean; refreshError: string | null; forceRefresh: () => Promise; clearTokens: () => void; } const TokenContext = createContext(null); interface TokenRefreshManagerProps { children: ReactNode; autoRefresh?: boolean; refreshThreshold?: number; // seconds before expiration to refresh onTokenExpired?: () => void; onRefreshFailed?: (error: string) => void; } export function TokenRefreshManager({ children, autoRefresh = true, refreshThreshold = 300, // 5 minutes onTokenExpired, onRefreshFailed, }: TokenRefreshManagerProps) { const [accessToken, setAccessToken] = useState(null); const [refreshToken, setRefreshToken] = useState(null); const [isRefreshing, setIsRefreshing] = useState(false); const [refreshError, setRefreshError] = useState(null); const [tokenExpiration, setTokenExpiration] = useState(null); const refreshTimeoutRef = useRef(null); // Load tokens from storage on mount useEffect(() => { const loadTokens = () => { try { const storedAccess = localStorage.getItem('access_token'); const storedRefresh = localStorage.getItem('refresh_token'); const storedExpiration = localStorage.getItem('token_expiration'); if (storedAccess) { setAccessToken(storedAccess); } if (storedRefresh) { setRefreshToken(storedRefresh); } if (storedExpiration) { setTokenExpiration(new Date(storedExpiration)); } } catch (error) { console.error('Failed to load tokens from storage:', error); } }; loadTokens(); }, []); // Save tokens to storage const saveTokens = (access: string, refresh: string, expiration: Date) => { try { localStorage.setItem('access_token', access); localStorage.setItem('refresh_token', refresh); localStorage.setItem('token_expiration', expiration.toISOString()); } catch (error) { console.error('Failed to save tokens to storage:', error); } }; // Clear tokens from storage const clearTokensFromStorage = () => { try { localStorage.removeItem('access_token'); localStorage.removeItem('refresh_token'); localStorage.removeItem('token_expiration'); } catch (error) { console.error('Failed to clear tokens from storage:', error); } }; // Clear tokens (logout) const clearTokens = () => { setAccessToken(null); setRefreshToken(null); setTokenExpiration(null); setRefreshError(null); clearTokensFromStorage(); if (refreshTimeoutRef.current) { clearTimeout(refreshTimeoutRef.current); } }; // Refresh access token const refreshAccessToken = async (): Promise => { if (!refreshToken) { setRefreshError('No refresh token available'); return false; } try { setIsRefreshing(true); setRefreshError(null); const response = await fetch(API_PATHS.AUTH_REFRESH, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ refreshToken, }), }); if (!response.ok) { if (response.status === 401) { // Refresh token is invalid/expired clearTokens(); onTokenExpired?.(); return false; } throw new Error('Token refresh failed'); } const data = await response.json(); // Update tokens setAccessToken(data.accessToken); setRefreshToken(data.refreshToken || refreshToken); const expiration = data.expiresAt ? new Date(data.expiresAt) : new Date(Date.now() + 3600 * 1000); // Default 1 hour setTokenExpiration(expiration); saveTokens(data.accessToken, data.refreshToken || refreshToken, expiration); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; setRefreshError(errorMessage); onRefreshFailed?.(errorMessage); // Don't clear tokens on network error, but clear on auth error if (!navigator.onLine) { return false; } return false; } finally { setIsRefreshing(false); } }; // Force refresh const forceRefresh = async () => { await refreshAccessToken(); }; // Set up auto-refresh useEffect(() => { if (!autoRefresh || !tokenExpiration || !refreshToken) { return; } const scheduleRefresh = () => { if (refreshTimeoutRef.current) { clearTimeout(refreshTimeoutRef.current); } const now = new Date(); const timeUntilExpiration = tokenExpiration.getTime() - now.getTime(); const refreshTime = timeUntilExpiration - refreshThreshold * 1000; if (refreshTime <= 0) { // Token is already expired or will expire soon refreshAccessToken(); } else { // Schedule refresh refreshTimeoutRef.current = setTimeout(() => { refreshAccessToken(); }, refreshTime); } }; scheduleRefresh(); return () => { if (refreshTimeoutRef.current) { clearTimeout(refreshTimeoutRef.current); } }; }, [autoRefresh, tokenExpiration, refreshToken, refreshThreshold]); // Add token to API requests useEffect(() => { const originalFetch = window.fetch; window.fetch = async (input, init) => { let requestInit = init || {}; // Add auth header if we have a token if (accessToken && !requestInit.headers) { requestInit.headers = { 'Authorization': `Bearer ${accessToken}`, }; } else if (accessToken && requestInit.headers) { (requestInit.headers as Record)['Authorization'] = `Bearer ${accessToken}`; } try { const response = await originalFetch(input, requestInit); // Handle 401 responses (token expired) if (response.status === 401 && accessToken) { // Try to refresh the token const refreshed = await refreshAccessToken(); if (refreshed) { // Retry the request with new token const retryInit = { ...requestInit, headers: { ...(requestInit.headers as Record), 'Authorization': `Bearer ${accessToken}`, }, }; return originalFetch(input, retryInit); } else { // Refresh failed, redirect to login clearTokens(); onTokenExpired?.(); } } return response; } catch (error) { // Handle network errors throw error; } }; return () => { window.fetch = originalFetch; }; }, [accessToken]); const contextValue: TokenContextType = { accessToken, refreshToken, isRefreshing, refreshError, forceRefresh, clearTokens, }; return ( {children} ); } // Hook to use token context export function useAuth() { const context = useContext(TokenContext); if (!context) { throw new Error('useAuth must be used within a TokenRefreshManager'); } return context; } // Component to show refresh status (for debugging) export function TokenStatus() { const { accessToken, isRefreshing, refreshError, forceRefresh } = useAuth(); return (

Auth Status

Token:{' '} {accessToken ? 'Present' : 'Missing'}

Status:{' '} {isRefreshing ? 'Refreshing...' : 'Idle'}

{refreshError && (

Error: {refreshError}

)}
); }