import axios from 'axios'; import { getAccessToken, getRefreshToken, getExpiresTime, setToken, removeToken } from './token'; import { useAuthStore } from '@/stores/useAuthStore'; import { getMessageApi } from './messageHolder'; const request = axios.create({ baseURL: import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_BASE_URL, timeout: 30000, }); let isRefreshing = false; let refreshSubscribers: Array<(token: string) => void> = []; function onRefreshed(token: string) { refreshSubscribers.forEach((cb) => cb(token)); refreshSubscribers = []; } function addRefreshSubscriber(cb: (token: string) => void) { refreshSubscribers.push(cb); } function isRemembered(): boolean { return localStorage.getItem('ACCESS_TOKEN') !== null; } let proactiveRefreshTimer: ReturnType | null = null; export function scheduleProactiveRefresh() { if (proactiveRefreshTimer) { clearTimeout(proactiveRefreshTimer); proactiveRefreshTimer = null; } const expiresTime = getExpiresTime(); if (!expiresTime) return; const delay = Math.max(expiresTime - Date.now() - 5 * 60 * 1000, 10_000); proactiveRefreshTimer = setTimeout(() => { refreshAccessToken().catch(() => {}); }, delay); } export function clearProactiveRefresh() { if (proactiveRefreshTimer) { clearTimeout(proactiveRefreshTimer); proactiveRefreshTimer = null; } } export async function refreshAccessToken(): Promise { const refreshToken = getRefreshToken(); if (!refreshToken || isRefreshing) return null; isRefreshing = true; try { const resp = await axios.post( `${import.meta.env.VITE_BASE_URL}${import.meta.env.VITE_API_BASE_URL}/system/auth/refresh-token?refreshToken=${refreshToken}`, null, ); const newAccessToken = resp.data?.data?.accessToken; const newRefreshToken = resp.data?.data?.refreshToken; if (newAccessToken) { const remember = isRemembered(); const expiresTime = resp.data?.data?.expiresTime; setToken(newAccessToken, newRefreshToken || refreshToken, remember, expiresTime); useAuthStore.setState({ token: newAccessToken }); onRefreshed(newAccessToken); if (expiresTime) scheduleProactiveRefresh(); return newAccessToken; } return null; } catch { return null; } finally { isRefreshing = false; } } request.interceptors.request.use((config) => { const token = getAccessToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); request.interceptors.response.use( (response) => { if (response.config.responseType === 'blob') return response; const { data } = response; if (data.code == 401) { const err = Object.assign(new Error(data.msg || 'Unauthorized'), { config: response.config, response: { status: 401 }, }); window.location.href = '/login'; return Promise.reject(err); } else if (data.code !== 0) { getMessageApi().error(data.msg || 'Request failed'); return Promise.reject(new Error(data.msg || 'Request failed')); } return data; }, async (error) => { const originalRequest = error.config; if (error.response?.status === 401 && !originalRequest?._retry) { const refreshToken = getRefreshToken(); if (refreshToken) { if (isRefreshing) { return new Promise((resolve) => { addRefreshSubscriber((token: string) => { originalRequest.headers.Authorization = `Bearer ${token}`; resolve(request(originalRequest)); }); }); } originalRequest._retry = true; const newToken = await refreshAccessToken(); if (newToken) { originalRequest.headers.Authorization = `Bearer ${newToken}`; return request(originalRequest); } } removeToken(); useAuthStore.getState().reset(); window.location.href = '/login'; return Promise.reject(error); } // Show error message for non-401 HTTP errors const msg = error.response?.data?.message || error.response?.data?.msg || error.message || 'Request failed'; try { getMessageApi().error(msg); } catch { /* ignore */ } return Promise.reject(error); }, ); // eslint-disable-next-line @typescript-eslint/no-explicit-any export function download(url: string, params?: any) { // eslint-disable-next-line @typescript-eslint/no-explicit-any return request.get(url, { params, responseType: 'blob' }).then((resp: any) => { const blob = new Blob([resp.data]); const contentDisposition = resp.headers?.['content-disposition']; let fileName = 'download'; if (contentDisposition) { const match = contentDisposition.match(/filename\*?=(?:UTF-8'')?([^;\s]+)/i); if (match) fileName = decodeURIComponent(match[1]); } const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = fileName; link.click(); URL.revokeObjectURL(link.href); }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any export function upload(url: string, data: Record) { const formData = new FormData(); Object.entries(data).forEach(([key, value]) => { formData.append(key, value); }); return request.post(url, formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); } export default request;