mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
# Description of Changes This pull request introduces several improvements and new features across the authentication and admin data APIs, with a particular focus on multi-factor authentication (MFA) support and better handling of user settings. The changes include integrating MFA status into account data responses, masking sensitive user settings in admin views, and refactoring code to use more robust user creation methods. Additionally, there are minor code cleanups and consistency improvements. ### Multi-factor Authentication (MFA) Integration * Added `mfaEnabled` and `mfaRequired` fields to the `AccountData` response, populated using the new `MfaService`, to provide clients with MFA status information for users. [[1]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1R430-R431) [[2]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1R72) [[3]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1L83-R86) [[4]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1R98) [[5]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1R574-R575) ### User Settings Handling and Security * Admin settings API now returns user settings for each user, with the `mfaSecret` field masked to protect sensitive information. This is achieved by fetching settings via `findByIdWithSettings` and copying/masking the relevant field before returning. [[1]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1R252-R259) [[2]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1L302-R322) [[3]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1R378) [[4]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1R563) ### Refactoring and Code Consistency * Refactored user creation in `InitialSecuritySetup` to use the `SaveUserRequest` builder and `saveUserCore` for better maintainability and clarity, replacing direct calls to `saveUser`. [[1]](diffhunk://#diff-0c7960a6283a07c4905ac9785b2820b412574c9f86918ada30caba0356d34850R22) [[2]](diffhunk://#diff-0c7960a6283a07c4905ac9785b2820b412574c9f86918ada30caba0356d34850L116-R124) [[3]](diffhunk://#diff-0c7960a6283a07c4905ac9785b2820b412574c9f86918ada30caba0356d34850L130-R144) [[4]](diffhunk://#diff-0c7960a6283a07c4905ac9785b2820b412574c9f86918ada30caba0356d34850L140-R160) * Standardized checks for internal team membership by comparing with `TeamService.INTERNAL_TEAM_NAME` on the left side for consistency. [[1]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1L267-R273) [[2]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1L332-R351) [[3]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1L421-R443) [[4]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1L449-R471) [[5]](diffhunk://#diff-2ead183708656f2c6894b28457623820c83b1ed4b0814533caa0e8f0dd6fbcd1L462-R485) ### API and DTO Changes * Changed the login API to accept `UsernameAndPassMfa` instead of `UsernameAndPass`, paving the way for MFA code support in authentication requests. [[1]](diffhunk://#diff-9ca4f9246abe79368552264e2e18d7ed039e084c70c0794eb02cfd1b75fbd8a8L30-R41) [[2]](diffhunk://#diff-9ca4f9246abe79368552264e2e18d7ed039e084c70c0794eb02cfd1b75fbd8a8L61-R71) * Updated import statements and controller dependencies to include new DTOs and services related to MFA. [[1]](diffhunk://#diff-9ca4f9246abe79368552264e2e18d7ed039e084c70c0794eb02cfd1b75fbd8a8L14-R19) [[2]](diffhunk://#diff-9ca4f9246abe79368552264e2e18d7ed039e084c70c0794eb02cfd1b75fbd8a8R56-R57) These updates improve security, prepare the system for MFA rollout, and make admin and authentication APIs more robust and informative. --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Copilot <[email protected]>
148 lines
5.3 KiB
TypeScript
148 lines
5.3 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) {
|
|
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);
|
|
}
|
|
);
|
|
}
|