import { useAuth } from 'react-oidc-context'; import { useCallback, useRef } from 'react'; const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001'; let isRedirecting = false; export const useAuthenticatedFetch = () => { const auth = useAuth(); const silentRefreshInProgress = useRef | null>(null); const isTokenExpired = useCallback(() => { if (!auth.user?.expires_at) return true; const expiresAt = auth.user.expires_at * 1000; const now = Date.now(); const bufferMs = 60 * 1000; return now >= expiresAt - bufferMs; }, [auth.user?.expires_at]); const redirectToLogin = useCallback(() => { if (isRedirecting) return; isRedirecting = true; auth.signinRedirect(); }, [auth]); const attemptSilentRefresh = useCallback(async (): Promise => { if (silentRefreshInProgress.current) { return silentRefreshInProgress.current; } silentRefreshInProgress.current = (async () => { try { const user = await auth.signinSilent(); return user?.access_token || null; } catch { return null; } finally { silentRefreshInProgress.current = null; } })() as Promise; return silentRefreshInProgress.current; }, [auth]); const ensureValidToken = useCallback(async (): Promise => { if (isRedirecting) { throw new Error('Session expired, redirecting to login'); } if (!auth.user?.access_token) { throw new Error('Not authenticated'); } if (isTokenExpired()) { const newToken = await attemptSilentRefresh(); if (newToken) { return newToken; } redirectToLogin(); throw new Error('Session expired, redirecting to login'); } return auth.user.access_token; }, [auth.user?.access_token, isTokenExpired, attemptSilentRefresh, redirectToLogin]); const authFetch = useCallback( async (path: string, options: RequestInit = {}): Promise => { if (isRedirecting) { throw new Error('Session expired, redirecting to login'); } if (!navigator.onLine) { throw new Error('You appear to be offline. Please check your connection.'); } const token = await ensureValidToken(); const url = path.startsWith('http') ? path : `${API_URL}${path}`; let response: Response; try { response = await fetch(url, { ...options, headers: { ...options.headers, Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, }); } catch (err) { if (!navigator.onLine) { throw new Error('You appear to be offline. Please check your connection.'); } throw new Error('Network error. Please try again.'); } if (response.status === 401) { const newToken = await attemptSilentRefresh(); if (newToken) { return fetch(url, { ...options, headers: { ...options.headers, Authorization: `Bearer ${newToken}`, 'Content-Type': 'application/json', }, }); } redirectToLogin(); throw new Error('Session expired, redirecting to login'); } if (response.status >= 500) { throw new Error('Server error. Please try again later.'); } return response; }, [ensureValidToken, attemptSilentRefresh, redirectToLogin] ); return { authFetch, isAuthenticated: auth.isAuthenticated, isLoading: auth.isLoading, user: auth.user, }; };