import { API_BASE_URL } from './config'; // Token management let accessToken: string | null = null; export function setAccessToken(token: string | null): void { accessToken = token; if (token) { localStorage.setItem('access_token', token); } else { localStorage.removeItem('access_token'); } } export function getAccessToken(): string | null { if (accessToken) return accessToken; if (typeof window !== 'undefined') { accessToken = localStorage.getItem('access_token'); } return accessToken; } export function clearTokens(): void { accessToken = null; if (typeof window !== 'undefined') { localStorage.removeItem('access_token'); localStorage.removeItem('refresh_token'); } } // Parse JWT payload without library dependency export function parseJwt(token: string): Record | null { try { const base64Url = token.split('.')[1]; const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); const jsonPayload = decodeURIComponent( atob(base64) .split('') .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) .join('') ); return JSON.parse(jsonPayload); } catch { return null; } } interface FetchOptions extends RequestInit { skipAuth?: boolean; } export function invalidateApiCache() { // No-op: Caching disabled for real-time admin management } // Core API fetch wrapper with auth header injection export async function apiFetch( endpoint: string, options: FetchOptions = {} ): Promise { const { skipAuth = false, headers: customHeaders, ...rest } = options; const method = (rest.method || 'GET').toUpperCase(); return runApiFetch(endpoint, { skipAuth, headers: customHeaders, ...rest }, method); } async function runApiFetch( endpoint: string, options: FetchOptions, method: string ): Promise { const { skipAuth = false, headers: customHeaders, ...rest } = options; const headers: Record = { 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', ...(customHeaders as Record), }; if (method !== 'GET' && method !== 'HEAD' && !headers['Content-Type']) { if (!(rest.body instanceof FormData)) { headers['Content-Type'] = 'application/json'; } } if (!skipAuth) { const token = getAccessToken(); if (token) { headers['Authorization'] = `Bearer ${token}`; } } else { delete headers['Authorization']; } try { const response = await fetch(`${API_BASE_URL}${endpoint}`, { cache: 'no-store', ...rest, headers, credentials: 'include', }); if (response.status === 401 && !skipAuth) { // Try token refresh const refreshed = await refreshAccessToken(); if (refreshed) { headers['Authorization'] = `Bearer ${getAccessToken()}`; const retryResponse = await fetch(`${API_BASE_URL}${endpoint}`, { ...rest, headers, credentials: 'include', }); if (!retryResponse.ok) { const err = await retryResponse.json().catch(() => ({ detail: 'Request failed' })); throw new ApiError(retryResponse.status, formatErrorMessage(err.detail)); } return retryResponse.json(); } // Refresh failed — redirect to login clearTokens(); if (typeof window !== 'undefined') { window.location.href = '/login'; } throw new ApiError(401, 'Session expired'); } if (!response.ok) { const err = await response.json().catch(() => ({ detail: 'Request failed' })); throw new ApiError(response.status, formatErrorMessage(err.detail)); } return response.json(); } catch (error: any) { if (error instanceof ApiError) { throw error; } console.error('apiFetch network/CORS error:', error); throw new ApiError( 503, 'Network issue — please refresh page or check connection' ); } } async function refreshAccessToken(): Promise { try { const refreshToken = typeof window !== 'undefined' ? localStorage.getItem('refresh_token') : null; const headers: Record = { 'Content-Type': 'application/json' }; if (refreshToken) { headers['Authorization'] = `Bearer ${refreshToken}`; headers['X-Refresh-Token'] = refreshToken; } const response = await fetch(`${API_BASE_URL}/api/v1/auth/refresh`, { method: 'POST', headers, credentials: 'include', }); if (!response.ok) return false; const data = await response.json(); if (data.access_token) { setAccessToken(data.access_token); } if (data.refresh_token) { localStorage.setItem('refresh_token', data.refresh_token); } return true; } catch { return false; } } export function formatErrorMessage(detail: any): string { if (!detail) return 'Request failed'; if (typeof detail === 'string') return detail; if (Array.isArray(detail)) { return detail .map((item: any) => { if (typeof item === 'string') return item; if (item && typeof item === 'object') { const loc = Array.isArray(item.loc) ? item.loc.slice(1).join('.') : ''; const msg = item.msg || item.message || JSON.stringify(item); return loc ? `${loc}: ${msg}` : msg; } return String(item); }) .join(' | '); } if (typeof detail === 'object') { return detail.message || detail.msg || JSON.stringify(detail); } return String(detail); } export class ApiError extends Error { status: number; constructor(status: number, message: string) { super(message); this.status = status; this.name = 'ApiError'; } }