import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios'; import { alert } from '@app/components/toast'; import { setupApiInterceptors as coreSetup } from '@core/services/apiClientSetup'; import { tauriBackendService } from '@app/services/tauriBackendService'; import { createBackendNotReadyError } from '@app/constants/backendErrors'; import { operationRouter } from '@app/services/operationRouter'; import { authService } from '@app/services/authService'; import { connectionModeService } from '@app/services/connectionModeService'; import i18n from '@app/i18n'; const BACKEND_TOAST_COOLDOWN_MS = 4000; let lastBackendToast = 0; // Extended config for custom properties interface ExtendedRequestConfig extends InternalAxiosRequestConfig { operationName?: string; skipBackendReadyCheck?: boolean; _retry?: boolean; } /** * Desktop-specific API interceptors * - Reuses the core interceptors * - Dynamically sets base URL based on connection mode * - Adds auth token for remote server requests * - Blocks API calls while the bundled backend is still starting * - Handles auth token refresh on 401 errors */ export function setupApiInterceptors(client: AxiosInstance): void { coreSetup(client); // Request interceptor: Set base URL and auth headers dynamically client.interceptors.request.use( async (config: InternalAxiosRequestConfig) => { const extendedConfig = config as ExtendedRequestConfig; // Get the operation name from config if provided const operation = extendedConfig.operationName; // Get the appropriate base URL for this operation const baseUrl = await operationRouter.getBaseUrl(operation); // Build the full URL if (extendedConfig.url && !extendedConfig.url.startsWith('http')) { extendedConfig.url = `${baseUrl}${extendedConfig.url}`; } // Debug logging console.debug(`[apiClientSetup] Request to: ${extendedConfig.url}`); // Add auth token for remote requests and enable credentials const isRemote = await operationRouter.isSelfHostedMode(); if (isRemote) { // Self-hosted mode: enable credentials for session management extendedConfig.withCredentials = true; const token = await authService.getAuthToken(); if (token) { extendedConfig.headers.Authorization = `Bearer ${token}`; } else { console.warn('[apiClientSetup] Self-hosted mode but no auth token available'); } } else { // SaaS mode: disable credentials (security disabled on local backend) extendedConfig.withCredentials = false; } // Backend readiness check (for local backend) const skipCheck = extendedConfig.skipBackendReadyCheck === true; const isSaaS = await operationRouter.isSaaSMode(); if (isSaaS && !skipCheck && !tauriBackendService.isBackendHealthy()) { const method = (extendedConfig.method || 'get').toLowerCase(); if (method !== 'get') { const now = Date.now(); if (now - lastBackendToast > BACKEND_TOAST_COOLDOWN_MS) { lastBackendToast = now; alert({ alertType: 'error', title: i18n.t('backendHealth.offline', 'Backend Offline'), body: i18n.t('backendHealth.wait', 'Please wait for the backend to finish launching and try again.'), isPersistentPopup: false, }); } } return Promise.reject(createBackendNotReadyError()); } return extendedConfig; }, (error) => Promise.reject(error) ); // Response interceptor: Handle auth errors client.interceptors.response.use( (response) => { return response; }, async (error) => { const originalRequest = error.config as ExtendedRequestConfig; // Handle 401 Unauthorized - try to refresh token if (error.response?.status === 401 && !originalRequest._retry) { if (originalRequest.skipAuthRedirect) { return Promise.reject(error); } originalRequest._retry = true; const isRemote = await operationRouter.isSelfHostedMode(); if (isRemote) { const serverConfig = await connectionModeService.getServerConfig(); if (serverConfig) { const refreshed = await authService.refreshToken(serverConfig.url); if (refreshed) { // Retry the original request with new token const token = await authService.getAuthToken(); if (token) { originalRequest.headers.Authorization = `Bearer ${token}`; } return client(originalRequest); } } } // Refresh failed or not in remote mode - user needs to login again alert({ alertType: 'error', title: i18n.t('auth.sessionExpired', 'Session Expired'), body: i18n.t('auth.pleaseLoginAgain', 'Please login again.'), isPersistentPopup: false, }); } // Handle 403 Forbidden - unauthorized access if (error.response?.status === 403) { alert({ alertType: 'error', title: i18n.t('auth.accessDenied', 'Access Denied'), body: i18n.t('auth.insufficientPermissions', 'You do not have permission to perform this action.'), isPersistentPopup: false, }); } return Promise.reject(error); } ); }