diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java index 3c2e6f33a..0c8c55655 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java @@ -1,11 +1,13 @@ package stirling.software.SPDF.controller.api; import java.io.IOException; +import java.util.HashMap; import java.util.Map; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import io.swagger.v3.oas.annotations.Hidden; @@ -46,4 +48,301 @@ public class SettingsController { public ResponseEntity> getDisabledEndpoints() { return ResponseEntity.ok(endpointConfiguration.getEndpointStatuses()); } + + // ========== GENERAL SETTINGS ========== + + @GetMapping("/admin/settings/general") + @Hidden + public ResponseEntity> getGeneralSettings() { + Map settings = new HashMap<>(); + settings.put("ui", applicationProperties.getUi()); + settings.put("system", Map.of( + "defaultLocale", applicationProperties.getSystem().getDefaultLocale(), + "showUpdate", applicationProperties.getSystem().isShowUpdate(), + "showUpdateOnlyAdmin", applicationProperties.getSystem().getShowUpdateOnlyAdmin(), + "customHTMLFiles", applicationProperties.getSystem().isCustomHTMLFiles(), + "fileUploadLimit", applicationProperties.getSystem().getFileUploadLimit() + )); + return ResponseEntity.ok(settings); + } + + @PostMapping("/admin/settings/general") + @Hidden + public ResponseEntity updateGeneralSettings(@RequestBody Map settings) throws IOException { + // Update UI settings + if (settings.containsKey("ui")) { + Map ui = (Map) settings.get("ui"); + if (ui.containsKey("appName")) { + GeneralUtils.saveKeyToSettings("ui.appName", ui.get("appName")); + applicationProperties.getUi().setAppName(ui.get("appName")); + } + if (ui.containsKey("homeDescription")) { + GeneralUtils.saveKeyToSettings("ui.homeDescription", ui.get("homeDescription")); + applicationProperties.getUi().setHomeDescription(ui.get("homeDescription")); + } + if (ui.containsKey("appNameNavbar")) { + GeneralUtils.saveKeyToSettings("ui.appNameNavbar", ui.get("appNameNavbar")); + applicationProperties.getUi().setAppNameNavbar(ui.get("appNameNavbar")); + } + } + + // Update System settings + if (settings.containsKey("system")) { + Map system = (Map) settings.get("system"); + if (system.containsKey("defaultLocale")) { + GeneralUtils.saveKeyToSettings("system.defaultLocale", system.get("defaultLocale")); + applicationProperties.getSystem().setDefaultLocale((String) system.get("defaultLocale")); + } + if (system.containsKey("showUpdate")) { + GeneralUtils.saveKeyToSettings("system.showUpdate", system.get("showUpdate")); + applicationProperties.getSystem().setShowUpdate((Boolean) system.get("showUpdate")); + } + if (system.containsKey("showUpdateOnlyAdmin")) { + GeneralUtils.saveKeyToSettings("system.showUpdateOnlyAdmin", system.get("showUpdateOnlyAdmin")); + applicationProperties.getSystem().setShowUpdateOnlyAdmin((Boolean) system.get("showUpdateOnlyAdmin")); + } + if (system.containsKey("fileUploadLimit")) { + GeneralUtils.saveKeyToSettings("system.fileUploadLimit", system.get("fileUploadLimit")); + applicationProperties.getSystem().setFileUploadLimit((String) system.get("fileUploadLimit")); + } + } + + return ResponseEntity.ok("General settings updated. Restart required for changes to take effect."); + } + + // ========== SECURITY SETTINGS ========== + + @GetMapping("/admin/settings/security") + @Hidden + public ResponseEntity> getSecuritySettings() { + Map settings = new HashMap<>(); + ApplicationProperties.Security security = applicationProperties.getSecurity(); + + settings.put("enableLogin", security.getEnableLogin()); + settings.put("csrfDisabled", security.getCsrfDisabled()); + settings.put("loginMethod", security.getLoginMethod()); + settings.put("loginAttemptCount", security.getLoginAttemptCount()); + settings.put("loginResetTimeMinutes", security.getLoginResetTimeMinutes()); + settings.put("initialLogin", Map.of( + "username", security.getInitialLogin().getUsername() != null ? security.getInitialLogin().getUsername() : "" + )); + + // JWT settings + ApplicationProperties.Security.Jwt jwt = security.getJwt(); + settings.put("jwt", Map.of( + "enableKeystore", jwt.isEnableKeystore(), + "enableKeyRotation", jwt.isEnableKeyRotation(), + "enableKeyCleanup", jwt.isEnableKeyCleanup(), + "keyRetentionDays", jwt.getKeyRetentionDays(), + "secureCookie", jwt.isSecureCookie() + )); + + return ResponseEntity.ok(settings); + } + + @PostMapping("/admin/settings/security") + @Hidden + public ResponseEntity updateSecuritySettings(@RequestBody Map settings) throws IOException { + if (settings.containsKey("enableLogin")) { + GeneralUtils.saveKeyToSettings("security.enableLogin", settings.get("enableLogin")); + applicationProperties.getSecurity().setEnableLogin((Boolean) settings.get("enableLogin")); + } + if (settings.containsKey("csrfDisabled")) { + GeneralUtils.saveKeyToSettings("security.csrfDisabled", settings.get("csrfDisabled")); + applicationProperties.getSecurity().setCsrfDisabled((Boolean) settings.get("csrfDisabled")); + } + if (settings.containsKey("loginMethod")) { + GeneralUtils.saveKeyToSettings("security.loginMethod", settings.get("loginMethod")); + applicationProperties.getSecurity().setLoginMethod((String) settings.get("loginMethod")); + } + if (settings.containsKey("loginAttemptCount")) { + GeneralUtils.saveKeyToSettings("security.loginAttemptCount", settings.get("loginAttemptCount")); + applicationProperties.getSecurity().setLoginAttemptCount((Integer) settings.get("loginAttemptCount")); + } + if (settings.containsKey("loginResetTimeMinutes")) { + GeneralUtils.saveKeyToSettings("security.loginResetTimeMinutes", settings.get("loginResetTimeMinutes")); + applicationProperties.getSecurity().setLoginResetTimeMinutes(((Number) settings.get("loginResetTimeMinutes")).longValue()); + } + + // JWT settings + if (settings.containsKey("jwt")) { + Map jwt = (Map) settings.get("jwt"); + if (jwt.containsKey("secureCookie")) { + GeneralUtils.saveKeyToSettings("security.jwt.secureCookie", jwt.get("secureCookie")); + applicationProperties.getSecurity().getJwt().setSecureCookie((Boolean) jwt.get("secureCookie")); + } + if (jwt.containsKey("keyRetentionDays")) { + GeneralUtils.saveKeyToSettings("security.jwt.keyRetentionDays", jwt.get("keyRetentionDays")); + applicationProperties.getSecurity().getJwt().setKeyRetentionDays((Integer) jwt.get("keyRetentionDays")); + } + } + + return ResponseEntity.ok("Security settings updated. Restart required for changes to take effect."); + } + + // ========== CONNECTIONS SETTINGS (OAuth/SAML) ========== + + @GetMapping("/admin/settings/connections") + @Hidden + public ResponseEntity> getConnectionsSettings() { + Map settings = new HashMap<>(); + ApplicationProperties.Security security = applicationProperties.getSecurity(); + + // OAuth2 settings + ApplicationProperties.Security.OAUTH2 oauth2 = security.getOauth2(); + settings.put("oauth2", Map.of( + "enabled", oauth2.getEnabled(), + "issuer", oauth2.getIssuer() != null ? oauth2.getIssuer() : "", + "clientId", oauth2.getClientId() != null ? oauth2.getClientId() : "", + "provider", oauth2.getProvider() != null ? oauth2.getProvider() : "", + "autoCreateUser", oauth2.getAutoCreateUser(), + "blockRegistration", oauth2.getBlockRegistration(), + "useAsUsername", oauth2.getUseAsUsername() != null ? oauth2.getUseAsUsername() : "" + )); + + // SAML2 settings + ApplicationProperties.Security.SAML2 saml2 = security.getSaml2(); + settings.put("saml2", Map.of( + "enabled", saml2.getEnabled(), + "provider", saml2.getProvider() != null ? saml2.getProvider() : "", + "autoCreateUser", saml2.getAutoCreateUser(), + "blockRegistration", saml2.getBlockRegistration(), + "registrationId", saml2.getRegistrationId() + )); + + return ResponseEntity.ok(settings); + } + + @PostMapping("/admin/settings/connections") + @Hidden + public ResponseEntity updateConnectionsSettings(@RequestBody Map settings) throws IOException { + // OAuth2 settings + if (settings.containsKey("oauth2")) { + Map oauth2 = (Map) settings.get("oauth2"); + if (oauth2.containsKey("enabled")) { + GeneralUtils.saveKeyToSettings("security.oauth2.enabled", oauth2.get("enabled")); + applicationProperties.getSecurity().getOauth2().setEnabled((Boolean) oauth2.get("enabled")); + } + if (oauth2.containsKey("issuer")) { + GeneralUtils.saveKeyToSettings("security.oauth2.issuer", oauth2.get("issuer")); + applicationProperties.getSecurity().getOauth2().setIssuer((String) oauth2.get("issuer")); + } + if (oauth2.containsKey("clientId")) { + GeneralUtils.saveKeyToSettings("security.oauth2.clientId", oauth2.get("clientId")); + applicationProperties.getSecurity().getOauth2().setClientId((String) oauth2.get("clientId")); + } + if (oauth2.containsKey("clientSecret")) { + GeneralUtils.saveKeyToSettings("security.oauth2.clientSecret", oauth2.get("clientSecret")); + applicationProperties.getSecurity().getOauth2().setClientSecret((String) oauth2.get("clientSecret")); + } + if (oauth2.containsKey("provider")) { + GeneralUtils.saveKeyToSettings("security.oauth2.provider", oauth2.get("provider")); + applicationProperties.getSecurity().getOauth2().setProvider((String) oauth2.get("provider")); + } + if (oauth2.containsKey("autoCreateUser")) { + GeneralUtils.saveKeyToSettings("security.oauth2.autoCreateUser", oauth2.get("autoCreateUser")); + applicationProperties.getSecurity().getOauth2().setAutoCreateUser((Boolean) oauth2.get("autoCreateUser")); + } + if (oauth2.containsKey("blockRegistration")) { + GeneralUtils.saveKeyToSettings("security.oauth2.blockRegistration", oauth2.get("blockRegistration")); + applicationProperties.getSecurity().getOauth2().setBlockRegistration((Boolean) oauth2.get("blockRegistration")); + } + if (oauth2.containsKey("useAsUsername")) { + GeneralUtils.saveKeyToSettings("security.oauth2.useAsUsername", oauth2.get("useAsUsername")); + applicationProperties.getSecurity().getOauth2().setUseAsUsername((String) oauth2.get("useAsUsername")); + } + } + + // SAML2 settings + if (settings.containsKey("saml2")) { + Map saml2 = (Map) settings.get("saml2"); + if (saml2.containsKey("enabled")) { + GeneralUtils.saveKeyToSettings("security.saml2.enabled", saml2.get("enabled")); + applicationProperties.getSecurity().getSaml2().setEnabled((Boolean) saml2.get("enabled")); + } + if (saml2.containsKey("provider")) { + GeneralUtils.saveKeyToSettings("security.saml2.provider", saml2.get("provider")); + applicationProperties.getSecurity().getSaml2().setProvider((String) saml2.get("provider")); + } + if (saml2.containsKey("autoCreateUser")) { + GeneralUtils.saveKeyToSettings("security.saml2.autoCreateUser", saml2.get("autoCreateUser")); + applicationProperties.getSecurity().getSaml2().setAutoCreateUser((Boolean) saml2.get("autoCreateUser")); + } + if (saml2.containsKey("blockRegistration")) { + GeneralUtils.saveKeyToSettings("security.saml2.blockRegistration", saml2.get("blockRegistration")); + applicationProperties.getSecurity().getSaml2().setBlockRegistration((Boolean) saml2.get("blockRegistration")); + } + } + + return ResponseEntity.ok("Connection settings updated. Restart required for changes to take effect."); + } + + // ========== PRIVACY SETTINGS ========== + + @GetMapping("/admin/settings/privacy") + @Hidden + public ResponseEntity> getPrivacySettings() { + Map settings = new HashMap<>(); + + settings.put("enableAnalytics", applicationProperties.getSystem().getEnableAnalytics()); + settings.put("googleVisibility", applicationProperties.getSystem().getGooglevisibility()); + settings.put("metricsEnabled", applicationProperties.getMetrics().getEnabled()); + + return ResponseEntity.ok(settings); + } + + @PostMapping("/admin/settings/privacy") + @Hidden + public ResponseEntity updatePrivacySettings(@RequestBody Map settings) throws IOException { + if (settings.containsKey("enableAnalytics")) { + GeneralUtils.saveKeyToSettings("system.enableAnalytics", settings.get("enableAnalytics")); + applicationProperties.getSystem().setEnableAnalytics((Boolean) settings.get("enableAnalytics")); + } + if (settings.containsKey("googleVisibility")) { + GeneralUtils.saveKeyToSettings("system.googlevisibility", settings.get("googleVisibility")); + applicationProperties.getSystem().setGooglevisibility((Boolean) settings.get("googleVisibility")); + } + if (settings.containsKey("metricsEnabled")) { + GeneralUtils.saveKeyToSettings("metrics.enabled", settings.get("metricsEnabled")); + applicationProperties.getMetrics().setEnabled((Boolean) settings.get("metricsEnabled")); + } + + return ResponseEntity.ok("Privacy settings updated. Restart required for changes to take effect."); + } + + // ========== ADVANCED SETTINGS ========== + + @GetMapping("/admin/settings/advanced") + @Hidden + public ResponseEntity> getAdvancedSettings() { + Map settings = new HashMap<>(); + + settings.put("endpoints", applicationProperties.getEndpoints()); + settings.put("enableAlphaFunctionality", applicationProperties.getSystem().getEnableAlphaFunctionality()); + settings.put("maxDPI", applicationProperties.getSystem().getMaxDPI()); + settings.put("enableUrlToPDF", applicationProperties.getSystem().getEnableUrlToPDF()); + settings.put("customPaths", applicationProperties.getSystem().getCustomPaths()); + settings.put("tempFileManagement", applicationProperties.getSystem().getTempFileManagement()); + + return ResponseEntity.ok(settings); + } + + @PostMapping("/admin/settings/advanced") + @Hidden + public ResponseEntity updateAdvancedSettings(@RequestBody Map settings) throws IOException { + if (settings.containsKey("enableAlphaFunctionality")) { + GeneralUtils.saveKeyToSettings("system.enableAlphaFunctionality", settings.get("enableAlphaFunctionality")); + applicationProperties.getSystem().setEnableAlphaFunctionality((Boolean) settings.get("enableAlphaFunctionality")); + } + if (settings.containsKey("maxDPI")) { + GeneralUtils.saveKeyToSettings("system.maxDPI", settings.get("maxDPI")); + applicationProperties.getSystem().setMaxDPI((Integer) settings.get("maxDPI")); + } + if (settings.containsKey("enableUrlToPDF")) { + GeneralUtils.saveKeyToSettings("system.enableUrlToPDF", settings.get("enableUrlToPDF")); + applicationProperties.getSystem().setEnableUrlToPDF((Boolean) settings.get("enableUrlToPDF")); + } + + return ResponseEntity.ok("Advanced settings updated. Restart required for changes to take effect."); + } } diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index f6f8d828c..8a2f91e03 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -3207,8 +3207,218 @@ "activity": "Activity", "account": "Account", "config": "Config", + "adminSettings": "Admin Settings", "allTools": "All Tools" }, + "admin": { + "error": "Error", + "success": "Success", + "status": { + "active": "Active", + "inactive": "Inactive" + }, + "settings": { + "title": "Admin Settings", + "workspace": "Workspace", + "fetchError": "Failed to load settings", + "saveError": "Failed to save settings", + "saved": "Settings saved successfully", + "save": "Save Changes", + "restartRequired": "Settings changes require a server restart to take effect.", + "general": { + "title": "General", + "description": "Configure general application settings including branding and default behaviour.", + "ui": "User Interface", + "system": "System", + "appName": "Application Name", + "appName.description": "The name displayed in the browser tab and home page", + "appNameNavbar": "Navbar Brand", + "appNameNavbar.description": "The name displayed in the navigation bar", + "homeDescription": "Home Description", + "homeDescription.description": "The description text shown on the home page", + "defaultLocale": "Default Locale", + "defaultLocale.description": "The default language for new users (e.g., en_US, es_ES)", + "fileUploadLimit": "File Upload Limit", + "fileUploadLimit.description": "Maximum file upload size (e.g., 100MB, 1GB)", + "showUpdate": "Show Update Notifications", + "showUpdate.description": "Display notifications when a new version is available", + "showUpdateOnlyAdmin": "Show Updates to Admins Only", + "showUpdateOnlyAdmin.description": "Restrict update notifications to admin users only", + "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)" + }, + "security": { + "title": "Security", + "description": "Configure authentication, login behaviour, and security policies.", + "authentication": "Authentication", + "enableLogin": "Enable Login", + "enableLogin.description": "Require users to log in before accessing the application", + "loginMethod": "Login Method", + "loginMethod.description": "The authentication method to use for user login", + "loginMethod.all": "All Methods", + "loginMethod.normal": "Username/Password Only", + "loginMethod.oauth2": "OAuth2 Only", + "loginMethod.saml2": "SAML2 Only", + "loginAttemptCount": "Login Attempt Limit", + "loginAttemptCount.description": "Maximum number of failed login attempts before account lockout", + "loginResetTimeMinutes": "Login Reset Time (minutes)", + "loginResetTimeMinutes.description": "Time before failed login attempts are reset", + "csrfDisabled": "Disable CSRF Protection", + "csrfDisabled.description": "Disable Cross-Site Request Forgery protection (not recommended)", + "initialLogin": "Initial Login", + "initialLogin.username": "Initial Username", + "initialLogin.username.description": "The username for the initial admin account", + "initialLogin.password": "Initial Password", + "initialLogin.password.description": "The password for the initial admin account", + "jwt": "JWT Configuration", + "jwt.secureCookie": "Secure Cookie", + "jwt.secureCookie.description": "Require HTTPS for JWT cookies (recommended for production)", + "jwt.keyRetentionDays": "Key Retention Days", + "jwt.keyRetentionDays.description": "Number of days to retain old JWT keys for verification", + "jwt.persistence": "Enable Key Persistence", + "jwt.persistence.description": "Store JWT keys persistently to survive server restarts", + "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" + }, + "connections": { + "title": "Connections", + "description": "Configure external authentication providers like OAuth2 and SAML.", + "oauth2": "OAuth2", + "oauth2.enabled": "Enable OAuth2", + "oauth2.enabled.description": "Allow users to authenticate using OAuth2 providers", + "oauth2.provider": "Provider", + "oauth2.provider.description": "The OAuth2 provider to use for authentication", + "oauth2.issuer": "Issuer URL", + "oauth2.issuer.description": "The OAuth2 provider issuer URL", + "oauth2.clientId": "Client ID", + "oauth2.clientId.description": "The OAuth2 client ID from your provider", + "oauth2.clientSecret": "Client Secret", + "oauth2.clientSecret.description": "The OAuth2 client secret from your provider", + "oauth2.useAsUsername": "Use as Username", + "oauth2.useAsUsername.description": "The OAuth2 claim to use as the username (e.g., email, sub)", + "oauth2.autoCreateUser": "Auto Create Users", + "oauth2.autoCreateUser.description": "Automatically create user accounts on first OAuth2 login", + "oauth2.blockRegistration": "Block Registration", + "oauth2.blockRegistration.description": "Prevent new user registration via OAuth2", + "oauth2.scopes": "OAuth2 Scopes", + "oauth2.scopes.description": "Comma-separated list of OAuth2 scopes to request (e.g., openid, profile, email)", + "saml2": "SAML2", + "saml2.enabled": "Enable SAML2", + "saml2.enabled.description": "Allow users to authenticate using SAML2 providers", + "saml2.provider": "Provider", + "saml2.provider.description": "The SAML2 provider name", + "saml2.registrationId": "Registration ID", + "saml2.registrationId.description": "The SAML2 registration identifier", + "saml2.autoCreateUser": "Auto Create Users", + "saml2.autoCreateUser.description": "Automatically create user accounts on first SAML2 login", + "saml2.blockRegistration": "Block Registration", + "saml2.blockRegistration.description": "Prevent new user registration via SAML2" + }, + "privacy": { + "title": "Privacy", + "description": "Configure privacy and data collection settings.", + "analytics": "Analytics & Tracking", + "enableAnalytics": "Enable Analytics", + "enableAnalytics.description": "Collect anonymous usage analytics to help improve the application", + "metricsEnabled": "Enable Metrics", + "metricsEnabled.description": "Enable collection of performance and usage metrics", + "searchEngine": "Search Engine Visibility", + "googleVisibility": "Google Visibility", + "googleVisibility.description": "Allow search engines to index this application" + }, + "advanced": { + "title": "Advanced", + "description": "Configure advanced features and experimental functionality.", + "features": "Feature Flags", + "processing": "Processing", + "endpoints": "Endpoints", + "endpoints.manage": "Manage API Endpoints", + "endpoints.description": "Endpoint management is configured via YAML. See documentation for details on enabling/disabling specific endpoints.", + "enableAlphaFunctionality": "Enable Alpha Features", + "enableAlphaFunctionality.description": "Enable experimental and alpha-stage features (may be unstable)", + "enableUrlToPDF": "Enable URL to PDF", + "enableUrlToPDF.description": "Allow conversion of web pages to PDF documents", + "maxDPI": "Maximum DPI", + "maxDPI.description": "Maximum DPI for image processing (0 = unlimited)", + "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" + }, + "mail": { + "title": "Mail Server", + "description": "Configure SMTP settings for sending email notifications.", + "smtp": "SMTP Configuration", + "enabled": "Enable Mail", + "enabled.description": "Enable email notifications and SMTP functionality", + "host": "SMTP Host", + "host.description": "The hostname or IP address of your SMTP server", + "port": "SMTP Port", + "port.description": "The port number for SMTP connection (typically 25, 465, or 587)", + "username": "SMTP Username", + "username.description": "Username for SMTP authentication", + "password": "SMTP Password", + "password.description": "Password for SMTP authentication", + "from": "From Address", + "from.description": "The email address to use as the sender" + }, + "legal": { + "title": "Legal Documents", + "description": "Configure links to legal documents and policies.", + "termsAndConditions": "Terms and Conditions", + "termsAndConditions.description": "URL or filename to terms and conditions", + "privacyPolicy": "Privacy Policy", + "privacyPolicy.description": "URL or filename to privacy policy", + "accessibilityStatement": "Accessibility Statement", + "accessibilityStatement.description": "URL or filename to accessibility statement", + "cookiePolicy": "Cookie Policy", + "cookiePolicy.description": "URL or filename to cookie policy", + "impressum": "Impressum", + "impressum.description": "URL or filename to impressum (required in some jurisdictions)" + }, + "premium": { + "title": "Premium & Enterprise", + "description": "Configure premium and enterprise features.", + "license": "License", + "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" + }, + "endpoints": { + "title": "API Endpoints", + "description": "Control which API endpoints and endpoint groups are available.", + "management": "Endpoint Management", + "toRemove": "Disabled Endpoints", + "toRemove.description": "Select individual endpoints to disable", + "groupsToRemove": "Disabled Endpoint Groups", + "groupsToRemove.description": "Select endpoint groups to disable", + "note": "Note: Disabling endpoints restricts API access but does not remove UI components. Restart required for changes to take effect." + } + } + }, "fileUpload": { "selectFile": "Select a file", "selectFiles": "Select files", diff --git a/frontend/src/components/shared/AppConfigModal.tsx b/frontend/src/components/shared/AppConfigModal.tsx index 160547180..c3876ccd5 100644 --- a/frontend/src/components/shared/AppConfigModal.tsx +++ b/frontend/src/components/shared/AppConfigModal.tsx @@ -44,13 +44,17 @@ const AppConfigModal: React.FC = ({ opened, onClose }) => { console.log('Logout placeholder for SaaS compatibility'); }; + // TODO: Replace with actual role check from JWT/auth context + const isAdmin = true; // Emulated admin role for now + // Left navigation structure and icons const configNavSections = useMemo(() => createConfigNavSections( Overview, - handleLogout + handleLogout, + isAdmin ), - [] + [isAdmin] ); const activeLabel = useMemo(() => { diff --git a/frontend/src/components/shared/config/configNavSections.tsx b/frontend/src/components/shared/config/configNavSections.tsx index ac5b670ee..1c0e51d9f 100644 --- a/frontend/src/components/shared/config/configNavSections.tsx +++ b/frontend/src/components/shared/config/configNavSections.tsx @@ -2,6 +2,15 @@ import React from 'react'; import { NavKey } from './types'; import HotkeysSection from './configSections/HotkeysSection'; import GeneralSection from './configSections/GeneralSection'; +import AdminGeneralSection from './configSections/AdminGeneralSection'; +import AdminSecuritySection from './configSections/AdminSecuritySection'; +import AdminConnectionsSection from './configSections/AdminConnectionsSection'; +import AdminPrivacySection from './configSections/AdminPrivacySection'; +import AdminAdvancedSection from './configSections/AdminAdvancedSection'; +import AdminMailSection from './configSections/AdminMailSection'; +import AdminLegalSection from './configSections/AdminLegalSection'; +import AdminPremiumSection from './configSections/AdminPremiumSection'; +import AdminEndpointsSection from './configSections/AdminEndpointsSection'; export interface ConfigNavItem { key: NavKey; @@ -27,7 +36,8 @@ export interface ConfigColors { export const createConfigNavSections = ( Overview: React.ComponentType<{ onLogoutClick: () => void }>, - onLogoutClick: () => void + onLogoutClick: () => void, + isAdmin: boolean = false ): ConfigNavSection[] => { const sections: ConfigNavSection[] = [ { @@ -60,5 +70,68 @@ export const createConfigNavSections = ( }, ]; + // Add Admin Settings section if user is admin + if (isAdmin) { + sections.push({ + title: 'Admin Settings', + items: [ + { + key: 'adminGeneral', + label: 'General', + icon: 'settings-rounded', + component: + }, + { + key: 'adminSecurity', + label: 'Security', + icon: 'shield-rounded', + component: + }, + { + key: 'adminConnections', + label: 'Connections', + icon: 'link-rounded', + component: + }, + { + key: 'adminMail', + label: 'Mail', + icon: 'mail-rounded', + component: + }, + { + key: 'adminLegal', + label: 'Legal', + icon: 'gavel-rounded', + component: + }, + { + key: 'adminPrivacy', + label: 'Privacy', + icon: 'visibility-rounded', + component: + }, + { + key: 'adminPremium', + label: 'Premium', + icon: 'star-rounded', + component: + }, + { + key: 'adminEndpoints', + label: 'Endpoints', + icon: 'api-rounded', + component: + }, + { + key: 'adminAdvanced', + label: 'Advanced', + icon: 'tune-rounded', + component: + }, + ], + }); + } + return sections; }; \ No newline at end of file diff --git a/frontend/src/components/shared/config/configSections/AdminAdvancedSection.tsx b/frontend/src/components/shared/config/configSections/AdminAdvancedSection.tsx new file mode 100644 index 000000000..c2af91edc --- /dev/null +++ b/frontend/src/components/shared/config/configSections/AdminAdvancedSection.tsx @@ -0,0 +1,202 @@ +import { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { NumberInput, Switch, Button, Stack, Paper, Text, Loader, Group, Accordion, TextInput } from '@mantine/core'; +import { alert } from '../../../toast'; + +interface AdvancedSettingsData { + enableAlphaFunctionality?: boolean; + maxDPI?: number; + enableUrlToPDF?: boolean; + tessdataDir?: string; + disableSanitize?: boolean; +} + +export default function AdminAdvancedSection() { + const { t } = useTranslation(); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [settings, setSettings] = useState({}); + + useEffect(() => { + fetchSettings(); + }, []); + + 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 + }); + } + } catch (error) { + console.error('Failed to fetch advanced 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 { + // Use delta update endpoint with dot notation + const deltaSettings = { + 'system.enableAlphaFunctionality': settings.enableAlphaFunctionality, + 'system.maxDPI': settings.maxDPI, + 'system.enableUrlToPDF': settings.enableUrlToPDF, + 'system.tessdataDir': settings.tessdataDir, + 'system.disableSanitize': settings.disableSanitize + }; + + 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.saved', 'Settings saved. Restart required for changes to take effect.'), + }); + } 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.advanced.title', 'Advanced')} + + {t('admin.settings.advanced.description', 'Configure advanced features and experimental functionality.')} + +
+ + {/* Feature Flags */} + + + {t('admin.settings.advanced.features', 'Feature Flags')} + +
+
+ {t('admin.settings.advanced.enableAlphaFunctionality', 'Enable Alpha Features')} + + {t('admin.settings.advanced.enableAlphaFunctionality.description', 'Enable experimental and alpha-stage features (may be unstable)')} + +
+ setSettings({ ...settings, enableAlphaFunctionality: e.target.checked })} + /> +
+ +
+
+ {t('admin.settings.advanced.enableUrlToPDF', 'Enable URL to PDF')} + + {t('admin.settings.advanced.enableUrlToPDF.description', 'Allow conversion of web pages to PDF documents (internal use only)')} + +
+ setSettings({ ...settings, enableUrlToPDF: e.target.checked })} + /> +
+ +
+
+ {t('admin.settings.advanced.disableSanitize', 'Disable HTML Sanitization')} + + {t('admin.settings.advanced.disableSanitize.description', 'Disable HTML sanitization (WARNING: Security risk - can lead to XSS injections)')} + +
+ setSettings({ ...settings, disableSanitize: e.target.checked })} + /> +
+
+
+ + {/* Processing Settings */} + + + {t('admin.settings.advanced.processing', 'Processing')} + +
+ setSettings({ ...settings, maxDPI: Number(value) })} + min={0} + max={3000} + /> +
+ +
+ setSettings({ ...settings, tessdataDir: e.target.value })} + placeholder="/usr/share/tessdata" + /> +
+
+
+ + {/* Endpoints Info */} + + + + + {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.')} + + + + + + + {/* Save Button */} + + + +
+ ); +} diff --git a/frontend/src/components/shared/config/configSections/AdminConnectionsSection.tsx b/frontend/src/components/shared/config/configSections/AdminConnectionsSection.tsx new file mode 100644 index 000000000..e4da8dd24 --- /dev/null +++ b/frontend/src/components/shared/config/configSections/AdminConnectionsSection.tsx @@ -0,0 +1,341 @@ +import { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { TextInput, Switch, Button, Stack, Paper, Text, Loader, Group, Select, Badge, PasswordInput } from '@mantine/core'; +import { alert } from '../../../toast'; +import LocalIcon from '../../LocalIcon'; + +interface OAuth2Settings { + enabled?: boolean; + issuer?: string; + clientId?: string; + clientSecret?: string; + provider?: string; + autoCreateUser?: boolean; + blockRegistration?: boolean; + useAsUsername?: string; + scopes?: string; +} + +interface SAML2Settings { + enabled?: boolean; + provider?: string; + autoCreateUser?: boolean; + blockRegistration?: boolean; + registrationId?: string; +} + +interface ConnectionsSettingsData { + oauth2?: OAuth2Settings; + saml2?: SAML2Settings; +} + +export default function AdminConnectionsSection() { + const { t } = useTranslation(); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [settings, setSettings] = useState({}); + + useEffect(() => { + fetchSettings(); + }, []); + + const fetchSettings = async () => { + try { + // OAuth2 and SAML2 are nested under security section + const response = await fetch('/api/v1/admin/settings/section/security'); + if (response.ok) { + const data = await response.json(); + // Extract oauth2 and saml2 from security section + setSettings({ + oauth2: data.oauth2 || {}, + saml2: data.saml2 || {} + }); + } + } catch (error) { + console.error('Failed to fetch connections 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 { + // Use delta update endpoint with dot notation for nested oauth2/saml2 settings + const deltaSettings: Record = {}; + + // Convert oauth2 settings to dot notation + if (settings.oauth2) { + Object.keys(settings.oauth2).forEach(key => { + deltaSettings[`security.oauth2.${key}`] = (settings.oauth2 as any)[key]; + }); + } + + // Convert saml2 settings to dot notation + if (settings.saml2) { + Object.keys(settings.saml2).forEach(key => { + deltaSettings[`security.saml2.${key}`] = (settings.saml2 as any)[key]; + }); + } + + 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.saved', 'Settings saved. Restart required for changes to take effect.'), + }); + } 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 ( + + + + ); + } + + const getProviderIcon = (provider?: string) => { + switch (provider?.toLowerCase()) { + case 'google': + return ; + case 'github': + return ; + case 'keycloak': + return ; + default: + return ; + } + }; + + return ( + +
+ {t('admin.settings.connections.title', 'Connections')} + + {t('admin.settings.connections.description', 'Configure external authentication providers like OAuth2 and SAML.')} + +
+ + {/* OAuth2 Settings */} + + + + + + {t('admin.settings.connections.oauth2', 'OAuth2')} + + + {settings.oauth2?.enabled ? t('admin.status.active', 'Active') : t('admin.status.inactive', 'Inactive')} + + + +
+
+ {t('admin.settings.connections.oauth2.enabled', 'Enable OAuth2')} + + {t('admin.settings.connections.oauth2.enabled.description', 'Allow users to authenticate using OAuth2 providers')} + +
+ setSettings({ ...settings, oauth2: { ...settings.oauth2, enabled: e.target.checked } })} + /> +
+ +
+ setSettings({ ...settings, loginMethod: value || 'all' })} + data={[ + { value: 'all', label: t('admin.settings.security.loginMethod.all', 'All Methods') }, + { value: 'normal', label: t('admin.settings.security.loginMethod.normal', 'Username/Password Only') }, + { value: 'oauth2', label: t('admin.settings.security.loginMethod.oauth2', 'OAuth2 Only') }, + { value: 'saml2', label: t('admin.settings.security.loginMethod.saml2', 'SAML2 Only') }, + ]} + comboboxProps={{ zIndex: 1400 }} + /> +
+ +
+ setSettings({ ...settings, loginAttemptCount: Number(value) })} + min={0} + max={100} + /> +
+ +
+ setSettings({ ...settings, loginResetTimeMinutes: Number(value) })} + min={0} + max={1440} + /> +
+ +
+
+ {t('admin.settings.security.csrfDisabled', 'Disable CSRF Protection')} + + {t('admin.settings.security.csrfDisabled.description', 'Disable Cross-Site Request Forgery protection (not recommended)')} + +
+ setSettings({ ...settings, csrfDisabled: e.target.checked })} + /> +
+
+
+ + {/* Initial Login Credentials */} + + + {t('admin.settings.security.initialLogin', 'Initial Login')} + +
+ setSettings({ ...settings, initialLogin: { ...settings.initialLogin, username: e.target.value } })} + placeholder="admin" + /> +
+ +
+ setSettings({ ...settings, initialLogin: { ...settings.initialLogin, password: e.target.value } })} + placeholder="••••••••" + /> +
+
+
+ + {/* JWT Settings */} + + + {t('admin.settings.security.jwt', 'JWT Configuration')} + +
+
+ {t('admin.settings.security.jwt.persistence', 'Enable Key Persistence')} + + {t('admin.settings.security.jwt.persistence.description', 'Store JWT keys persistently (required for multi-instance deployments)')} + +
+ setSettings({ ...settings, jwt: { ...settings.jwt, persistence: e.target.checked } })} + /> +
+ +
+
+ {t('admin.settings.security.jwt.enableKeyRotation', 'Enable Key Rotation')} + + {t('admin.settings.security.jwt.enableKeyRotation.description', 'Automatically rotate JWT signing keys for improved security')} + +
+ setSettings({ ...settings, jwt: { ...settings.jwt, enableKeyRotation: e.target.checked } })} + /> +
+ +
+
+ {t('admin.settings.security.jwt.enableKeyCleanup', 'Enable Key Cleanup')} + + {t('admin.settings.security.jwt.enableKeyCleanup.description', 'Automatically remove old JWT keys after retention period')} + +
+ setSettings({ ...settings, jwt: { ...settings.jwt, enableKeyCleanup: e.target.checked } })} + /> +
+ +
+ setSettings({ ...settings, jwt: { ...settings.jwt, keyRetentionDays: Number(value) } })} + min={1} + max={365} + /> +
+ +
+
+ {t('admin.settings.security.jwt.secureCookie', 'Secure Cookie')} + + {t('admin.settings.security.jwt.secureCookie.description', 'Require HTTPS for JWT cookies (recommended for production)')} + +
+ setSettings({ ...settings, jwt: { ...settings.jwt, secureCookie: e.target.checked } })} + /> +
+
+
+ + {/* Save Button */} + + + +
+ ); +} diff --git a/frontend/src/components/shared/config/types.ts b/frontend/src/components/shared/config/types.ts index 2ef734422..cd0b59e9a 100644 --- a/frontend/src/components/shared/config/types.ts +++ b/frontend/src/components/shared/config/types.ts @@ -13,7 +13,16 @@ export type NavKey = | 'requests' | 'developer' | 'api-keys' - | 'hotkeys'; + | 'hotkeys' + | 'adminGeneral' + | 'adminSecurity' + | 'adminConnections' + | 'adminPrivacy' + | 'adminAdvanced' + | 'adminMail' + | 'adminLegal' + | 'adminPremium' + | 'adminEndpoints'; // some of these are not used yet, but appear in figma designs \ No newline at end of file