Files
track/frontend/src/services/api.ts

163 lines
4.6 KiB
TypeScript

import axios, { type AxiosRequestConfig } from "axios";
// ============================================================
// Token 存储 Key
// ============================================================
const ACCESS_TOKEN_KEY = "track_admin_token";
const REFRESH_TOKEN_KEY = "track_admin_refresh_token";
const USER_KEY = "track_admin_user";
// ============================================================
// Token 读写工具
// ============================================================
export function getAccessToken(): string | null {
return localStorage.getItem(ACCESS_TOKEN_KEY);
}
export function getRefreshToken(): string | null {
return localStorage.getItem(REFRESH_TOKEN_KEY);
}
export function setTokens(accessToken: string, refreshToken: string) {
localStorage.setItem(ACCESS_TOKEN_KEY, accessToken);
localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
}
export function clearAuth() {
localStorage.removeItem(ACCESS_TOKEN_KEY);
localStorage.removeItem(REFRESH_TOKEN_KEY);
localStorage.removeItem(USER_KEY);
}
// ============================================================
// Axios 实例
// ============================================================
const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000,
headers: {
"Content-Type": "application/json",
},
});
// ============================================================
// 请求拦截器 — 注入 Access Token
// ============================================================
api.interceptors.request.use(
(config) => {
const token = getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error),
);
// ============================================================
// 响应拦截器 — 双 Token 无感刷新 + 并发请求队列
// ============================================================
let isRefreshing = false;
let retryQueue: Array<{
resolve: (token: string) => void;
reject: (error: unknown) => void;
}> = [];
/** 处理队列中的所有挂起请求 */
function processQueue(error: unknown, token: string | null) {
retryQueue.forEach((p) => {
if (token) {
p.resolve(token);
} else {
p.reject(error);
}
});
retryQueue = [];
}
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest: AxiosRequestConfig & { _retry?: boolean } =
error.config;
const status = error.response?.status;
// 仅处理 401
if (status !== 401) {
return Promise.reject(error);
}
// 跳过登录和刷新接口自身(避免死循环)
const url = originalRequest.url ?? "";
if (url.includes("/auth/login") || url.includes("/auth/refresh")) {
return Promise.reject(error);
}
// 避免对同一请求重复刷新
if (originalRequest._retry) {
clearAuth();
if (window.location.pathname.startsWith("/admin")) {
window.location.href = "/admin/login";
}
return Promise.reject(error);
}
originalRequest._retry = true;
// ---- 并发请求队列机制 ----
if (isRefreshing) {
// 已有刷新进行中,加入队列等待
return new Promise<string>((resolve, reject) => {
retryQueue.push({ resolve, reject });
}).then((newToken) => {
originalRequest.headers = originalRequest.headers || {};
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return api(originalRequest);
});
}
isRefreshing = true;
try {
const refreshToken = getRefreshToken();
if (!refreshToken) {
throw new Error("无 Refresh Token");
}
// 调用刷新接口
const { data } = await axios.post(
`${import.meta.env.VITE_API_BASE_URL}/auth/refresh`,
{ refresh_token: refreshToken },
{ headers: { "Content-Type": "application/json" } },
);
const newAccessToken: string = data.access_token;
setTokens(newAccessToken, refreshToken); // 更新 Access Token
// 重放队列中的所有请求
processQueue(null, newAccessToken);
// 重试当前请求
originalRequest.headers = originalRequest.headers || {};
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
return api(originalRequest);
} catch (refreshError) {
// Refresh Token 也过期 — 彻底登出
processQueue(refreshError, null);
clearAuth();
if (window.location.pathname.startsWith("/admin")) {
window.location.href = "/admin/login";
}
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
},
);
export default api;