import axios from "axios"; const baseURL = (import.meta.env.VITE_BACKEND_URL as string) || "http://localhost:8080"; const REFRESH_ENDPOINT = "/api/auth/refresh"; const ACCESS_TOKEN_COOKIE = "access_token"; const REFRESH_THRESHOLD_MS = 60_000; const api = axios.create({ baseURL, withCredentials: true, headers: { "Content-Type": "application/json", }, }); type JwtPayload = { exp?: number; }; const getCookieValue = (name: string): string | undefined => { if (typeof document === "undefined") { return undefined; } const cookies = document.cookie?.split(";") ?? []; const cookie = cookies .map((c) => c.trim()) .find((c) => c.startsWith(`${name}=`)); return cookie ? decodeURIComponent(cookie.split("=")[1]) : undefined; }; const decodeJwtPayload = (token: string): JwtPayload | null => { try { const [, payload] = token.split("."); if (!payload) return null; const base64 = payload.replace(/-/g, "+").replace(/_/g, "/"); const decoded = typeof atob === "function" ? atob(base64) : ""; return decoded ? JSON.parse(decoded) : null; } catch { return null; } }; const isTokenExpiringSoon = (token?: string) => { if (!token) return false; const payload = decodeJwtPayload(token); if (!payload?.exp) return false; const expiresAt = payload.exp * 1000; const millisUntilExpiry = expiresAt - Date.now(); return millisUntilExpiry <= REFRESH_THRESHOLD_MS; }; export const setAuthToken = (token?: string) => { if (token) { api.defaults.headers.common["Authorization"] = `Bearer ${token}`; } else { delete api.defaults.headers.common["Authorization"]; } }; let refreshPromise: Promise | null = null; const refreshAccessToken = async () => { if (!refreshPromise) { refreshPromise = api .post(REFRESH_ENDPOINT) .then(() => { const token = getCookieValue(ACCESS_TOKEN_COOKIE); setAuthToken(token); }) .catch((error) => { throw error; }) .finally(() => { refreshPromise = null; }); } return refreshPromise; }; api.interceptors.request.use(async (config) => { const isRefreshCall = config.url?.includes(REFRESH_ENDPOINT); if (!isRefreshCall) { const token = getCookieValue(ACCESS_TOKEN_COOKIE); if (isTokenExpiringSoon(token)) { try { await refreshAccessToken(); } catch (error) { refreshPromise = null; throw error; } } const updatedToken = getCookieValue(ACCESS_TOKEN_COOKIE); if (updatedToken) { config.headers = config.headers ?? {}; config.headers.Authorization = `Bearer ${updatedToken}`; } } return config; }); export default api;