// 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/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 access token, then restore selected organization context on the new JWT. await axios.post( `${process.env.NEXT_PUBLIC_API_URL}/auth/refresh`, {}, { withCredentials: true }, ); const orgId = typeof window !== 'undefined' ? localStorage.getItem('currentOrganizationId') : null; if (orgId) { try { await axios.post( `${process.env.NEXT_PUBLIC_API_URL}/auth/select-organization`, { organizationId: orgId }, { withCredentials: true }, ); } catch { /* original retry may still succeed if refresh preserved organizationId */ } } 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); } );