import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios'; import { env } from '@/lib/env'; // Import tokenStorage dynamically to avoid circular dependencies const getTokenStorage = () => { if (typeof window !== 'undefined') { // Dynamic import to avoid SSR issues return require('@/lib/services/auth/tokenStorage').tokenStorage; } return null; }; // Track if we're currently refreshing to prevent multiple refresh attempts let isRefreshing = false; let refreshSubscribers: ((token: string) => void)[] = []; function subscribeTokenRefresh(cb: (token: string) => void) { refreshSubscribers.push(cb); } function onTokenRefreshed(token: string) { refreshSubscribers.forEach((cb) => cb(token)); refreshSubscribers = []; } // Create axios instance with default config const apiClient: AxiosInstance = axios.create({ baseURL: env.NEXT_PUBLIC_DJANGO_API_URL, timeout: 30000, headers: { 'Content-Type': 'application/json', }, }); // Request interceptor - Add auth token apiClient.interceptors.request.use( (config: InternalAxiosRequestConfig) => { // Add auth token if available (client-side only) if (typeof window !== 'undefined') { const tokenStorage = getTokenStorage(); const token = tokenStorage?.getAccessToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } } // Log requests in development if (process.env.NODE_ENV === 'development') { console.log(`[API Request] ${config.method?.toUpperCase()} ${config.url}`); } return config; }, (error) => { return Promise.reject(error); } ); // Response interceptor - Handle errors and token refresh apiClient.interceptors.response.use( (response) => { // Log responses in development if (process.env.NODE_ENV === 'development') { console.log(`[API Response] ${response.config.method?.toUpperCase()} ${response.config.url}`, response.status); } return response; }, async (error: AxiosError) => { const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean }; // Log errors in development if (process.env.NODE_ENV === 'development') { console.error('[API Error]', error.message, error.response?.status); } // Handle 401 Unauthorized - attempt token refresh if (error.response?.status === 401 && originalRequest && !originalRequest._retry) { if (typeof window !== 'undefined') { const tokenStorage = getTokenStorage(); const refreshToken = tokenStorage?.getRefreshToken(); // Only attempt refresh if we have a refresh token if (refreshToken && !isRefreshing) { originalRequest._retry = true; if (!isRefreshing) { isRefreshing = true; try { // Attempt to refresh the token const response = await axios.post( `${env.NEXT_PUBLIC_DJANGO_API_URL}/api/v1/auth/token/refresh`, { refresh: refreshToken } ); const { access, refresh: newRefreshToken } = response.data; // Store new tokens tokenStorage.setAccessToken(access); if (newRefreshToken) { tokenStorage.setRefreshToken(newRefreshToken); } // Notify all subscribers onTokenRefreshed(access); isRefreshing = false; // Retry the original request with new token if (originalRequest.headers) { originalRequest.headers.Authorization = `Bearer ${access}`; } return apiClient(originalRequest); } catch (refreshError) { // Refresh failed, clear tokens and redirect to login isRefreshing = false; tokenStorage?.clearTokens(); // Redirect to login page if (window.location.pathname !== '/login') { window.location.href = '/login?session_expired=true'; } return Promise.reject(refreshError); } } else { // If already refreshing, wait for the refresh to complete return new Promise((resolve) => { subscribeTokenRefresh((token: string) => { if (originalRequest.headers) { originalRequest.headers.Authorization = `Bearer ${token}`; } resolve(apiClient(originalRequest)); }); }); } } else { // No refresh token available, clear storage and redirect tokenStorage?.clearTokens(); if (window.location.pathname !== '/login') { window.location.href = '/login?session_expired=true'; } } } } // Retry logic for network errors const config = error.config as AxiosRequestConfig & { _retryCount?: number }; if (!error.response && config && !config._retryCount) { config._retryCount = (config._retryCount || 0) + 1; if (config._retryCount <= 3) { // Exponential backoff const delay = Math.min(1000 * Math.pow(2, config._retryCount - 1), 10000); await new Promise((resolve) => setTimeout(resolve, delay)); return apiClient(config); } } return Promise.reject(error); } ); export { apiClient }; export type { AxiosError };