imeeting/imeeting-h5/src/api/http.ts

163 lines
4.1 KiB
TypeScript

import axios from "axios";
import { message } from "antd";
import { buildLoginRedirect, clearAuth, getAccessToken, getRefreshToken, saveTokens } from "@/utils/auth";
declare module "axios" {
interface AxiosRequestConfig {
suppressErrorToast?: boolean;
}
interface InternalAxiosRequestConfig {
suppressErrorToast?: boolean;
_retry?: boolean;
}
}
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 SUCCESS_CODE = "200";
const REFRESH_AHEAD_MS = 60 * 1000;
const http = axios.create({
baseURL: "/",
timeout: 15000,
});
const refreshClient = axios.create({
baseURL: "/",
timeout: 15000,
});
let refreshPromise: Promise<string | null> | null = null;
function isSuccessCode(code: unknown) {
return String(code) === SUCCESS_CODE;
}
function isAuthWhitelistRequest(url?: string) {
return AUTH_WHITELIST.some((path) => (url || "").includes(path));
}
function getTokenExpireAt(token: string): number | null {
try {
const payload = token.split(".")[1];
if (!payload) {
return null;
}
const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
const parsed = JSON.parse(decodeURIComponent(escape(window.atob(normalized))));
return typeof parsed.exp === "number" ? parsed.exp * 1000 : null;
} catch {
return null;
}
}
function isTokenExpiringSoon(token: string) {
const expireAt = getTokenExpireAt(token);
if (!expireAt) {
return false;
}
return expireAt - Date.now() <= REFRESH_AHEAD_MS;
}
async function refreshAccessToken(): Promise<string | null> {
if (refreshPromise) {
return refreshPromise;
}
const refreshToken = getRefreshToken();
if (!refreshToken) {
return null;
}
refreshPromise = refreshClient
.post("/sys/auth/refresh", { refreshToken })
.then((resp) => {
const body = resp.data;
if (!body || !isSuccessCode(body.code) || !body.data?.accessToken || !body.data?.refreshToken) {
throw new Error(body?.msg || "刷新登录态失败");
}
saveTokens(body.data.accessToken, body.data.refreshToken);
return body.data.accessToken as string;
})
.catch(() => {
clearAuth();
return null;
})
.finally(() => {
refreshPromise = null;
});
return refreshPromise;
}
http.interceptors.request.use(async (config) => {
if (!isAuthWhitelistRequest(config.url)) {
const currentToken = getAccessToken();
if (currentToken && isTokenExpiringSoon(currentToken)) {
await refreshAccessToken();
}
}
const token = getAccessToken();
if (token) {
config.headers = config.headers || {};
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
http.interceptors.response.use(
(resp) => {
const body = resp.data;
if (body && !isSuccessCode(body.code)) {
const errorMsg = body.msg || "请求失败";
if (!resp.config?.suppressErrorToast) {
message.error(errorMsg);
}
const err = new Error(errorMsg);
(err as Error & { code?: string }).code = body.code;
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)) {
clearAuth();
window.location.href = buildLoginRedirect();
return Promise.reject(error);
}
const errorMsg = error.response?.data?.msg || error.message || "网络异常";
if (!originalRequest.suppressErrorToast) {
message.error(errorMsg);
}
return Promise.reject(error);
},
);
export default http;