import axios, { AxiosRequestConfig, AxiosResponse, AxiosError, InternalAxiosRequestConfig, } from 'axios'; import { toast } from 'react-toastify'; import { getRouteLang } from './getLanguage'; // ─── Error message extractor ─────────────────────────────────────────────────── function extractErrorMessage(error: AxiosError): string { const data = error.response?.data as Record | undefined; if (!data) { if (error.code === 'ECONNABORTED') return 'Request timed out. Please try again.'; if (!navigator.onLine) return 'No internet connection.'; return error.message || 'An unexpected error occurred.'; } // Simple string fields: { message, detail, error } if (typeof data.message === 'string' && data.message) return data.message; if (typeof data.detail === 'string' && data.detail) return data.detail; if (typeof data.error === 'string' && data.error) return data.error; // Wrapped: { errors: { field: ["msg"] } } if (data.errors && typeof data.errors === 'object') { const first = Object.values(data.errors as Record)[0]; if (Array.isArray(first) && first.length > 0) return String(first[0]); if (typeof first === 'string') return first; } // DRF field-level errors at top level: { phone: ["msg"], name: ["msg"] } for (const val of Object.values(data)) { if (Array.isArray(val) && val.length > 0 && typeof val[0] === 'string') { return val[0]; } if (typeof val === 'string' && val) return val; } return 'An unexpected error occurred.'; } // ─── Constants ───────────────────────────────────────────────────────────────── // const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL; const baseUrl = 'https://api.anti-plagiat.uz/api/v1'; const DEFAULT_LOCALE = 'uz'; // fallback locale for redirect // ─── Token helpers ───────────────────────────────────────────────────────────── // Adjust key names to match whatever your auth stores them under. export const TokenStorage = { getAccess: (): string | null => localStorage.getItem('access_token'), getRefresh: (): string | null => localStorage.getItem('refresh_token'), setAccess: (token: string): void => localStorage.setItem('access_token', token), setRefresh: (token: string): void => localStorage.setItem('refresh_token', token), setTokens: (access: string, refresh: string): void => { localStorage.setItem('access_token', access); localStorage.setItem('refresh_token', refresh); }, clear: (): void => { localStorage.removeItem('access_token'); localStorage.removeItem('refresh_token'); }, }; // ─── Redirect helper ─────────────────────────────────────────────────────────── function redirectToMain(): void { // Detect current locale from the URL path, fall back to DEFAULT_LOCALE const pathLocale = window.location.pathname.split('/')[1]; const validLocales = ['uz', 'ru', 'en']; const locale = validLocales.includes(pathLocale) ? pathLocale : DEFAULT_LOCALE; window.location.href = `/${locale}`; } // ─── Axios instance ──────────────────────────────────────────────────────────── const api = axios.create({ baseURL: baseUrl, timeout: 15_000, }); // ─── Flag to prevent multiple simultaneous refresh calls ────────────────────── let isRefreshing = false; // Queue of failed requests waiting for the new token type FailedRequestResolver = (token: string) => void; let failedQueue: { resolve: FailedRequestResolver; reject: (err: unknown) => void; }[] = []; function processQueue(error: unknown, token: string | null = null): void { failedQueue.forEach(({ resolve, reject }) => { if (error) { reject(error); } else if (token) { resolve(token); } }); failedQueue = []; } // ─── Request interceptor — attach access token ──────────────────────────────── api.interceptors.request.use( (config: InternalAxiosRequestConfig) => { const token = TokenStorage.getAccess(); if (token) { config.headers.Authorization = `Bearer ${token}`; } config.headers['Accept-Language'] = getRouteLang(); return config; }, (error: AxiosError) => Promise.reject(error), ); // ─── Response interceptor — handle 401, refresh, retry ─────────────────────── api.interceptors.response.use( // ── 2xx: pass through unchanged ──────────────────────────────────────────── (response: AxiosResponse) => response, // ── Error: attempt token refresh on 401 ──────────────────────────────────── async (error: AxiosError) => { const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean; }; const status = error.response?.status; // const responseData = error.response?.data as Record | undefined; const requestUrl = originalRequest.url ?? ''; const isAuthEndpoint = requestUrl.includes('/users/login/') || requestUrl.includes('/users/register/'); // 403 with token_not_valid means the token is expired — clear and redirect if (status === 403) { TokenStorage.clear(); redirectToMain(); return Promise.reject(error); } // For auth endpoints, 401 means wrong credentials — show error, don't refresh if (isAuthEndpoint || status !== 401 || originalRequest._retry) { toast.error(extractErrorMessage(error)); return Promise.reject(error); } const refreshToken = TokenStorage.getRefresh(); // No refresh token available — clear everything and redirect if (!refreshToken) { TokenStorage.clear(); redirectToMain(); return Promise.reject(error); } // If a refresh is already in progress, queue this request if (isRefreshing) { return new Promise((resolve, reject) => { failedQueue.push({ resolve: (token: string) => { originalRequest.headers.Authorization = `Bearer ${token}`; resolve(api(originalRequest)); }, reject, }); }); } // Mark that we're refreshing and this request has been retried originalRequest._retry = true; isRefreshing = true; try { // ── Call your refresh endpoint ────────────────────────────────────────── // Adjust the URL and payload shape to match your backend. const { data } = await axios.post<{ access_token: string; refresh_token: string; }>( `${baseUrl}/auth/refresh`, // ← your refresh endpoint { refresh_token: refreshToken }, { headers: { 'Accept-Language': getRouteLang() } }, ); const { access_token, refresh_token } = data; TokenStorage.setTokens(access_token, refresh_token); // Update the Authorization header for the retried request originalRequest.headers.Authorization = `Bearer ${access_token}`; // Unblock all queued requests with the new token processQueue(null, access_token); // Retry the original failed request return api(originalRequest); } catch (refreshError) { // Refresh itself failed — clear tokens and redirect to home processQueue(refreshError, null); TokenStorage.clear(); redirectToMain(); return Promise.reject(refreshError); } finally { isRefreshing = false; } }, ); // ─── Public request function ─────────────────────────────────────────────────── export const apiRequest = async ( method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', url: string, data?: unknown, config?: Omit, ): Promise> => { const response = await api.request({ method, url, data, ...config, headers: { ...config?.headers, // Accept-Language is already set in the request interceptor, // but config?.headers can still override it if needed. }, }); console.log('resposne: ', response); return response; };