import axios from 'axios'; import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios'; import { Api, HttpClient } from './api'; const BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:1337'; // ─── Raw axios instance with interceptors ──────────────────────────────────── export const axiosInstance: AxiosInstance = axios.create({ baseURL: BASE_URL, timeout: 15_000, }); // Request interceptor — inject access token axiosInstance.interceptors.request.use( (config: InternalAxiosRequestConfig) => { const token = _getAccessToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error), ); // Response interceptor — silent token refresh on 401 let _isRefreshing = false; let _failedQueue: Array<{ resolve: (v: unknown) => void; reject: (r: unknown) => void }> = []; function _processQueue(error: unknown, token: string | null) { _failedQueue.forEach(({ resolve, reject }) => { if (error) reject(error); else resolve(token); }); _failedQueue = []; } axiosInstance.interceptors.response.use( (response) => { if (response.data !== null && typeof response.data === 'object' && 'data' in response.data) { response.data = response.data.data; } return response; }, async (error) => { const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }; if (error.response?.status === 401 && !originalRequest._retry) { if (_isRefreshing) { return new Promise((resolve, reject) => { _failedQueue.push({ resolve, reject }); }).then((token) => { originalRequest.headers.Authorization = `Bearer ${token}`; return axiosInstance(originalRequest); }); } originalRequest._retry = true; _isRefreshing = true; const refreshToken = localStorage.getItem('refreshToken'); if (!refreshToken) { _processQueue(error, null); _isRefreshing = false; _redirectToLogin(); return Promise.reject(error); } try { const res = await axios.post<{ data: { accessToken: string; refreshToken: string } }>( `${BASE_URL}/api/v1/auth/refresh`, { refreshToken }, ); const { accessToken, refreshToken: newRefresh } = res.data.data; _setAccessToken(accessToken); localStorage.setItem('refreshToken', newRefresh); _processQueue(null, accessToken); originalRequest.headers.Authorization = `Bearer ${accessToken}`; return axiosInstance(originalRequest); } catch (refreshError) { _processQueue(refreshError, null); _clearAuth(); _redirectToLogin(); return Promise.reject(refreshError); } finally { _isRefreshing = false; } } return Promise.reject(error); }, ); // ─── In-memory token storage ───────────────────────────────────────────────── // Access token lives only in memory; refresh token lives in localStorage let _accessToken: string | null = null; export function _getAccessToken() { return _accessToken; } export function _setAccessToken(token: string) { _accessToken = token; } export function _clearAuth() { _accessToken = null; localStorage.removeItem('refreshToken'); } function _redirectToLogin() { // Dynamic import to avoid circular dependency with router import('@/router').then(({ router }) => router.replace('/login')); } // ─── Typed API client ───────────────────────────────────────────────────────── const httpClient = new HttpClient({ baseURL: BASE_URL, securityWorker: () => { const token = _getAccessToken(); return token ? { headers: { Authorization: `Bearer ${token}` } } : {}; }, }); // Plug our axios instance into the generated client httpClient.instance = axiosInstance; export const apiClient = new Api(httpClient);