File size: 2,989 Bytes
49e7bf6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | /**
* 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<T>(
endpoint: string,
options: RequestInit & { params?: Record<string, string> } = {}
): Promise<T> {
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: <T>(url: string, p?: Record<string, string>) => request<T>(url, { method: "GET", params: p }),
post: <T>(url: string, body: any) => request<T>(url, { method: "POST", body: JSON.stringify(body) }),
put: <T>(url: string, body: any) => request<T>(url, { method: "PUT", body: JSON.stringify(body) }),
delete: <T>(url: string) => request<T>(url, { method: "DELETE" }),
// For PICO/Avatar uploads later:
upload: <T>(url: string, formData: FormData) => request<T>(url, { method: "POST", body: formData }),
};
|