formatting and UI

This commit is contained in:
Anthony Stirling 2025-07-29 11:50:15 +01:00
parent dc6735d360
commit 30461cd91e
7 changed files with 221 additions and 125 deletions

View File

@ -9,7 +9,6 @@ import java.nio.file.Paths;
import java.util.*; import java.util.*;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
@ -35,7 +34,6 @@ import stirling.software.common.configuration.InstallationPathConfig;
import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.model.ApplicationProperties; import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.service.UserServiceInterface; import stirling.software.common.service.UserServiceInterface;
import stirling.software.common.util.CheckProgramInstall;
import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.GeneralUtils;
@ -57,10 +55,10 @@ public class UIDataController {
public ResponseEntity<HomeData> getHomeData() { public ResponseEntity<HomeData> getHomeData() {
String showSurvey = System.getenv("SHOW_SURVEY"); String showSurvey = System.getenv("SHOW_SURVEY");
boolean showSurveyValue = showSurvey == null || "true".equalsIgnoreCase(showSurvey); boolean showSurveyValue = showSurvey == null || "true".equalsIgnoreCase(showSurvey);
HomeData data = new HomeData(); HomeData data = new HomeData();
data.setShowSurveyFromDocker(showSurveyValue); data.setShowSurveyFromDocker(showSurveyValue);
return ResponseEntity.ok(data); return ResponseEntity.ok(data);
} }
@ -69,18 +67,19 @@ public class UIDataController {
public ResponseEntity<LicensesData> getLicensesData() { public ResponseEntity<LicensesData> getLicensesData() {
LicensesData data = new LicensesData(); LicensesData data = new LicensesData();
Resource resource = new ClassPathResource("static/3rdPartyLicenses.json"); Resource resource = new ClassPathResource("static/3rdPartyLicenses.json");
try { try {
InputStream is = resource.getInputStream(); InputStream is = resource.getInputStream();
String json = new String(is.readAllBytes(), StandardCharsets.UTF_8); String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
Map<String, List<Dependency>> licenseData = mapper.readValue(json, new TypeReference<>() {}); Map<String, List<Dependency>> licenseData =
mapper.readValue(json, new TypeReference<>() {});
data.setDependencies(licenseData.get("dependencies")); data.setDependencies(licenseData.get("dependencies"));
} catch (IOException e) { } catch (IOException e) {
log.error("Failed to load licenses data", e); log.error("Failed to load licenses data", e);
data.setDependencies(Collections.emptyList()); data.setDependencies(Collections.emptyList());
} }
return ResponseEntity.ok(data); return ResponseEntity.ok(data);
} }
@ -90,25 +89,31 @@ public class UIDataController {
PipelineData data = new PipelineData(); PipelineData data = new PipelineData();
List<String> pipelineConfigs = new ArrayList<>(); List<String> pipelineConfigs = new ArrayList<>();
List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>(); List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>();
if (new java.io.File(runtimePathConfig.getPipelineDefaultWebUiConfigs()).exists()) { if (new java.io.File(runtimePathConfig.getPipelineDefaultWebUiConfigs()).exists()) {
try (Stream<Path> paths = Files.walk(Paths.get(runtimePathConfig.getPipelineDefaultWebUiConfigs()))) { try (Stream<Path> paths =
List<Path> jsonFiles = paths.filter(Files::isRegularFile) Files.walk(Paths.get(runtimePathConfig.getPipelineDefaultWebUiConfigs()))) {
.filter(p -> p.toString().endsWith(".json")) List<Path> jsonFiles =
.toList(); paths.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".json"))
.toList();
for (Path jsonFile : jsonFiles) { for (Path jsonFile : jsonFiles) {
String content = Files.readString(jsonFile, StandardCharsets.UTF_8); String content = Files.readString(jsonFile, StandardCharsets.UTF_8);
pipelineConfigs.add(content); pipelineConfigs.add(content);
} }
for (String config : pipelineConfigs) { for (String config : pipelineConfigs) {
Map<String, Object> jsonContent = new ObjectMapper() Map<String, Object> jsonContent =
.readValue(config, new TypeReference<Map<String, Object>>() {}); new ObjectMapper()
.readValue(config, new TypeReference<Map<String, Object>>() {});
String name = (String) jsonContent.get("name"); String name = (String) jsonContent.get("name");
if (name == null || name.length() < 1) { if (name == null || name.length() < 1) {
String filename = jsonFiles.get(pipelineConfigs.indexOf(config)) String filename =
.getFileName().toString(); jsonFiles
.get(pipelineConfigs.indexOf(config))
.getFileName()
.toString();
name = filename.substring(0, filename.lastIndexOf('.')); name = filename.substring(0, filename.lastIndexOf('.'));
} }
Map<String, String> configWithName = new HashMap<>(); Map<String, String> configWithName = new HashMap<>();
@ -120,17 +125,17 @@ public class UIDataController {
log.error("Failed to load pipeline configs", e); log.error("Failed to load pipeline configs", e);
} }
} }
if (pipelineConfigsWithNames.isEmpty()) { if (pipelineConfigsWithNames.isEmpty()) {
Map<String, String> configWithName = new HashMap<>(); Map<String, String> configWithName = new HashMap<>();
configWithName.put("json", ""); configWithName.put("json", "");
configWithName.put("name", "No preloaded configs found"); configWithName.put("name", "No preloaded configs found");
pipelineConfigsWithNames.add(configWithName); pipelineConfigsWithNames.add(configWithName);
} }
data.setPipelineConfigsWithNames(pipelineConfigsWithNames); data.setPipelineConfigsWithNames(pipelineConfigsWithNames);
data.setPipelineConfigs(pipelineConfigs); data.setPipelineConfigs(pipelineConfigs);
return ResponseEntity.ok(data); return ResponseEntity.ok(data);
} }
@ -141,26 +146,25 @@ public class UIDataController {
if (userService != null) { if (userService != null) {
username = userService.getCurrentUsername(); username = userService.getCurrentUsername();
} }
List<SignatureFile> signatures = signatureService.getAvailableSignatures(username); List<SignatureFile> signatures = signatureService.getAvailableSignatures(username);
List<FontResource> fonts = getFontNames(); List<FontResource> fonts = getFontNames();
SignData data = new SignData(); SignData data = new SignData();
data.setSignatures(signatures); data.setSignatures(signatures);
data.setFonts(fonts); data.setFonts(fonts);
return ResponseEntity.ok(data); return ResponseEntity.ok(data);
} }
@GetMapping("/ocr-pdf") @GetMapping("/ocr-pdf")
@Operation(summary = "Get OCR PDF data") @Operation(summary = "Get OCR PDF data")
public ResponseEntity<OcrData> getOcrPdfData() { public ResponseEntity<OcrData> getOcrPdfData() {
List<String> languages = getAvailableTesseractLanguages(); List<String> languages = getAvailableTesseractLanguages();
OcrData data = new OcrData(); OcrData data = new OcrData();
data.setLanguages(languages); data.setLanguages(languages);
return ResponseEntity.ok(data); return ResponseEntity.ok(data);
} }
@ -181,39 +185,49 @@ public class UIDataController {
private List<FontResource> getFontNames() { private List<FontResource> getFontNames() {
List<FontResource> fontNames = new ArrayList<>(); List<FontResource> fontNames = new ArrayList<>();
fontNames.addAll(getFontNamesFromLocation("classpath:static/fonts/*.woff2")); fontNames.addAll(getFontNamesFromLocation("classpath:static/fonts/*.woff2"));
fontNames.addAll(getFontNamesFromLocation( fontNames.addAll(
"file:" + InstallationPathConfig.getStaticPath() + "fonts" + java.io.File.separator + "*")); getFontNamesFromLocation(
"file:"
+ InstallationPathConfig.getStaticPath()
+ "fonts"
+ java.io.File.separator
+ "*"));
return fontNames; return fontNames;
} }
private List<FontResource> getFontNamesFromLocation(String locationPattern) { private List<FontResource> getFontNamesFromLocation(String locationPattern) {
try { try {
Resource[] resources = GeneralUtils.getResourcesFromLocationPattern(locationPattern, resourceLoader); Resource[] resources =
GeneralUtils.getResourcesFromLocationPattern(locationPattern, resourceLoader);
return Arrays.stream(resources) return Arrays.stream(resources)
.map(resource -> { .map(
try { resource -> {
String filename = resource.getFilename(); try {
if (filename != null) { String filename = resource.getFilename();
int lastDotIndex = filename.lastIndexOf('.'); if (filename != null) {
if (lastDotIndex != -1) { int lastDotIndex = filename.lastIndexOf('.');
String name = filename.substring(0, lastDotIndex); if (lastDotIndex != -1) {
String extension = filename.substring(lastDotIndex + 1); String name = filename.substring(0, lastDotIndex);
return new FontResource(name, extension); String extension = filename.substring(lastDotIndex + 1);
return new FontResource(name, extension);
}
}
return null;
} catch (Exception e) {
throw ExceptionUtils.createRuntimeException(
"error.fontLoadingFailed",
"Error processing font file",
e);
} }
} })
return null;
} catch (Exception e) {
throw ExceptionUtils.createRuntimeException("error.fontLoadingFailed", "Error processing font file", e);
}
})
.filter(Objects::nonNull) .filter(Objects::nonNull)
.toList(); .toList();
} catch (Exception e) { } catch (Exception e) {
throw ExceptionUtils.createRuntimeException("error.fontDirectoryReadFailed", "Failed to read font directory", e); throw ExceptionUtils.createRuntimeException(
"error.fontDirectoryReadFailed", "Failed to read font directory", e);
} }
} }
// Data classes // Data classes
@Data @Data
public static class HomeData { public static class HomeData {
@ -256,13 +270,19 @@ public class UIDataController {
private static String getFormatFromExtension(String extension) { private static String getFormatFromExtension(String extension) {
switch (extension) { switch (extension) {
case "ttf": return "truetype"; case "ttf":
case "woff": return "woff"; return "truetype";
case "woff2": return "woff2"; case "woff":
case "eot": return "embedded-opentype"; return "woff";
case "svg": return "svg"; case "woff2":
default: return ""; return "woff2";
case "eot":
return "embedded-opentype";
case "svg":
return "svg";
default:
return "";
} }
} }
} }
} }

View File

@ -1,9 +1,10 @@
package stirling.software.proprietary.controller.api; package stirling.software.proprietary.controller.api;
import static stirling.software.common.util.ProviderUtils.validateProvider;
import java.time.Instant; import java.time.Instant;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -14,7 +15,6 @@ import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
@ -24,7 +24,6 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.Data; import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties; import stirling.software.common.model.ApplicationProperties;
@ -54,8 +53,6 @@ import stirling.software.proprietary.security.service.DatabaseService;
import stirling.software.proprietary.security.service.TeamService; import stirling.software.proprietary.security.service.TeamService;
import stirling.software.proprietary.security.session.SessionPersistentRegistry; import stirling.software.proprietary.security.session.SessionPersistentRegistry;
import static stirling.software.common.util.ProviderUtils.validateProvider;
@Slf4j @Slf4j
@RestController @RestController
@RequestMapping("/api/v1/proprietary/ui-data") @RequestMapping("/api/v1/proprietary/ui-data")
@ -105,7 +102,7 @@ public class ProprietaryUIDataController {
data.setRetentionDays(auditConfig.getRetentionDays()); data.setRetentionDays(auditConfig.getRetentionDays());
data.setAuditLevels(AuditLevel.values()); data.setAuditLevels(AuditLevel.values());
data.setAuditEventTypes(AuditEventType.values()); data.setAuditEventTypes(AuditEventType.values());
return ResponseEntity.ok(data); return ResponseEntity.ok(data);
} }
@ -120,7 +117,8 @@ public class ProprietaryUIDataController {
if (oauth != null && oauth.getEnabled()) { if (oauth != null && oauth.getEnabled()) {
if (oauth.isSettingsValid()) { if (oauth.isSettingsValid()) {
String firstChar = String.valueOf(oauth.getProvider().charAt(0)); String firstChar = String.valueOf(oauth.getProvider().charAt(0));
String clientName = oauth.getProvider().replaceFirst(firstChar, firstChar.toUpperCase()); String clientName =
oauth.getProvider().replaceFirst(firstChar, firstChar.toUpperCase());
providerList.put("/oauth2/authorization/" + oauth.getProvider(), clientName); providerList.put("/oauth2/authorization/" + oauth.getProvider(), clientName);
} }
@ -128,40 +126,46 @@ public class ProprietaryUIDataController {
if (client != null) { if (client != null) {
GoogleProvider google = client.getGoogle(); GoogleProvider google = client.getGoogle();
if (validateProvider(google)) { if (validateProvider(google)) {
providerList.put("/oauth2/authorization/" + google.getName(), google.getClientName()); providerList.put(
"/oauth2/authorization/" + google.getName(), google.getClientName());
} }
GitHubProvider github = client.getGithub(); GitHubProvider github = client.getGithub();
if (validateProvider(github)) { if (validateProvider(github)) {
providerList.put("/oauth2/authorization/" + github.getName(), github.getClientName()); providerList.put(
"/oauth2/authorization/" + github.getName(), github.getClientName());
} }
KeycloakProvider keycloak = client.getKeycloak(); KeycloakProvider keycloak = client.getKeycloak();
if (validateProvider(keycloak)) { if (validateProvider(keycloak)) {
providerList.put("/oauth2/authorization/" + keycloak.getName(), keycloak.getClientName()); providerList.put(
"/oauth2/authorization/" + keycloak.getName(),
keycloak.getClientName());
} }
} }
} }
SAML2 saml2 = securityProps.getSaml2(); SAML2 saml2 = securityProps.getSaml2();
if (securityProps.isSaml2Active() && if (securityProps.isSaml2Active()
applicationProperties.getSystem().getEnableAlphaFunctionality() && && applicationProperties.getSystem().getEnableAlphaFunctionality()
applicationProperties.getPremium().isEnabled()) { && applicationProperties.getPremium().isEnabled()) {
String samlIdp = saml2.getProvider(); String samlIdp = saml2.getProvider();
String saml2AuthenticationPath = "/saml2/authenticate/" + saml2.getRegistrationId(); String saml2AuthenticationPath = "/saml2/authenticate/" + saml2.getRegistrationId();
if (!applicationProperties.getPremium().getProFeatures().isSsoAutoLogin()) { if (!applicationProperties.getPremium().getProFeatures().isSsoAutoLogin()) {
providerList.put(saml2AuthenticationPath, samlIdp + " (SAML 2)"); providerList.put(saml2AuthenticationPath, samlIdp + " (SAML 2)");
} }
} }
// Remove null entries // Remove null entries
providerList.entrySet().removeIf(entry -> entry.getKey() == null || entry.getValue() == null); providerList
.entrySet()
.removeIf(entry -> entry.getKey() == null || entry.getValue() == null);
data.setProviderList(providerList); data.setProviderList(providerList);
data.setLoginMethod(securityProps.getLoginMethod()); data.setLoginMethod(securityProps.getLoginMethod());
data.setAltLogin(!providerList.isEmpty() && securityProps.isAltLogin()); data.setAltLogin(!providerList.isEmpty() && securityProps.isAltLogin());
return ResponseEntity.ok(data); return ResponseEntity.ok(data);
} }
@ -172,12 +176,12 @@ public class ProprietaryUIDataController {
List<User> allUsers = userRepository.findAllWithTeam(); List<User> allUsers = userRepository.findAllWithTeam();
Iterator<User> iterator = allUsers.iterator(); Iterator<User> iterator = allUsers.iterator();
Map<String, String> roleDetails = Role.getAllRoleDetails(); Map<String, String> roleDetails = Role.getAllRoleDetails();
Map<String, Boolean> userSessions = new HashMap<>(); Map<String, Boolean> userSessions = new HashMap<>();
Map<String, Date> userLastRequest = new HashMap<>(); Map<String, Date> userLastRequest = new HashMap<>();
int activeUsers = 0; int activeUsers = 0;
int disabledUsers = 0; int disabledUsers = 0;
while (iterator.hasNext()) { while (iterator.hasNext()) {
User user = iterator.next(); User user = iterator.next();
if (user != null) { if (user != null) {
@ -193,7 +197,8 @@ public class ProprietaryUIDataController {
} }
// Check if user is part of the Internal team // Check if user is part of the Internal team
if (user.getTeam() != null && user.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME)) { if (user.getTeam() != null
&& user.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
shouldRemove = true; shouldRemove = true;
} }
@ -201,19 +206,23 @@ public class ProprietaryUIDataController {
iterator.remove(); iterator.remove();
continue; continue;
} }
// Session status and last request time // Session status and last request time
int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval(); int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval();
boolean hasActiveSession = false; boolean hasActiveSession = false;
Date lastRequest = null; Date lastRequest = null;
Optional<SessionEntity> latestSession = sessionPersistentRegistry.findLatestSession(user.getUsername()); Optional<SessionEntity> latestSession =
sessionPersistentRegistry.findLatestSession(user.getUsername());
if (latestSession.isPresent()) { if (latestSession.isPresent()) {
SessionEntity sessionEntity = latestSession.get(); SessionEntity sessionEntity = latestSession.get();
Date lastAccessedTime = sessionEntity.getLastRequest(); Date lastAccessedTime = sessionEntity.getLastRequest();
Instant now = Instant.now(); Instant now = Instant.now();
Instant expirationTime = lastAccessedTime.toInstant().plus(maxInactiveInterval, ChronoUnit.SECONDS); Instant expirationTime =
lastAccessedTime
.toInstant()
.plus(maxInactiveInterval, ChronoUnit.SECONDS);
if (now.isAfter(expirationTime)) { if (now.isAfter(expirationTime)) {
sessionPersistentRegistry.expireSession(sessionEntity.getSessionId()); sessionPersistentRegistry.expireSession(sessionEntity.getSessionId());
} else { } else {
@ -223,33 +232,40 @@ public class ProprietaryUIDataController {
} else { } else {
lastRequest = new Date(0); lastRequest = new Date(0);
} }
userSessions.put(user.getUsername(), hasActiveSession); userSessions.put(user.getUsername(), hasActiveSession);
userLastRequest.put(user.getUsername(), lastRequest); userLastRequest.put(user.getUsername(), lastRequest);
if (hasActiveSession) activeUsers++; if (hasActiveSession) activeUsers++;
if (!user.isEnabled()) disabledUsers++; if (!user.isEnabled()) disabledUsers++;
} }
} }
// Sort users by active status and last request date
List<User> sortedUsers = allUsers.stream()
.sorted((u1, u2) -> {
boolean u1Active = userSessions.get(u1.getUsername());
boolean u2Active = userSessions.get(u2.getUsername());
if (u1Active && !u2Active) return -1;
if (!u1Active && u2Active) return 1;
Date u1LastRequest = userLastRequest.getOrDefault(u1.getUsername(), new Date(0));
Date u2LastRequest = userLastRequest.getOrDefault(u2.getUsername(), new Date(0));
return u2LastRequest.compareTo(u1LastRequest);
})
.toList();
List<Team> allTeams = teamRepository.findAll().stream() // Sort users by active status and last request date
.filter(team -> !team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) List<User> sortedUsers =
.toList(); allUsers.stream()
.sorted(
(u1, u2) -> {
boolean u1Active = userSessions.get(u1.getUsername());
boolean u2Active = userSessions.get(u2.getUsername());
if (u1Active && !u2Active) return -1;
if (!u1Active && u2Active) return 1;
Date u1LastRequest =
userLastRequest.getOrDefault(
u1.getUsername(), new Date(0));
Date u2LastRequest =
userLastRequest.getOrDefault(
u2.getUsername(), new Date(0));
return u2LastRequest.compareTo(u1LastRequest);
})
.toList();
List<Team> allTeams =
teamRepository.findAll().stream()
.filter(team -> !team.getName().equals(TeamService.INTERNAL_TEAM_NAME))
.toList();
AdminSettingsData data = new AdminSettingsData(); AdminSettingsData data = new AdminSettingsData();
data.setUsers(sortedUsers); data.setUsers(sortedUsers);
data.setCurrentUsername(authentication.getName()); data.setCurrentUsername(authentication.getName());
@ -261,7 +277,7 @@ public class ProprietaryUIDataController {
data.setDisabledUsers(disabledUsers); data.setDisabledUsers(disabledUsers);
data.setTeams(allTeams); data.setTeams(allTeams);
data.setMaxPaidUsers(applicationProperties.getPremium().getMaxUsers()); data.setMaxPaidUsers(applicationProperties.getPremium().getMaxUsers());
return ResponseEntity.ok(data); return ResponseEntity.ok(data);
} }
@ -272,7 +288,7 @@ public class ProprietaryUIDataController {
if (authentication == null || !authentication.isAuthenticated()) { if (authentication == null || !authentication.isAuthenticated()) {
return ResponseEntity.status(401).build(); return ResponseEntity.status(401).build();
} }
Object principal = authentication.getPrincipal(); Object principal = authentication.getPrincipal();
String username = null; String username = null;
boolean isOAuth2Login = false; boolean isOAuth2Login = false;
@ -287,16 +303,16 @@ public class ProprietaryUIDataController {
username = saml2User.name(); username = saml2User.name();
isSaml2Login = true; isSaml2Login = true;
} }
if (username == null) { if (username == null) {
return ResponseEntity.status(401).build(); return ResponseEntity.status(401).build();
} }
Optional<User> user = userRepository.findByUsernameIgnoreCaseWithSettings(username); Optional<User> user = userRepository.findByUsernameIgnoreCaseWithSettings(username);
if (user.isEmpty()) { if (user.isEmpty()) {
return ResponseEntity.status(404).build(); return ResponseEntity.status(404).build();
} }
String settingsJson; String settingsJson;
try { try {
settingsJson = objectMapper.writeValueAsString(user.get().getSettings()); settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
@ -304,7 +320,7 @@ public class ProprietaryUIDataController {
log.error("Error converting settings map", e); log.error("Error converting settings map", e);
return ResponseEntity.status(500).build(); return ResponseEntity.status(500).build();
} }
AccountData data = new AccountData(); AccountData data = new AccountData();
data.setUsername(username); data.setUsername(username);
data.setRole(user.get().getRolesAsString()); data.setRole(user.get().getRolesAsString());
@ -312,7 +328,7 @@ public class ProprietaryUIDataController {
data.setChangeCredsFlag(user.get().isFirstLogin()); data.setChangeCredsFlag(user.get().isFirstLogin());
data.setOAuth2Login(isOAuth2Login); data.setOAuth2Login(isOAuth2Login);
data.setSaml2Login(isSaml2Login); data.setSaml2Login(isSaml2Login);
return ResponseEntity.ok(data); return ResponseEntity.ok(data);
} }
@ -321,9 +337,10 @@ public class ProprietaryUIDataController {
@Operation(summary = "Get teams list data") @Operation(summary = "Get teams list data")
public ResponseEntity<TeamsData> getTeamsData() { public ResponseEntity<TeamsData> getTeamsData() {
List<TeamWithUserCountDTO> allTeamsWithCounts = teamRepository.findAllTeamsWithUserCount(); List<TeamWithUserCountDTO> allTeamsWithCounts = teamRepository.findAllTeamsWithUserCount();
List<TeamWithUserCountDTO> teamsWithCounts = allTeamsWithCounts.stream() List<TeamWithUserCountDTO> teamsWithCounts =
.filter(team -> !team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) allTeamsWithCounts.stream()
.toList(); .filter(team -> !team.getName().equals(TeamService.INTERNAL_TEAM_NAME))
.toList();
List<Object[]> teamActivities = sessionRepository.findLatestActivityByTeam(); List<Object[]> teamActivities = sessionRepository.findLatestActivityByTeam();
Map<Long, Date> teamLastRequest = new HashMap<>(); Map<Long, Date> teamLastRequest = new HashMap<>();
@ -332,11 +349,11 @@ public class ProprietaryUIDataController {
Date lastActivity = (Date) result[1]; Date lastActivity = (Date) result[1];
teamLastRequest.put(teamId, lastActivity); teamLastRequest.put(teamId, lastActivity);
} }
TeamsData data = new TeamsData(); TeamsData data = new TeamsData();
data.setTeamsWithCounts(teamsWithCounts); data.setTeamsWithCounts(teamsWithCounts);
data.setTeamLastRequest(teamLastRequest); data.setTeamLastRequest(teamLastRequest);
return ResponseEntity.ok(data); return ResponseEntity.ok(data);
} }
@ -344,8 +361,10 @@ public class ProprietaryUIDataController {
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@Operation(summary = "Get team details data") @Operation(summary = "Get team details data")
public ResponseEntity<TeamDetailsData> getTeamDetailsData(@PathVariable("id") Long id) { public ResponseEntity<TeamDetailsData> getTeamDetailsData(@PathVariable("id") Long id) {
Team team = teamRepository.findById(id) Team team =
.orElseThrow(() -> new RuntimeException("Team not found")); teamRepository
.findById(id)
.orElseThrow(() -> new RuntimeException("Team not found"));
if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) { if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
return ResponseEntity.status(403).build(); return ResponseEntity.status(403).build();
@ -353,10 +372,19 @@ public class ProprietaryUIDataController {
List<User> teamUsers = userRepository.findAllByTeamId(id); List<User> teamUsers = userRepository.findAllByTeamId(id);
List<User> allUsers = userRepository.findAllWithTeam(); List<User> allUsers = userRepository.findAllWithTeam();
List<User> availableUsers = allUsers.stream() List<User> availableUsers =
.filter(user -> (user.getTeam() == null || !user.getTeam().getId().equals(id)) && allUsers.stream()
(user.getTeam() == null || !user.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME))) .filter(
.toList(); user ->
(user.getTeam() == null
|| !user.getTeam().getId().equals(id))
&& (user.getTeam() == null
|| !user.getTeam()
.getName()
.equals(
TeamService
.INTERNAL_TEAM_NAME)))
.toList();
List<Object[]> userSessions = sessionRepository.findLatestSessionByTeamId(id); List<Object[]> userSessions = sessionRepository.findLatestSessionByTeamId(id);
Map<String, Date> userLastRequest = new HashMap<>(); Map<String, Date> userLastRequest = new HashMap<>();
@ -365,13 +393,13 @@ public class ProprietaryUIDataController {
Date lastRequest = (Date) result[1]; Date lastRequest = (Date) result[1];
userLastRequest.put(username, lastRequest); userLastRequest.put(username, lastRequest);
} }
TeamDetailsData data = new TeamDetailsData(); TeamDetailsData data = new TeamDetailsData();
data.setTeam(team); data.setTeam(team);
data.setTeamUsers(teamUsers); data.setTeamUsers(teamUsers);
data.setAvailableUsers(availableUsers); data.setAvailableUsers(availableUsers);
data.setUserLastRequest(userLastRequest); data.setUserLastRequest(userLastRequest);
return ResponseEntity.ok(data); return ResponseEntity.ok(data);
} }
@ -382,12 +410,12 @@ public class ProprietaryUIDataController {
List<FileInfo> backupList = databaseService.getBackupList(); List<FileInfo> backupList = databaseService.getBackupList();
String dbVersion = databaseService.getH2Version(); String dbVersion = databaseService.getH2Version();
boolean isVersionUnknown = "Unknown".equalsIgnoreCase(dbVersion); boolean isVersionUnknown = "Unknown".equalsIgnoreCase(dbVersion);
DatabaseData data = new DatabaseData(); DatabaseData data = new DatabaseData();
data.setBackupFiles(backupList); data.setBackupFiles(backupList);
data.setDatabaseVersion(dbVersion); data.setDatabaseVersion(dbVersion);
data.setVersionUnknown(isVersionUnknown); data.setVersionUnknown(isVersionUnknown);
return ResponseEntity.ok(data); return ResponseEntity.ok(data);
} }
@ -453,4 +481,4 @@ public class ProprietaryUIDataController {
private String databaseVersion; private String databaseVersion;
private boolean versionUnknown; private boolean versionUnknown;
} }
} }

