import axios, { AxiosResponse } from "axios" import { API_URL_ROOT } from "../../api" import { clearStoredTokens, extractTokensFromResponsePayload, getAccessToken, setStoredTokens, } from "@/auth/tokenStore" const baseURL = API_URL_ROOT 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 => { // 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 { 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 = "/signin" } } return Promise.reject(refreshErr) } finally { isRefreshing = false } } export default api