Files
History-admin/src/config/config.ts
T

156 lines
4.7 KiB
TypeScript

import axios, { AxiosResponse } from "axios";
import { API_URL_ROOT } from "../../api";
import {
clearStoredTokens,
extractTokensFromResponsePayload,
getAccessToken,
setStoredTokens,
} from "@/auth/tokenStore";
export const baseURL = API_URL_ROOT;
export const api = axios.create({
baseURL,
// Support both cookie-based auth (httpOnly) and Bearer JWT.
withCredentials: true,
});
// Dedicated instance for refresh to avoid interceptor loops and handle baseURL correctly.
const refreshApi = axios.create({
baseURL,
withCredentials: true,
});
let isRefreshing = false;
let queue: any[] = [];
function shouldRedirectToSigninOnRefreshFailure(status?: number): boolean {
return status === 401 || status === 404;
}
const processQueue = (error?: any) => {
queue.forEach((p) => {
if (error) p.reject(error);
else p.resolve();
});
queue = [];
};
api.interceptors.request.use((config: any) => {
if (config.skipAuth) return config;
const token = config.authToken || getAccessToken();
if (token) {
const headers: any = config.headers || {};
// If it's a retry after refresh, we MUST update the Authorization header with the fresh token.
// Otherwise, we only set it if not already present.
const hasAuth = !!(headers.Authorization || headers.authorization || (typeof headers.get === "function" && headers.get("Authorization")));
if (config._retry || !hasAuth) {
if (typeof headers.set === "function") headers.set("Authorization", `Bearer ${token}`);
else headers.Authorization = `Bearer ${token}`;
}
config.headers = headers;
}
return config;
});
function isAuthTokenExpiredMessage(message: string): boolean {
const normalized = message.trim().toLowerCase();
if (!normalized) return false;
// Be specific: don't match general "unauthorized" or "access denied" which could be 403.
// Match only messages clearly indicating token expiration or invalidity.
return (
normalized.includes("invalid or expired jwt") ||
normalized.includes("jwt expired") ||
normalized.includes("token expired") ||
normalized.includes("invalid token") ||
normalized.includes("expired token") ||
normalized.includes("token is invalid") ||
normalized.includes("not authenticated")
);
}
api.interceptors.response.use(
async (res: AxiosResponse): Promise<AxiosResponse> => {
// Opportunistically persist tokens from signin/refresh responses.
const tokens = extractTokensFromResponsePayload(res?.data);
if (tokens) setStoredTokens(tokens);
// Handle backends that return 200 OK with status:false + expired token message.
const data = res.data;
const originalRequest = res.config as any;
const url = String(originalRequest?.url || "");
if (
data &&
data.status === false &&
isAuthTokenExpiredMessage(data.message || "") &&
!originalRequest._retry &&
!originalRequest.skipRefresh &&
!url.includes("/auth/")
) {
return performRefreshAndRetry(originalRequest);
}
return res;
},
async (err) => {
const originalRequest = err.config as any;
const url = String(originalRequest?.url || "");
if (err.response?.status === 401 && !originalRequest._retry && !originalRequest.skipRefresh && !url.includes("/auth/")) {
return performRefreshAndRetry(originalRequest);
}
return Promise.reject(err);
}
);
async function performRefreshAndRetry(originalRequest: any): Promise<AxiosResponse> {
if (isRefreshing) {
return new Promise((resolve, reject) => {
queue.push({
resolve: () => resolve(api(originalRequest)),
reject,
});
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const tryCookieRefresh = async () => {
return refreshApi.post("/auth/refresh", {});
};
const refreshRes: any = await tryCookieRefresh();
const nextTokens = extractTokensFromResponsePayload(refreshRes?.data);
if (nextTokens) setStoredTokens(nextTokens);
// Some backends may return only a new access token.
else {
const maybeAccess = (refreshRes?.data?.data?.access_token ?? refreshRes?.data?.access_token) as unknown;
if (typeof maybeAccess === "string" && maybeAccess.trim()) {
setStoredTokens({ access_token: maybeAccess });
}
}
processQueue();
return api(originalRequest);
} catch (refreshErr: any) {
processQueue(refreshErr);
// Treat missing/invalid refresh endpoints or sessions as logged-out state.
if (shouldRedirectToSigninOnRefreshFailure(refreshErr?.response?.status)) {
clearStoredTokens();
if (typeof window !== "undefined") {
window.location.href = "/auth/signin";
}
}
return Promise.reject(refreshErr);
} finally {
isRefreshing = false;
}
}