/** * Production-Grade API Client * - Unified Fetch Wrapper with JWT injection. * - Centralized 401 Interception (Auto-Logout). * - Support for JSON and Multipart/Form-Data. */ const BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api/v1"; export type ApiError = { message: string; status?: number; detail?: any; }; async function request( endpoint: string, options: RequestInit & { params?: Record } = {} ): Promise { const { params, headers: customHeaders, ...config } = options; // 1. Construct URL with Search Params const url = new URL(`${BASE_URL}${endpoint}`); if (params) { Object.entries(params).forEach(([key, val]) => url.searchParams.append(key, val)); } // 2. Token Retrieval (Direct localStorage for non-React context utility) const token = typeof window !== "undefined" ? localStorage.getItem("token") : null; // 3. Header Synthesis - Build headers as Record const headers: Record = {}; // Add Content-Type if not FormData const isFormData = config.body instanceof FormData; if (!isFormData) { headers["Content-Type"] = "application/json"; } // Add Authorization if token exists if (token) { headers["Authorization"] = `Bearer ${token}`; } // Merge custom headers if (customHeaders) { if (customHeaders instanceof Headers) { customHeaders.forEach((value, key) => { headers[key] = value; }); } else if (Array.isArray(customHeaders)) { customHeaders.forEach(([key, value]) => { headers[key] = value; }); } else { Object.assign(headers, customHeaders as Record); } } const finalConfig: RequestInit = { ...config, headers, }; try { const response = await fetch(url.toString(), finalConfig); // 4. Global Interceptor: Handle Unauthorized if (response.status === 401) { if (typeof window !== "undefined") { localStorage.removeItem("token"); // Force hard-redirect to clear state if token is dead window.location.href = "/login?error=session_expired"; } throw new Error("Unauthorized access. Please log in again."); } // 5. Success Handlers if (response.status === 204) return {} as T; const data = await response.json(); if (!response.ok) { // Return the specific backend detail if available (FastAPI style) const errorMsg = data.detail || "The research server encountered an issue."; return Promise.reject({ message: errorMsg, status: response.status, detail: data }); } return data as T; } catch (err: any) { // Handle Network Failures return Promise.reject({ message: err.message || "Unable to connect to the research server. Check your connection.", status: err.status || 500 }); } } export const api = { get: (url: string, p?: Record) => request(url, { method: "GET", params: p }), post: (url: string, body: any) => request(url, { method: "POST", body: JSON.stringify(body) }), put: (url: string, body: any) => request(url, { method: "PUT", body: JSON.stringify(body) }), delete: (url: string) => request(url, { method: "DELETE" }), // For PICO/Avatar uploads later: upload: (url: string, formData: FormData) => request(url, { method: "POST", body: formData }), };