import axios from "axios"; /** * API Client with token management. * * Security model: * - Access token (15min): stored in memory only (this module's closure) * - Refresh token (30d): stored in localStorage (TODO: migrate to HttpOnly cookie) * - Automatic refresh on 401 * - Refresh token rotation on every refresh * * NOTE: Full HttpOnly cookie auth requires backend Set-Cookie support. * Current implementation keeps refresh in localStorage but access in memory, * which is already better than both in localStorage. */ const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || ""; // Access token lives in memory only — not accessible to XSS let accessToken: string | null = null; export function setAccessToken(token: string | null) { accessToken = token; } export function getAccessToken(): string | null { return accessToken; } // On page load, try to restore from a previous session if (typeof window !== "undefined") { // Migrate: if old localStorage access_token exists, use it then remove const legacy = localStorage.getItem("access_token"); if (legacy) { accessToken = legacy; localStorage.removeItem("access_token"); } } export const apiClient = axios.create({ baseURL: `${API_BASE_URL}/api/v1`, headers: { "Content-Type": "application/json" }, withCredentials: true, }); // Attach access token from memory apiClient.interceptors.request.use( (config) => { if (accessToken) { config.headers.Authorization = `Bearer ${accessToken}`; } return config; }, (error) => Promise.reject(error) ); // Handle 401 with refresh token rotation apiClient.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; try { const refreshToken = localStorage.getItem("refresh_token"); if (!refreshToken) throw new Error("No refresh token"); // Refresh — server rotates the refresh token const response = await axios.post(`${API_BASE_URL}/api/v1/auth/refresh`, { refresh_token: refreshToken, }); const { access_token, refresh_token: newRefreshToken } = response.data; // Store new access token in MEMORY only accessToken = access_token; // Store rotated refresh token localStorage.setItem("refresh_token", newRefreshToken); originalRequest.headers.Authorization = `Bearer ${access_token}`; return apiClient(originalRequest); } catch (refreshError) { // Refresh failed — clear everything and redirect accessToken = null; localStorage.removeItem("refresh_token"); if (typeof window !== "undefined") { window.location.href = "/login"; } return Promise.reject(refreshError); } } return Promise.reject(error); } ); export default apiClient;