import axios from 'axios'; import { tokenStorage } from '@/lib/utils/tokenStorage'; const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'; export const apiClient = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json', }, withCredentials: true, }); // Request interceptor to add auth token apiClient.interceptors.request.use( (config) => { const token = tokenStorage.getAccessToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => { return Promise.reject(error); } ); // Response interceptor to handle token refresh apiClient.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; // Only handle token refresh on client side if (typeof window === 'undefined') { return Promise.reject(error); } // If error is 401 and we haven't tried to refresh yet if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; try { const refreshToken = tokenStorage.getRefreshToken(); const deviceId = tokenStorage.getDeviceId(); console.log('[API Client] Attempting token refresh, refreshToken exists:', !!refreshToken, 'deviceId exists:', !!deviceId); if (!refreshToken) { console.error('[API Client] No refresh token found in storage'); throw new Error('No refresh token'); } // Use a plain axios instance without interceptors to avoid loops const refreshPayload: { refreshToken: string; deviceId?: string } = { refreshToken, }; if (deviceId) { refreshPayload.deviceId = deviceId; } const refreshResponse = await axios.create().post( `${API_BASE_URL}/api/v1/auth/refresh`, refreshPayload, { headers: { 'Content-Type': 'application/json' }, withCredentials: true } ); const response = refreshResponse; // Handle different response structures let newAccessToken; let newRefreshToken; if (response.data?.data?.tokens?.accessToken) { newAccessToken = response.data.data.tokens.accessToken; newRefreshToken = response.data.data.tokens.refreshToken; } else if (response.data?.tokens?.accessToken) { newAccessToken = response.data.tokens.accessToken; newRefreshToken = response.data.tokens.refreshToken; } else if (response.data?.accessToken) { newAccessToken = response.data.accessToken; newRefreshToken = response.data.refreshToken; } else { throw new Error('Invalid token refresh response'); } // Update tokens in storage tokenStorage.setAccessToken(newAccessToken); if (newRefreshToken) { tokenStorage.setRefreshToken(newRefreshToken); } // Retry original request with new token originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; return apiClient(originalRequest); } catch (refreshError: any) { console.error('[API Client] Token refresh failed:', refreshError); // Only clear tokens if this is a real auth failure (not a network error) // and not during the initial page load where React Strict Mode might cause issues const isAuthFailure = refreshError?.response?.status === 401 || refreshError?.response?.status === 403; // Check if this is likely a React Strict Mode double-invocation // by seeing if we're in development mode and the error happened very quickly const isDevelopment = process.env.NODE_ENV === 'development'; if (isAuthFailure && !isDevelopment) { console.log('[API Client] Auth failure in production, clearing tokens'); tokenStorage.clearTokens(); } else if (isDevelopment) { console.log('[API Client] Development mode - not clearing tokens to avoid React Strict Mode issues'); } // Avoid redirect loop - only redirect if not already on login page // and only in production or after a real auth failure if (!window.location.pathname.includes('/login') && isAuthFailure && !isDevelopment) { window.location.href = '/login'; } return Promise.reject(refreshError); } } return Promise.reject(error); } ); export default apiClient;