import axios, { type AxiosInstance, type AxiosError, type InternalAxiosRequestConfig } from 'axios'; import type { RootState } from '@/store/store'; import { navigate } from '@/utils/navigation'; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api/v1'; // Create axios instance const apiClient: AxiosInstance = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json', }, }); // Request interceptor to add auth token to ALL requests apiClient.interceptors.request.use( (config: InternalAxiosRequestConfig) => { // For FormData requests, let axios automatically set Content-Type with boundary // Remove the default application/json Content-Type if FormData is detected if (config.data instanceof FormData && config.headers) { delete config.headers['Content-Type']; } // Always try to get token from Redux store and add to Authorization header try { const store = (window as any).__REDUX_STORE__; if (store) { const state = store.getState() as RootState; const token = state?.auth?.accessToken; // Add Bearer token to all requests if token exists if (token && config.headers) { config.headers.Authorization = `Bearer ${token}`; } } } catch (error) { // Silently fail if store is not available console.warn('Redux store not available for token injection'); } return config; }, (error: AxiosError) => { return Promise.reject(error); } ); // Queue to store requests while token is being refreshed let isRefreshing = false; let failedQueue: any[] = []; const processQueue = (error: any, token: string | null = null) => { failedQueue.forEach((prom) => { if (error) { prom.reject(error); } else { prom.resolve(token); } }); failedQueue = []; }; // Response interceptor for error handling and token refresh apiClient.interceptors.response.use( (response) => response, async (error: AxiosError) => { const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }; // Handle 401 Unauthorized errors if (error.response?.status === 401 && !originalRequest._retry) { const requestUrl = originalRequest.url || ''; // Skip refresh for auth endpoints to avoid loops const isAuthEndpoint = requestUrl.includes('/auth/login') || requestUrl.includes('/auth/logout') || requestUrl.includes('/auth/refresh'); if (isAuthEndpoint) { return Promise.reject(error); } // If already refreshing, queue this request if (isRefreshing) { return new Promise(function (resolve, reject) { failedQueue.push({ resolve, reject }); }) .then((token) => { if (originalRequest.headers) { originalRequest.headers.Authorization = `Bearer ${token}`; } return apiClient(originalRequest); }) .catch((err) => { return Promise.reject(err); }); } originalRequest._retry = true; isRefreshing = true; try { const store = (window as any).__REDUX_STORE__; if (store) { const state = store.getState() as RootState; const refreshToken = state?.auth?.refreshToken; if (refreshToken) { // Call refresh API directly using axios to avoid cycles and interceptors const response = await axios.post(`${API_BASE_URL}/auth/refresh`, { refresh_token: refreshToken, }); if (response.data?.success) { const { access_token } = response.data.data; // Update store with new tokens // We use action type string to avoid circular dependency with authSlice store.dispatch({ type: 'auth/setTokens', payload: response.data.data }); processQueue(null, access_token); if (originalRequest.headers) { originalRequest.headers.Authorization = `Bearer ${access_token}`; } isRefreshing = false; return apiClient(originalRequest); } } } } catch (refreshError) { processQueue(refreshError, null); } finally { isRefreshing = false; } // If we reach here, refresh failed or was not possible - redirect to login const isTenantRoute = window.location.pathname.startsWith('/tenant/') || window.location.pathname === '/tenant'; let redirectPath = '/'; try { const store = (window as any).__REDUX_STORE__; if (store) { const state = store.getState() as RootState; const isSuperAdmin = state.auth.roles.includes('super_admin'); redirectPath = isSuperAdmin ? '/' : (isTenantRoute ? '/tenant/login' : '/'); store.dispatch({ type: 'auth/logout' }); } } catch (e) { redirectPath = isTenantRoute ? '/tenant/login' : '/'; } navigate(redirectPath, { replace: true }); } return Promise.reject(error); } ); export default apiClient;