From 1117ce6164df60c240ab6e1aadadd02acfa7bb71 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:02:45 +0000 Subject: [PATCH 01/30] Settings display demo and login fix (#4884) # Description of Changes image image image --- ## 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. --- .dockerignore | 74 ++++++++ .../common/model/ApplicationProperties.java | 9 + .../software/common/util/RequestUriUtils.java | 3 + .../controller/api/misc/ConfigController.java | 11 +- .../src/main/resources/settings.yml.template | 1 + .../api/ProprietaryUIDataController.java | 5 + .../filter/UserAuthenticationFilter.java | 3 +- build.gradle | 2 +- docker/unified/nginx.conf | 3 + frontend/public/branding/old/favicon.ico | Bin 0 -> 15086 bytes frontend/public/branding/old/favicon.png | Bin 0 -> 8860 bytes frontend/public/branding/old/favicon.svg | 1 + .../public/locales/en-GB/translation.json | 13 ++ .../core/components/shared/AppConfigModal.tsx | 11 +- .../components/shared/FirstLoginModal.tsx | 2 +- .../core/components/shared/LandingPage.tsx | 4 +- .../shared/config/LoginRequiredBanner.tsx | 38 ++++ .../shared/config/configNavSections.tsx | 123 +------------ .../config/configSections/ProviderCard.tsx | 10 +- .../tools/FullscreenToolSurface.tsx | 5 +- .../src/core/contexts/AppConfigContext.tsx | 1 + frontend/src/core/hooks/useLoginRequired.ts | 89 ++++++++++ frontend/src/core/hooks/useLogoPath.ts | 31 ++++ frontend/src/core/pages/HomePage.tsx | 5 +- frontend/src/core/services/accountService.ts | 10 ++ .../src/core/services/httpErrorHandler.ts | 27 ++- .../src/proprietary/auth/springAuthClient.ts | 2 +- .../components/shared/LoginRightCarousel.tsx | 6 +- .../shared/config/configNavSections.tsx | 162 ++++++++++++++++-- .../configSections/AdminAdvancedSection.tsx | 94 ++++++++-- .../configSections/AdminAuditSection.tsx | 36 +++- .../AdminConnectionsSection.tsx | 90 ++++++---- .../configSections/AdminDatabaseSection.tsx | 54 ++++-- .../configSections/AdminEndpointsSection.tsx | 34 +++- .../configSections/AdminFeaturesSection.tsx | 47 +++-- .../configSections/AdminGeneralSection.tsx | 124 +++++++++++--- .../configSections/AdminLegalSection.tsx | 24 ++- .../configSections/AdminMailSection.tsx | 0 .../configSections/AdminPremiumSection.tsx | 27 ++- .../configSections/AdminPrivacySection.tsx | 49 ++++-- .../configSections/AdminSecuritySection.tsx | 141 +++++++++------ .../configSections/AdminUsageSection.tsx | 74 +++++++- .../config/configSections/PeopleSection.tsx | 132 +++++++++++--- .../config/configSections/TeamsSection.tsx | 34 ++-- .../audit/AuditChartsSection.tsx | 35 +++- .../configSections/audit/AuditEventsTable.tsx | 61 ++++++- .../audit/AuditExportSection.tsx | 17 +- .../configSections/audit/AuditFiltersForm.tsx | 8 +- .../audit/AuditSystemStatus.tsx | 0 .../usage/UsageAnalyticsChart.tsx | 0 .../usage/UsageAnalyticsTable.tsx | 0 frontend/src/proprietary/routes/Landing.tsx | 12 +- frontend/src/proprietary/routes/Login.tsx | 125 +++++++++----- .../proprietary/routes/authShared/auth.css | 2 +- .../routes/login/LoggedInState.tsx | 10 +- .../proprietary/routes/login/LoginHeader.tsx | 1 - .../proprietary/routes/login/OAuthButtons.tsx | 52 ++++-- .../services/userManagementService.ts | 0 58 files changed, 1475 insertions(+), 459 deletions(-) create mode 100644 .dockerignore create mode 100644 frontend/public/branding/old/favicon.ico create mode 100644 frontend/public/branding/old/favicon.png create mode 100644 frontend/public/branding/old/favicon.svg create mode 100644 frontend/src/core/components/shared/config/LoginRequiredBanner.tsx create mode 100644 frontend/src/core/hooks/useLoginRequired.ts create mode 100644 frontend/src/core/hooks/useLogoPath.ts rename frontend/src/{core => proprietary}/components/shared/config/configSections/AdminAdvancedSection.tsx (91%) rename frontend/src/{core => proprietary}/components/shared/config/configSections/AdminAuditSection.tsx (72%) rename frontend/src/{core => proprietary}/components/shared/config/configSections/AdminConnectionsSection.tsx (85%) rename frontend/src/{core => proprietary}/components/shared/config/configSections/AdminDatabaseSection.tsx (85%) rename frontend/src/{core => proprietary}/components/shared/config/configSections/AdminEndpointsSection.tsx (82%) rename frontend/src/{core => proprietary}/components/shared/config/configSections/AdminFeaturesSection.tsx (83%) rename frontend/src/{core => proprietary}/components/shared/config/configSections/AdminGeneralSection.tsx (81%) rename frontend/src/{core => proprietary}/components/shared/config/configSections/AdminLegalSection.tsx (89%) rename frontend/src/{core => proprietary}/components/shared/config/configSections/AdminMailSection.tsx (100%) rename frontend/src/{core => proprietary}/components/shared/config/configSections/AdminPremiumSection.tsx (84%) rename frontend/src/{core => proprietary}/components/shared/config/configSections/AdminPrivacySection.tsx (78%) rename frontend/src/{core => proprietary}/components/shared/config/configSections/AdminSecuritySection.tsx (85%) rename frontend/src/{core => proprietary}/components/shared/config/configSections/AdminUsageSection.tsx (62%) rename frontend/src/{core => proprietary}/components/shared/config/configSections/audit/AuditChartsSection.tsx (81%) rename frontend/src/{core => proprietary}/components/shared/config/configSections/audit/AuditEventsTable.tsx (80%) rename frontend/src/{core => proprietary}/components/shared/config/configSections/audit/AuditExportSection.tsx (86%) rename frontend/src/{core => proprietary}/components/shared/config/configSections/audit/AuditFiltersForm.tsx (91%) rename frontend/src/{core => proprietary}/components/shared/config/configSections/audit/AuditSystemStatus.tsx (100%) rename frontend/src/{core => proprietary}/components/shared/config/configSections/usage/UsageAnalyticsChart.tsx (100%) rename frontend/src/{core => proprietary}/components/shared/config/configSections/usage/UsageAnalyticsTable.tsx (100%) rename frontend/src/{core => proprietary}/services/userManagementService.ts (100%) diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..54b49fd807 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,74 @@ +# Node modules and build artifacts +node_modules +frontend/node_modules +frontend/dist +frontend/build +frontend/.vite +frontend/.tauri + +# Gradle build artifacts +.gradle +build +bin +target +out + +# Git +.git +.gitignore + +# IDE +.vscode +.idea +*.iml +*.iws +*.ipr + +# Logs +*.log +logs + +# Environment files +.env +.env.* +!.env.example + +# OS files +.DS_Store +Thumbs.db + +# Java compiled files +*.class +*.jar +*.war +*.ear + +# Test reports +test-results +coverage + +# Docker +docker-compose.override.yml +.dockerignore + +# Temporary files +tmp +temp +*.tmp +*.swp +*~ + +# Runtime database and config files (locked by running app) +app/core/configs/** +stirling/** +stirling-pdf-DB*.mv.db +stirling-pdf-DB*.trace.db + +# Documentation +*.md +!README.md +docs + +# CI/CD +.github +.gitlab-ci.yml diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index 4b5a202c06..6a6ee8453b 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -505,10 +505,19 @@ public class ApplicationProperties { public static class Ui { private String appNameNavbar; private List languages; + private String logoStyle = "classic"; // Options: "classic" (default) or "modern" public String getAppNameNavbar() { return appNameNavbar != null && !appNameNavbar.trim().isEmpty() ? appNameNavbar : null; } + + public String getLogoStyle() { + // Validate and return either "modern" or "classic" + if ("modern".equalsIgnoreCase(logoStyle)) { + return "modern"; + } + return "classic"; // default + } } @Data diff --git a/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java b/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java index 23d369bf37..a0d7f36109 100644 --- a/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java @@ -75,6 +75,9 @@ public class RequestUriUtils { || trimmedUri.startsWith("/api/v1/auth/login") || trimmedUri.startsWith("/api/v1/auth/refresh") || trimmedUri.startsWith("/api/v1/auth/logout") + || trimmedUri.startsWith( + "/api/v1/proprietary/ui-data/login") // Login page config (SSO providers + + // enableLogin) || trimmedUri.startsWith("/v1/api-docs") || trimmedUri.startsWith("/api/v1/invite/validate") || trimmedUri.startsWith("/api/v1/invite/accept") diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java index 02b5233b8b..ffbec5a7d8 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java @@ -10,6 +10,8 @@ import org.springframework.web.bind.annotation.RequestParam; import io.swagger.v3.oas.annotations.Hidden; +import lombok.extern.slf4j.Slf4j; + import stirling.software.SPDF.config.EndpointConfiguration; import stirling.software.SPDF.config.InitialSetup; import stirling.software.common.annotations.api.ConfigApi; @@ -20,6 +22,7 @@ import stirling.software.common.service.UserServiceInterface; @ConfigApi @Hidden +@Slf4j public class ConfigController { private final ApplicationProperties applicationProperties; @@ -59,9 +62,15 @@ public class ConfigController { // Extract values from ApplicationProperties configData.put("appNameNavbar", applicationProperties.getUi().getAppNameNavbar()); configData.put("languages", applicationProperties.getUi().getLanguages()); + configData.put("logoStyle", applicationProperties.getUi().getLogoStyle()); // Security settings - configData.put("enableLogin", applicationProperties.getSecurity().getEnableLogin()); + // enableLogin requires both the config flag AND proprietary features to be loaded + // If userService is null, proprietary module isn't loaded + // (DISABLE_ADDITIONAL_FEATURES=true or DOCKER_ENABLE_SECURITY=false) + boolean enableLogin = + applicationProperties.getSecurity().getEnableLogin() && userService != null; + configData.put("enableLogin", enableLogin); // Mail settings - check both SMTP enabled AND invites enabled boolean smtpEnabled = applicationProperties.getMail().isEnabled(); diff --git a/app/core/src/main/resources/settings.yml.template b/app/core/src/main/resources/settings.yml.template index 5f8ca51be0..139260872c 100644 --- a/app/core/src/main/resources/settings.yml.template +++ b/app/core/src/main/resources/settings.yml.template @@ -176,6 +176,7 @@ system: ui: appNameNavbar: '' # name displayed on the navigation bar + logoStyle: classic # Options: 'classic' (default - classic S icon) or 'modern' (minimalist logo) languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled. endpoints: diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/ProprietaryUIDataController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/ProprietaryUIDataController.java index ed9106f2f1..a5e0b8a0f4 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/ProprietaryUIDataController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/ProprietaryUIDataController.java @@ -116,6 +116,10 @@ public class ProprietaryUIDataController { LoginData data = new LoginData(); Map providerList = new HashMap<>(); Security securityProps = applicationProperties.getSecurity(); + + // Add enableLogin flag so frontend doesn't need to call /app-config + data.setEnableLogin(securityProps.getEnableLogin()); + OAUTH2 oauth = securityProps.getOauth2(); if (oauth != null && oauth.getEnabled()) { @@ -448,6 +452,7 @@ public class ProprietaryUIDataController { @Data public static class LoginData { + private Boolean enableLogin; private Map providerList; private String loginMethod; private boolean altLogin; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/filter/UserAuthenticationFilter.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/filter/UserAuthenticationFilter.java index cdf7bdd0d5..9d1dbc96fd 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/filter/UserAuthenticationFilter.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/filter/UserAuthenticationFilter.java @@ -223,7 +223,8 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { || trimmedUri.startsWith("/saml2") || trimmedUri.startsWith("/api/v1/auth/login") || trimmedUri.startsWith("/api/v1/auth/refresh") - || trimmedUri.startsWith("/api/v1/auth/logout"); + || trimmedUri.startsWith("/api/v1/auth/logout") + || trimmedUri.startsWith("/api/v1/proprietary/ui-data/login"); } private enum UserLoginType { diff --git a/build.gradle b/build.gradle index 9acb8c9f00..2451aa45c7 100644 --- a/build.gradle +++ b/build.gradle @@ -57,7 +57,7 @@ repositories { allprojects { group = 'stirling.software' - version = '1.4.0' + version = '2.0.0' configurations.configureEach { exclude group: 'commons-logging', module: 'commons-logging' diff --git a/docker/unified/nginx.conf b/docker/unified/nginx.conf index 77ee17f89c..1e47b8619a 100644 --- a/docker/unified/nginx.conf +++ b/docker/unified/nginx.conf @@ -1,3 +1,6 @@ +# Run nginx as non-root user +pid /tmp/nginx.pid; + events { worker_connections 1024; } diff --git a/frontend/public/branding/old/favicon.ico b/frontend/public/branding/old/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8ad57cac700ed029414ee1060d8bfe038bd3f460 GIT binary patch literal 15086 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 literal 0 HcmV?d00001 diff --git a/frontend/public/branding/old/favicon.png b/frontend/public/branding/old/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..5edc6eae26832dfa8d113dc54db899515d7c1fc2 GIT binary patch literal 8860 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 literal 0 HcmV?d00001 diff --git a/frontend/public/branding/old/favicon.svg b/frontend/public/branding/old/favicon.svg new file mode 100644 index 0000000000..0fef4393aa --- /dev/null +++ b/frontend/public/branding/old/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 9cfe5380bc..8c93539c6d 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -3177,6 +3177,7 @@ "rememberme": "Remember me", "invalid": "Invalid username or password.", "locked": "Your account has been locked.", + "sessionExpired": "Your session has expired. Please sign in again.", "signinTitle": "Please sign in", "ssoSignIn": "Login via Single Sign-on", "oAuth2AutoCreateDisabled": "OAUTH2 Auto-Create User Disabled", @@ -3681,6 +3682,12 @@ "saveSuccess": "Settings saved successfully", "save": "Save Changes", "restartRequired": "Restart Required", + "loginRequired": "Login mode must be enabled to modify admin settings", + "loginDisabled": { + "title": "Login Mode Required", + "message": "Login mode must be enabled to modify admin settings. Please set SECURITY_ENABLELOGIN=true in your environment or security.enableLogin: true in settings.yml, then restart the server.", + "readOnly": "The settings below show example values for reference. Enable login mode to view and edit actual configuration." + }, "restart": { "title": "Restart Required", "message": "Settings have been saved successfully. A server restart is required for the changes to take effect.", @@ -3751,6 +3758,12 @@ "description": "Default producer for PDF metadata" } }, + "logoStyle": { + "label": "Logo Style", + "description": "Choose between the modern minimalist logo or the classic S icon", + "classic": "Classic", + "modern": "Modern" + }, "customPaths": { "label": "Custom Paths", "description": "Configure custom file system paths for pipeline processing and external tools", diff --git a/frontend/src/core/components/shared/AppConfigModal.tsx b/frontend/src/core/components/shared/AppConfigModal.tsx index f9937f775f..e7ddf0f0e0 100644 --- a/frontend/src/core/components/shared/AppConfigModal.tsx +++ b/frontend/src/core/components/shared/AppConfigModal.tsx @@ -143,16 +143,15 @@ const AppConfigModal: React.FC = ({ opened, onClose }) => {
{ - if (!isDisabled) { - setActive(item.key); - navigate(`/settings/${item.key}`); - } + // Allow navigation even when disabled - the content inside will be disabled + setActive(item.key); + navigate(`/settings/${item.key}`); }} className={`modal-nav-item ${isMobile ? 'mobile' : ''}`} style={{ background: isActive ? colors.navItemActiveBg : 'transparent', - opacity: isDisabled ? 0.5 : 1, - cursor: isDisabled ? 'not-allowed' : 'pointer', + opacity: isDisabled ? 0.6 : 1, + cursor: 'pointer', }} data-tour={`admin-${item.key}-nav`} > diff --git a/frontend/src/core/components/shared/FirstLoginModal.tsx b/frontend/src/core/components/shared/FirstLoginModal.tsx index fa3e42bae6..7cd034edd7 100644 --- a/frontend/src/core/components/shared/FirstLoginModal.tsx +++ b/frontend/src/core/components/shared/FirstLoginModal.tsx @@ -52,7 +52,7 @@ export default function FirstLoginModal({ opened, onPasswordChanged, username }: setLoading(true); setError(''); - await accountService.changePassword(currentPassword, newPassword); + await accountService.changePasswordOnLogin(currentPassword, newPassword); alert({ alertType: 'success', diff --git a/frontend/src/core/components/shared/LandingPage.tsx b/frontend/src/core/components/shared/LandingPage.tsx index f25aaedfcb..9ee11cbd26 100644 --- a/frontend/src/core/components/shared/LandingPage.tsx +++ b/frontend/src/core/components/shared/LandingPage.tsx @@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next'; import { useFileHandler } from '@app/hooks/useFileHandler'; import { useFilesModalContext } from '@app/contexts/FilesModalContext'; import { BASE_PATH } from '@app/constants/app'; +import { useLogoPath } from '@app/hooks/useLogoPath'; const LandingPage = () => { const { addFiles } = useFileHandler(); @@ -14,6 +15,7 @@ const LandingPage = () => { const { t } = useTranslation(); const { openFilesModal } = useFilesModalContext(); const [isUploadHover, setIsUploadHover] = React.useState(false); + const logoPath = useLogoPath(); const handleFileDrop = async (files: File[]) => { await addFiles(files); @@ -72,7 +74,7 @@ const LandingPage = () => { }} > Stirling PDF Logo} + title={t('admin.settings.loginDisabled.title', 'Login Mode Required')} + color="blue" + variant="light" + styles={{ + root: { + borderLeft: '4px solid var(--mantine-color-blue-6)' + } + }} + > + + {t('admin.settings.loginDisabled.message', 'Login mode must be enabled to modify admin settings. Please set SECURITY_ENABLELOGIN=true in your environment or security.enableLogin: true in settings.yml, then restart the server.')} + + + {t('admin.settings.loginDisabled.readOnly', 'The settings below show example values for reference. Enable login mode to view and edit actual configuration.')} + + + ); +} diff --git a/frontend/src/core/components/shared/config/configNavSections.tsx b/frontend/src/core/components/shared/config/configNavSections.tsx index a1f66480b8..36dd4dfcd2 100644 --- a/frontend/src/core/components/shared/config/configNavSections.tsx +++ b/frontend/src/core/components/shared/config/configNavSections.tsx @@ -2,18 +2,6 @@ import React from 'react'; import { NavKey } from '@app/components/shared/config/types'; import HotkeysSection from '@app/components/shared/config/configSections/HotkeysSection'; import GeneralSection from '@app/components/shared/config/configSections/GeneralSection'; -import AdminGeneralSection from '@app/components/shared/config/configSections/AdminGeneralSection'; -import AdminSecuritySection from '@app/components/shared/config/configSections/AdminSecuritySection'; -import AdminConnectionsSection from '@app/components/shared/config/configSections/AdminConnectionsSection'; -import AdminPrivacySection from '@app/components/shared/config/configSections/AdminPrivacySection'; -import AdminDatabaseSection from '@app/components/shared/config/configSections/AdminDatabaseSection'; -import AdminAdvancedSection from '@app/components/shared/config/configSections/AdminAdvancedSection'; -import AdminLegalSection from '@app/components/shared/config/configSections/AdminLegalSection'; -import AdminPremiumSection from '@app/components/shared/config/configSections/AdminPremiumSection'; -import AdminFeaturesSection from '@app/components/shared/config/configSections/AdminFeaturesSection'; -import AdminEndpointsSection from '@app/components/shared/config/configSections/AdminEndpointsSection'; -import AdminAuditSection from '@app/components/shared/config/configSections/AdminAuditSection'; -import AdminUsageSection from '@app/components/shared/config/configSections/AdminUsageSection'; export interface ConfigNavItem { key: NavKey; @@ -40,8 +28,8 @@ export interface ConfigColors { } export const createConfigNavSections = ( - isAdmin: boolean = false, - runningEE: boolean = false, + _isAdmin: boolean = false, + _runningEE: boolean = false, _loginEnabled: boolean = false ): ConfigNavSection[] => { const sections: ConfigNavSection[] = [ @@ -64,112 +52,5 @@ export const createConfigNavSections = ( }, ]; - // Add Admin sections if user is admin - if (isAdmin) { - // Configuration - sections.push({ - title: 'Configuration', - items: [ - { - key: 'adminGeneral', - label: 'System Settings', - icon: 'settings-rounded', - component: - }, - { - key: 'adminFeatures', - label: 'Features', - icon: 'extension-rounded', - component: - }, - { - key: 'adminEndpoints', - label: 'Endpoints', - icon: 'api-rounded', - component: - }, - { - key: 'adminDatabase', - label: 'Database', - icon: 'storage-rounded', - component: - }, - { - key: 'adminAdvanced', - label: 'Advanced', - icon: 'tune-rounded', - component: - }, - ], - }); - - // Security & Authentication - sections.push({ - title: 'Security & Authentication', - items: [ - { - key: 'adminSecurity', - label: 'Security', - icon: 'shield-rounded', - component: - }, - { - key: 'adminConnections', - label: 'Connections', - icon: 'link-rounded', - component: - }, - ], - }); - - // Licensing & Analytics - sections.push({ - title: 'Licensing & Analytics', - items: [ - { - key: 'adminPremium', - label: 'Premium', - icon: 'star-rounded', - component: - }, - { - key: 'adminAudit', - label: 'Audit', - icon: 'fact-check-rounded', - component: , - disabled: !runningEE, - disabledTooltip: 'Requires Enterprise license' - }, - { - key: 'adminUsage', - label: 'Usage Analytics', - icon: 'analytics-rounded', - component: , - disabled: !runningEE, - disabledTooltip: 'Requires Enterprise license' - }, - ], - }); - - // Policies & Privacy - sections.push({ - title: 'Policies & Privacy', - items: [ - { - key: 'adminLegal', - label: 'Legal', - icon: 'gavel-rounded', - component: - }, - { - key: 'adminPrivacy', - label: 'Privacy', - icon: 'visibility-rounded', - component: - }, - ], - }); - } - return sections; }; diff --git a/frontend/src/core/components/shared/config/configSections/ProviderCard.tsx b/frontend/src/core/components/shared/config/configSections/ProviderCard.tsx index 6209fb4b6d..3d07338ac6 100644 --- a/frontend/src/core/components/shared/config/configSections/ProviderCard.tsx +++ b/frontend/src/core/components/shared/config/configSections/ProviderCard.tsx @@ -10,6 +10,7 @@ interface ProviderCardProps { settings?: Record; onSave?: (settings: Record) => void; onDisconnect?: () => void; + disabled?: boolean; } export default function ProviderCard({ @@ -18,6 +19,7 @@ export default function ProviderCard({ settings = {}, onSave, onDisconnect, + disabled = false, }: ProviderCardProps) { const { t } = useTranslation(); const [expanded, setExpanded] = useState(false); @@ -39,6 +41,7 @@ export default function ProviderCard({ }; const handleFieldChange = (key: string, value: any) => { + if (disabled) return; // Block changes when disabled setLocalSettings((prev) => ({ ...prev, [key]: value })); }; @@ -63,6 +66,7 @@ export default function ProviderCard({ handleFieldChange(field.key, e.target.checked)} + disabled={disabled} />
); @@ -76,6 +80,7 @@ export default function ProviderCard({ placeholder={field.placeholder} value={value} onChange={(e) => handleFieldChange(field.key, e.target.value)} + disabled={disabled} /> ); @@ -88,6 +93,7 @@ export default function ProviderCard({ placeholder={field.placeholder} value={value} onChange={(e) => handleFieldChange(field.key, e.target.value)} + disabled={disabled} /> ); @@ -100,6 +106,7 @@ export default function ProviderCard({ placeholder={field.placeholder} value={value} onChange={(e) => handleFieldChange(field.key, e.target.value)} + disabled={disabled} /> ); } @@ -174,11 +181,12 @@ export default function ProviderCard({ color="red" size="sm" onClick={onDisconnect} + disabled={disabled} > {t('admin.settings.connections.disconnect', 'Disconnect')} )} - diff --git a/frontend/src/core/components/tools/FullscreenToolSurface.tsx b/frontend/src/core/components/tools/FullscreenToolSurface.tsx index 169aab44c5..7ac5d82c97 100644 --- a/frontend/src/core/components/tools/FullscreenToolSurface.tsx +++ b/frontend/src/core/components/tools/FullscreenToolSurface.tsx @@ -8,6 +8,7 @@ import { ToolRegistryEntry } from '@app/data/toolsTaxonomy'; import { ToolId } from '@app/types/toolId'; import { useFocusTrap } from '@app/hooks/useFocusTrap'; import { BASE_PATH } from '@app/constants/app'; +import { useLogoPath } from '@app/hooks/useLogoPath'; import { Tooltip } from '@app/components/shared/Tooltip'; import '@app/components/tools/ToolPanel.css'; import { ToolPanelGeometry } from '@app/hooks/tools/useToolPanelGeometry'; @@ -51,9 +52,7 @@ const FullscreenToolSurface = ({ useFocusTrap(surfaceRef, !isExiting); const brandAltText = t("home.mobile.brandAlt", "Stirling PDF logo"); - const brandIconSrc = `${BASE_PATH}/branding/StirlingPDFLogoNoText${ - colorScheme === "dark" ? "Dark" : "Light" - }.svg`; + const brandIconSrc = useLogoPath(); const brandTextSrc = `${BASE_PATH}/branding/StirlingPDFLogo${ colorScheme === "dark" ? "White" : "Black" }Text.svg`; diff --git a/frontend/src/core/contexts/AppConfigContext.tsx b/frontend/src/core/contexts/AppConfigContext.tsx index 97cae06712..ad47b415b8 100644 --- a/frontend/src/core/contexts/AppConfigContext.tsx +++ b/frontend/src/core/contexts/AppConfigContext.tsx @@ -19,6 +19,7 @@ export interface AppConfig { serverPort?: number; appNameNavbar?: string; languages?: string[]; + logoStyle?: 'modern' | 'classic'; enableLogin?: boolean; enableEmailInvites?: boolean; isAdmin?: boolean; diff --git a/frontend/src/core/hooks/useLoginRequired.ts b/frontend/src/core/hooks/useLoginRequired.ts new file mode 100644 index 0000000000..ea9af38d90 --- /dev/null +++ b/frontend/src/core/hooks/useLoginRequired.ts @@ -0,0 +1,89 @@ +import { useAppConfig } from '@app/contexts/AppConfigContext'; +import { alert } from '@app/components/toast'; +import { useTranslation } from 'react-i18next'; + +/** + * Hook to manage login-required functionality in admin sections + * Provides login state, validation, and alert functionality + */ +export function useLoginRequired() { + const { config } = useAppConfig(); + const { t } = useTranslation(); + const loginEnabled = config?.enableLogin ?? true; + + /** + * Show alert when user tries to modify settings with login disabled + */ + const showLoginRequiredAlert = () => { + alert({ + alertType: 'warning', + title: t('admin.error', 'Error'), + body: t('admin.settings.loginRequired', 'Login mode must be enabled to modify admin settings'), + }); + }; + + /** + * Validate that login is enabled before allowing action + * Returns true if login is enabled, false otherwise (and shows alert) + */ + const validateLoginEnabled = (): boolean => { + if (!loginEnabled) { + showLoginRequiredAlert(); + return false; + } + return true; + }; + + /** + * Wrap an async handler to check login state before executing + */ + const withLoginCheck = Promise>( + handler: T + ): T => { + return (async (...args: any[]) => { + if (!validateLoginEnabled()) { + return; + } + return handler(...args); + }) as T; + }; + + /** + * Get styles for disabled inputs (cursor not-allowed) + */ + const getDisabledStyles = () => { + if (!loginEnabled) { + return { + input: { cursor: 'not-allowed' }, + track: { cursor: 'not-allowed' }, + thumb: { cursor: 'not-allowed' } + }; + } + return undefined; + }; + + /** + * Wrap fetch function to skip API call when login disabled + */ + const withLoginCheckForFetch = Promise>( + fetchHandler: T, + skipWhenDisabled: boolean = true + ): T => { + return (async (...args: any[]) => { + if (!loginEnabled && skipWhenDisabled) { + // Skip fetch when login disabled - component will use default/empty values + return; + } + return fetchHandler(...args); + }) as T; + }; + + return { + loginEnabled, + showLoginRequiredAlert, + validateLoginEnabled, + withLoginCheck, + withLoginCheckForFetch, + getDisabledStyles, + }; +} diff --git a/frontend/src/core/hooks/useLogoPath.ts b/frontend/src/core/hooks/useLogoPath.ts new file mode 100644 index 0000000000..db97f9c08e --- /dev/null +++ b/frontend/src/core/hooks/useLogoPath.ts @@ -0,0 +1,31 @@ +import { useMemo } from 'react'; +import { useAppConfig } from '@app/contexts/AppConfigContext'; +import { useMantineColorScheme } from '@mantine/core'; +import { BASE_PATH } from '@app/constants/app'; + +/** + * Hook to get the correct logo path based on app config (logo style) and theme (light/dark) + * + * Logo styles: + * - classic: branding/old/favicon.svg (classic S logo - default) + * - modern: StirlingPDFLogoNoText{Light|Dark}.svg (minimalist modern design) + * + * @returns The path to the appropriate logo SVG file + */ +export function useLogoPath(): string { + const { config } = useAppConfig(); + const { colorScheme } = useMantineColorScheme(); + + return useMemo(() => { + const logoStyle = config?.logoStyle || 'classic'; + + if (logoStyle === 'classic') { + // Classic logo (old favicon) - same for both light and dark modes + return `${BASE_PATH}/branding/old/favicon.svg`; + } + + // Modern logo - different for light and dark modes + const themeSuffix = colorScheme === 'dark' ? 'Dark' : 'Light'; + return `${BASE_PATH}/branding/StirlingPDFLogoNoText${themeSuffix}.svg`; + }, [config?.logoStyle, colorScheme]); +} diff --git a/frontend/src/core/pages/HomePage.tsx b/frontend/src/core/pages/HomePage.tsx index 6df688e55d..2731ad1004 100644 --- a/frontend/src/core/pages/HomePage.tsx +++ b/frontend/src/core/pages/HomePage.tsx @@ -8,6 +8,7 @@ import { BASE_PATH } from "@app/constants/app"; import { useBaseUrl } from "@app/hooks/useBaseUrl"; import { useIsMobile } from "@app/hooks/useIsMobile"; import { useAppConfig } from "@app/contexts/AppConfigContext"; +import { useLogoPath } from "@app/hooks/useLogoPath"; import AppsIcon from '@mui/icons-material/AppsRounded'; import ToolPanel from "@app/components/tools/ToolPanel"; @@ -60,9 +61,7 @@ export default function HomePage() { }, [config]); const brandAltText = t("home.mobile.brandAlt", "Stirling PDF logo"); - const brandIconSrc = `${BASE_PATH}/branding/StirlingPDFLogoNoText${ - colorScheme === "dark" ? "Dark" : "Light" - }.svg`; + const brandIconSrc = useLogoPath(); const brandTextSrc = `${BASE_PATH}/branding/StirlingPDFLogo${ colorScheme === "dark" ? "White" : "Black" }Text.svg`; diff --git a/frontend/src/core/services/accountService.ts b/frontend/src/core/services/accountService.ts index f546ed8bf5..0b2aa52e07 100644 --- a/frontend/src/core/services/accountService.ts +++ b/frontend/src/core/services/accountService.ts @@ -31,4 +31,14 @@ export const accountService = { formData.append('newPassword', newPassword); await apiClient.post('/api/v1/user/change-password', formData); }, + + /** + * Change user password on first login (resets firstLogin flag) + */ + async changePasswordOnLogin(currentPassword: string, newPassword: string): Promise { + const formData = new FormData(); + formData.append('currentPassword', currentPassword); + formData.append('newPassword', newPassword); + await apiClient.post('/api/v1/user/change-password-on-login', formData); + }, }; diff --git a/frontend/src/core/services/httpErrorHandler.ts b/frontend/src/core/services/httpErrorHandler.ts index fa5160adf2..53e04516a4 100644 --- a/frontend/src/core/services/httpErrorHandler.ts +++ b/frontend/src/core/services/httpErrorHandler.ts @@ -92,6 +92,32 @@ export async function handleHttpError(error: any): Promise { if (error?.config?.suppressErrorToast === true) { return false; // Don't show global toast, but continue rejection } + + // Handle 401 authentication errors + const status: number | undefined = error?.response?.status; + if (status === 401) { + const pathname = window.location.pathname; + + // Check if we're already on an auth page + const isAuthPage = pathname.includes('/login') || + pathname.includes('/signup') || + pathname.includes('/auth/') || + pathname.includes('/invite/'); + + // If not on auth page, redirect to login with expired session message + if (!isAuthPage) { + console.debug('[httpErrorHandler] 401 detected, redirecting to login'); + // Store the current location so we can redirect back after login + const currentLocation = window.location.pathname + window.location.search; + // Redirect to login with state + window.location.href = `/login?expired=true&from=${encodeURIComponent(currentLocation)}`; + return true; // Suppress toast since we're redirecting + } + + // On auth pages, suppress the toast (user is already trying to authenticate) + console.debug('[httpErrorHandler] Suppressing 401 on auth page:', pathname); + return true; + } // Compute title/body (friendly) from the error object const { title, body } = extractAxiosErrorMessage(error); @@ -112,7 +138,6 @@ export async function handleHttpError(error: any): Promise { // 2) Generic-vs-special dedupe by endpoint const url: string | undefined = error?.config?.url; - const status: number | undefined = error?.response?.status; const now = Date.now(); const isSpecial = status === 422 || diff --git a/frontend/src/proprietary/auth/springAuthClient.ts b/frontend/src/proprietary/auth/springAuthClient.ts index 5d3e2d4dcb..04fb559576 100644 --- a/frontend/src/proprietary/auth/springAuthClient.ts +++ b/frontend/src/proprietary/auth/springAuthClient.ts @@ -251,7 +251,7 @@ class SpringAuthClient { * This redirects to the Spring OAuth2 authorization endpoint */ async signInWithOAuth(params: { - provider: 'github' | 'google' | 'apple' | 'azure'; + provider: 'github' | 'google' | 'apple' | 'azure' | 'keycloak' | 'oidc'; options?: { redirectTo?: string; queryParams?: Record }; }): Promise<{ error: AuthError | null }> { try { diff --git a/frontend/src/proprietary/components/shared/LoginRightCarousel.tsx b/frontend/src/proprietary/components/shared/LoginRightCarousel.tsx index 00b5dac984..f7157a43e5 100644 --- a/frontend/src/proprietary/components/shared/LoginRightCarousel.tsx +++ b/frontend/src/proprietary/components/shared/LoginRightCarousel.tsx @@ -1,9 +1,9 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { memo, useEffect, useMemo, useRef, useState } from 'react'; import { BASE_PATH } from '@app/constants/app'; type ImageSlide = { src: string; alt?: string; cornerModelUrl?: string; title?: string; subtitle?: string; followMouseTilt?: boolean; tiltMaxDeg?: number } -export default function LoginRightCarousel({ +function LoginRightCarousel({ imageSlides = [], showBackground = true, initialSeconds = 5, @@ -157,3 +157,5 @@ export default function LoginRightCarousel({ ); } + +export default memo(LoginRightCarousel); diff --git a/frontend/src/proprietary/components/shared/config/configNavSections.tsx b/frontend/src/proprietary/components/shared/config/configNavSections.tsx index 9be7640cc5..bc63e979ad 100644 --- a/frontend/src/proprietary/components/shared/config/configNavSections.tsx +++ b/frontend/src/proprietary/components/shared/config/configNavSections.tsx @@ -2,41 +2,181 @@ import React from 'react'; import { createConfigNavSections as createCoreConfigNavSections, ConfigNavSection } from '@core/components/shared/config/configNavSections'; import PeopleSection from '@app/components/shared/config/configSections/PeopleSection'; import TeamsSection from '@app/components/shared/config/configSections/TeamsSection'; +import AdminGeneralSection from '@app/components/shared/config/configSections/AdminGeneralSection'; +import AdminSecuritySection from '@app/components/shared/config/configSections/AdminSecuritySection'; +import AdminConnectionsSection from '@app/components/shared/config/configSections/AdminConnectionsSection'; +import AdminPrivacySection from '@app/components/shared/config/configSections/AdminPrivacySection'; +import AdminDatabaseSection from '@app/components/shared/config/configSections/AdminDatabaseSection'; +import AdminAdvancedSection from '@app/components/shared/config/configSections/AdminAdvancedSection'; +import AdminLegalSection from '@app/components/shared/config/configSections/AdminLegalSection'; +import AdminPremiumSection from '@app/components/shared/config/configSections/AdminPremiumSection'; +import AdminFeaturesSection from '@app/components/shared/config/configSections/AdminFeaturesSection'; +import AdminEndpointsSection from '@app/components/shared/config/configSections/AdminEndpointsSection'; +import AdminAuditSection from '@app/components/shared/config/configSections/AdminAuditSection'; +import AdminUsageSection from '@app/components/shared/config/configSections/AdminUsageSection'; import ApiKeys from '@app/components/shared/config/configSections/ApiKeys'; /** - * Proprietary extension of createConfigNavSections that adds workspace sections + * Proprietary extension of createConfigNavSections that adds all admin and workspace sections */ export const createConfigNavSections = ( isAdmin: boolean = false, runningEE: boolean = false, loginEnabled: boolean = false ): ConfigNavSection[] => { - // Get the core sections - const sections = createCoreConfigNavSections(isAdmin, runningEE); + // Get the core sections (just Preferences) + const sections = createCoreConfigNavSections(isAdmin, runningEE, loginEnabled); - // Add Workspace section if user is admin - if (isAdmin) { - const workspaceSection: ConfigNavSection = { + // Add Admin sections if user is admin OR if login is disabled (but mark as disabled) + if (isAdmin || !loginEnabled) { + const requiresLogin = !loginEnabled; + + // Workspace + sections.push({ title: 'Workspace', items: [ { key: 'people', label: 'People', icon: 'group-rounded', - component: + component: , + disabled: requiresLogin, + disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined }, { key: 'teams', label: 'Teams', icon: 'groups-rounded', - component: + component: , + disabled: requiresLogin, + disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined }, ], - }; + }); - // Insert workspace section after Preferences (at index 1) - sections.splice(1, 0, workspaceSection); + // Configuration + sections.push({ + title: 'Configuration', + items: [ + { + key: 'adminGeneral', + label: 'System Settings', + icon: 'settings-rounded', + component: , + disabled: requiresLogin, + disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined + }, + { + key: 'adminFeatures', + label: 'Features', + icon: 'extension-rounded', + component: , + disabled: requiresLogin, + disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined + }, + { + key: 'adminEndpoints', + label: 'Endpoints', + icon: 'api-rounded', + component: , + disabled: requiresLogin, + disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined + }, + { + key: 'adminDatabase', + label: 'Database', + icon: 'storage-rounded', + component: , + disabled: requiresLogin, + disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined + }, + { + key: 'adminAdvanced', + label: 'Advanced', + icon: 'tune-rounded', + component: , + disabled: requiresLogin, + disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined + }, + ], + }); + + // Security & Authentication + sections.push({ + title: 'Security & Authentication', + items: [ + { + key: 'adminSecurity', + label: 'Security', + icon: 'shield-rounded', + component: , + disabled: requiresLogin, + disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined + }, + { + key: 'adminConnections', + label: 'Connections', + icon: 'link-rounded', + component: , + disabled: requiresLogin, + disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined + }, + ], + }); + + // Licensing & Analytics + sections.push({ + title: 'Licensing & Analytics', + items: [ + { + key: 'adminPremium', + label: 'Premium', + icon: 'star-rounded', + component: , + disabled: requiresLogin, + disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined + }, + { + key: 'adminAudit', + label: 'Audit', + icon: 'fact-check-rounded', + component: , + disabled: !runningEE || requiresLogin, + disabledTooltip: requiresLogin ? 'Enable login mode first' : 'Requires Enterprise license' + }, + { + key: 'adminUsage', + label: 'Usage Analytics', + icon: 'analytics-rounded', + component: , + disabled: !runningEE || requiresLogin, + disabledTooltip: requiresLogin ? 'Enable login mode first' : 'Requires Enterprise license' + }, + ], + }); + + // Policies & Privacy + sections.push({ + title: 'Policies & Privacy', + items: [ + { + key: 'adminLegal', + label: 'Legal', + icon: 'gavel-rounded', + component: , + disabled: requiresLogin, + disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined + }, + { + key: 'adminPrivacy', + label: 'Privacy', + icon: 'visibility-rounded', + component: , + disabled: requiresLogin, + disabledTooltip: requiresLogin ? 'Enable login mode first' : undefined + }, + ], + }); } // Add Developer section if login is enabled diff --git a/frontend/src/core/components/shared/config/configSections/AdminAdvancedSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminAdvancedSection.tsx similarity index 91% rename from frontend/src/core/components/shared/config/configSections/AdminAdvancedSection.tsx rename to frontend/src/proprietary/components/shared/config/configSections/AdminAdvancedSection.tsx index ffb9c8d0cd..15ee65b3dd 100644 --- a/frontend/src/core/components/shared/config/configSections/AdminAdvancedSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminAdvancedSection.tsx @@ -7,6 +7,8 @@ import { useRestartServer } from '@app/components/shared/config/useRestartServer import { useAdminSettings } from '@app/hooks/useAdminSettings'; import PendingBadge from '@app/components/shared/config/PendingBadge'; import apiClient from '@app/services/apiClient'; +import { useLoginRequired } from '@app/hooks/useLoginRequired'; +import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner'; interface AdvancedSettingsData { enableAlphaFunctionality?: boolean; @@ -55,6 +57,7 @@ interface AdvancedSettingsData { export default function AdminAdvancedSection() { const { t } = useTranslation(); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); + const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired(); const { settings, @@ -165,10 +168,15 @@ export default function AdminAdvancedSection() { }); useEffect(() => { - fetchSettings(); - }, []); + if (loginEnabled) { + fetchSettings(); + } + }, [loginEnabled]); const handleSave = async () => { + if (!validateLoginEnabled()) { + return; + } try { await saveSettings(); showRestartModal(); @@ -181,7 +189,9 @@ export default function AdminAdvancedSection() { } }; - if (loading) { + const actualLoading = loginEnabled ? loading : false; + + if (actualLoading) { return ( @@ -191,6 +201,7 @@ export default function AdminAdvancedSection() { return ( +
{t('admin.settings.advanced.title', 'Advanced')} @@ -213,7 +224,12 @@ export default function AdminAdvancedSection() { setSettings({ ...settings, enableAlphaFunctionality: e.target.checked })} + onChange={(e) => { + if (!loginEnabled) return; + setSettings({ ...settings, enableAlphaFunctionality: e.target.checked }); + }} + disabled={!loginEnabled} + styles={getDisabledStyles()} /> @@ -229,7 +245,12 @@ export default function AdminAdvancedSection() { setSettings({ ...settings, enableUrlToPDF: e.target.checked })} + onChange={(e) => { + if (!loginEnabled) return; + setSettings({ ...settings, enableUrlToPDF: e.target.checked }); + }} + disabled={!loginEnabled} + styles={getDisabledStyles()} /> @@ -245,7 +266,12 @@ export default function AdminAdvancedSection() { setSettings({ ...settings, disableSanitize: e.target.checked })} + onChange={(e) => { + if (!loginEnabled) return; + setSettings({ ...settings, disableSanitize: e.target.checked }); + }} + disabled={!loginEnabled} + styles={getDisabledStyles()} /> @@ -271,6 +297,7 @@ export default function AdminAdvancedSection() { onChange={(value) => setSettings({ ...settings, maxDPI: Number(value) })} min={0} max={3000} + disabled={!loginEnabled} />
@@ -286,6 +313,7 @@ export default function AdminAdvancedSection() { value={settings.tessdataDir || ''} onChange={(e) => setSettings({ ...settings, tessdataDir: e.target.value })} placeholder="/usr/share/tessdata" + disabled={!loginEnabled} />
@@ -311,6 +339,7 @@ export default function AdminAdvancedSection() { tempFileManagement: { ...settings.tempFileManagement, baseTmpDir: e.target.value } })} placeholder="Default: java.io.tmpdir/stirling-pdf" + disabled={!loginEnabled} /> @@ -324,6 +353,7 @@ export default function AdminAdvancedSection() { tempFileManagement: { ...settings.tempFileManagement, libreofficeDir: e.target.value } })} placeholder="Default: baseTmpDir/libreoffice" + disabled={!loginEnabled} /> @@ -337,6 +367,7 @@ export default function AdminAdvancedSection() { tempFileManagement: { ...settings.tempFileManagement, systemTempDir: e.target.value } })} placeholder="System temp directory path" + disabled={!loginEnabled} /> @@ -350,6 +381,7 @@ export default function AdminAdvancedSection() { tempFileManagement: { ...settings.tempFileManagement, prefix: e.target.value } })} placeholder="stirling-pdf-" + disabled={!loginEnabled} /> @@ -364,6 +396,7 @@ export default function AdminAdvancedSection() { })} min={1} max={720} + disabled={!loginEnabled} /> @@ -378,6 +411,7 @@ export default function AdminAdvancedSection() { })} min={1} max={1440} + disabled={!loginEnabled} /> @@ -391,10 +425,15 @@ export default function AdminAdvancedSection() { setSettings({ - ...settings, - tempFileManagement: { ...settings.tempFileManagement, startupCleanup: e.target.checked } - })} + onChange={(e) => { + if (!loginEnabled) return; + setSettings({ + ...settings, + tempFileManagement: { ...settings.tempFileManagement, startupCleanup: e.target.checked } + }); + }} + disabled={!loginEnabled} + styles={getDisabledStyles()} /> @@ -410,10 +449,15 @@ export default function AdminAdvancedSection() { setSettings({ - ...settings, - tempFileManagement: { ...settings.tempFileManagement, cleanupSystemTemp: e.target.checked } - })} + onChange={(e) => { + if (!loginEnabled) return; + setSettings({ + ...settings, + tempFileManagement: { ...settings.tempFileManagement, cleanupSystemTemp: e.target.checked } + }); + }} + disabled={!loginEnabled} + styles={getDisabledStyles()} /> @@ -448,6 +492,7 @@ export default function AdminAdvancedSection() { })} min={1} max={100} + disabled={!loginEnabled} />
@@ -485,6 +531,7 @@ export default function AdminAdvancedSection() { })} min={1} max={100} + disabled={!loginEnabled} /> @@ -522,6 +570,7 @@ export default function AdminAdvancedSection() { })} min={1} max={100} + disabled={!loginEnabled} /> @@ -559,6 +609,7 @@ export default function AdminAdvancedSection() { })} min={1} max={100} + disabled={!loginEnabled} /> @@ -596,6 +648,7 @@ export default function AdminAdvancedSection() { })} min={1} max={100} + disabled={!loginEnabled} /> @@ -633,6 +687,7 @@ export default function AdminAdvancedSection() { })} min={1} max={100} + disabled={!loginEnabled} /> @@ -670,6 +726,7 @@ export default function AdminAdvancedSection() { })} min={1} max={100} + disabled={!loginEnabled} /> @@ -707,6 +765,7 @@ export default function AdminAdvancedSection() { })} min={1} max={100} + disabled={!loginEnabled} /> @@ -744,6 +804,7 @@ export default function AdminAdvancedSection() { })} min={1} max={100} + disabled={!loginEnabled} /> @@ -781,6 +843,7 @@ export default function AdminAdvancedSection() { })} min={1} max={100} + disabled={!loginEnabled} /> @@ -805,7 +869,7 @@ export default function AdminAdvancedSection() { {/* Save Button */} - diff --git a/frontend/src/core/components/shared/config/configSections/AdminAuditSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminAuditSection.tsx similarity index 72% rename from frontend/src/core/components/shared/config/configSections/AdminAuditSection.tsx rename to frontend/src/proprietary/components/shared/config/configSections/AdminAuditSection.tsx index 23152bdd84..c7142b3d27 100644 --- a/frontend/src/core/components/shared/config/configSections/AdminAuditSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminAuditSection.tsx @@ -6,9 +6,12 @@ import AuditSystemStatus from '@app/components/shared/config/configSections/audi import AuditChartsSection from '@app/components/shared/config/configSections/audit/AuditChartsSection'; import AuditEventsTable from '@app/components/shared/config/configSections/audit/AuditEventsTable'; import AuditExportSection from '@app/components/shared/config/configSections/audit/AuditExportSection'; +import { useLoginRequired } from '@app/hooks/useLoginRequired'; +import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner'; const AdminAuditSection: React.FC = () => { const { t } = useTranslation(); + const { loginEnabled } = useLoginRequired(); const [systemStatus, setSystemStatus] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -27,10 +30,24 @@ const AdminAuditSection: React.FC = () => { } }; - fetchSystemStatus(); - }, []); + if (loginEnabled) { + fetchSystemStatus(); + } else { + // Provide example audit system status when login is disabled + setSystemStatus({ + enabled: true, + level: 'INFO', + retentionDays: 90, + totalEvents: 1234, + }); + setLoading(false); + } + }, [loginEnabled]); - if (loading) { + // Override loading state when login is disabled + const actualLoading = loginEnabled ? loading : false; + + if (actualLoading) { return (
@@ -56,32 +73,33 @@ const AdminAuditSection: React.FC = () => { return ( + {systemStatus.enabled ? ( - + {t('audit.tabs.dashboard', 'Dashboard')} - + {t('audit.tabs.events', 'Audit Events')} - + {t('audit.tabs.export', 'Export')} - + - + - + ) : ( diff --git a/frontend/src/core/components/shared/config/configSections/AdminConnectionsSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminConnectionsSection.tsx similarity index 85% rename from frontend/src/core/components/shared/config/configSections/AdminConnectionsSection.tsx rename to frontend/src/proprietary/components/shared/config/configSections/AdminConnectionsSection.tsx index ab3dc7dc0a..bbb19380a5 100644 --- a/frontend/src/core/components/shared/config/configSections/AdminConnectionsSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminConnectionsSection.tsx @@ -12,6 +12,8 @@ import { Provider, } from '@app/components/shared/config/configSections/providerDefinitions'; import apiClient from '@app/services/apiClient'; +import { useLoginRequired } from '@app/hooks/useLoginRequired'; +import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner'; interface ConnectionsSettingsData { oauth2?: { @@ -45,15 +47,10 @@ interface ConnectionsSettingsData { export default function AdminConnectionsSection() { const { t } = useTranslation(); + const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired(); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); - const { - settings, - setSettings, - loading, - fetchSettings, - isFieldPending, - } = useAdminSettings({ + const adminSettings = useAdminSettings({ sectionName: 'connections', fetchTransformer: async () => { // Fetch security settings (oauth2, saml2) @@ -106,57 +103,75 @@ export default function AdminConnectionsSection() { } }); + const { + settings, + setSettings, + loading, + fetchSettings, + isFieldPending, + } = adminSettings; + useEffect(() => { - fetchSettings(); - }, []); + if (loginEnabled) { + fetchSettings(); + } + }, [loginEnabled, fetchSettings]); + + // Override loading state when login is disabled + const actualLoading = loginEnabled ? loading : false; const isProviderConfigured = (provider: Provider): boolean => { if (provider.id === 'saml2') { - return settings.saml2?.enabled === true; + return settings?.saml2?.enabled === true; } if (provider.id === 'smtp') { - return settings.mail?.enabled === true; + return settings?.mail?.enabled === true; } if (provider.id === 'oauth2-generic') { - return settings.oauth2?.enabled === true; + return settings?.oauth2?.enabled === true; } // Check if specific OAuth2 provider is configured (has clientId) - const providerSettings = settings.oauth2?.client?.[provider.id]; + const providerSettings = settings?.oauth2?.client?.[provider.id]; return !!(providerSettings?.clientId); }; const getProviderSettings = (provider: Provider): Record => { if (provider.id === 'saml2') { - return settings.saml2 || {}; + return settings?.saml2 || {}; } if (provider.id === 'smtp') { - return settings.mail || {}; + return settings?.mail || {}; } if (provider.id === 'oauth2-generic') { // Generic OAuth2 settings are at the root oauth2 level return { - enabled: settings.oauth2?.enabled, - provider: settings.oauth2?.provider, - issuer: settings.oauth2?.issuer, - clientId: settings.oauth2?.clientId, - clientSecret: settings.oauth2?.clientSecret, - scopes: settings.oauth2?.scopes, - useAsUsername: settings.oauth2?.useAsUsername, - autoCreateUser: settings.oauth2?.autoCreateUser, - blockRegistration: settings.oauth2?.blockRegistration, + enabled: settings?.oauth2?.enabled, + provider: settings?.oauth2?.provider, + issuer: settings?.oauth2?.issuer, + clientId: settings?.oauth2?.clientId, + clientSecret: settings?.oauth2?.clientSecret, + scopes: settings?.oauth2?.scopes, + useAsUsername: settings?.oauth2?.useAsUsername, + autoCreateUser: settings?.oauth2?.autoCreateUser, + blockRegistration: settings?.oauth2?.blockRegistration, }; } // Specific OAuth2 provider settings - return settings.oauth2?.client?.[provider.id] || {}; + return settings?.oauth2?.client?.[provider.id] || {}; }; const handleProviderSave = async (provider: Provider, providerSettings: Record) => { + // Block save if login is disabled + if (!validateLoginEnabled()) { + return; + } + try { if (provider.id === 'smtp') { // Mail settings use a different endpoint @@ -218,7 +233,12 @@ export default function AdminConnectionsSection() { }; const handleProviderDisconnect = async (provider: Provider) => { - try { + // Block disconnect if login is disabled + if (!validateLoginEnabled()) { + return; + } + + try{ if (provider.id === 'smtp') { // Mail settings use a different endpoint const response = await apiClient.put('/api/v1/admin/settings/section/mail', { enabled: false }); @@ -271,7 +291,7 @@ export default function AdminConnectionsSection() { } }; - if (loading) { + if (actualLoading) { return ( @@ -280,9 +300,14 @@ export default function AdminConnectionsSection() { } const handleSSOAutoLoginSave = async () => { + // Block save if login is disabled + if (!validateLoginEnabled()) { + return; + } + try { const deltaSettings = { - 'premium.proFeatures.ssoAutoLogin': settings.ssoAutoLogin + 'premium.proFeatures.ssoAutoLogin': settings?.ssoAutoLogin }; const response = await apiClient.put('/api/v1/admin/settings', { settings: deltaSettings }); @@ -311,6 +336,8 @@ export default function AdminConnectionsSection() { return ( + + {/* Header */}
@@ -341,11 +368,14 @@ export default function AdminConnectionsSection() {
{ + if (!loginEnabled) return; // Block change when login disabled setSettings({ ...settings, ssoAutoLogin: e.target.checked }); handleSSOAutoLoginSave(); }} + disabled={!loginEnabled} + styles={getDisabledStyles()} /> @@ -369,6 +399,7 @@ export default function AdminConnectionsSection() { settings={getProviderSettings(provider)} onSave={(providerSettings) => handleProviderSave(provider, providerSettings)} onDisconnect={() => handleProviderDisconnect(provider)} + disabled={!loginEnabled} /> ))}
@@ -392,6 +423,7 @@ export default function AdminConnectionsSection() { provider={provider} isConfigured={false} onSave={(providerSettings) => handleProviderSave(provider, providerSettings)} + disabled={!loginEnabled} /> ))}
diff --git a/frontend/src/core/components/shared/config/configSections/AdminDatabaseSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminDatabaseSection.tsx similarity index 85% rename from frontend/src/core/components/shared/config/configSections/AdminDatabaseSection.tsx rename to frontend/src/proprietary/components/shared/config/configSections/AdminDatabaseSection.tsx index a289537815..fdc4768bb2 100644 --- a/frontend/src/core/components/shared/config/configSections/AdminDatabaseSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminDatabaseSection.tsx @@ -6,6 +6,8 @@ import RestartConfirmationModal from '@app/components/shared/config/RestartConfi import { useRestartServer } from '@app/components/shared/config/useRestartServer'; import { useAdminSettings } from '@app/hooks/useAdminSettings'; import PendingBadge from '@app/components/shared/config/PendingBadge'; +import { useLoginRequired } from '@app/hooks/useLoginRequired'; +import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner'; import apiClient from '@app/services/apiClient'; interface DatabaseSettingsData { @@ -21,6 +23,7 @@ interface DatabaseSettingsData { export default function AdminDatabaseSection() { const { t } = useTranslation(); + const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired(); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); const { @@ -78,10 +81,16 @@ export default function AdminDatabaseSection() { }); useEffect(() => { - fetchSettings(); - }, []); + if (loginEnabled) { + fetchSettings(); + } + }, [loginEnabled, fetchSettings]); const handleSave = async () => { + if (!validateLoginEnabled()) { + return; + } + try { await saveSettings(); showRestartModal(); @@ -94,7 +103,10 @@ export default function AdminDatabaseSection() { } }; - if (loading) { + // Override loading state when login is disabled + const actualLoading = loginEnabled ? loading : false; + + if (actualLoading) { return ( @@ -104,6 +116,8 @@ export default function AdminDatabaseSection() { return ( + +
@@ -130,14 +144,19 @@ export default function AdminDatabaseSection() {
setSettings({ ...settings, enableCustomDatabase: e.target.checked })} + checked={settings?.enableCustomDatabase || false} + onChange={(e) => { + if (!loginEnabled) return; + setSettings({ ...settings, enableCustomDatabase: e.target.checked }); + }} + disabled={!loginEnabled} + styles={getDisabledStyles()} />
- {settings.enableCustomDatabase && ( + {settings?.enableCustomDatabase && ( <>
} description={t('admin.settings.database.customUrl.description', 'Full JDBC connection string (e.g., jdbc:postgresql://localhost:5432/postgres). If provided, individual connection settings below are not used.')} - value={settings.customDatabaseUrl || ''} + value={settings?.customDatabaseUrl || ''} onChange={(e) => setSettings({ ...settings, customDatabaseUrl: e.target.value })} placeholder="jdbc:postgresql://localhost:5432/postgres" + disabled={!loginEnabled} />
@@ -163,7 +183,7 @@ export default function AdminDatabaseSection() { } description={t('admin.settings.database.type.description', 'Type of database (not used if custom URL is provided)')} - value={settings.type || 'postgresql'} + value={settings?.type || 'postgresql'} onChange={(value) => setSettings({ ...settings, type: value || 'postgresql' })} data={[ { value: 'postgresql', label: 'PostgreSQL' }, @@ -171,6 +191,7 @@ export default function AdminDatabaseSection() { { value: 'mysql', label: 'MySQL' }, { value: 'mariadb', label: 'MariaDB' } ]} + disabled={!loginEnabled} />
@@ -183,9 +204,10 @@ export default function AdminDatabaseSection() { } description={t('admin.settings.database.hostName.description', 'Database server hostname (not used if custom URL is provided)')} - value={settings.hostName || ''} + value={settings?.hostName || ''} onChange={(e) => setSettings({ ...settings, hostName: e.target.value })} placeholder="localhost" + disabled={!loginEnabled} /> @@ -198,10 +220,11 @@ export default function AdminDatabaseSection() { } description={t('admin.settings.database.port.description', 'Database server port (not used if custom URL is provided)')} - value={settings.port || 5432} + value={settings?.port || 5432} onChange={(value) => setSettings({ ...settings, port: Number(value) })} min={1} max={65535} + disabled={!loginEnabled} /> @@ -214,9 +237,10 @@ export default function AdminDatabaseSection() { } description={t('admin.settings.database.name.description', 'Name of the database (not used if custom URL is provided)')} - value={settings.name || ''} + value={settings?.name || ''} onChange={(e) => setSettings({ ...settings, name: e.target.value })} placeholder="postgres" + disabled={!loginEnabled} /> @@ -229,9 +253,10 @@ export default function AdminDatabaseSection() { } description={t('admin.settings.database.username.description', 'Database authentication username')} - value={settings.username || ''} + value={settings?.username || ''} onChange={(e) => setSettings({ ...settings, username: e.target.value })} placeholder="postgres" + disabled={!loginEnabled} /> @@ -244,9 +269,10 @@ export default function AdminDatabaseSection() { } description={t('admin.settings.database.password.description', 'Database authentication password')} - value={settings.password || ''} + value={settings?.password || ''} onChange={(e) => setSettings({ ...settings, password: e.target.value })} placeholder="••••••••" + disabled={!loginEnabled} /> @@ -256,7 +282,7 @@ export default function AdminDatabaseSection() { {/* Save Button */} - diff --git a/frontend/src/core/components/shared/config/configSections/AdminEndpointsSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminEndpointsSection.tsx similarity index 82% rename from frontend/src/core/components/shared/config/configSections/AdminEndpointsSection.tsx rename to frontend/src/proprietary/components/shared/config/configSections/AdminEndpointsSection.tsx index 56e51381ba..8bcf305bbb 100644 --- a/frontend/src/core/components/shared/config/configSections/AdminEndpointsSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminEndpointsSection.tsx @@ -6,6 +6,8 @@ import RestartConfirmationModal from '@app/components/shared/config/RestartConfi import { useRestartServer } from '@app/components/shared/config/useRestartServer'; import { useAdminSettings } from '@app/hooks/useAdminSettings'; import PendingBadge from '@app/components/shared/config/PendingBadge'; +import { useLoginRequired } from '@app/hooks/useLoginRequired'; +import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner'; interface EndpointsSettingsData { toRemove?: string[]; @@ -14,6 +16,7 @@ interface EndpointsSettingsData { export default function AdminEndpointsSection() { const { t } = useTranslation(); + const { loginEnabled, validateLoginEnabled } = useLoginRequired(); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); const { @@ -29,10 +32,16 @@ export default function AdminEndpointsSection() { }); useEffect(() => { - fetchSettings(); - }, []); + if (loginEnabled) { + fetchSettings(); + } + }, [loginEnabled, fetchSettings]); const handleSave = async () => { + if (!validateLoginEnabled()) { + return; + } + try { await saveSettings(); showRestartModal(); @@ -45,7 +54,10 @@ export default function AdminEndpointsSection() { } }; - if (loading) { + // Override loading state when login is disabled + const actualLoading = loginEnabled ? loading : false; + + if (actualLoading) { return ( @@ -102,6 +114,8 @@ export default function AdminEndpointsSection() { return ( + +
{t('admin.settings.endpoints.title', 'API Endpoints')} @@ -123,12 +137,16 @@ export default function AdminEndpointsSection() { } description={t('admin.settings.endpoints.toRemove.description', 'Select individual endpoints to disable')} value={settings.toRemove || []} - onChange={(value) => setSettings({ ...settings, toRemove: value })} + onChange={(value) => { + if (!loginEnabled) return; + setSettings({ ...settings, toRemove: value }); + }} data={commonEndpoints.map(endpoint => ({ value: endpoint, label: endpoint }))} searchable clearable placeholder="Select endpoints to disable" comboboxProps={{ zIndex: 1400 }} + disabled={!loginEnabled} />
@@ -142,12 +160,16 @@ export default function AdminEndpointsSection() { } description={t('admin.settings.endpoints.groupsToRemove.description', 'Select endpoint groups to disable')} value={settings.groupsToRemove || []} - onChange={(value) => setSettings({ ...settings, groupsToRemove: value })} + onChange={(value) => { + if (!loginEnabled) return; + setSettings({ ...settings, groupsToRemove: value }); + }} data={commonGroups.map(group => ({ value: group, label: group }))} searchable clearable placeholder="Select groups to disable" comboboxProps={{ zIndex: 1400 }} + disabled={!loginEnabled} /> @@ -160,7 +182,7 @@ export default function AdminEndpointsSection() { - diff --git a/frontend/src/core/components/shared/config/configSections/AdminFeaturesSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminFeaturesSection.tsx similarity index 83% rename from frontend/src/core/components/shared/config/configSections/AdminFeaturesSection.tsx rename to frontend/src/proprietary/components/shared/config/configSections/AdminFeaturesSection.tsx index 72e19be1ac..20ec8ed737 100644 --- a/frontend/src/core/components/shared/config/configSections/AdminFeaturesSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminFeaturesSection.tsx @@ -7,6 +7,8 @@ import { useRestartServer } from '@app/components/shared/config/useRestartServer import { useAdminSettings } from '@app/hooks/useAdminSettings'; import PendingBadge from '@app/components/shared/config/PendingBadge'; import apiClient from '@app/services/apiClient'; +import { useLoginRequired } from '@app/hooks/useLoginRequired'; +import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner'; interface FeaturesSettingsData { serverCertificate?: { @@ -19,6 +21,7 @@ interface FeaturesSettingsData { export default function AdminFeaturesSection() { const { t } = useTranslation(); + const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired(); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); const { @@ -69,10 +72,15 @@ export default function AdminFeaturesSection() { }); useEffect(() => { - fetchSettings(); - }, []); + if (loginEnabled) { + fetchSettings(); + } + }, [loginEnabled]); const handleSave = async () => { + if (!validateLoginEnabled()) { + return; + } try { await saveSettings(); showRestartModal(); @@ -85,7 +93,9 @@ export default function AdminFeaturesSection() { } }; - if (loading) { + const actualLoading = loginEnabled ? loading : false; + + if (actualLoading) { return ( @@ -95,6 +105,7 @@ export default function AdminFeaturesSection() { return ( +
{t('admin.settings.features.title', 'Features')} @@ -124,10 +135,15 @@ export default function AdminFeaturesSection() { setSettings({ - ...settings, - serverCertificate: { ...settings.serverCertificate, enabled: e.target.checked } - })} + onChange={(e) => { + if (!loginEnabled) return; + setSettings({ + ...settings, + serverCertificate: { ...settings.serverCertificate, enabled: e.target.checked } + }); + }} + disabled={!loginEnabled} + styles={getDisabledStyles()} /> @@ -148,6 +164,7 @@ export default function AdminFeaturesSection() { serverCertificate: { ...settings.serverCertificate, organizationName: e.target.value } })} placeholder="Stirling-PDF" + disabled={!loginEnabled} />
@@ -167,6 +184,7 @@ export default function AdminFeaturesSection() { })} min={1} max={3650} + disabled={!loginEnabled} /> @@ -180,10 +198,15 @@ export default function AdminFeaturesSection() { setSettings({ - ...settings, - serverCertificate: { ...settings.serverCertificate, regenerateOnStartup: e.target.checked } - })} + onChange={(e) => { + if (!loginEnabled) return; + setSettings({ + ...settings, + serverCertificate: { ...settings.serverCertificate, regenerateOnStartup: e.target.checked } + }); + }} + disabled={!loginEnabled} + styles={getDisabledStyles()} /> @@ -193,7 +216,7 @@ export default function AdminFeaturesSection() { {/* Save Button */} - diff --git a/frontend/src/core/components/shared/config/configSections/AdminGeneralSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx similarity index 81% rename from frontend/src/core/components/shared/config/configSections/AdminGeneralSection.tsx rename to frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx index 4f354a8a95..3e2b445a63 100644 --- a/frontend/src/core/components/shared/config/configSections/AdminGeneralSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx @@ -1,17 +1,20 @@ import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { TextInput, Switch, Button, Stack, Paper, Text, Loader, Group, MultiSelect, Badge } from '@mantine/core'; +import { TextInput, Switch, Button, Stack, Paper, Text, Loader, Group, MultiSelect, Badge, SegmentedControl } from '@mantine/core'; import { alert } from '@app/components/toast'; import RestartConfirmationModal from '@app/components/shared/config/RestartConfirmationModal'; import { useRestartServer } from '@app/components/shared/config/useRestartServer'; import { useAdminSettings } from '@app/hooks/useAdminSettings'; import PendingBadge from '@app/components/shared/config/PendingBadge'; import apiClient from '@app/services/apiClient'; +import { useLoginRequired } from '@app/hooks/useLoginRequired'; +import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner'; interface GeneralSettingsData { ui: { appNameNavbar?: string; languages?: string[]; + logoStyle?: 'modern' | 'classic'; }; system: { defaultLocale?: string; @@ -40,6 +43,7 @@ interface GeneralSettingsData { export default function AdminGeneralSection() { const { t } = useTranslation(); + const { loginEnabled, validateLoginEnabled } = useLoginRequired(); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); const { @@ -108,14 +112,15 @@ export default function AdminGeneralSection() { saveTransformer: (settings) => { const deltaSettings: Record = { // UI settings - 'ui.appNameNavbar': settings.ui.appNameNavbar, - 'ui.languages': settings.ui.languages, + 'ui.appNameNavbar': settings.ui?.appNameNavbar, + 'ui.languages': settings.ui?.languages, + 'ui.logoStyle': settings.ui?.logoStyle, // System settings - 'system.defaultLocale': settings.system.defaultLocale, - 'system.showUpdate': settings.system.showUpdate, - 'system.showUpdateOnlyAdmin': settings.system.showUpdateOnlyAdmin, - 'system.customHTMLFiles': settings.system.customHTMLFiles, - 'system.fileUploadLimit': settings.system.fileUploadLimit, + 'system.defaultLocale': settings.system?.defaultLocale, + 'system.showUpdate': settings.system?.showUpdate, + 'system.showUpdateOnlyAdmin': settings.system?.showUpdateOnlyAdmin, + 'system.customHTMLFiles': settings.system?.customHTMLFiles, + 'system.fileUploadLimit': settings.system?.fileUploadLimit, // Premium custom metadata 'premium.proFeatures.customMetadata.autoUpdateMetadata': settings.customMetadata?.autoUpdateMetadata, 'premium.proFeatures.customMetadata.author': settings.customMetadata?.author, @@ -124,10 +129,10 @@ export default function AdminGeneralSection() { }; if (settings.customPaths) { - deltaSettings['system.customPaths.pipeline.watchedFoldersDir'] = settings.customPaths.pipeline?.watchedFoldersDir; - deltaSettings['system.customPaths.pipeline.finishedFoldersDir'] = settings.customPaths.pipeline?.finishedFoldersDir; - deltaSettings['system.customPaths.operations.weasyprint'] = settings.customPaths.operations?.weasyprint; - deltaSettings['system.customPaths.operations.unoconvert'] = settings.customPaths.operations?.unoconvert; + deltaSettings['system.customPaths.pipeline.watchedFoldersDir'] = settings.customPaths?.pipeline?.watchedFoldersDir; + deltaSettings['system.customPaths.pipeline.finishedFoldersDir'] = settings.customPaths?.pipeline?.finishedFoldersDir; + deltaSettings['system.customPaths.operations.weasyprint'] = settings.customPaths?.operations?.weasyprint; + deltaSettings['system.customPaths.operations.unoconvert'] = settings.customPaths?.operations?.unoconvert; } return { @@ -138,10 +143,21 @@ export default function AdminGeneralSection() { }); useEffect(() => { - fetchSettings(); - }, []); + // Only fetch real settings if login is enabled + if (loginEnabled) { + fetchSettings(); + } + }, [loginEnabled, fetchSettings]); + + // Override loading state when login is disabled + const actualLoading = loginEnabled ? loading : false; const handleSave = async () => { + // Block save if login is disabled + if (!validateLoginEnabled()) { + return; + } + try { await saveSettings(); showRestartModal(); @@ -154,7 +170,7 @@ export default function AdminGeneralSection() { } }; - if (loading) { + if (actualLoading) { return ( @@ -164,6 +180,8 @@ export default function AdminGeneralSection() { return ( + +
{t('admin.settings.general.title', 'System Settings')} @@ -185,9 +203,55 @@ export default function AdminGeneralSection() { } description={t('admin.settings.general.appNameNavbar.description', 'The name displayed in the navigation bar')} - value={settings.ui.appNameNavbar || ''} + value={settings.ui?.appNameNavbar || ''} onChange={(e) => setSettings({ ...settings, ui: { ...settings.ui, appNameNavbar: e.target.value } })} placeholder="Stirling PDF" + disabled={!loginEnabled} + /> +
+ +
+ + + {t('admin.settings.general.logoStyle.label', 'Logo Style')} + + + + + {t('admin.settings.general.logoStyle.description', 'Choose between the modern minimalist logo or the classic S icon')} + + setSettings({ ...settings, ui: { ...settings.ui, logoStyle: value as 'modern' | 'classic' } })} + data={[ + { + value: 'classic', + label: ( +
+ Classic logo + {t('admin.settings.general.logoStyle.classic', 'Classic')} +
+ ) + }, + { + value: 'modern', + label: ( +
+ Modern logo + {t('admin.settings.general.logoStyle.modern', 'Modern')} +
+ ) + }, + ]} + disabled={!loginEnabled} />
@@ -200,7 +264,7 @@ export default function AdminGeneralSection() { } description={t('admin.settings.general.languages.description', 'Limit which languages are available (empty = all languages)')} - value={settings.ui.languages || []} + value={settings.ui?.languages || []} onChange={(value) => setSettings({ ...settings, ui: { ...settings.ui, languages: value } })} data={[ { value: 'de_DE', label: 'Deutsch' }, @@ -218,6 +282,7 @@ export default function AdminGeneralSection() { clearable placeholder="Select languages" comboboxProps={{ zIndex: 1400 }} + disabled={!loginEnabled} /> @@ -230,9 +295,10 @@ export default function AdminGeneralSection() { } description={t('admin.settings.general.defaultLocale.description', 'The default language for new users (e.g., en_US, es_ES)')} - value={settings.system.defaultLocale || ''} + value={ settings.system?.defaultLocale || ''} onChange={(e) => setSettings({ ...settings, system: { ...settings.system, defaultLocale: e.target.value } })} placeholder="en_US" + disabled={!loginEnabled} /> @@ -245,9 +311,10 @@ export default function AdminGeneralSection() { } description={t('admin.settings.general.fileUploadLimit.description', 'Maximum file upload size (e.g., 100MB, 1GB)')} - value={settings.system.fileUploadLimit || ''} + value={ settings.system?.fileUploadLimit || ''} onChange={(e) => setSettings({ ...settings, system: { ...settings.system, fileUploadLimit: e.target.value } })} placeholder="100MB" + disabled={!loginEnabled} /> @@ -260,8 +327,9 @@ export default function AdminGeneralSection() { setSettings({ ...settings, system: { ...settings.system, showUpdate: e.target.checked } })} + disabled={!loginEnabled} /> @@ -276,8 +344,9 @@ export default function AdminGeneralSection() { setSettings({ ...settings, system: { ...settings.system, showUpdateOnlyAdmin: e.target.checked } })} + disabled={!loginEnabled} /> @@ -292,8 +361,9 @@ export default function AdminGeneralSection() { setSettings({ ...settings, system: { ...settings.system, customHTMLFiles: e.target.checked } })} + disabled={!loginEnabled} /> @@ -326,6 +396,7 @@ export default function AdminGeneralSection() { autoUpdateMetadata: e.target.checked } })} + disabled={!loginEnabled} /> @@ -349,6 +420,7 @@ export default function AdminGeneralSection() { } })} placeholder="username" + disabled={!loginEnabled} /> @@ -370,6 +442,7 @@ export default function AdminGeneralSection() { } })} placeholder="Stirling-PDF" + disabled={!loginEnabled} /> @@ -391,6 +464,7 @@ export default function AdminGeneralSection() { } })} placeholder="Stirling-PDF" + disabled={!loginEnabled} />
@@ -429,6 +503,7 @@ export default function AdminGeneralSection() { } })} placeholder="/pipeline/watchedFolders" + disabled={!loginEnabled} /> @@ -453,6 +528,7 @@ export default function AdminGeneralSection() { } })} placeholder="/pipeline/finishedFolders" + disabled={!loginEnabled} /> @@ -479,6 +555,7 @@ export default function AdminGeneralSection() { } })} placeholder="/opt/venv/bin/weasyprint" + disabled={!loginEnabled} /> @@ -503,6 +580,7 @@ export default function AdminGeneralSection() { } })} placeholder="/opt/venv/bin/unoconvert" + disabled={!loginEnabled} />
@@ -510,7 +588,7 @@ export default function AdminGeneralSection() { {/* Save Button */} - diff --git a/frontend/src/core/components/shared/config/configSections/AdminLegalSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminLegalSection.tsx similarity index 89% rename from frontend/src/core/components/shared/config/configSections/AdminLegalSection.tsx rename to frontend/src/proprietary/components/shared/config/configSections/AdminLegalSection.tsx index f47e52a07c..c927547ae1 100644 --- a/frontend/src/core/components/shared/config/configSections/AdminLegalSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminLegalSection.tsx @@ -7,6 +7,8 @@ import RestartConfirmationModal from '@app/components/shared/config/RestartConfi import { useRestartServer } from '@app/components/shared/config/useRestartServer'; import { useAdminSettings } from '@app/hooks/useAdminSettings'; import PendingBadge from '@app/components/shared/config/PendingBadge'; +import { useLoginRequired } from '@app/hooks/useLoginRequired'; +import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner'; interface LegalSettingsData { termsAndConditions?: string; @@ -18,6 +20,7 @@ interface LegalSettingsData { export default function AdminLegalSection() { const { t } = useTranslation(); + const { loginEnabled, validateLoginEnabled } = useLoginRequired(); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); const { @@ -33,10 +36,15 @@ export default function AdminLegalSection() { }); useEffect(() => { - fetchSettings(); - }, []); + if (loginEnabled) { + fetchSettings(); + } + }, [loginEnabled]); const handleSave = async () => { + if (!validateLoginEnabled()) { + return; + } try { await saveSettings(); showRestartModal(); @@ -49,7 +57,9 @@ export default function AdminLegalSection() { } }; - if (loading) { + const actualLoading = loginEnabled ? loading : false; + + if (actualLoading) { return ( @@ -59,6 +69,7 @@ export default function AdminLegalSection() { return ( +
{t('admin.settings.legal.title', 'Legal Documents')} @@ -95,6 +106,7 @@ export default function AdminLegalSection() { value={settings.termsAndConditions || ''} onChange={(e) => setSettings({ ...settings, termsAndConditions: e.target.value })} placeholder="https://example.com/terms" + disabled={!loginEnabled} />
@@ -110,6 +122,7 @@ export default function AdminLegalSection() { value={settings.privacyPolicy || ''} onChange={(e) => setSettings({ ...settings, privacyPolicy: e.target.value })} placeholder="https://example.com/privacy" + disabled={!loginEnabled} /> @@ -125,6 +138,7 @@ export default function AdminLegalSection() { value={settings.accessibilityStatement || ''} onChange={(e) => setSettings({ ...settings, accessibilityStatement: e.target.value })} placeholder="https://example.com/accessibility" + disabled={!loginEnabled} /> @@ -140,6 +154,7 @@ export default function AdminLegalSection() { value={settings.cookiePolicy || ''} onChange={(e) => setSettings({ ...settings, cookiePolicy: e.target.value })} placeholder="https://example.com/cookies" + disabled={!loginEnabled} /> @@ -155,13 +170,14 @@ export default function AdminLegalSection() { value={settings.impressum || ''} onChange={(e) => setSettings({ ...settings, impressum: e.target.value })} placeholder="https://example.com/impressum" + disabled={!loginEnabled} />
- diff --git a/frontend/src/core/components/shared/config/configSections/AdminMailSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminMailSection.tsx similarity index 100% rename from frontend/src/core/components/shared/config/configSections/AdminMailSection.tsx rename to frontend/src/proprietary/components/shared/config/configSections/AdminMailSection.tsx diff --git a/frontend/src/core/components/shared/config/configSections/AdminPremiumSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminPremiumSection.tsx similarity index 84% rename from frontend/src/core/components/shared/config/configSections/AdminPremiumSection.tsx rename to frontend/src/proprietary/components/shared/config/configSections/AdminPremiumSection.tsx index 8e18696cfc..5c5de2f213 100644 --- a/frontend/src/core/components/shared/config/configSections/AdminPremiumSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminPremiumSection.tsx @@ -7,6 +7,8 @@ import RestartConfirmationModal from '@app/components/shared/config/RestartConfi import { useRestartServer } from '@app/components/shared/config/useRestartServer'; import { useAdminSettings } from '@app/hooks/useAdminSettings'; import PendingBadge from '@app/components/shared/config/PendingBadge'; +import { useLoginRequired } from '@app/hooks/useLoginRequired'; +import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner'; interface PremiumSettingsData { key?: string; @@ -15,6 +17,7 @@ interface PremiumSettingsData { export default function AdminPremiumSection() { const { t } = useTranslation(); + const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired(); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); const { @@ -30,10 +33,15 @@ export default function AdminPremiumSection() { }); useEffect(() => { - fetchSettings(); - }, []); + if (loginEnabled) { + fetchSettings(); + } + }, [loginEnabled]); const handleSave = async () => { + if (!validateLoginEnabled()) { + return; + } try { await saveSettings(); showRestartModal(); @@ -46,7 +54,9 @@ export default function AdminPremiumSection() { } }; - if (loading) { + const actualLoading = loginEnabled ? loading : false; + + if (actualLoading) { return ( @@ -56,6 +66,7 @@ export default function AdminPremiumSection() { return ( +
{t('admin.settings.premium.title', 'Premium & Enterprise')} @@ -98,6 +109,7 @@ export default function AdminPremiumSection() { value={settings.key || ''} onChange={(e) => setSettings({ ...settings, key: e.target.value })} placeholder="00000000-0000-0000-0000-000000000000" + disabled={!loginEnabled} />
@@ -111,7 +123,12 @@ export default function AdminPremiumSection() { setSettings({ ...settings, enabled: e.target.checked })} + onChange={(e) => { + if (!loginEnabled) return; + setSettings({ ...settings, enabled: e.target.checked }); + }} + disabled={!loginEnabled} + styles={getDisabledStyles()} /> @@ -120,7 +137,7 @@ export default function AdminPremiumSection() { - diff --git a/frontend/src/core/components/shared/config/configSections/AdminPrivacySection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminPrivacySection.tsx similarity index 78% rename from frontend/src/core/components/shared/config/configSections/AdminPrivacySection.tsx rename to frontend/src/proprietary/components/shared/config/configSections/AdminPrivacySection.tsx index 3f4a360a90..b3edede1c8 100644 --- a/frontend/src/core/components/shared/config/configSections/AdminPrivacySection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminPrivacySection.tsx @@ -6,6 +6,8 @@ import RestartConfirmationModal from '@app/components/shared/config/RestartConfi import { useRestartServer } from '@app/components/shared/config/useRestartServer'; import { useAdminSettings } from '@app/hooks/useAdminSettings'; import PendingBadge from '@app/components/shared/config/PendingBadge'; +import { useLoginRequired } from '@app/hooks/useLoginRequired'; +import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner'; import apiClient from '@app/services/apiClient'; interface PrivacySettingsData { @@ -16,6 +18,7 @@ interface PrivacySettingsData { export default function AdminPrivacySection() { const { t } = useTranslation(); + const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired(); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); const { @@ -76,10 +79,16 @@ export default function AdminPrivacySection() { }); useEffect(() => { - fetchSettings(); - }, []); + if (loginEnabled) { + fetchSettings(); + } + }, [loginEnabled, fetchSettings]); const handleSave = async () => { + if (!validateLoginEnabled()) { + return; + } + try { await saveSettings(); showRestartModal(); @@ -92,7 +101,10 @@ export default function AdminPrivacySection() { } }; - if (loading) { + // Override loading state when login is disabled + const actualLoading = loginEnabled ? loading : false; + + if (actualLoading) { return ( @@ -102,6 +114,8 @@ export default function AdminPrivacySection() { return ( + +
{t('admin.settings.privacy.title', 'Privacy')} @@ -123,8 +137,13 @@ export default function AdminPrivacySection() {
setSettings({ ...settings, enableAnalytics: e.target.checked })} + checked={settings?.enableAnalytics || false} + onChange={(e) => { + if (!loginEnabled) return; + setSettings({ ...settings, enableAnalytics: e.target.checked }); + }} + disabled={!loginEnabled} + styles={getDisabledStyles()} /> @@ -139,8 +158,13 @@ export default function AdminPrivacySection() { setSettings({ ...settings, metricsEnabled: e.target.checked })} + checked={settings?.metricsEnabled || false} + onChange={(e) => { + if (!loginEnabled) return; + setSettings({ ...settings, metricsEnabled: e.target.checked }); + }} + disabled={!loginEnabled} + styles={getDisabledStyles()} /> @@ -162,8 +186,13 @@ export default function AdminPrivacySection() { setSettings({ ...settings, googleVisibility: e.target.checked })} + checked={settings?.googleVisibility || false} + onChange={(e) => { + if (!loginEnabled) return; + setSettings({ ...settings, googleVisibility: e.target.checked }); + }} + disabled={!loginEnabled} + styles={getDisabledStyles()} /> @@ -173,7 +202,7 @@ export default function AdminPrivacySection() { {/* Save Button */} - diff --git a/frontend/src/core/components/shared/config/configSections/AdminSecuritySection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminSecuritySection.tsx similarity index 85% rename from frontend/src/core/components/shared/config/configSections/AdminSecuritySection.tsx rename to frontend/src/proprietary/components/shared/config/configSections/AdminSecuritySection.tsx index d9540937bf..8934f4c457 100644 --- a/frontend/src/core/components/shared/config/configSections/AdminSecuritySection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminSecuritySection.tsx @@ -8,6 +8,8 @@ import { useRestartServer } from '@app/components/shared/config/useRestartServer import { useAdminSettings } from '@app/hooks/useAdminSettings'; import PendingBadge from '@app/components/shared/config/PendingBadge'; import apiClient from '@app/services/apiClient'; +import { useLoginRequired } from '@app/hooks/useLoginRequired'; +import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner'; interface SecuritySettingsData { enableLogin?: boolean; @@ -44,6 +46,7 @@ interface SecuritySettingsData { export default function AdminSecuritySection() { const { t } = useTranslation(); + const { loginEnabled, validateLoginEnabled } = useLoginRequired(); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); const { @@ -157,10 +160,20 @@ export default function AdminSecuritySection() { }); useEffect(() => { - fetchSettings(); - }, []); + if (loginEnabled) { + fetchSettings(); + } + }, [loginEnabled, fetchSettings]); + + // Override loading state when login is disabled + const actualLoading = loginEnabled ? loading : false; const handleSave = async () => { + // Block save if login is disabled + if (!validateLoginEnabled()) { + return; + } + try { await saveSettings(); showRestartModal(); @@ -173,7 +186,7 @@ export default function AdminSecuritySection() { } }; - if (loading) { + if (actualLoading) { return ( @@ -183,6 +196,8 @@ export default function AdminSecuritySection() { return ( + +
{t('admin.settings.security.title', 'Security')} @@ -204,8 +219,9 @@ export default function AdminSecuritySection() {
setSettings({ ...settings, enableLogin: e.target.checked })} + disabled={!loginEnabled} /> @@ -215,7 +231,7 @@ export default function AdminSecuritySection() { setInviteLinkForm({ ...inviteLinkForm, role: value || 'ROLE_USER' })} + comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }} + /> +