import { toast } from "react-toastify"; import { API_BASE_URL } from "../constant"; import { clearAuthCookies, refreshLoggedInFlag } from "./authCookies"; type ApiRequestOptions = Omit & { body?: unknown; toast?: boolean; successMessage?: string; errorMessage?: string; silent?: boolean; }; let isRefreshing = false; let refreshQueue: (() => void)[] = []; const API_TOAST_OPTIONS = { autoClose: 2000 }; const hardLogout = () => { clearAuthCookies(); toast.error( "Your session has expired. Please sign in again.", API_TOAST_OPTIONS ); window.location.href = "/signin"; }; const refreshAccessToken = async () => { const res = await fetch(`${API_BASE_URL}/api/auth/refresh`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}), }); if (!res.ok) throw new Error("Refresh failed"); }; const waitForRefresh = (): Promise => new Promise((resolve) => refreshQueue.push(resolve)); const doRefresh = async () => { if (isRefreshing) { await waitForRefresh(); return; } isRefreshing = true; try { await refreshAccessToken(); refreshLoggedInFlag(); } finally { isRefreshing = false; refreshQueue.forEach((cb) => cb()); refreshQueue = []; } }; const request = async ( path: string, options: ApiRequestOptions = {}, _isRetry = false ): Promise => { const { body, headers, toast: shouldToast = true, successMessage, errorMessage, silent = false, ...rest } = options; const showToast = shouldToast && !silent; const requestHeaders = new Headers(headers); let requestBody: BodyInit | undefined; if (body !== undefined) { if (body instanceof FormData) { requestBody = body; } else { requestHeaders.set("Content-Type", "application/json"); requestBody = JSON.stringify(body); } } const url = path.startsWith("http") ? path : `${API_BASE_URL.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}` }`; const response = await fetch(url, { ...rest, headers: requestHeaders, credentials: "include", body: requestBody, }); if (response.status === 401 && !url.includes("/api/auth/signin")) { if (!_isRetry) { try { await doRefresh(); return request(path, options, true); } catch { hardLogout(); throw new Error("Session expired"); } } hardLogout(); throw new Error("Unauthorized"); } const contentType = response.headers.get("content-type") || ""; const data = response.status === 204 ? null : contentType.includes("application/json") ? await response.json() : await response.text(); if (!response.ok) { const backendError = typeof data === "object" && data !== null ? (data as any).detail || (data as any).message : undefined; const finalErrorMessage = backendError || errorMessage || "Request failed"; if (showToast) { toast.error(finalErrorMessage, API_TOAST_OPTIONS); } throw new Error(finalErrorMessage); } const method = String(rest.method || "GET").toUpperCase(); if (showToast && method !== "GET") { const defaultSuccess = method === "POST" ? "Created successfully" : method === "PUT" || method === "PATCH" ? "Updated successfully" : method === "DELETE" ? "Deleted successfully" : "Action successful"; const msg = (successMessage as string) ?? (data?.message as string) ?? defaultSuccess; toast.success(msg, API_TOAST_OPTIONS); } return data as T; }; export const apiClient = { get: (path: string, options?: ApiRequestOptions) => request(path, { ...options, method: "GET" }), post: (path: string, body?: unknown, options?: ApiRequestOptions) => request(path, { ...options, method: "POST", body }), put: (path: string, body?: unknown, options?: ApiRequestOptions) => request(path, { ...options, method: "PUT", body }), patch: (path: string, body?: unknown, options?: ApiRequestOptions) => request(path, { ...options, method: "PATCH", body }), delete: (path: string, options?: ApiRequestOptions) => request(path, { ...options, method: "DELETE" }), };