// src/lib/api/client.ts import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'; import type { ApiError } from '@/types/api'; interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig { _retry?: boolean; } export const apiClient = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL, withCredentials: true, // ✅ REQUIRED FOR COOKIES headers: { 'Content-Type': 'application/json', }, timeout: 10000, }); /** Invitation preview/accept must work with no cookies (public API, no JWT). */ function isPublicInvitationRequest(url: string | undefined): boolean { if (!url) return false; return ( url.includes('/staff/invitations/preview') || url.includes('/staff/invitations/accept') || url.includes('/organizations/invitations/preview') || url.includes('/organizations/invitations/accept') ); } /** Session bootstrap / auth endpoints where 401 means "not logged in", not "retry refresh". */ function shouldSkipRefreshRetry(url: string | undefined): boolean { if (!url) return false; return ( url.includes('/auth/profile') || url.includes('/auth/refresh') || url.includes('/auth/login') || url.includes('/auth/register') || url.includes('/auth/logout') || url.includes('/auth/forgot-password') || url.includes('/auth/profile/password') ); } // ❌ REMOVE request interceptor completely (no Authorization header) // ✅ Response interceptor apiClient.interceptors.response.use( (response) => response, async (error: AxiosError) => { const originalRequest = error.config as CustomAxiosRequestConfig; if ( error.response?.status === 401 && !originalRequest._retry && !isPublicInvitationRequest(originalRequest.url) && !shouldSkipRefreshRetry(originalRequest.url) ) { originalRequest._retry = true; try { // ✅ refresh via cookie (no body needed ideally) await axios.post( `${process.env.NEXT_PUBLIC_API_URL}/auth/refresh`, {}, { withCredentials: true } ); return apiClient(originalRequest); } catch (refreshError) { if (typeof window !== 'undefined') { return Promise.reject(error); // ✅ just fail silently } return Promise.reject(refreshError); } } const apiError: ApiError = { statusCode: error.response?.status || 500, message: (error.response?.data as any)?.message || error.message || 'An unexpected error occurred', error: (error.response?.data as any)?.error, }; return Promise.reject(apiError); } );