import axios from "axios"; // --- ENV CONFIG --- const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "http://localhost:8000"; const LOGIN_PATH = import.meta.env.VITE_LOGIN_PATH || "/login"; // --- ERROR EVENT BUS --- const ERROR_EVENT = "api:error"; type ApiErrorDetail = { message: string; status?: number; url?: string; data?: unknown; }; // Use interface instead of arrow function types for readability interface ApiErrorHandler { (e: CustomEvent): void; } function notifyError(detail: ApiErrorDetail) { window.dispatchEvent(new CustomEvent(ERROR_EVENT, { detail })); // eslint-disable-next-line no-console console.error("[API ERROR]", detail); } function onError(handler: ApiErrorHandler) { const wrapped = handler as EventListener; window.addEventListener(ERROR_EVENT, wrapped as EventListener); return () => window.removeEventListener(ERROR_EVENT, wrapped); } // --- AXIOS INSTANCES --- // Always send cookies. Django will set auth cookies; browser will include them automatically. function createAxios(baseURL: string): any { const instance = axios.create({ baseURL, withCredentials: true, // cookies headers: { "Content-Type": "application/json", }, timeout: 20000, }); return instance; } // Use a single behavior for both: cookies are always sent const apiPublic = createAxios(API_BASE_URL); const apiAuth = createAxios(API_BASE_URL); // --- REQUEST INTERCEPTOR (PUBLIC) --- // Ensure no Authorization header is ever sent by the public client apiPublic.interceptors.request.use(function (config: any) { if (config?.headers && (config.headers as any).Authorization) { delete (config.headers as any).Authorization; } return config; }); // --- REQUEST INTERCEPTOR (AUTH) --- // Do not attach Authorization header; rely on cookies set by Django. apiAuth.interceptors.request.use(function (config: any) { (config as any)._retryCount = (config as any)._retryCount || 0; return config; }); // --- RESPONSE INTERCEPTOR (AUTH) --- // Simplified: on 401, redirect to login. Server manages refresh via cookies. apiAuth.interceptors.response.use( function (response: any) { return response; }, async function (error: any) { if (!error.response) { alert("Backend connection is unavailable. Please check your network."); notifyError({ message: "Network error or backend unavailable", url: error.config?.url, }); return Promise.reject(error); } const status = error.response.status; if (status === 401) { ClearTokens(); window.location.assign(LOGIN_PATH); return Promise.reject(error); } notifyError({ message: (error.response.data as any)?.detail || (error.response.data as any)?.message || `Request failed with status ${status}`, status, url: error.config?.url, data: error.response.data, }); return Promise.reject(error); } ); // --- PUBLIC CLIENT: still emits errors and alerts on network failure --- apiPublic.interceptors.response.use( function (response: any) { return response; }, async function (error: any) { if (!error.response) { alert("Backend connection is unavailable. Please check your network."); notifyError({ message: "Network error or backend unavailable", url: error.config?.url, }); return Promise.reject(error); } notifyError({ message: (error.response.data as any)?.detail || (error.response.data as any)?.message || `Request failed with status ${error.response.status}`, status: error.response.status, url: error.config?.url, data: error.response.data, }); return Promise.reject(error); } ); function Logout() { try { const LogOutResponse = apiAuth.post("/api/logout/"); if (LogOutResponse.body.detail != "Logout successful") { throw new Error("Logout failed"); } ClearTokens(); } catch (error) { console.error("Error during logout:", error); } } function ClearTokens(){ document.cookie = "access_token=; Max-Age=0; path=/"; document.cookie = "refresh_token=; Max-Age=0; path=/"; } // --- EXPORT DEFAULT API WRAPPER --- const Client = { // Axios instances auth: apiAuth, public: apiPublic, Logout, // Error subscription onError, }; export default Client; /** USAGE EXAMPLES (TypeScript/React) Import the client -------------------------------------------------- import Client from "@/api/Client"; Login: obtain tokens and persist to cookies -------------------------------------------------- async function login(username: string, password: string) { // SimpleJWT default login endpoint (adjust if your backend differs) // Example backend endpoint: POST /api/token/ -> { access, refresh } const res = await Client.public.post("/api/token/", { username, password }); const { access, refresh } = res.data; Client.setTokens(access, refresh); // After this, Client.auth will automatically attach Authorization header // and refresh when receiving a 401 (up to 2 retries). } Public request (no cookies, no Authorization) -------------------------------------------------- // The public client does NOT send cookies or Authorization. async function listPublicItems() { const res = await Client.public.get("/api/public/items/"); return res.data; } Authenticated requests (auto Bearer header + refresh on 401) -------------------------------------------------- async function fetchProfile() { const res = await Client.auth.get("/api/users/me/"); return res.data; } async function updateProfile(payload: { first_name?: string; last_name?: string }) { const res = await Client.auth.patch("/api/users/me/", payload); return res.data; } Global error handling (UI notifications) -------------------------------------------------- import { useEffect } from "react"; function useApiErrors(showToast: (msg: string) => void) { useEffect(function () { const unsubscribe = Client.onError(function (e) { const { message, status } = e.detail; showToast(status ? String(status) + ": " + message : message); }); return unsubscribe; }, [showToast]); } // Note: Network connectivity issues trigger an alert and also dispatch api:error. // All errors are logged to console for developers. Logout -------------------------------------------------- function logout() { Client.clearTokens(); window.location.assign("/login"); } Route protection (PrivateRoute) -------------------------------------------------- // If you created src/routes/PrivateRoute.tsx, wrap your protected routes with it. // PrivateRoute checks for "access_token" cookie presence and redirects to /login if missing. // Example: // // } > // }> // } /> // } /> // // // } /> // Refresh and retry flow (what happens on 401) -------------------------------------------------- // 1) Client.auth request receives 401 from backend // 2) Client tries to refresh access token using refresh_token cookie // 3) If refresh succeeds, original request is retried (max 2 times) // 4) If still 401 (or no refresh token), tokens are cleared and user is redirected to /login Environment variables (optional overrides) -------------------------------------------------- // VITE_API_BASE_URL default: "http://localhost:8000" // VITE_API_REFRESH_URL default: "/api/token/refresh/" // VITE_LOGIN_PATH default: "/login" */