const RAW_API_BASE = ( (import.meta.env.VITE_API_URL as string | undefined) ?? (import.meta.env.VITE_API_BASE_URL as string | undefined) ?? '/api' ).trim(); const API_BASE = (RAW_API_BASE || '/api').replace(/\/+$/, ''); interface RequestOptions extends RequestInit { params?: Record; } function toRequestBody(body: unknown): BodyInit | undefined { if (body === undefined || body === null) return undefined; if ( body instanceof FormData || body instanceof Blob || body instanceof URLSearchParams || body instanceof ArrayBuffer ) { return body; } return JSON.stringify(body); } class ApiError extends Error { constructor( public status: number, public data: unknown, ) { super(`API Error ${status}`); this.name = 'ApiError'; } } async function request(path: string, options: RequestOptions = {}): Promise { const { params, ...fetchOptions } = options; let url = `${API_BASE}${path}`; if (params) { const searchParams = new URLSearchParams(params); url += `?${searchParams.toString()}`; } const token = localStorage.getItem('access_token'); const headers: Record = { ...(fetchOptions.headers as Record), }; if (token) { headers['Authorization'] = `Bearer ${token}`; } if (fetchOptions.body && typeof fetchOptions.body === 'string') { headers['Content-Type'] = 'application/json'; } const res = await fetch(url, { ...fetchOptions, credentials: fetchOptions.credentials ?? 'include', headers, }); const shouldHandle401WithRefresh = res.status === 401 && path !== '/auth/login' && path !== '/auth/register' && path !== '/auth/refresh'; if (shouldHandle401WithRefresh) { // Try refresh const refreshed = await refreshToken(); if (refreshed) { headers['Authorization'] = `Bearer ${localStorage.getItem('access_token')}`; const retry = await fetch(url, { ...fetchOptions, credentials: fetchOptions.credentials ?? 'include', headers, }); if (!retry.ok) throw new ApiError(retry.status, await retry.json().catch(() => null)); if (retry.status === 204) return undefined as T; return retry.json(); } localStorage.removeItem('access_token'); window.location.href = '/login'; throw new ApiError(401, null); } if (!res.ok) { throw new ApiError(res.status, await res.json().catch(() => null)); } if (res.status === 204) return undefined as T; return res.json(); } async function refreshToken(): Promise { try { const res = await fetch(`${API_BASE}/auth/refresh`, { method: 'POST', credentials: 'include', }); if (!res.ok) return false; const data = await res.json(); localStorage.setItem('access_token', data.accessToken); return true; } catch { return false; } } export const api = { get: (path: string, params?: Record) => request(path, { params }), post: (path: string, body?: unknown) => request(path, { method: 'POST', body: toRequestBody(body), }), put: (path: string, body?: unknown) => request(path, { method: 'PUT', body: toRequestBody(body), }), patch: (path: string, body?: unknown) => request(path, { method: 'PATCH', body: toRequestBody(body), }), delete: (path: string) => request(path, { method: 'DELETE' }), }; export { ApiError };