import { boot } from 'quasar/wrappers'; import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; import axios from 'axios'; import { useLogin } from 'src/vueLib/login/useLogin'; const host = import.meta.env.VITE_API_URL; // Create axios instance export const appApi: AxiosInstance = axios.create({ baseURL: host, timeout: 10000, withCredentials: true, }); interface RetryRequestConfig extends AxiosRequestConfig { _retry?: boolean; } const noRefreshEndpoints = ['/login', '/login/refresh', '/logout']; // ========= Refresh Queue Handling ========= // let isRefreshing = false; interface FailedRequest { resolve: (value?: unknown) => void; reject: (reason?: Error) => void; } let failedQueue: FailedRequest[] = []; const processQueue = (error: Error | null): void => { failedQueue.forEach((prom) => { if (error) prom.reject(error); else prom.resolve(); }); failedQueue = []; }; // ========================================= // appApi.interceptors.response.use( (response: AxiosResponse) => response, async (error: AxiosError): Promise => { const { refresh, logout } = useLogin(); const originalRequest = error.config as RetryRequestConfig | undefined; // Skip refresh for login/logout endpoints if ( !originalRequest || noRefreshEndpoints.some((url) => originalRequest.url?.includes(url ?? '')) ) { const message = error instanceof Error ? error.message : JSON.stringify(error); throw new Error(message); } // Handle unauthorized responses if (error.response?.status === 401 && !originalRequest._retry) { const data = error.response?.data; const serverMessage = typeof data === 'object' && data !== null && 'message' in data ? (data as { message: string }).message : undefined; if (['no refresh token', 'is expired'].some((msg) => serverMessage?.includes(msg))) { console.warn('[Axios] No refresh token — logging out'); try { await logout(); } catch (logoutErr) { console.error('[Axios] Logout failed:', logoutErr); } throw new Error('Session expired: no refresh token'); } if (isRefreshing) { // Wait until refresh completes return new Promise((resolve, reject) => { failedQueue.push({ resolve: () => { void appApi(originalRequest).then(resolve).catch(reject); }, reject, }); }); } originalRequest._retry = true; isRefreshing = true; try { const refreshed = await refresh().catch(() => false); processQueue(null); if (refreshed) { // Token refreshed successfully → retry request return appApi(originalRequest); } // Refresh returned false → logout console.warn('[Axios] Refresh returned false, logging out'); await logout(); throw new Error('Token refresh failed'); } catch (err) { const e = err instanceof Error ? err : new Error(String(err)); console.error('[Axios] Token refresh failed:', e.message); // Always logout, even if refresh throws try { await logout(); } catch (logoutErr) { console.error('[Axios] Logout failed after token refresh error:', logoutErr); } processQueue(e); throw e; } finally { isRefreshing = false; } } // Not a 401 — rethrow as Error let msg = ''; if (error && (error as AxiosError).isAxiosError) { const axiosError = error as AxiosError<{ message?: string }>; msg = axiosError.response?.data?.message ?? axiosError.message; } else { msg = error instanceof Error ? error.message : JSON.stringify(error); } throw new Error(msg); }, ); // ======== Boot registration for Quasar ======== // export default boot(({ app }) => { app.config.globalProperties.$axios = axios; app.config.globalProperties.$appApi = appApi; }); export { axios };