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] 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 000000000..54b49fd80 --- /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 4b5a202c0..6a6ee8453 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 23d369bf3..a0d7f3610 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 02b5233b8..ffbec5a7d 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 5f8ca51be..139260872 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 ed9106f2f..a5e0b8a0f 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 cdf7bdd0d..9d1dbc96f 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 9acb8c9f0..2451aa45c 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 77ee17f89..1e47b8619 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 zcmeHOdvp}#684<*61yUJ-$sz-DJ(*;jTq`^qLEO}<2K-$wIG<(zErx_s8I<269&>_8j~^mH8y*pD9ztmrse zJ-MSJa7lmI!?Jq^B1dm3JQLlMlg0J89bQ{Udo>(u^M}xW#1Dj{t-f>Ztv-8utA7O| z-{J! zKzmyaoWSqA6m@}WDEhkt#^mZ_Y!Pj6+s#0zvQ>oOvYLS4G#kNTsVK-a|0Rp9;Bxx; zc}bj~)@q67^T?8*$2W;8wgvpbPpk}o^W;yH~Ie$7Mc9tj*acic9NYCl{%{9X8JTiG$G4Q8Eg zeWv&mXIgtb_&|K_WW%G=O`-Lgblv85B<8PXSH#$m?ifzg|EZs^eAkx1hXVJ2&mCG@ zO4pgm{7?Kv8}{tf90|UMw8n5E{$l@#me-Hd0nH1X#zpJkxcKr5dji|{^W3F2?AxVj z3EGhI7*2;j^`nv<4u1VBT^mOqH{-gQ*2>R48#@PteG$o@>=<#)2fbxqmp7&}hEw5B z-%;W%uHofmXk8y%-y5_JpmkE1Z}eOc%U`r%)ylmaQXRv|_>UdSfz{*;?>of(2T9z6 z5JLNq;C`g4(g_o9k6i~v+9J$8U^1}ErEnytQ@Bb{j+MlxjRa%YkJh?e0@*1rtpMFvYn>U#- zeiM#CN#jv;eO<8u?z%(Q=kc2sf2H=*RQXHqqje6gt0&*XVXQ~Mgz=ng%tYEQe4N1l zOi{{lAePnK8-8Y_zx(%keEw>7wJX*BL-!$1%F!ix-qAsznWy_h?lh&j{0p&d4aY`!#eHYVWDe)NjSsjpJV1()B%D9c~OeDHSU-Ih7- zT40$2XVmPfPBr^puNZ?6fmWeLsODe<)C|DA7;+ITWHq-oQJI{Hax33M6|KgLnyzLj(!r zPx3nXm+G{JBE};^by`@5TJmF6Ctljh?XqHp+4 ztow1HJW1cZMK9PZPIcFtu@-4XcXw?*G&KA&6SkqQXYl!5q$3#?FS1!WI|64Z?}@p> z{~T|xF6(HkE^Kf2=L6V(o-QBMJqC65CethZebMt4E7ki5bu?H%gS9kAn!S|c_;+iQ zr>v#Pz2t~cGczC=AHEJO1-qsC>G=8QOtzLr_cxbyS zW1;~Hm)W6YryHDhKR6JdZ*swm>5+Nc`OoNGSYy#6>~+KYMwivt(HH%9qF%k$3td=m zAVR1=2p;!m#kP<=7Z_eVqcRArG=8y?A{wW6?c1vE%C#?boIZs%nC0oG)FyG3*&GE+U3}6UNvIms!B=tcuBn=1+*u1qu3TuQ7l27oEhx%5hi3^MRax(Vq|8Z-lRQ zR`Pj4F^4J54H|VW+~FED(r6nRSh2MD4 zl-l9V*Uj+9Npd!ex=G6DXV%0W19JY~z01bWB{ZK9=FW~DV_LWy?H@NrE(RB?U(I~- zbvpKM+Y-*J1U!{7F|*tADdud0haQxB2eJBDw)m@b^ndW48EP;O65wHj7Z(clQ%vuX zhv>U5R8a`0azF`w%II=;N%c^2;#>Hi==% zmm`-0h*JqTHAT12{P|Xl339`+?>>$D({<=4Kk}hH+*#x~C-eG}Kapk4mtOO`Z!S)L zXE0t+xD38i41{hxMdUSN=0 ziS@m;$By_rj<))DV_l={o>{5X-W=UlJRd!h>#FfN>u8Ncbu+;llw7G9EL%Qayq{$L zZ3ZZqZ-s(+78o^>gWOTv38%$cPxA}qSlu?u?0D^qiyWa^YpScR_WIzuae5d%Qrm6R z=lX&fSZgawGOKmW07zZogvjhG=A$UJnKRJ4o=D22G`C+S0R~fJU15<=` z!1z2p>@INt#u}jbV{s3rL0kUXtJ$Y*sN1`Dp!f`_{ge;fv9*$~Rck&}#jkItJ}A!N z8h+m}ZtSX_S�@muvs|MjO^zIdKo-gikkGqt=2#ds^?Z?E1NK?TwtBuU8o}Mwqu| z%{0Q;&>Ak>o>8uUO0NB;brt*`|HJo9@DA2j_?%YA%M<6oaC?<%-Q#lYZ@F0qy8UK8 z&o1WEC)^s_K8syHLu{{9>aLOYpDS~;xbD-zoTqd!^-ns<+AyqNt=jOg*q$WLEpOqN z>6AVv_Bh;LrTWJNX?x)q1}K%9Y#M(UyD!4$QCzM^@|-isdj;l6-id81e`VOP@?TN^ jz;EVDtg?K1^j+U$OjA1qfxU?bW{tEdpa<`vc 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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 aJFjz2q?ldh(5nyMk6D=57=Jv05B)#XI~864 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 000000000..0fef4393a --- /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 9cfe5380b..8c93539c6 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 f9937f775..e7ddf0f0e 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 fa3e42bae..7cd034edd 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 f25aaedfc..9ee11cbd2 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 a1f66480b..36dd4dfcd 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 6209fb4b6..3d07338ac 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 169aab44c..7ac5d82c9 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 97cae0671..ad47b415b 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 000000000..ea9af38d9 --- /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 000000000..db97f9c08 --- /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 6df688e55..2731ad100 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 f546ed8bf..0b2aa52e0 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 fa5160adf..53e04516a 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 5d3e2d4dc..04fb55957 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 00b5dac98..f7157a43e 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 9be7640cc..bc63e979a 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 ffb9c8d0c..15ee65b3d 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 23152bdd8..c7142b3d2 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 ab3dc7dc0..bbb19380a 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 a28953781..fdc4768bb 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 56e51381b..8bcf305bb 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 72e19be1a..20ec8ed73 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 4f354a8a9..3e2b445a6 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 f47e52a07..c927547ae 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 8e18696cf..5c5de2f21 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 3f4a360a9..b3edede1c 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 d9540937b..8934f4c45 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() {