import axios, { type AxiosError, type InternalAxiosRequestConfig } from "axios"; import { useAuthStore } from "@modules/auth/store/authStore"; import { getApiBaseUrl } from "./config"; const apiBaseUrl = getApiBaseUrl(); export const apiClient = axios.create({ baseURL: apiBaseUrl, timeout: 10_000 }); export const authClient = axios.create({ baseURL: apiBaseUrl, timeout: 10_000, withCredentials: true }); let getAccessToken: () => string | null = () => null; let refreshPromise: Promise | null = null; export function setAccessTokenGetter(getter: () => string | null): void { getAccessToken = getter; } export function applyAuthHeader(headers: Record): Record { const token = getAccessToken(); if (token) { return { ...headers, Authorization: `Bearer ${token}` }; } return headers; } export async function bootstrapSessionRefresh(): Promise { return refreshAccessToken(); } async function refreshAccessToken(): Promise { if (!refreshPromise) { refreshPromise = authClient .post("/api/v1/auth/refresh") .then((response) => { const { access_token: accessToken, user } = response.data; useAuthStore.getState().setSession(accessToken, user); return accessToken as string; }) .catch(() => { useAuthStore.getState().clearSession(); return null; }) .finally(() => { refreshPromise = null; }); } return refreshPromise; } apiClient.interceptors.request.use((config) => { config.headers = applyAuthHeader(config.headers as Record) as typeof config.headers; return config; }); apiClient.interceptors.response.use( (response) => response, async (error: AxiosError) => { const detail = error.response && typeof error.response.data === "object" && error.response.data !== null ? (error.response.data as { detail?: string }).detail : undefined; if ( error.response?.status === 403 && (detail === "ACCOUNT_BLOCKED" || detail === "EMAIL_NOT_VERIFIED") ) { useAuthStore.getState().clearSession(); return Promise.reject(error); } if (error.response?.status === 401 && detail === "TOKEN_REVOKED") { useAuthStore.getState().clearSession(); return Promise.reject(error); } const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }; if (error.response?.status !== 401 || !originalRequest || originalRequest._retry) { return Promise.reject(error); } if (originalRequest.url?.includes("/api/v1/auth/")) { return Promise.reject(error); } originalRequest._retry = true; const accessToken = await refreshAccessToken(); if (!accessToken) { return Promise.reject(error); } originalRequest.headers = applyAuthHeader( originalRequest.headers as Record ) as typeof originalRequest.headers; return apiClient.request(originalRequest); } );