diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/ServerCertificateService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/ServerCertificateService.java index a743b21fe..e1edb67d8 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/service/ServerCertificateService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/ServerCertificateService.java @@ -30,6 +30,8 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.common.configuration.InstallationPathConfig; import stirling.software.common.service.ServerCertificateServiceInterface; +import stirling.software.proprietary.security.configuration.ee.KeygenLicenseVerifier.License; +import stirling.software.proprietary.security.configuration.ee.LicenseKeyChecker; @Service @Slf4j @@ -51,6 +53,12 @@ public class ServerCertificateService implements ServerCertificateServiceInterfa @Value("${system.serverCertificate.regenerateOnStartup:false}") private boolean regenerateOnStartup; + private final LicenseKeyChecker licenseKeyChecker; + + public ServerCertificateService(LicenseKeyChecker licenseKeyChecker) { + this.licenseKeyChecker = licenseKeyChecker; + } + static { Security.addProvider(new BouncyCastleProvider()); } @@ -59,8 +67,13 @@ public class ServerCertificateService implements ServerCertificateServiceInterfa return Paths.get(InstallationPathConfig.getConfigPath(), KEYSTORE_FILENAME); } + private boolean hasProOrEnterpriseAccess() { + License license = licenseKeyChecker.getPremiumLicenseEnabledResult(); + return license == License.PRO || license == License.ENTERPRISE; + } + public boolean isEnabled() { - return enabled; + return enabled && hasProOrEnterpriseAccess(); } public boolean hasServerCertificate() { @@ -73,6 +86,11 @@ public class ServerCertificateService implements ServerCertificateServiceInterfa return; } + if (!hasProOrEnterpriseAccess()) { + log.info("Server certificate feature requires Pro or Enterprise license"); + return; + } + Path keystorePath = getKeystorePath(); if (!Files.exists(keystorePath) || regenerateOnStartup) { @@ -88,6 +106,10 @@ public class ServerCertificateService implements ServerCertificateServiceInterfa } public KeyStore getServerKeyStore() throws Exception { + if (!hasProOrEnterpriseAccess()) { + throw new IllegalStateException("Server certificate feature requires Pro or Enterprise license"); + } + if (!enabled || !hasServerCertificate()) { throw new IllegalStateException("Server certificate is not available"); } @@ -114,6 +136,10 @@ public class ServerCertificateService implements ServerCertificateServiceInterfa } public void uploadServerCertificate(InputStream p12Stream, String password) throws Exception { + if (!hasProOrEnterpriseAccess()) { + throw new IllegalStateException("Server certificate feature requires Pro or Enterprise license"); + } + // Validate the uploaded certificate KeyStore uploadedKeyStore = KeyStore.getInstance("PKCS12"); uploadedKeyStore.load(p12Stream, password.toCharArray()); @@ -174,6 +200,10 @@ public class ServerCertificateService implements ServerCertificateServiceInterfa } private void generateServerCertificate() throws Exception { + if (!hasProOrEnterpriseAccess()) { + throw new IllegalStateException("Server certificate feature requires Pro or Enterprise license"); + } + // Generate key pair KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); keyPairGenerator.initialize(2048, new SecureRandom()); diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index a05338de1..52809394e 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -3250,7 +3250,28 @@ "customHTMLFiles": "Custom HTML Files", "customHTMLFiles.description": "Allow serving custom HTML files from the customFiles directory", "languages": "Available Languages", - "languages.description": "Languages that users can select from (leave empty to enable all languages)" + "languages.description": "Languages that users can select from (leave empty to enable all languages)", + "customMetadata": "Custom Metadata", + "customMetadata.autoUpdate": "Auto Update Metadata", + "customMetadata.autoUpdate.description": "Automatically update PDF metadata on all processed documents", + "customMetadata.author": "Default Author", + "customMetadata.author.description": "Default author for PDF metadata (e.g., username)", + "customMetadata.creator": "Default Creator", + "customMetadata.creator.description": "Default creator for PDF metadata", + "customMetadata.producer": "Default Producer", + "customMetadata.producer.description": "Default producer for PDF metadata", + "customPaths": "Custom Paths", + "customPaths.description": "Configure custom file system paths for pipeline processing and external tools", + "customPaths.pipeline": "Pipeline Directories", + "customPaths.pipeline.watchedFoldersDir": "Watched Folders Directory", + "customPaths.pipeline.watchedFoldersDir.description": "Directory where pipeline monitors for incoming PDFs (leave empty for default: /pipeline/watchedFolders)", + "customPaths.pipeline.finishedFoldersDir": "Finished Folders Directory", + "customPaths.pipeline.finishedFoldersDir.description": "Directory where processed PDFs are outputted (leave empty for default: /pipeline/finishedFolders)", + "customPaths.operations": "External Tool Paths", + "customPaths.operations.weasyprint": "WeasyPrint Executable", + "customPaths.operations.weasyprint.description": "Path to WeasyPrint executable for HTML to PDF conversion (leave empty for default: /opt/venv/bin/weasyprint)", + "customPaths.operations.unoconvert": "Unoconvert Executable", + "customPaths.operations.unoconvert.description": "Path to LibreOffice unoconvert for document conversions (leave empty for default: /opt/venv/bin/unoconvert)" }, "security": { "title": "Security", @@ -3289,7 +3310,39 @@ "jwt.enableKeyRotation": "Enable Key Rotation", "jwt.enableKeyRotation.description": "Automatically rotate JWT signing keys periodically", "jwt.enableKeyCleanup": "Enable Key Cleanup", - "jwt.enableKeyCleanup.description": "Automatically remove expired JWT keys" + "jwt.enableKeyCleanup.description": "Automatically remove expired JWT keys", + "audit": "Audit Logging", + "audit.enabled": "Enable Audit Logging", + "audit.enabled.description": "Track user actions and system events for compliance and security monitoring", + "audit.level": "Audit Level", + "audit.level.description": "0=OFF, 1=BASIC, 2=STANDARD, 3=VERBOSE", + "audit.retentionDays": "Audit Retention (days)", + "audit.retentionDays.description": "Number of days to retain audit logs", + "htmlUrlSecurity": "HTML URL Security", + "htmlUrlSecurity.description": "Configure URL access restrictions for HTML processing to prevent SSRF attacks", + "htmlUrlSecurity.enabled": "Enable URL Security", + "htmlUrlSecurity.enabled.description": "Enable URL security restrictions for HTML to PDF conversions", + "htmlUrlSecurity.level": "Security Level", + "htmlUrlSecurity.level.description": "MAX: whitelist only, MEDIUM: block internal networks, OFF: no restrictions", + "htmlUrlSecurity.level.max": "Maximum (Whitelist Only)", + "htmlUrlSecurity.level.medium": "Medium (Block Internal)", + "htmlUrlSecurity.level.off": "Off (No Restrictions)", + "htmlUrlSecurity.advanced": "Advanced Settings", + "htmlUrlSecurity.allowedDomains": "Allowed Domains (Whitelist)", + "htmlUrlSecurity.allowedDomains.description": "One domain per line (e.g., cdn.example.com). Only these domains allowed when level is MAX", + "htmlUrlSecurity.blockedDomains": "Blocked Domains (Blacklist)", + "htmlUrlSecurity.blockedDomains.description": "One domain per line (e.g., malicious.com). Additional domains to block", + "htmlUrlSecurity.internalTlds": "Internal TLDs", + "htmlUrlSecurity.internalTlds.description": "One TLD per line (e.g., .local, .internal). Block domains with these TLD patterns", + "htmlUrlSecurity.networkBlocking": "Network Blocking", + "htmlUrlSecurity.blockPrivateNetworks": "Block Private Networks", + "htmlUrlSecurity.blockPrivateNetworks.description": "Block RFC 1918 private networks (10.x.x.x, 192.168.x.x, 172.16-31.x.x)", + "htmlUrlSecurity.blockLocalhost": "Block Localhost", + "htmlUrlSecurity.blockLocalhost.description": "Block localhost and loopback addresses (127.x.x.x, ::1)", + "htmlUrlSecurity.blockLinkLocal": "Block Link-Local Addresses", + "htmlUrlSecurity.blockLinkLocal.description": "Block link-local addresses (169.254.x.x, fe80::/10)", + "htmlUrlSecurity.blockCloudMetadata": "Block Cloud Metadata Endpoints", + "htmlUrlSecurity.blockCloudMetadata.description": "Block cloud provider metadata endpoints (169.254.169.254)" }, "connections": { "title": "Connections", @@ -3300,6 +3353,9 @@ "disconnect": "Disconnect", "disconnected": "Provider disconnected successfully", "disconnectError": "Failed to disconnect provider", + "ssoAutoLogin": "SSO Auto Login", + "ssoAutoLogin.enable": "Enable SSO Auto Login", + "ssoAutoLogin.description": "Automatically redirect to SSO login when authentication is required", "oauth2": "OAuth2", "oauth2.enabled": "Enable OAuth2", "oauth2.enabled.description": "Allow users to authenticate using OAuth2 providers", @@ -3331,6 +3387,27 @@ "saml2.blockRegistration": "Block Registration", "saml2.blockRegistration.description": "Prevent new user registration via SAML2" }, + "database": { + "title": "Database", + "description": "Configure custom database connection settings for enterprise deployments.", + "configuration": "Database Configuration", + "enableCustom": "Enable Custom Database", + "enableCustom.description": "Use your own custom database configuration instead of the default embedded database", + "customUrl": "Custom Database URL", + "customUrl.description": "Full JDBC connection string (e.g., jdbc:postgresql://localhost:5432/postgres). If provided, individual connection settings below are not used.", + "type": "Database Type", + "type.description": "Type of database (not used if custom URL is provided)", + "hostName": "Host Name", + "hostName.description": "Database server hostname (not used if custom URL is provided)", + "port": "Port", + "port.description": "Database server port (not used if custom URL is provided)", + "name": "Database Name", + "name.description": "Name of the database (not used if custom URL is provided)", + "username": "Username", + "username.description": "Database authentication username", + "password": "Password", + "password.description": "Database authentication password" + }, "privacy": { "title": "Privacy", "description": "Configure privacy and data collection settings.", @@ -3360,7 +3437,41 @@ "tessdataDir": "Tessdata Directory", "tessdataDir.description": "Path to the tessdata directory for OCR language files", "disableSanitize": "Disable HTML Sanitization", - "disableSanitize.description": "WARNING: Security risk - disabling HTML sanitization can lead to XSS vulnerabilities" + "disableSanitize.description": "WARNING: Security risk - disabling HTML sanitization can lead to XSS vulnerabilities", + "tempFileManagement": "Temp File Management", + "tempFileManagement.description": "Configure temporary file storage and cleanup behavior", + "tempFileManagement.baseTmpDir": "Base Temp Directory", + "tempFileManagement.baseTmpDir.description": "Base directory for temporary files (leave empty for default: java.io.tmpdir/stirling-pdf)", + "tempFileManagement.libreofficeDir": "LibreOffice Temp Directory", + "tempFileManagement.libreofficeDir.description": "Directory for LibreOffice temp files (leave empty for default: baseTmpDir/libreoffice)", + "tempFileManagement.systemTempDir": "System Temp Directory", + "tempFileManagement.systemTempDir.description": "System temp directory to clean (only used if cleanupSystemTemp is enabled)", + "tempFileManagement.prefix": "Temp File Prefix", + "tempFileManagement.prefix.description": "Prefix for temp file names", + "tempFileManagement.maxAgeHours": "Max Age (hours)", + "tempFileManagement.maxAgeHours.description": "Maximum age in hours before temp files are cleaned up", + "tempFileManagement.cleanupIntervalMinutes": "Cleanup Interval (minutes)", + "tempFileManagement.cleanupIntervalMinutes.description": "How often to run cleanup (in minutes)", + "tempFileManagement.startupCleanup": "Startup Cleanup", + "tempFileManagement.startupCleanup.description": "Clean up old temp files on application startup", + "tempFileManagement.cleanupSystemTemp": "Cleanup System Temp", + "tempFileManagement.cleanupSystemTemp.description": "Whether to clean broader system temp directory (use with caution)", + "processExecutor": "Process Executor Limits", + "processExecutor.description": "Configure session limits and timeouts for each process executor", + "processExecutor.sessionLimit": "Session Limit", + "processExecutor.sessionLimit.description": "Maximum concurrent instances", + "processExecutor.timeout": "Timeout (minutes)", + "processExecutor.timeout.description": "Maximum execution time", + "processExecutor.libreOffice": "LibreOffice", + "processExecutor.pdfToHtml": "PDF to HTML", + "processExecutor.qpdf": "QPDF", + "processExecutor.tesseract": "Tesseract OCR", + "processExecutor.pythonOpenCv": "Python OpenCV", + "processExecutor.weasyPrint": "WeasyPrint", + "processExecutor.installApp": "Install App", + "processExecutor.calibre": "Calibre", + "processExecutor.ghostscript": "Ghostscript", + "processExecutor.ocrMyPdf": "OCRmyPDF" }, "mail": { "title": "Mail Server", @@ -3395,30 +3506,30 @@ }, "premium": { "title": "Premium & Enterprise", - "description": "Configure premium and enterprise features.", - "license": "License", + "description": "Configure your premium or enterprise license key.", + "license": "License Configuration", "key": "License Key", "key.description": "Enter your premium or enterprise license key", "enabled": "Enable Premium Features", "enabled.description": "Enable license key checks for pro/enterprise features", - "proFeatures": "Pro Features", - "ssoAutoLogin": "SSO Auto Login", - "ssoAutoLogin.description": "Automatically redirect to SSO login", - "customMetadata.autoUpdate": "Auto Update Metadata", - "customMetadata.autoUpdate.description": "Automatically update PDF metadata", - "customMetadata.author": "Default Author", - "customMetadata.author.description": "Default author for PDF metadata", - "customMetadata.creator": "Default Creator", - "customMetadata.creator.description": "Default creator for PDF metadata", - "customMetadata.producer": "Default Producer", - "customMetadata.producer.description": "Default producer for PDF metadata", - "enterpriseFeatures": "Enterprise Features", - "audit.enabled": "Enable Audit Logging", - "audit.enabled.description": "Track user actions and system events", - "audit.level": "Audit Level", - "audit.level.description": "0=OFF, 1=BASIC, 2=STANDARD, 3=VERBOSE", - "audit.retentionDays": "Audit Retention (days)", - "audit.retentionDays.description": "Number of days to retain audit logs" + "movedFeatures": { + "title": "Premium Features Distributed", + "message": "Premium and Enterprise features are now organized in their respective sections:" + } + }, + "features": { + "title": "Features", + "description": "Configure optional features and functionality.", + "serverCertificate": "Server Certificate", + "serverCertificate.description": "Configure server-side certificate generation for \"Sign with Stirling-PDF\" functionality", + "serverCertificate.enabled": "Enable Server Certificate", + "serverCertificate.enabled.description": "Enable server-side certificate for \"Sign with Stirling-PDF\" option", + "serverCertificate.organizationName": "Organization Name", + "serverCertificate.organizationName.description": "Organization name for generated certificates", + "serverCertificate.validity": "Certificate Validity (days)", + "serverCertificate.validity.description": "Number of days the certificate will be valid", + "serverCertificate.regenerateOnStartup": "Regenerate on Startup", + "serverCertificate.regenerateOnStartup.description": "Generate new certificate on each application startup" }, "endpoints": { "title": "API Endpoints", diff --git a/frontend/src/components/shared/config/configNavSections.tsx b/frontend/src/components/shared/config/configNavSections.tsx index e129919be..bb26f1735 100644 --- a/frontend/src/components/shared/config/configNavSections.tsx +++ b/frontend/src/components/shared/config/configNavSections.tsx @@ -6,9 +6,11 @@ import AdminGeneralSection from './configSections/AdminGeneralSection'; import AdminSecuritySection from './configSections/AdminSecuritySection'; import AdminConnectionsSection from './configSections/AdminConnectionsSection'; import AdminPrivacySection from './configSections/AdminPrivacySection'; +import AdminDatabaseSection from './configSections/AdminDatabaseSection'; import AdminAdvancedSection from './configSections/AdminAdvancedSection'; import AdminLegalSection from './configSections/AdminLegalSection'; import AdminPremiumSection from './configSections/AdminPremiumSection'; +import AdminFeaturesSection from './configSections/AdminFeaturesSection'; import AdminEndpointsSection from './configSections/AdminEndpointsSection'; export interface ConfigNavItem { @@ -104,12 +106,24 @@ export const createConfigNavSections = ( icon: 'visibility-rounded', component: }, + { + key: 'adminDatabase', + label: 'Database', + icon: 'storage-rounded', + component: + }, { key: 'adminPremium', label: 'Premium', icon: 'star-rounded', component: }, + { + key: 'adminFeatures', + label: 'Features', + icon: 'extension-rounded', + component: + }, { key: 'adminEndpoints', label: 'Endpoints', diff --git a/frontend/src/components/shared/config/configSections/AdminAdvancedSection.tsx b/frontend/src/components/shared/config/configSections/AdminAdvancedSection.tsx index d282fa2b2..f14ae50dd 100644 --- a/frontend/src/components/shared/config/configSections/AdminAdvancedSection.tsx +++ b/frontend/src/components/shared/config/configSections/AdminAdvancedSection.tsx @@ -11,6 +11,42 @@ interface AdvancedSettingsData { enableUrlToPDF?: boolean; tessdataDir?: string; disableSanitize?: boolean; + tempFileManagement?: { + baseTmpDir?: string; + libreofficeDir?: string; + systemTempDir?: string; + prefix?: string; + maxAgeHours?: number; + cleanupIntervalMinutes?: number; + startupCleanup?: boolean; + cleanupSystemTemp?: boolean; + }; + processExecutor?: { + sessionLimit?: { + libreOfficeSessionLimit?: number; + pdfToHtmlSessionLimit?: number; + qpdfSessionLimit?: number; + tesseractSessionLimit?: number; + pythonOpenCvSessionLimit?: number; + weasyPrintSessionLimit?: number; + installAppSessionLimit?: number; + calibreSessionLimit?: number; + ghostscriptSessionLimit?: number; + ocrMyPdfSessionLimit?: number; + }; + timeoutMinutes?: { + libreOfficetimeoutMinutes?: number; + pdfToHtmltimeoutMinutes?: number; + pythonOpenCvtimeoutMinutes?: number; + weasyPrinttimeoutMinutes?: number; + installApptimeoutMinutes?: number; + calibretimeoutMinutes?: number; + tesseractTimeoutMinutes?: number; + qpdfTimeoutMinutes?: number; + ghostscriptTimeoutMinutes?: number; + ocrMyPdfTimeoutMinutes?: number; + }; + }; } export default function AdminAdvancedSection() { @@ -26,17 +62,32 @@ export default function AdminAdvancedSection() { const fetchSettings = async () => { try { - const response = await fetch('/api/v1/admin/settings/section/system'); - if (response.ok) { - const data = await response.json(); - setSettings({ - enableAlphaFunctionality: data.enableAlphaFunctionality || false, - maxDPI: data.maxDPI || 0, - enableUrlToPDF: data.enableUrlToPDF || false, - tessdataDir: data.tessdataDir || '', - disableSanitize: data.disableSanitize || false - }); - } + const [systemResponse, processExecutorResponse] = await Promise.all([ + fetch('/api/v1/admin/settings/section/system'), + fetch('/api/v1/admin/settings/section/processExecutor') + ]); + + const systemData = systemResponse.ok ? await systemResponse.json() : {}; + const processExecutorData = processExecutorResponse.ok ? await processExecutorResponse.json() : {}; + + setSettings({ + enableAlphaFunctionality: systemData.enableAlphaFunctionality || false, + maxDPI: systemData.maxDPI || 0, + enableUrlToPDF: systemData.enableUrlToPDF || false, + tessdataDir: systemData.tessdataDir || '', + disableSanitize: systemData.disableSanitize || false, + tempFileManagement: systemData.tempFileManagement || { + baseTmpDir: '', + libreofficeDir: '', + systemTempDir: '', + prefix: 'stirling-pdf-', + maxAgeHours: 24, + cleanupIntervalMinutes: 30, + startupCleanup: true, + cleanupSystemTemp: false + }, + processExecutor: processExecutorData || {} + }); } catch (error) { console.error('Failed to fetch advanced settings:', error); alert({ @@ -53,7 +104,7 @@ export default function AdminAdvancedSection() { setSaving(true); try { // Use delta update endpoint with dot notation - const deltaSettings = { + const deltaSettings: Record = { 'system.enableAlphaFunctionality': settings.enableAlphaFunctionality, 'system.maxDPI': settings.maxDPI, 'system.enableUrlToPDF': settings.enableUrlToPDF, @@ -61,6 +112,30 @@ export default function AdminAdvancedSection() { 'system.disableSanitize': settings.disableSanitize }; + // Add temp file management settings + if (settings.tempFileManagement) { + deltaSettings['system.tempFileManagement.baseTmpDir'] = settings.tempFileManagement.baseTmpDir; + deltaSettings['system.tempFileManagement.libreofficeDir'] = settings.tempFileManagement.libreofficeDir; + deltaSettings['system.tempFileManagement.systemTempDir'] = settings.tempFileManagement.systemTempDir; + deltaSettings['system.tempFileManagement.prefix'] = settings.tempFileManagement.prefix; + deltaSettings['system.tempFileManagement.maxAgeHours'] = settings.tempFileManagement.maxAgeHours; + deltaSettings['system.tempFileManagement.cleanupIntervalMinutes'] = settings.tempFileManagement.cleanupIntervalMinutes; + deltaSettings['system.tempFileManagement.startupCleanup'] = settings.tempFileManagement.startupCleanup; + deltaSettings['system.tempFileManagement.cleanupSystemTemp'] = settings.tempFileManagement.cleanupSystemTemp; + } + + // Add process executor settings + if (settings.processExecutor?.sessionLimit) { + Object.entries(settings.processExecutor.sessionLimit).forEach(([key, value]) => { + deltaSettings[`processExecutor.sessionLimit.${key}`] = value; + }); + } + if (settings.processExecutor?.timeoutMinutes) { + Object.entries(settings.processExecutor.timeoutMinutes).forEach(([key, value]) => { + deltaSettings[`processExecutor.timeoutMinutes.${key}`] = value; + }); + } + const response = await fetch('/api/v1/admin/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, @@ -174,20 +249,510 @@ export default function AdminAdvancedSection() { - {/* Endpoints Info */} + {/* Temp File Management */} - - - - {t('admin.settings.advanced.endpoints.manage', 'Manage API Endpoints')} - - - - {t('admin.settings.advanced.endpoints.description', 'Endpoint management is configured via YAML. See documentation for details on enabling/disabling specific endpoints.')} + +
+ {t('admin.settings.advanced.tempFileManagement', 'Temp File Management')} + + {t('admin.settings.advanced.tempFileManagement.description', 'Configure temporary file storage and cleanup behavior')} + +
+ +
+ setSettings({ + ...settings, + tempFileManagement: { ...settings.tempFileManagement, baseTmpDir: e.target.value } + })} + placeholder="Default: java.io.tmpdir/stirling-pdf" + /> +
+ +
+ setSettings({ + ...settings, + tempFileManagement: { ...settings.tempFileManagement, libreofficeDir: e.target.value } + })} + placeholder="Default: baseTmpDir/libreoffice" + /> +
+ +
+ setSettings({ + ...settings, + tempFileManagement: { ...settings.tempFileManagement, systemTempDir: e.target.value } + })} + placeholder="System temp directory path" + /> +
+ +
+ setSettings({ + ...settings, + tempFileManagement: { ...settings.tempFileManagement, prefix: e.target.value } + })} + placeholder="stirling-pdf-" + /> +
+ +
+ setSettings({ + ...settings, + tempFileManagement: { ...settings.tempFileManagement, maxAgeHours: Number(value) } + })} + min={1} + max={720} + /> +
+ +
+ setSettings({ + ...settings, + tempFileManagement: { ...settings.tempFileManagement, cleanupIntervalMinutes: Number(value) } + })} + min={1} + max={1440} + /> +
+ +
+
+ {t('admin.settings.advanced.tempFileManagement.startupCleanup', 'Startup Cleanup')} + + {t('admin.settings.advanced.tempFileManagement.startupCleanup.description', 'Clean up old temp files on application startup')} - - - +
+ setSettings({ + ...settings, + tempFileManagement: { ...settings.tempFileManagement, startupCleanup: e.target.checked } + })} + /> +
+ +
+
+ {t('admin.settings.advanced.tempFileManagement.cleanupSystemTemp', 'Cleanup System Temp')} + + {t('admin.settings.advanced.tempFileManagement.cleanupSystemTemp.description', 'Whether to clean broader system temp directory (use with caution)')} + +
+ setSettings({ + ...settings, + tempFileManagement: { ...settings.tempFileManagement, cleanupSystemTemp: e.target.checked } + })} + /> +
+
+
+ + {/* Process Executor Limits */} + + + {t('admin.settings.advanced.processExecutor', 'Process Executor Limits')} + + {t('admin.settings.advanced.processExecutor.description', 'Configure session limits and timeouts for each process executor')} + + + + {/* LibreOffice */} + + {t('admin.settings.advanced.processExecutor.libreOffice', 'LibreOffice')} + + + setSettings({ + ...settings, + processExecutor: { + ...settings.processExecutor, + sessionLimit: { ...settings.processExecutor?.sessionLimit, libreOfficeSessionLimit: Number(value) } + } + })} + min={1} + max={100} + /> + setSettings({ + ...settings, + processExecutor: { + ...settings.processExecutor, + timeoutMinutes: { ...settings.processExecutor?.timeoutMinutes, libreOfficetimeoutMinutes: Number(value) } + } + })} + min={1} + max={240} + /> + + + + + {/* PDF to HTML */} + + {t('admin.settings.advanced.processExecutor.pdfToHtml', 'PDF to HTML')} + + + setSettings({ + ...settings, + processExecutor: { + ...settings.processExecutor, + sessionLimit: { ...settings.processExecutor?.sessionLimit, pdfToHtmlSessionLimit: Number(value) } + } + })} + min={1} + max={100} + /> + setSettings({ + ...settings, + processExecutor: { + ...settings.processExecutor, + timeoutMinutes: { ...settings.processExecutor?.timeoutMinutes, pdfToHtmltimeoutMinutes: Number(value) } + } + })} + min={1} + max={240} + /> + + + + + {/* QPDF */} + + {t('admin.settings.advanced.processExecutor.qpdf', 'QPDF')} + + + setSettings({ + ...settings, + processExecutor: { + ...settings.processExecutor, + sessionLimit: { ...settings.processExecutor?.sessionLimit, qpdfSessionLimit: Number(value) } + } + })} + min={1} + max={100} + /> + setSettings({ + ...settings, + processExecutor: { + ...settings.processExecutor, + timeoutMinutes: { ...settings.processExecutor?.timeoutMinutes, qpdfTimeoutMinutes: Number(value) } + } + })} + min={1} + max={240} + /> + + + + + {/* Tesseract OCR */} + + {t('admin.settings.advanced.processExecutor.tesseract', 'Tesseract OCR')} + + + setSettings({ + ...settings, + processExecutor: { + ...settings.processExecutor, + sessionLimit: { ...settings.processExecutor?.sessionLimit, tesseractSessionLimit: Number(value) } + } + })} + min={1} + max={100} + /> + setSettings({ + ...settings, + processExecutor: { + ...settings.processExecutor, + timeoutMinutes: { ...settings.processExecutor?.timeoutMinutes, tesseractTimeoutMinutes: Number(value) } + } + })} + min={1} + max={240} + /> + + + + + {/* Python OpenCV */} + + {t('admin.settings.advanced.processExecutor.pythonOpenCv', 'Python OpenCV')} + + + setSettings({ + ...settings, + processExecutor: { + ...settings.processExecutor, + sessionLimit: { ...settings.processExecutor?.sessionLimit, pythonOpenCvSessionLimit: Number(value) } + } + })} + min={1} + max={100} + /> + setSettings({ + ...settings, + processExecutor: { + ...settings.processExecutor, + timeoutMinutes: { ...settings.processExecutor?.timeoutMinutes, pythonOpenCvtimeoutMinutes: Number(value) } + } + })} + min={1} + max={240} + /> + + + + + {/* WeasyPrint */} + + {t('admin.settings.advanced.processExecutor.weasyPrint', 'WeasyPrint')} + + + setSettings({ + ...settings, + processExecutor: { + ...settings.processExecutor, + sessionLimit: { ...settings.processExecutor?.sessionLimit, weasyPrintSessionLimit: Number(value) } + } + })} + min={1} + max={100} + /> + setSettings({ + ...settings, + processExecutor: { + ...settings.processExecutor, + timeoutMinutes: { ...settings.processExecutor?.timeoutMinutes, weasyPrinttimeoutMinutes: Number(value) } + } + })} + min={1} + max={240} + /> + + + + + {/* Install App */} + + {t('admin.settings.advanced.processExecutor.installApp', 'Install App')} + + + setSettings({ + ...settings, + processExecutor: { + ...settings.processExecutor, + sessionLimit: { ...settings.processExecutor?.sessionLimit, installAppSessionLimit: Number(value) } + } + })} + min={1} + max={100} + /> + setSettings({ + ...settings, + processExecutor: { + ...settings.processExecutor, + timeoutMinutes: { ...settings.processExecutor?.timeoutMinutes, installApptimeoutMinutes: Number(value) } + } + })} + min={1} + max={240} + /> + + + + + {/* Calibre */} + + {t('admin.settings.advanced.processExecutor.calibre', 'Calibre')} + + + setSettings({ + ...settings, + processExecutor: { + ...settings.processExecutor, + sessionLimit: { ...settings.processExecutor?.sessionLimit, calibreSessionLimit: Number(value) } + } + })} + min={1} + max={100} + /> + setSettings({ + ...settings, + processExecutor: { + ...settings.processExecutor, + timeoutMinutes: { ...settings.processExecutor?.timeoutMinutes, calibretimeoutMinutes: Number(value) } + } + })} + min={1} + max={240} + /> + + + + + {/* Ghostscript */} + + {t('admin.settings.advanced.processExecutor.ghostscript', 'Ghostscript')} + + + setSettings({ + ...settings, + processExecutor: { + ...settings.processExecutor, + sessionLimit: { ...settings.processExecutor?.sessionLimit, ghostscriptSessionLimit: Number(value) } + } + })} + min={1} + max={100} + /> + setSettings({ + ...settings, + processExecutor: { + ...settings.processExecutor, + timeoutMinutes: { ...settings.processExecutor?.timeoutMinutes, ghostscriptTimeoutMinutes: Number(value) } + } + })} + min={1} + max={240} + /> + + + + + {/* OCRmyPDF */} + + {t('admin.settings.advanced.processExecutor.ocrMyPdf', 'OCRmyPDF')} + + + setSettings({ + ...settings, + processExecutor: { + ...settings.processExecutor, + sessionLimit: { ...settings.processExecutor?.sessionLimit, ocrMyPdfSessionLimit: Number(value) } + } + })} + min={1} + max={100} + /> + setSettings({ + ...settings, + processExecutor: { + ...settings.processExecutor, + timeoutMinutes: { ...settings.processExecutor?.timeoutMinutes, ocrMyPdfTimeoutMinutes: Number(value) } + } + })} + min={1} + max={240} + /> + + + + + {/* Save Button */} diff --git a/frontend/src/components/shared/config/configSections/AdminConnectionsSection.tsx b/frontend/src/components/shared/config/configSections/AdminConnectionsSection.tsx index 7fd3b72a1..b9d0dda82 100644 --- a/frontend/src/components/shared/config/configSections/AdminConnectionsSection.tsx +++ b/frontend/src/components/shared/config/configSections/AdminConnectionsSection.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { Stack, Text, Loader, Group, Divider } from '@mantine/core'; +import { Stack, Text, Loader, Group, Divider, Paper, Switch, Badge } from '@mantine/core'; import { alert } from '../../../toast'; import RestartConfirmationModal from '../RestartConfirmationModal'; import { useRestartServer } from '../useRestartServer'; @@ -39,6 +39,7 @@ interface ConnectionsSettingsData { password?: string; from?: string; }; + ssoAutoLogin?: boolean; } export default function AdminConnectionsSection() { @@ -61,10 +62,15 @@ export default function AdminConnectionsSection() { const mailResponse = await fetch('/api/v1/admin/settings/section/mail'); const mailData = mailResponse.ok ? await mailResponse.json() : {}; + // Fetch premium settings for SSO Auto Login + const premiumResponse = await fetch('/api/v1/admin/settings/section/premium'); + const premiumData = premiumResponse.ok ? await premiumResponse.json() : {}; + setSettings({ oauth2: securityData.oauth2 || {}, saml2: securityData.saml2 || {}, - mail: mailData || {} + mail: mailData || {}, + ssoAutoLogin: premiumData.proFeatures?.ssoAutoLogin || false }); } catch (error) { console.error('Failed to fetch connections settings:', error); @@ -263,6 +269,37 @@ export default function AdminConnectionsSection() { ); } + const handleSSOAutoLoginSave = async () => { + try { + const deltaSettings = { + 'premium.proFeatures.ssoAutoLogin': settings.ssoAutoLogin + }; + + const response = await fetch('/api/v1/admin/settings', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ settings: deltaSettings }), + }); + + if (response.ok) { + alert({ + alertType: 'success', + title: t('admin.success', 'Success'), + body: t('admin.settings.saveSuccess', 'Settings saved successfully'), + }); + showRestartModal(); + } else { + throw new Error('Failed to save'); + } + } catch (error) { + alert({ + alertType: 'error', + title: t('admin.error', 'Error'), + body: t('admin.settings.saveError', 'Failed to save settings'), + }); + } + }; + const linkedProviders = ALL_PROVIDERS.filter((p) => isProviderConfigured(p)); const availableProviders = ALL_PROVIDERS.filter((p) => !isProviderConfigured(p)); @@ -281,6 +318,32 @@ export default function AdminConnectionsSection() { + {/* SSO Auto Login - Premium Feature */} + + + + {t('admin.settings.connections.ssoAutoLogin', 'SSO Auto Login')} + PRO + + +
+
+ {t('admin.settings.connections.ssoAutoLogin.enable', 'Enable SSO Auto Login')} + + {t('admin.settings.connections.ssoAutoLogin.description', 'Automatically redirect to SSO login when authentication is required')} + +
+ { + setSettings({ ...settings, ssoAutoLogin: e.target.checked }); + handleSSOAutoLoginSave(); + }} + /> +
+
+
+ {/* Linked Services Section - Only show if there are linked providers */} {linkedProviders.length > 0 && ( <> diff --git a/frontend/src/components/shared/config/configSections/AdminDatabaseSection.tsx b/frontend/src/components/shared/config/configSections/AdminDatabaseSection.tsx new file mode 100644 index 000000000..b18f86462 --- /dev/null +++ b/frontend/src/components/shared/config/configSections/AdminDatabaseSection.tsx @@ -0,0 +1,230 @@ +import { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { NumberInput, Switch, Button, Stack, Paper, Text, Loader, Group, TextInput, PasswordInput, Select, Badge } from '@mantine/core'; +import { alert } from '../../../toast'; +import RestartConfirmationModal from '../RestartConfirmationModal'; +import { useRestartServer } from '../useRestartServer'; + +interface DatabaseSettingsData { + enableCustomDatabase?: boolean; + customDatabaseUrl?: string; + username?: string; + password?: string; + type?: string; + hostName?: string; + port?: number; + name?: string; +} + +export default function AdminDatabaseSection() { + const { t } = useTranslation(); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [settings, setSettings] = useState({}); + const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); + + useEffect(() => { + fetchSettings(); + }, []); + + const fetchSettings = async () => { + try { + const response = await fetch('/api/v1/admin/settings/section/system'); + const systemData = response.ok ? await response.json() : {}; + + setSettings(systemData.datasource || { + enableCustomDatabase: false, + customDatabaseUrl: '', + username: '', + password: '', + type: 'postgresql', + hostName: 'localhost', + port: 5432, + name: 'postgres' + }); + } catch (error) { + console.error('Failed to fetch database settings:', error); + alert({ + alertType: 'error', + title: t('admin.error', 'Error'), + body: t('admin.settings.fetchError', 'Failed to load settings'), + }); + } finally { + setLoading(false); + } + }; + + const handleSave = async () => { + setSaving(true); + try { + const deltaSettings: Record = { + 'system.datasource.enableCustomDatabase': settings.enableCustomDatabase, + 'system.datasource.customDatabaseUrl': settings.customDatabaseUrl, + 'system.datasource.username': settings.username, + 'system.datasource.password': settings.password, + 'system.datasource.type': settings.type, + 'system.datasource.hostName': settings.hostName, + 'system.datasource.port': settings.port, + 'system.datasource.name': settings.name + }; + + const response = await fetch('/api/v1/admin/settings', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ settings: deltaSettings }), + }); + + if (response.ok) { + showRestartModal(); + } else { + throw new Error('Failed to save'); + } + } catch (error) { + alert({ + alertType: 'error', + title: t('admin.error', 'Error'), + body: t('admin.settings.saveError', 'Failed to save settings'), + }); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ( + + + + ); + } + + return ( + +
+ +
+ {t('admin.settings.database.title', 'Database')} + + {t('admin.settings.database.description', 'Configure custom database connection settings for enterprise deployments.')} + +
+ ENTERPRISE +
+
+ + {/* Database Configuration */} + + + {t('admin.settings.database.configuration', 'Database Configuration')} + +
+
+ {t('admin.settings.database.enableCustom', 'Enable Custom Database')} + + {t('admin.settings.database.enableCustom.description', 'Use your own custom database configuration instead of the default embedded database')} + +
+ setSettings({ ...settings, enableCustomDatabase: e.target.checked })} + /> +
+ + {settings.enableCustomDatabase && ( + <> +
+ setSettings({ ...settings, customDatabaseUrl: e.target.value })} + placeholder="jdbc:postgresql://localhost:5432/postgres" + /> +
+ +
+ setSettings({ + ...settings, + html: { + ...settings.html, + urlSecurity: { ...settings.html?.urlSecurity, level: value || 'MEDIUM' } + } + })} + data={[ + { value: 'MAX', label: t('admin.settings.security.htmlUrlSecurity.level.max', 'Maximum (Whitelist Only)') }, + { value: 'MEDIUM', label: t('admin.settings.security.htmlUrlSecurity.level.medium', 'Medium (Block Internal)') }, + { value: 'OFF', label: t('admin.settings.security.htmlUrlSecurity.level.off', 'Off (No Restrictions)') }, + ]} + comboboxProps={{ zIndex: 1400 }} + /> +
+ + + + {t('admin.settings.security.htmlUrlSecurity.advanced', 'Advanced Settings')} + + + {/* Allowed Domains */} +
+