/** * 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, ...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 const authHeader = token ? { Authorization: `Bearer ${token}` } : {}; // Don't set Content-Type if sending FormData (browser handles it) const isFormData = config.body instanceof FormData; const contentTypeHeader = isFormData ? {} : { "Content-Type": "application/json" }; const finalConfig: RequestInit = { ...config, headers: { ...contentTypeHeader, ...authHeader, ...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 }), };