import axios, { type InternalAxiosRequestConfig } from "axios" const API_URL = import.meta.env.VITE_API_URL || "" const api = axios.create({ baseURL: `${API_URL}/api/v1`, withCredentials: true, // Crucial for HttpOnly cookies }) // Optional callback for unauthorized errors (fallback if refresh fails) let onUnauthorized: (() => void) | null = null export const registerUnauthorizedCallback = (callback: () => void) => { onUnauthorized = callback } // State to manage multiple concurrent refreshes let isRefreshing = false let refreshSubscribers: ((token: string) => void)[] = [] let refreshErrorSubscribers: ((error: any) => void)[] = [] const subscribeTokenRefresh = (onSuccess: (token: string) => void, onError: (error: any) => void) => { refreshSubscribers.push(onSuccess) refreshErrorSubscribers.push(onError) } const onRefreshed = (token: string) => { refreshSubscribers.forEach((callback) => callback(token)) refreshSubscribers = [] refreshErrorSubscribers = [] } const onRefreshFailed = (error: any) => { refreshErrorSubscribers.forEach((callback) => callback(error)) refreshSubscribers = [] refreshErrorSubscribers = [] } // Add a response interceptor to handle 401s api.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean } // Only handle 401 if it's not already a retry and not an auth endpoint // We allow /auth/login and /auth/register to fail normally to show errors const isAuthEndpoint = /^\/auth\/(login|register|oidc\/callback)/.test(originalRequest.url || "") const isRefreshEndpoint = /^\/auth\/refresh/.test(originalRequest.url || "") if (error.response?.status === 401 && !isAuthEndpoint && !isRefreshEndpoint && !originalRequest._retry) { if (isRefreshing) { // Wait for the current refresh to complete return new Promise((resolve, reject) => { subscribeTokenRefresh( (_token) => resolve(api(originalRequest)), (err) => reject(err) ) }) } originalRequest._retry = true isRefreshing = true try { console.log("Reactive refresh: Access token expired, attempting to refresh...") // Call refresh endpoint const response = await api.post("/auth/refresh") const { access_token } = response.data isRefreshing = false onRefreshed(access_token) // Retry the original request return api(originalRequest) } catch (refreshError) { isRefreshing = false onRefreshFailed(refreshError) console.error("Reactive refresh failed:", refreshError) // Final failure - session is dead handleUnauthorized() return Promise.reject(refreshError) } } // If it's a 401 on an endpoint we don't/can't refresh (like refresh itself or login) if (error.response?.status === 401) { handleUnauthorized() } return Promise.reject(error) } ) /** * Shared helper to trigger logout/unauthorized cleanup. */ function handleUnauthorized() { if (onUnauthorized) { onUnauthorized() } } export default api