import { useCallback, useEffect, useRef } from 'react'; const useAuthManager = ({ apiClient, token, appStatus, appDispatch, notifyApiError, setStatusMessage, setLoading, }) => { const tokenRef = useRef(token); const refreshPromiseRef = useRef(null); const initialRefreshAttemptedRef = useRef(Boolean(token)); const refreshAccessToken = useCallback(async () => { console.log('[Auth] Attempting to refresh access token…'); appDispatch({ type: 'TOKEN_REFRESH_START' }); try { const { data } = await apiClient.post('/auth/refresh'); if (data?.access_token) { appDispatch({ type: 'TOKEN_REFRESH_SUCCESS', token: data.access_token, tenant: data.tenant || null, }); console.log('[Auth] Access token refreshed at', new Date().toISOString()); return data.access_token; } throw new Error('Missing access token in refresh response'); } catch (error) { console.warn('[Auth] Failed to refresh access token', error); appDispatch({ type: 'TOKEN_REFRESH_FAILURE', error: error?.message || null }); throw error; } }, [apiClient, appDispatch]); useEffect(() => { tokenRef.current = token; }, [token]); useEffect(() => { if (!token && !initialRefreshAttemptedRef.current && appStatus === 'logged-out') { initialRefreshAttemptedRef.current = true; console.log('[Auth] Attempting refresh at startup'); refreshAccessToken().catch(() => {}); } }, [token, appStatus, refreshAccessToken]); useEffect(() => { const requestInterceptor = apiClient.interceptors.request.use((config) => { const currentToken = tokenRef.current; if (currentToken) { config.headers = config.headers || {}; if (!config.headers.Authorization) { config.headers.Authorization = `Bearer ${currentToken}`; } } return config; }); const responseInterceptor = apiClient.interceptors.response.use( (response) => response, async (error) => { const { response, config } = error; if (!response || !config) { return Promise.reject(error); } const status = response.status; const url = typeof config.url === 'string' ? config.url : ''; const isAuthRoute = url.includes('/auth/login') || url.includes('/auth/refresh'); if (status === 401 && !config._retry && !isAuthRoute) { console.warn('[Auth] 401 received for', url, '- attempting token refresh'); if (!refreshPromiseRef.current) { refreshPromiseRef.current = (async () => { try { return await refreshAccessToken(); } finally { refreshPromiseRef.current = null; } })(); } try { const newToken = await refreshPromiseRef.current; if (!newToken) { throw new Error('No token returned from refresh'); } config._retry = true; config.headers = config.headers || {}; config.headers.Authorization = `Bearer ${newToken}`; console.log('[Auth] Retrying original request', url); try { return await apiClient(config); } catch (retryError) { if (retryError?.response?.status === 401) { notifyApiError(retryError, 'Session expired. Please log in again.'); } throw retryError; } } catch (refreshError) { console.warn('[Auth] Refresh failed, clearing session'); notifyApiError(refreshError, 'Session expired. Please log in again.'); return Promise.reject(refreshError); } } return Promise.reject(error); }, ); return () => { apiClient.interceptors.request.eject(requestInterceptor); apiClient.interceptors.response.eject(responseInterceptor); }; }, [apiClient, notifyApiError, refreshAccessToken]); const handleLogout = useCallback(async () => { try { setLoading(true); await apiClient.post('/auth/logout'); } catch (error) { console.warn('[Auth] Failed to revoke refresh token during logout', error); } finally { setLoading(false); appDispatch({ type: 'LOGOUT' }); setStatusMessage('Logged out.', 'info'); } }, [apiClient, appDispatch, setLoading, setStatusMessage]); return { tokenRef, refreshAccessToken, handleLogout }; }; export default useAuthManager;