mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-03-04 02:20:19 +01:00
# Description of Changes ## OCR / Tesseract path handling Makes tessDataPath resolution deterministic with priority: config > TESSDATA_PREFIX env > default. Updates language discovery to use runtimePathConfig.getTessDataPath() instead of raw config value. Ensure default OCR dir is debian based not alpine ## Mobile scanner: feature gating + new conversion settings Adds system.mobileScannerSettings (convert-to-PDF + resolution + page format + stretch) exposed via backend config and configurable in the proprietary admin UI. Enforces enableMobileScanner on the MobileScannerController endpoints (403 when disabled). Frontend mobile upload flow can now optionally convert received images to PDF (pdf-lib + canvas). ## Desktop/Tauri connectivity work Expands tauri-plugin-http permissions and enables dangerous-settings. Adds a very comprehensive multi-stage server connection diagnostic routine (with lots of logging). <img width="688" height="475" alt="image" src="https://github.com/user-attachments/assets/6f9c1aec-58c7-449b-96b0-52f25430d741" /> --- ## 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.
258 lines
8.2 KiB
TypeScript
258 lines
8.2 KiB
TypeScript
import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
|
|
import apiClient from '@app/services/apiClient';
|
|
import { getSimulatedAppConfig } from '@app/testing/serverExperienceSimulations';
|
|
|
|
/**
|
|
* Sleep utility for delays
|
|
*/
|
|
function sleep(ms: number): Promise<void> {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
export interface AppConfigRetryOptions {
|
|
maxRetries?: number;
|
|
initialDelay?: number;
|
|
}
|
|
|
|
export interface AppConfig {
|
|
baseUrl?: string;
|
|
contextPath?: string;
|
|
serverPort?: number;
|
|
frontendUrl?: string;
|
|
appNameNavbar?: string;
|
|
languages?: string[];
|
|
defaultLocale?: string;
|
|
logoStyle?: 'modern' | 'classic';
|
|
enableLogin?: boolean;
|
|
showSettingsWhenNoLogin?: boolean;
|
|
enableEmailInvites?: boolean;
|
|
isAdmin?: boolean;
|
|
enableAlphaFunctionality?: boolean;
|
|
enableAnalytics?: boolean | null;
|
|
enablePosthog?: boolean | null;
|
|
enableScarf?: boolean | null;
|
|
enableDesktopInstallSlide?: boolean;
|
|
premiumEnabled?: boolean;
|
|
premiumKey?: string;
|
|
termsAndConditions?: string;
|
|
privacyPolicy?: string;
|
|
cookiePolicy?: string;
|
|
impressum?: string;
|
|
accessibilityStatement?: string;
|
|
runningProOrHigher?: boolean;
|
|
runningEE?: boolean;
|
|
license?: string;
|
|
SSOAutoLogin?: boolean;
|
|
serverCertificateEnabled?: boolean;
|
|
enableMobileScanner?: boolean;
|
|
mobileScannerConvertToPdf?: boolean;
|
|
mobileScannerImageResolution?: string;
|
|
mobileScannerPageFormat?: string;
|
|
mobileScannerStretchToFit?: boolean;
|
|
appVersion?: string;
|
|
machineType?: string;
|
|
activeSecurity?: boolean;
|
|
dependenciesReady?: boolean;
|
|
error?: string;
|
|
isNewServer?: boolean;
|
|
isNewUser?: boolean;
|
|
}
|
|
|
|
export type AppConfigBootstrapMode = 'blocking' | 'non-blocking';
|
|
|
|
interface AppConfigContextValue {
|
|
config: AppConfig | null;
|
|
loading: boolean;
|
|
error: string | null;
|
|
refetch: () => Promise<void>;
|
|
}
|
|
|
|
const AppConfigContext = createContext<AppConfigContextValue | undefined>({
|
|
config: null,
|
|
loading: true,
|
|
error: null,
|
|
refetch: async () => {},
|
|
});
|
|
|
|
/**
|
|
* Provider component that fetches and provides app configuration
|
|
* Should be placed at the top level of the app, before any components that need config
|
|
*/
|
|
export interface AppConfigProviderProps {
|
|
children: ReactNode;
|
|
retryOptions?: AppConfigRetryOptions;
|
|
initialConfig?: AppConfig | null;
|
|
bootstrapMode?: AppConfigBootstrapMode;
|
|
autoFetch?: boolean;
|
|
}
|
|
|
|
export const AppConfigProvider: React.FC<AppConfigProviderProps> = ({
|
|
children,
|
|
retryOptions,
|
|
initialConfig = null,
|
|
bootstrapMode = 'blocking',
|
|
autoFetch = true,
|
|
}) => {
|
|
const isBlockingMode = bootstrapMode === 'blocking';
|
|
const [config, setConfig] = useState<AppConfig | null>(initialConfig);
|
|
const [error, setError] = useState<string | null>(null);
|
|
// Track how many times we've attempted to fetch. useRef avoids re-renders that can trigger loops.
|
|
const fetchCountRef = React.useRef(0);
|
|
const [hasResolvedConfig, setHasResolvedConfig] = useState(Boolean(initialConfig) && !isBlockingMode);
|
|
const [loading, setLoading] = useState(!hasResolvedConfig);
|
|
|
|
const maxRetries = retryOptions?.maxRetries ?? 0;
|
|
const initialDelay = retryOptions?.initialDelay ?? 1000;
|
|
|
|
const fetchConfig = useCallback(async (force = false) => {
|
|
// Prevent duplicate fetches unless forced
|
|
if (!force && fetchCountRef.current > 0) {
|
|
console.debug('[AppConfig] Already fetched, skipping');
|
|
return;
|
|
}
|
|
|
|
// Mark that we've attempted a fetch to prevent repeated auto-fetch loops
|
|
fetchCountRef.current += 1;
|
|
|
|
const shouldBlockUI = !hasResolvedConfig || isBlockingMode;
|
|
if (shouldBlockUI) {
|
|
setLoading(true);
|
|
}
|
|
setError(null);
|
|
|
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
try {
|
|
const testConfig = getSimulatedAppConfig();
|
|
if (testConfig) {
|
|
setConfig(testConfig);
|
|
setHasResolvedConfig(true);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
if (attempt > 0) {
|
|
const delay = initialDelay * Math.pow(2, attempt - 1);
|
|
console.log(`[AppConfig] Retry attempt ${attempt}/${maxRetries} after ${delay}ms delay...`);
|
|
await sleep(delay);
|
|
} else {
|
|
console.log('[AppConfig] Fetching app config...');
|
|
}
|
|
|
|
// apiClient automatically adds JWT header if available via interceptors
|
|
// Always suppress error toast - we handle 401 errors locally
|
|
const response = await apiClient.get<AppConfig>(
|
|
'/api/v1/config/app-config',
|
|
{
|
|
suppressErrorToast: true,
|
|
skipAuthRedirect: true,
|
|
} as any,
|
|
);
|
|
const data = response.data;
|
|
|
|
console.debug('[AppConfig] Config fetched successfully:', data);
|
|
setConfig(data);
|
|
setHasResolvedConfig(true);
|
|
setLoading(false);
|
|
return; // Success - exit function
|
|
} catch (err: any) {
|
|
const status = err?.response?.status;
|
|
|
|
// On 401 (not authenticated), use default config with login enabled
|
|
// This allows the app to work even without authentication
|
|
if (status === 401) {
|
|
console.debug('[AppConfig] 401 error - using default config (login enabled)');
|
|
setConfig({ enableLogin: true });
|
|
setHasResolvedConfig(true);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
// Check if we should retry (network errors or 5xx errors)
|
|
const shouldRetry = (!status || status >= 500) && attempt < maxRetries;
|
|
|
|
if (shouldRetry) {
|
|
console.warn(`[AppConfig] Attempt ${attempt + 1} failed (status ${status || 'network error'}):`, err.message, '- will retry...');
|
|
continue;
|
|
}
|
|
|
|
// Final attempt failed or non-retryable error (4xx)
|
|
const errorMessage = err?.response?.data?.message || err?.message || 'Unknown error occurred';
|
|
setError(errorMessage);
|
|
console.error(`[AppConfig] Failed to fetch app config after ${attempt + 1} attempts:`, err);
|
|
// Preserve existing config (initial default or previous fetch). If nothing is set, assume login enabled.
|
|
setConfig((current) => current ?? { enableLogin: true });
|
|
setHasResolvedConfig(true);
|
|
break;
|
|
}
|
|
}
|
|
|
|
setLoading(false);
|
|
}, [hasResolvedConfig, isBlockingMode, maxRetries, initialDelay]);
|
|
|
|
useEffect(() => {
|
|
// Skip config fetch on auth pages (/login, /signup, /auth/callback, /invite/*)
|
|
// Config will be fetched after successful authentication via jwt-available event
|
|
const currentPath = window.location.pathname;
|
|
const isAuthPage = currentPath.includes('/login') ||
|
|
currentPath.includes('/signup') ||
|
|
currentPath.includes('/auth/callback') ||
|
|
currentPath.includes('/invite/');
|
|
|
|
// On auth pages, always skip the config fetch
|
|
// The config will be fetched after authentication via jwt-available event
|
|
if (isAuthPage) {
|
|
console.debug('[AppConfig] On auth page - using default config, skipping fetch');
|
|
setConfig({ enableLogin: true });
|
|
setHasResolvedConfig(true);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
// On non-auth pages, fetch config (will validate JWT if present)
|
|
if (autoFetch) {
|
|
fetchConfig();
|
|
}
|
|
}, [autoFetch, fetchConfig]);
|
|
|
|
// Listen for JWT availability (triggered on login/signup)
|
|
useEffect(() => {
|
|
const handleJwtAvailable = () => {
|
|
console.debug('[AppConfig] JWT available event - refetching config');
|
|
// Force refetch with JWT
|
|
fetchConfig(true);
|
|
};
|
|
|
|
window.addEventListener('jwt-available', handleJwtAvailable);
|
|
return () => window.removeEventListener('jwt-available', handleJwtAvailable);
|
|
}, [fetchConfig]);
|
|
|
|
const refetch = useCallback(() => fetchConfig(true), [fetchConfig]);
|
|
|
|
const value: AppConfigContextValue = {
|
|
config,
|
|
loading,
|
|
error,
|
|
refetch,
|
|
};
|
|
|
|
return (
|
|
<AppConfigContext.Provider value={value}>
|
|
{children}
|
|
</AppConfigContext.Provider>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Hook to access application configuration
|
|
* Must be used within AppConfigProvider
|
|
*/
|
|
export function useAppConfig(): AppConfigContextValue {
|
|
const context = useContext(AppConfigContext);
|
|
|
|
if (context === undefined) {
|
|
throw new Error('useAppConfig must be used within AppConfigProvider');
|
|
}
|
|
|
|
return context;
|
|
}
|