98 lines
2.8 KiB
JavaScript
98 lines
2.8 KiB
JavaScript
// The Lucia project.
|
|
// Copyright 2023-2026 DSP, inc. All rights reserved.
|
|
// Authors:
|
|
// imacat.yang@dsp.im (imacat), 2023/9/23
|
|
/**
|
|
* @module apiClient Centralized axios instance with request/response
|
|
* interceptors for authentication token management and automatic
|
|
* 401 token refresh with request queuing.
|
|
*/
|
|
|
|
import axios from 'axios';
|
|
import { getCookie, deleteCookie } from '@/utils/cookieUtil.js';
|
|
|
|
/** Axios instance configured with auth interceptors. */
|
|
const apiClient = axios.create();
|
|
|
|
// Request interceptor: automatically attach Authorization header
|
|
apiClient.interceptors.request.use((config) => {
|
|
const token = getCookie('luciaToken');
|
|
if (token) {
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
return config;
|
|
});
|
|
|
|
// Response interceptor: handle 401 by attempting token refresh
|
|
let isRefreshing = false;
|
|
let pendingRequests = [];
|
|
|
|
/**
|
|
* Resolves all pending requests with the new access token.
|
|
* @param {string} newToken - The refreshed access token.
|
|
*/
|
|
function onRefreshSuccess(newToken) {
|
|
pendingRequests.forEach((cb) => cb(newToken));
|
|
pendingRequests = [];
|
|
}
|
|
|
|
/**
|
|
* Rejects all pending requests with the refresh error.
|
|
* @param {Error} error - The token refresh error.
|
|
*/
|
|
function onRefreshFailure(error) {
|
|
pendingRequests.forEach((cb) => cb(null, error));
|
|
pendingRequests = [];
|
|
}
|
|
|
|
apiClient.interceptors.response.use(
|
|
(response) => response,
|
|
async (error) => {
|
|
const originalRequest = error.config;
|
|
|
|
// Only attempt refresh on 401, and not for auth endpoints or already-retried requests
|
|
if (
|
|
error.response?.status !== 401 ||
|
|
originalRequest._retried ||
|
|
originalRequest.url === '/api/oauth/token'
|
|
) {
|
|
return Promise.reject(error);
|
|
}
|
|
|
|
if (isRefreshing) {
|
|
// Queue this request until the refresh completes
|
|
return new Promise((resolve, reject) => {
|
|
pendingRequests.push((newToken, err) => {
|
|
if (err) return reject(err);
|
|
originalRequest.headers.Authorization = `Bearer ${newToken}`;
|
|
originalRequest._retried = true;
|
|
resolve(apiClient(originalRequest));
|
|
});
|
|
});
|
|
}
|
|
|
|
isRefreshing = true;
|
|
originalRequest._retried = true;
|
|
|
|
try {
|
|
// Dynamic import to avoid circular dependency with login store
|
|
const { refreshTokenAndGetNew } = await import('@/api/auth.js');
|
|
const newToken = await refreshTokenAndGetNew();
|
|
isRefreshing = false;
|
|
onRefreshSuccess(newToken);
|
|
originalRequest.headers.Authorization = `Bearer ${newToken}`;
|
|
return apiClient(originalRequest);
|
|
} catch (refreshError) {
|
|
isRefreshing = false;
|
|
onRefreshFailure(refreshError);
|
|
|
|
// Refresh failed: clear auth and redirect to login
|
|
deleteCookie('luciaToken');
|
|
window.location.href = '/login';
|
|
return Promise.reject(refreshError);
|
|
}
|
|
}
|
|
);
|
|
|
|
export default apiClient;
|