From 4ec75d4d8c3010a2993dfe28e2ae2a61c71afd66 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:07:51 +0000 Subject: [PATCH 1/2] allow static overrides (#5258) # Description of Changes --- ## 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) ### 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. --- .../software/SPDF/config/WebMvcConfig.java | 25 ++++++++++- .../web/ReactRoutingController.java | 45 ++++++++++++++++++- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java index 8eac8fa80..71c402aa8 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java @@ -33,14 +33,35 @@ public class WebMvcConfig implements WebMvcConfigurer { public void addResourceHandlers(ResourceHandlerRegistry registry) { // Cache hashed assets (JS/CSS with content hashes) for 1 year // These files have names like index-ChAS4tCC.js that change when content changes + // Check customFiles/static first, then fall back to classpath registry.addResourceHandler("/assets/**") - .addResourceLocations("classpath:/static/assets/") + .addResourceLocations( + "file:" + + stirling.software.common.configuration.InstallationPathConfig + .getStaticPath() + + "assets/", + "classpath:/static/assets/") .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic()); // Don't cache index.html - it needs to be fresh to reference latest hashed assets + // Note: index.html is handled by ReactRoutingController for dynamic processing registry.addResourceHandler("/index.html") - .addResourceLocations("classpath:/static/") + .addResourceLocations( + "file:" + + stirling.software.common.configuration.InstallationPathConfig + .getStaticPath(), + "classpath:/static/") .setCacheControl(CacheControl.noCache().mustRevalidate()); + + // Handle all other static resources (js, css, images, fonts, etc.) + // Check customFiles/static first for user overrides + registry.addResourceHandler("/**") + .addResourceLocations( + "file:" + + stirling.software.common.configuration.InstallationPathConfig + .getStaticPath(), + "classpath:/static/") + .setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS)); } @Override diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java index ab8f3b75b..eddff0e1e 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java @@ -3,9 +3,14 @@ package stirling.software.SPDF.controller.web; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -14,6 +19,11 @@ import org.springframework.web.bind.annotation.GetMapping; import jakarta.annotation.PostConstruct; import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.configuration.InstallationPathConfig; + +@Slf4j @Controller public class ReactRoutingController { @@ -22,24 +32,44 @@ public class ReactRoutingController { private String cachedIndexHtml; private boolean indexHtmlExists = false; + private boolean useExternalIndexHtml = false; @PostConstruct public void init() { - // Only cache if index.html exists (production builds) + log.info("Static files custom path: {}", InstallationPathConfig.getStaticPath()); + + // Check for external index.html first (customFiles/static/) + Path externalIndexPath = Paths.get(InstallationPathConfig.getStaticPath(), "index.html"); + log.debug("Checking for custom index.html at: {}", externalIndexPath); + if (Files.exists(externalIndexPath) && Files.isReadable(externalIndexPath)) { + log.info("Using custom index.html from: {}", externalIndexPath); + try { + this.cachedIndexHtml = processIndexHtml(); + this.indexHtmlExists = true; + this.useExternalIndexHtml = true; + return; + } catch (IOException e) { + log.warn("Failed to load custom index.html, falling back to classpath", e); + } + } + + // Fall back to classpath index.html ClassPathResource resource = new ClassPathResource("static/index.html"); if (resource.exists()) { try { this.cachedIndexHtml = processIndexHtml(); this.indexHtmlExists = true; + this.useExternalIndexHtml = false; } catch (IOException e) { // Failed to cache, will process on each request + log.warn("Failed to cache index.html", e); this.indexHtmlExists = false; } } } private String processIndexHtml() throws IOException { - ClassPathResource resource = new ClassPathResource("static/index.html"); + Resource resource = getIndexHtmlResource(); try (InputStream inputStream = resource.getInputStream()) { String html = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); @@ -62,6 +92,17 @@ public class ReactRoutingController { } } + private Resource getIndexHtmlResource() throws IOException { + // Check external location first + Path externalIndexPath = Paths.get(InstallationPathConfig.getStaticPath(), "index.html"); + if (Files.exists(externalIndexPath) && Files.isReadable(externalIndexPath)) { + return new FileSystemResource(externalIndexPath.toFile()); + } + + // Fall back to classpath + return new ClassPathResource("static/index.html"); + } + @GetMapping( value = {"/", "/index.html"}, produces = MediaType.TEXT_HTML_VALUE) From f9a44c4da45dd440f4f4ae3e42b85b6b0d81facd Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:52:48 +0000 Subject: [PATCH 2/2] Saml fixes (#5256) # Description of Changes --- ## 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) ### 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. --------- Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> --- .../common/configuration/AppConfig.java | 15 ++++++++--- .../software/SPDF/SPDFApplication.java | 6 ++--- .../controller/api/misc/ConfigController.java | 3 ++- .../security/CustomLogoutSuccessHandler.java | 2 +- .../configuration/SecurityConfiguration.java | 3 ++- .../security/saml2/Saml2Configuration.java | 25 +++++++++++++++---- .../UserLicenseSettingsServiceTest.java | 3 ++- 7 files changed, 41 insertions(+), 16 deletions(-) diff --git a/app/common/src/main/java/stirling/software/common/configuration/AppConfig.java b/app/common/src/main/java/stirling/software/common/configuration/AppConfig.java index 272c0b35c..1e9d67269 100644 --- a/app/common/src/main/java/stirling/software/common/configuration/AppConfig.java +++ b/app/common/src/main/java/stirling/software/common/configuration/AppConfig.java @@ -37,10 +37,6 @@ public class AppConfig { private final ApplicationProperties applicationProperties; - @Getter - @Value("${baseUrl:http://localhost}") - private String baseUrl; - @Getter @Value("${server.servlet.context-path:/}") private String contextPath; @@ -49,6 +45,17 @@ public class AppConfig { @Value("${server.port:8080}") private String serverPort; + /** + * Get the backend URL from system configuration. Falls back to http://localhost if not + * configured. + * + * @return The backend base URL for SAML/OAuth/API callbacks + */ + public String getBackendUrl() { + String backendUrl = applicationProperties.getSystem().getBackendUrl(); + return (backendUrl != null && !backendUrl.isBlank()) ? backendUrl : "http://localhost"; + } + @Value("${v2}") public boolean v2Enabled; diff --git a/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java b/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java index d3a4ce776..9cb4a786a 100644 --- a/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java +++ b/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java @@ -138,13 +138,13 @@ public class SPDFApplication { @PostConstruct public void init() { - String baseUrl = appConfig.getBaseUrl(); + String backendUrl = appConfig.getBackendUrl(); String contextPath = appConfig.getContextPath(); String serverPort = appConfig.getServerPort(); - baseUrlStatic = baseUrl; + baseUrlStatic = backendUrl; contextPathStatic = contextPath; serverPortStatic = serverPort; - String url = baseUrl + ":" + getStaticPort() + contextPath; + String url = backendUrl + ":" + getStaticPort() + contextPath; // Log Tauri mode information if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_TAURI_MODE", "false"))) { 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 95486ff9b..2f8d4b62b 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 @@ -66,7 +66,8 @@ public class ConfigController { AppConfig appConfig = applicationContext.getBean(AppConfig.class); // Extract key configuration values from AppConfig - configData.put("baseUrl", appConfig.getBaseUrl()); + // Note: Frontend expects "baseUrl" field name for compatibility + configData.put("baseUrl", appConfig.getBackendUrl()); configData.put("contextPath", appConfig.getContextPath()); configData.put("serverPort", appConfig.getServerPort()); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomLogoutSuccessHandler.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomLogoutSuccessHandler.java index 16272b37f..1f55dd25f 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomLogoutSuccessHandler.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomLogoutSuccessHandler.java @@ -198,7 +198,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { private SamlClient getSamlClient( String registrationId, SAML2 samlConf, List certificates) throws SamlException { - String serverUrl = appConfig.getBaseUrl() + ":" + appConfig.getServerPort(); + String serverUrl = appConfig.getBackendUrl() + ":" + appConfig.getServerPort(); String relyingPartyIdentifier = serverUrl + "/saml2/service-provider-metadata/" + registrationId; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java index 1226237c8..fa936211d 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java @@ -344,7 +344,8 @@ public class SecurityConfiguration { log.error("Error configuring SAML 2 login", e); throw new RuntimeException(e); } - }); + }) + .saml2Metadata(metadata -> {}); } } else { log.debug("Login is not enabled."); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java index 99be4b5b0..6ccffa1da 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java @@ -102,16 +102,31 @@ public class Saml2Configuration { log.error("Failed to load SAML2 SP credentials: {}", e.getMessage(), e); throw new IllegalStateException("Failed to load SAML2 SP credentials", e); } + + // Get backend URL from configuration (for SAML endpoints) + String backendUrl = applicationProperties.getSystem().getBackendUrl(); + if (backendUrl == null || backendUrl.isBlank()) { + backendUrl = "{baseUrl}"; // Fallback to Spring's auto-resolution + log.warn( + "system.backendUrl not configured - SAML metadata will use request-based URLs. Set system.backendUrl for production use."); + } else { + log.info("Using configured backend URL for SAML: {}", backendUrl); + } + + String entityId = + backendUrl + "/saml2/service-provider-metadata/" + samlConf.getRegistrationId(); + String acsLocation = backendUrl + "/login/saml2/sso/{registrationId}"; + String sloResponseLocation = backendUrl + "/login"; + RelyingPartyRegistration rp = RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId()) .signingX509Credentials(c -> c.add(signingCredential)) - .entityId(samlConf.getIdpIssuer()) + .entityId(entityId) .singleLogoutServiceBinding(Saml2MessageBinding.POST) .singleLogoutServiceLocation(samlConf.getIdpSingleLogoutUrl()) - .singleLogoutServiceResponseLocation("{baseUrl}/login") + .singleLogoutServiceResponseLocation(sloResponseLocation) .assertionConsumerServiceBinding(Saml2MessageBinding.POST) - .assertionConsumerServiceLocation( - "{baseUrl}/login/saml2/sso/{registrationId}") + .assertionConsumerServiceLocation(acsLocation) .authnRequestsSigned(true) .assertingPartyMetadata( metadata -> @@ -127,7 +142,7 @@ public class Saml2Configuration { .singleLogoutServiceLocation( samlConf.getIdpSingleLogoutUrl()) .singleLogoutServiceResponseLocation( - "{baseUrl}/login") + sloResponseLocation) .wantAuthnRequestsSigned(true)) .build(); diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/service/UserLicenseSettingsServiceTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/service/UserLicenseSettingsServiceTest.java index a7f8042f6..3f0214573 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/service/UserLicenseSettingsServiceTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/service/UserLicenseSettingsServiceTest.java @@ -51,7 +51,8 @@ class UserLicenseSettingsServiceTest { when(applicationProperties.getPremium()).thenReturn(premium); when(applicationProperties.getAutomaticallyGenerated()).thenReturn(automaticallyGenerated); - when(automaticallyGenerated.getIsNewServer()).thenReturn(false); // Default: not a new server + when(automaticallyGenerated.getIsNewServer()) + .thenReturn(false); // Default: not a new server when(settingsRepository.findSettings()).thenReturn(Optional.of(mockSettings)); when(userService.getTotalUsersCount()).thenReturn(80L); when(settingsRepository.save(any(UserLicenseSettings.class)))