Files
lucia-frontend/src/api/client.js

100 lines
2.9 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");
deleteCookie("luciaRefreshToken");
deleteCookie("isLuciaLoggedIn");
window.location.href = "/login";
return Promise.reject(refreshError);
}
},
);
export default apiClient;