imeeting/frontend/src/api/http.ts

193 lines
5.0 KiB
TypeScript

import axios from "axios";
import { message } from "antd";
declare module "axios" {
interface AxiosRequestConfig {
suppressErrorToast?: boolean;
}
interface InternalAxiosRequestConfig {
suppressErrorToast?: boolean;
}
}
const http = axios.create({
baseURL: "/",
timeout: 15000
});
const refreshClient = axios.create({
baseURL: "/",
timeout: 15000
});
const AUTH_WHITELIST = [
"/sys/auth/login",
"/sys/auth/refresh",
"/sys/auth/captcha",
"/sys/auth/device-code",
"/sys/auth/password-policy/public",
"/sys/auth/password-recovery/send-code",
"/sys/auth/password-recovery/reset"
];
const API_SUCCESS_CODE = "200";
const REFRESH_AHEAD_MS = 60 * 1000;
let refreshPromise: Promise<string | null> | null = null;
function isApiSuccessCode(code: unknown): boolean {
return String(code) === API_SUCCESS_CODE;
}
function getTokenPayload(token: string): Record<string, any> | null {
try {
const payload = token.split(".")[1];
if (!payload) {
return null;
}
const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
return JSON.parse(decodeURIComponent(escape(window.atob(normalized))));
} catch {
return null;
}
}
function getTokenExpireAt(token: string): number | null {
const payload = getTokenPayload(token);
if (!payload || typeof payload.exp !== "number") {
return null;
}
return payload.exp * 1000;
}
function isTokenExpiringSoon(token: string): boolean {
const expireAt = getTokenExpireAt(token);
if (!expireAt) {
return false;
}
return expireAt - Date.now() <= REFRESH_AHEAD_MS;
}
function clearAuthStorage() {
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
sessionStorage.removeItem("userProfile");
}
function persistTokens(data: { accessToken: string; refreshToken: string }) {
localStorage.setItem("accessToken", data.accessToken);
localStorage.setItem("refreshToken", data.refreshToken);
const payload = getTokenPayload(data.accessToken);
if (payload && payload.tenantId !== undefined && payload.tenantId !== null) {
localStorage.setItem("activeTenantId", String(payload.tenantId));
}
}
function isAuthWhitelistRequest(url?: string) {
return AUTH_WHITELIST.some((path) => (url || "").includes(path));
}
async function refreshAccessToken(): Promise<string | null> {
if (refreshPromise) {
return refreshPromise;
}
const refreshToken = localStorage.getItem("refreshToken");
if (!refreshToken) {
return null;
}
refreshPromise = refreshClient
.post("/sys/auth/refresh", { refreshToken })
.then((resp) => {
const body = resp.data;
if (!body || !isApiSuccessCode(body.code) || !body.data?.accessToken || !body.data?.refreshToken) {
throw new Error(body?.msg || "刷新登录态失败");
}
persistTokens(body.data);
return body.data.accessToken as string;
})
.catch(() => {
clearAuthStorage();
return null;
})
.finally(() => {
refreshPromise = null;
});
return refreshPromise;
}
http.interceptors.request.use(async (config) => {
if (!isAuthWhitelistRequest(config.url)) {
const currentToken = localStorage.getItem("accessToken");
if (currentToken && isTokenExpiringSoon(currentToken)) {
await refreshAccessToken();
}
}
const token = localStorage.getItem("accessToken");
if (token) {
config.headers = config.headers || {};
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
http.interceptors.response.use(
(resp) => {
const body = resp.data;
if (body && !isApiSuccessCode(body.code)) {
const errorMsg = body.msg || "请求失败";
if (!resp.config?.suppressErrorToast) {
message.error(errorMsg);
}
const err = new Error(errorMsg);
(err as any).code = body.code;
(err as any).msg = body.msg;
return Promise.reject(err);
}
return resp;
},
async (error) => {
const originalRequest = error.config || {};
if (
error.response?.status === 401 &&
!originalRequest._retry &&
!isAuthWhitelistRequest(originalRequest.url)
) {
originalRequest._retry = true;
const newToken = await refreshAccessToken();
if (newToken) {
originalRequest.headers = originalRequest.headers || {};
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return http(originalRequest);
}
}
if (error.response && (error.response.status === 401 || error.response.status === 403)) {
clearAuthStorage();
window.location.href = "/login?timeout=1";
return Promise.reject(error);
}
const body = error.response?.data;
const errorMsg = body?.msg || error.message || "网络异常";
if (!originalRequest.suppressErrorToast) {
message.error(errorMsg);
}
if (body && body.msg) {
const err = new Error(body.msg);
(err as any).code = body.code;
(err as any).msg = body.msg;
return Promise.reject(err);
}
return Promise.reject(error);
}
);
export default http;