276 lines
7.6 KiB
TypeScript
276 lines
7.6 KiB
TypeScript
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<ApiErrorDetail>): void;
|
|
}
|
|
|
|
function notifyError(detail: ApiErrorDetail) {
|
|
window.dispatchEvent(new CustomEvent<ApiErrorDetail>(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:
|
|
// <Routes>
|
|
// <Route element={<PrivateRoute />} >
|
|
// <Route element={<MainLayout />}>
|
|
// <Route path="/" element={<Dashboard />} />
|
|
// <Route path="/profile" element={<Profile />} />
|
|
// </Route>
|
|
// </Route>
|
|
// <Route path="/login" element={<Login />} />
|
|
// </Routes>
|
|
|
|
|
|
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"
|
|
*/
|