import axios from "axios"; const baseURL = (import.meta.env.VITE_BACKEND_URL as string) || "http://localhost:8080"; const REFRESH_ENDPOINT = "/api/auth/refresh"; const LOGOUT_ENDPOINT = "/api/auth/logout"; const ACCESS_TOKEN_COOKIE = "access_token"; const REFRESH_THRESHOLD_MS = 60_000; const api = axios.create({ baseURL, withCredentials: true, headers: { "Content-Type": "application/json", }, }); export type JwtPayload = { exp?: number; sub?: string; [key: string]: unknown; }; export 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; }; export 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; } }; export const getAccessToken = () => getCookieValue(ACCESS_TOKEN_COOKIE); export const getCurrentUsername = () => { const token = getAccessToken(); if (!token) return undefined; const payload = decodeJwtPayload(token); return typeof payload?.sub === "string" ? payload.sub : undefined; }; 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 clearAccessTokenCookie = () => { if (typeof document === "undefined") { return; } document.cookie = `${ACCESS_TOKEN_COOKIE}=; Max-Age=0; path=/`; }; const resetAuthState = () => { clearAccessTokenCookie(); setAuthToken(undefined); }; export const refreshAccessToken = async () => { if (!refreshPromise) { refreshPromise = api .post(REFRESH_ENDPOINT) .then(() => { const token = getAccessToken(); setAuthToken(token); }) .catch((error) => { throw error; }) .finally(() => { refreshPromise = null; }); } return refreshPromise; }; const ensureTokenFromRefresh = async () => { await refreshAccessToken(); const updatedToken = getAccessToken(); if (!updatedToken) { throw new Error("Token indisponível após atualização."); } setAuthToken(updatedToken); return updatedToken; }; export const validateSession = async () => { try { const token = getAccessToken(); if (!token) { await ensureTokenFromRefresh(); return true; } const payload = decodeJwtPayload(token); if (!payload?.sub) { await ensureTokenFromRefresh(); return true; } if (payload.exp && payload.exp * 1000 <= Date.now()) { await ensureTokenFromRefresh(); return true; } setAuthToken(token); if (isTokenExpiringSoon(token)) { await ensureTokenFromRefresh(); } return true; } catch { resetAuthState(); return false; } }; export const logout = async () => { try { await api.post(LOGOUT_ENDPOINT); } finally { resetAuthState(); } }; api.interceptors.request.use(async (config) => { const isRefreshCall = config.url?.includes(REFRESH_ENDPOINT); if (!isRefreshCall) { const token = getAccessToken(); if (isTokenExpiringSoon(token)) { try { await refreshAccessToken(); } catch (error) { refreshPromise = null; throw error; } } const updatedToken = getAccessToken(); if (updatedToken) { config.headers = config.headers ?? {}; config.headers.Authorization = `Bearer ${updatedToken}`; } } return config; }); api.interceptors.response.use( (response) => response, (error) => { if (error?.response?.status === 401) { resetAuthState(); const currentPath = window.location.pathname; if (currentPath !== "/login") { window.location.href = "/login"; } } return Promise.reject(error); } ); export default api;