// /src/api/axiosInstance.js import axios from 'axios'; import Cookies from 'js-cookie'; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api'; const axiosInstance = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, timeout: 15000 }); // Request interceptor: attach token axiosInstance.interceptors.request.use( (config) => { const token = localStorage.getItem('accessToken') || Cookies.get('accessToken'); if (token) { config.headers.Authorization = `Bearer ${token}`; } // Ask backend for Persian (or stored) localized error messages const lang = localStorage.getItem('lang') || 'fa'; config.headers['Accept-Language'] = lang; if (!config.params) config.params = {}; if (config.params.lang == null) { config.params.lang = lang; } const isFormData = typeof FormData !== 'undefined' && config.data instanceof FormData; if (isFormData) { // Let the browser set multipart boundary — a bare multipart/form-data header breaks Multer if (typeof config.headers?.delete === 'function') { config.headers.delete('Content-Type'); config.headers.delete('content-type'); } else { delete config.headers['Content-Type']; delete config.headers['content-type']; } config.timeout = Math.max(config.timeout || 0, 120000); } return config; }, (error) => Promise.reject(error) ); // Response interceptor: auto-refresh token on 401 let isRefreshing = false; let failedQueue = []; const processQueue = (error, token = null) => { failedQueue.forEach(prom => { if (error) { prom.reject(error); } else { prom.resolve(token); } }); failedQueue = []; }; axiosInstance.interceptors.response.use( (response) => response.data, async (error) => { const originalRequest = error.config; if (error.response?.status === 401 && !originalRequest._retry) { if (originalRequest.url.includes('/auth/login') || originalRequest.url.includes('/auth/refresh')) { return Promise.reject(error); } if (isRefreshing) { return new Promise((resolve, reject) => { failedQueue.push({ resolve, reject }); }) .then((token) => { originalRequest.headers.Authorization = `Bearer ${token}`; return axiosInstance(originalRequest); }) .catch((err) => Promise.reject(err)); } originalRequest._retry = true; isRefreshing = true; const refreshToken = localStorage.getItem('refreshToken') || Cookies.get('refreshToken'); if (!refreshToken) { isRefreshing = false; localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); window.location.href = '/login'; return Promise.reject(error); } try { const refreshResponse = await axios.post(`${API_BASE_URL}/auth/refresh`, { refreshToken }); const data = refreshResponse.data?.data || refreshResponse.data; const newAccessToken = data.accessToken; const newRefreshToken = data.refreshToken; localStorage.setItem('accessToken', newAccessToken); Cookies.set('accessToken', newAccessToken); if (newRefreshToken) { localStorage.setItem('refreshToken', newRefreshToken); Cookies.set('refreshToken', newRefreshToken); } axiosInstance.defaults.headers.common.Authorization = `Bearer ${newAccessToken}`; originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; processQueue(null, newAccessToken); return axiosInstance(originalRequest); } catch (refreshErr) { processQueue(refreshErr, null); localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); Cookies.remove('accessToken'); Cookies.remove('refreshToken'); window.location.href = '/login'; return Promise.reject(refreshErr); } finally { isRefreshing = false; } } return Promise.reject(error); } ); export default axiosInstance;