View File

@ -26,9 +26,9 @@ http {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }
# Proxy API calls to backend # Proxy API calls to backend (with query parameters and sub-paths)
location /api/ { location ~ ^/api(.*)$ {
proxy_pass ${VITE_API_BASE_URL}/api/; proxy_pass ${VITE_API_BASE_URL}/api$1;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View File

@ -575,6 +575,10 @@
"title": "Validate PDF Signature", "title": "Validate PDF Signature",
"desc": "Verify digital signatures and certificates in PDF documents" "desc": "Verify digital signatures and certificates in PDF documents"
}, },
"swagger": {
"title": "API Documentation",
"desc": "View API documentation and test endpoints"
},
"replaceColorPdf": { "replaceColorPdf": {
"title": "Advanced Colour options", "title": "Advanced Colour options",
"desc": "Replace colour for text and background in PDF and invert full colour of pdf to reduce file size" "desc": "Replace colour for text and background in PDF and invert full colour of pdf to reduce file size"
@ -1521,6 +1525,12 @@
}, },
"note": "Release notes are only available in English" "note": "Release notes are only available in English"
}, },
"swagger": {
"title": "API Documentation",
"header": "API Documentation",
"desc": "View and test the Stirling PDF API endpoints",
"tags": "api,documentation,swagger,endpoints,development"
},
"cookieBanner": { "cookieBanner": {
"popUp": { "popUp": {
"title": "How we use Cookies", "title": "How we use Cookies",

View File

@ -575,6 +575,10 @@
"title": "Validate PDF Signature", "title": "Validate PDF Signature",
"desc": "Verify digital signatures and certificates in PDF documents" "desc": "Verify digital signatures and certificates in PDF documents"
}, },
"swagger": {
"title": "API Documentation",
"desc": "View API documentation and test endpoints"
},
"replaceColorPdf": { "replaceColorPdf": {
"title": "Replace and Invert Color", "title": "Replace and Invert Color",
"desc": "Replace color for text and background in PDF and invert full color of pdf to reduce file size" "desc": "Replace color for text and background in PDF and invert full color of pdf to reduce file size"
@ -1521,6 +1525,12 @@
}, },
"note": "Release notes are only available in English" "note": "Release notes are only available in English"
}, },
"swagger": {
"title": "API Documentation",
"header": "API Documentation",
"desc": "View and test the Stirling PDF API endpoints",
"tags": "api,documentation,swagger,endpoints,development"
},
"cookieBanner": { "cookieBanner": {
"popUp": { "popUp": {
"title": "How we use Cookies", "title": "How we use Cookies",

View File

@ -2,6 +2,7 @@ import React, { useState, useCallback, useMemo, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import ContentCutIcon from "@mui/icons-material/ContentCut"; import ContentCutIcon from "@mui/icons-material/ContentCut";
import ZoomInMapIcon from "@mui/icons-material/ZoomInMap"; import ZoomInMapIcon from "@mui/icons-material/ZoomInMap";
import ApiIcon from "@mui/icons-material/Api";
import { useMultipleEndpointsEnabled } from "./useEndpointConfig"; import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
import { Tool, ToolDefinition, BaseToolProps, ToolRegistry } from "../types/tool"; import { Tool, ToolDefinition, BaseToolProps, ToolRegistry } from "../types/tool";
@ -26,6 +27,15 @@ const toolDefinitions: Record<string, ToolDefinition> = {
description: "Reduce PDF file size", description: "Reduce PDF file size",
endpoints: ["compress-pdf"] endpoints: ["compress-pdf"]
}, },
swagger: {
id: "swagger",
icon: <ApiIcon />,
component: React.lazy(() => import("../tools/SwaggerUI")),
maxFiles: 0,
category: "utility",
description: "Open API documentation",
endpoints: ["swagger-ui"]
},
}; };

View File

@ -0,0 +1,18 @@
import React, { useEffect } from 'react';
import { BaseToolProps } from '../types/tool';
const SwaggerUI: React.FC<BaseToolProps> = () => {
useEffect(() => {
// Redirect to Swagger UI
window.open('/swagger-ui/5.21.0/index.html', '_blank');
}, []);
return (
<div style={{ textAlign: 'center', padding: '2rem' }}>
<p>Opening Swagger UI in a new tab...</p>
<p>If it didn't open automatically, <a href="/swagger-ui/5.21.0/index.html" target="_blank" rel="noopener noreferrer">click here</a></p>
</div>
);
};
export default SwaggerUI;