import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'; import { useAuthStore } from '../stores/authStore'; const api = axios.create({ baseURL: '/api', headers: { 'Content-Type': 'application/json' }, withCredentials: true, // Send httpOnly cookies for refresh token }); api.interceptors.request.use((config) => { const token = useAuthStore.getState().token; if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // ─── Silent Refresh Logic ───────────────────────────────────────── let isRefreshing = false; let pendingQueue: Array<{ resolve: (token: string) => void; reject: (err: any) => void; }> = []; function processPendingQueue(error: any, token: string | null) { pendingQueue.forEach((p) => { if (error) { p.reject(error); } else { p.resolve(token!); } }); pendingQueue = []; } api.interceptors.response.use( (response) => response, async (error: AxiosError) => { const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }; // If 401 and we haven't retried yet, try refreshing the token if ( error.response?.status === 401 && originalRequest && !originalRequest._retry && !originalRequest.url?.includes('/auth/refresh') && !originalRequest.url?.includes('/auth/login') ) { originalRequest._retry = true; if (isRefreshing) { // Another request is already refreshing — queue this one return new Promise((resolve, reject) => { pendingQueue.push({ resolve: (token: string) => { originalRequest.headers.Authorization = `Bearer ${token}`; resolve(api(originalRequest)); }, reject: (err: any) => reject(err), }); }); } isRefreshing = true; try { const { data } = await axios.post('/api/auth/refresh', {}, { withCredentials: true }); const newToken = data.accessToken; useAuthStore.getState().setToken(newToken); originalRequest.headers.Authorization = `Bearer ${newToken}`; processPendingQueue(null, newToken); return api(originalRequest); } catch (refreshError) { processPendingQueue(refreshError, null); useAuthStore.getState().logout(); window.location.href = '/login'; return Promise.reject(refreshError); } finally { isRefreshing = false; } } // Non-retryable 401 (e.g. refresh failed, login failed) if (error.response?.status === 401 && originalRequest?.url?.includes('/auth/refresh')) { useAuthStore.getState().logout(); window.location.href = '/login'; } // Handle org suspended/archived — redirect to org selection const responseData = error.response?.data as any; if ( error.response?.status === 403 && typeof responseData?.message === 'string' && responseData.message.includes('has been') ) { const store = useAuthStore.getState(); store.setCurrentOrg({ id: '', name: '', role: '' }); // Clear current org window.location.href = '/select-org'; } return Promise.reject(error); }, ); export default api;