140 lines
4.1 KiB
TypeScript
140 lines
4.1 KiB
TypeScript
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<unknown, RetryRequestConfig>): Promise<AxiosResponse> => {
|
|
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<AxiosResponse>((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 };
|