Also fixed issue with csrf Also fixed issue with rust keychain --------- Co-authored-by: James Brunton <jbrunton96@gmail.com>
145 lines
5.2 KiB
TypeScript
145 lines
5.2 KiB
TypeScript
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) {
|
|
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);
|
|
}
|
|
);
|
|
}
|