Merge branch 'main' into feature/annotations

This commit is contained in:
Reece Browne 2025-12-17 14:31:08 +00:00 committed by GitHub
commit d34e552f2b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 540 additions and 363 deletions

View File

@ -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;

View File

@ -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"))) {

View File

@ -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

View File

@ -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());

View File

@ -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)

View File

@ -198,7 +198,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
private SamlClient getSamlClient(
String registrationId, SAML2 samlConf, List<X509Certificate> certificates)
throws SamlException {
String serverUrl = appConfig.getBaseUrl() + ":" + appConfig.getServerPort();
String serverUrl = appConfig.getBackendUrl() + ":" + appConfig.getServerPort();
String relyingPartyIdentifier =
serverUrl + "/saml2/service-provider-metadata/" + registrationId;

View File

@ -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.");

View File

@ -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();

View File

@ -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)))

File diff suppressed because it is too large Load Diff