import axios from 'axios'; import { toast } from 'sonner'; import { useUserStore } from '../store/userStore'; const getBaseURL = () => { if (process.env.NEXT_PUBLIC_API_URL) return process.env.NEXT_PUBLIC_API_URL; if (typeof window === 'undefined') { return 'http://127.0.0.1:4400/api'; } return process.env.NODE_ENV === 'development' ? 'http://localhost:4400/api' : '/api'; }; const baseURL = getBaseURL(); export const BASE_DOMAIN = process.env.NEXT_PUBLIC_API_URL?.replace('/api', '') || (typeof window !== 'undefined' && process.env.NODE_ENV === 'development' ? 'http://localhost:4400' : ''); export interface ApiErrorPayload { success: boolean; statusCode: number; message: string; code: string; details?: Array<{ field: string; message: string }> | Record; timestamp?: string; path?: string; } const api = axios.create({ baseURL, timeout: 10000, headers: { 'Content-Type': 'application/json', }, }); // Interceptor to add auth token in request headers api.interceptors.request.use( (config) => { let token: string | null = null; if (typeof window !== 'undefined' && window.localStorage) { try { token = localStorage.getItem('accessToken'); } catch (e) { console.warn('Storage access restricted:', e); } } if (token && config.headers) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error) ); // Interceptor to handle 401/403, automatic refresh token rotation, and standardized toasts let isRefreshing = false; let failedQueue: Array<{ resolve: (value: unknown) => void; reject: (reason?: unknown) => void; }> = []; const processQueue = (error: unknown, token: string | null = null) => { failedQueue.forEach((prom) => { if (error) { prom.reject(error); } else { prom.resolve(token); } }); failedQueue = []; }; api.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; // Handle 401 Unauthorized with token refresh retry if ( typeof window !== 'undefined' && error.response?.status === 401 && !originalRequest?._retry && !originalRequest?.url?.includes('/auth/') ) { if (isRefreshing) { return new Promise((resolve, reject) => { failedQueue.push({ resolve, reject }); }) .then((token) => { originalRequest.headers.Authorization = `Bearer ${token}`; return api(originalRequest); }) .catch((err) => Promise.reject(err)); } originalRequest._retry = true; isRefreshing = true; const refreshToken = localStorage.getItem('refreshToken'); if (refreshToken) { try { const res = await axios.post( `${baseURL}/auth/refresh-token`, { refreshToken }, { headers: { 'Content-Type': 'application/json' } } ); const { accessToken, refreshToken: newRefreshToken } = res.data?.data || {}; if (accessToken) { localStorage.setItem('accessToken', accessToken); if (newRefreshToken) { localStorage.setItem('refreshToken', newRefreshToken); } api.defaults.headers.common.Authorization = `Bearer ${accessToken}`; originalRequest.headers.Authorization = `Bearer ${accessToken}`; processQueue(null, accessToken); isRefreshing = false; return api(originalRequest); } } catch (refreshErr) { processQueue(refreshErr, null); isRefreshing = false; } } else { isRefreshing = false; } // If refresh failed or no refresh token exists, clear state and logout if (window.localStorage) { try { localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); } catch {} } try { useUserStore.getState().logout(); } catch {} } if (typeof window !== 'undefined') { const responseData = error.response?.data as ApiErrorPayload | undefined; const status = error.response?.status; const farsiMessage = responseData?.message || 'خطایی در ارتباط با سرور رخ داده است.'; if (status === 403) { if (window.localStorage) { try { localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); } catch {} } try { useUserStore.getState().logout(); } catch { // Ignore if store not ready } } // Show formatted Farsi toast if not explicitly suppressed and not 401/403/404 if (!error.config?.hideErrorToast && status !== 401 && status !== 403 && status !== 404) { let fullMessage = farsiMessage; if (responseData?.details && Array.isArray(responseData.details) && responseData.details.length > 0) { const detailStrings = responseData.details .map((d: unknown) => typeof d === 'string' ? d : ((d as Record)?.message || (d as Record)?.field)) .filter(Boolean); if (detailStrings.length > 0) { const joinedDetails = detailStrings.join('، '); if (!farsiMessage.includes(joinedDetails)) { fullMessage = `${farsiMessage} (${joinedDetails})`; } } } toast.error(fullMessage, { id: fullMessage }); (error as unknown as Record)._toastShown = true; } } return Promise.reject(error); } ); export default api;