mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-02 10:35:22 +00:00
Endpoint UI data for V2 (#4044)
# Description of Changes This pull request introduces several enhancements and new features, primarily focused on improving API documentation support, expanding backend functionality, and adding new frontend tools. Key changes include the integration of Swagger API documentation, the addition of a new `UIDataController` for backend data handling, and updates to the frontend to include a Swagger UI tool. ### Backend Enhancements: * **Swagger API Documentation Integration**: - Added support for dynamically configuring Swagger servers using the `SWAGGER_SERVER_URL` environment variable in `OpenApiConfig` (`[app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.javaR54-L63](diffhunk://#diff-6080fb3dc14efc430c9de1bf9fa4996a23deebc14230dde7788949b2c49cca68R54-L63)`). - Imported necessary Swagger dependencies (`[app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.javaR13](diffhunk://#diff-6080fb3dc14efc430c9de1bf9fa4996a23deebc14230dde7788949b2c49cca68R13)`). - Updated `nginx.conf` to proxy Swagger-related requests to the backend (`[docker/frontend/nginx.confR55-R92](diffhunk://#diff-6d35fafb4405bd052c6d5e48bd946bcef7c77552a74e1b902de45e85eee09aceR55-R92)`). * **New `UIDataController`**: - Introduced a new controller (`UIDataController`) to serve React UI data, including endpoints for home data, licenses, pipeline configurations, signature data, and OCR data (`[app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.javaR1-R301](diffhunk://#diff-3e7063d4e921c7b9e6eedfcad0e535ba3eff68476dcff5e6f28b00c388cff646R1-R301)`). * **Endpoint Handling**: - Modified `ConfigController` to include explicit parameter naming for better clarity in API requests (`[app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.javaL113-R120](diffhunk://#diff-43d19d45ae547fd79090596c06d58cb0eb7f722ed43eb4da59f9dec39f6def6eL113-R120)`). ### Frontend Enhancements: * **Swagger UI Tool**: - Added a new tool definition (`swagger`) in `useToolManagement.tsx`, with an icon and lazy-loaded component (`[frontend/src/hooks/useToolManagement.tsxR30-R38](diffhunk://#diff-57f8a6b3e75ecaec10ad445b01afe8fccc376af6f8ad4d693c68cf98e8863273R30-R38)`). - Implemented the `SwaggerUI` component to open the Swagger documentation in a new tab (`[frontend/src/tools/SwaggerUI.tsxR1-R18](diffhunk://#diff-ca9bdf83c5d611a5edff10255103d7939895635b33a258dd77db6571da6c4600R1-R18)`). * **Localization Updates**: - Updated English (US and GB) translation files to include Swagger-related strings (`[[1]](diffhunk://#diff-14c707e28788a3a84ed5293ff6689be73d4bca00e155beaf090f9b37c978babbR578-R581)`, `[[2]](diffhunk://#diff-14c707e28788a3a84ed5293ff6689be73d4bca00e155beaf090f9b37c978babbR1528-R1533)`, `[[3]](diffhunk://#diff-e4d543afa388d9eb8a423e45dfebb91641e3558d00848d70b285ebb91c40b249R578-R581)`, `[[4]](diffhunk://#diff-e4d543afa388d9eb8a423e45dfebb91641e3558d00848d70b285ebb91c40b249R1528-R1533)`). ### Workflow Updates: * **Environment Variable Additions**: - Added `SWAGGER_SERVER_URL` to the `PR-Auto-Deploy-V2.yml` and `deploy-on-v2-commit.yml` workflows for configuring Swagger server URLs (`[[1]](diffhunk://#diff-931fcb06ba030420d7044dde06465ad55b4e769a9bd374dcd6a0c76f79a5e30eR320)`, `[[2]](diffhunk://#diff-f8b6ec3c0af9cd2d8dffef6f3def2be6357fe596a606850ca7f5d799e1349069R150)`). <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## 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.
This commit is contained in:
parent
3b82602744
commit
1ce813bcd8
1
.github/workflows/PR-Auto-Deploy-V2.yml
vendored
1
.github/workflows/PR-Auto-Deploy-V2.yml
vendored
@ -317,6 +317,7 @@ jobs:
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||
SWAGGER_SERVER_URL: "http://${{ secrets.VPS_HOST }}:${V2_PORT}"
|
||||
restart: on-failure:5
|
||||
|
||||
stirling-pdf-v2-frontend:
|
||||
|
1
.github/workflows/deploy-on-v2-commit.yml
vendored
1
.github/workflows/deploy-on-v2-commit.yml
vendored
@ -147,6 +147,7 @@ jobs:
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||
SWAGGER_SERVER_URL: "http://${{ secrets.VPS_HOST }}:3000"
|
||||
restart: on-failure:5
|
||||
|
||||
frontend:
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -27,7 +27,7 @@ clientWebUI/
|
||||
!cucumber/exampleFiles/
|
||||
!cucumber/exampleFiles/example_html.zip
|
||||
exampleYmlFiles/stirling/
|
||||
stirling/
|
||||
/stirling/
|
||||
/testing/file_snapshots
|
||||
SwaggerDoc.json
|
||||
|
||||
|
@ -39,6 +39,11 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
|
||||
String queryString = request.getQueryString();
|
||||
if (queryString != null && !queryString.isEmpty()) {
|
||||
String requestURI = request.getRequestURI();
|
||||
|
||||
if (requestURI.contains("/api/")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Map<String, String> allowedParameters = new HashMap<>();
|
||||
|
||||
// Keep only the allowed parameters
|
||||
|
@ -10,6 +10,7 @@ import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.servers.Server;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@ -50,17 +51,26 @@ public class OpenApiConfig {
|
||||
.url("https://www.stirlingpdf.com")
|
||||
.email("contact@stirlingpdf.com"))
|
||||
.description(DEFAULT_DESCRIPTION);
|
||||
|
||||
OpenAPI openAPI = new OpenAPI().info(info);
|
||||
|
||||
// Add server configuration from environment variable
|
||||
String swaggerServerUrl = System.getenv("SWAGGER_SERVER_URL");
|
||||
if (swaggerServerUrl != null && !swaggerServerUrl.trim().isEmpty()) {
|
||||
Server server = new Server().url(swaggerServerUrl).description("API Server");
|
||||
openAPI.addServersItem(server);
|
||||
}
|
||||
|
||||
if (!applicationProperties.getSecurity().getEnableLogin()) {
|
||||
return new OpenAPI().components(new Components()).info(info);
|
||||
return openAPI.components(new Components());
|
||||
} else {
|
||||
SecurityScheme apiKeyScheme =
|
||||
new SecurityScheme()
|
||||
.type(SecurityScheme.Type.APIKEY)
|
||||
.in(SecurityScheme.In.HEADER)
|
||||
.name("X-API-KEY");
|
||||
return new OpenAPI()
|
||||
return openAPI
|
||||
.components(new Components().addSecuritySchemes("apiKey", apiKeyScheme))
|
||||
.info(info)
|
||||
.addSecurityItem(new SecurityRequirement().addList("apiKey"));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,301 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
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 java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.Dependency;
|
||||
import stirling.software.SPDF.model.SignatureFile;
|
||||
import stirling.software.SPDF.service.SignatureService;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.UserServiceInterface;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/ui-data")
|
||||
@Tag(name = "UI Data", description = "APIs for React UI data")
|
||||
public class UIDataController {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final SignatureService signatureService;
|
||||
private final UserServiceInterface userService;
|
||||
private final ResourceLoader resourceLoader;
|
||||
private final RuntimePathConfig runtimePathConfig;
|
||||
|
||||
public UIDataController(
|
||||
ApplicationProperties applicationProperties,
|
||||
SignatureService signatureService,
|
||||
@Autowired(required = false) UserServiceInterface userService,
|
||||
ResourceLoader resourceLoader,
|
||||
RuntimePathConfig runtimePathConfig) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.signatureService = signatureService;
|
||||
this.userService = userService;
|
||||
this.resourceLoader = resourceLoader;
|
||||
this.runtimePathConfig = runtimePathConfig;
|
||||
}
|
||||
|
||||
@GetMapping("/home")
|
||||
@Operation(summary = "Get home page data")
|
||||
public ResponseEntity<HomeData> getHomeData() {
|
||||
String showSurvey = System.getenv("SHOW_SURVEY");
|
||||
boolean showSurveyValue = showSurvey == null || "true".equalsIgnoreCase(showSurvey);
|
||||
|
||||
HomeData data = new HomeData();
|
||||
data.setShowSurveyFromDocker(showSurveyValue);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/licenses")
|
||||
@Operation(summary = "Get third-party licenses data")
|
||||
public ResponseEntity<LicensesData> getLicensesData() {
|
||||
LicensesData data = new LicensesData();
|
||||
Resource resource = new ClassPathResource("static/3rdPartyLicenses.json");
|
||||
|
||||
try {
|
||||
InputStream is = resource.getInputStream();
|
||||
String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
Map<String, List<Dependency>> licenseData =
|
||||
mapper.readValue(json, new TypeReference<>() {});
|
||||
data.setDependencies(licenseData.get("dependencies"));
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to load licenses data", e);
|
||||
data.setDependencies(Collections.emptyList());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/pipeline")
|
||||
@Operation(summary = "Get pipeline configuration data")
|
||||
public ResponseEntity<PipelineData> getPipelineData() {
|
||||
PipelineData data = new PipelineData();
|
||||
List<String> pipelineConfigs = new ArrayList<>();
|
||||
List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>();
|
||||
|
||||
if (new java.io.File(runtimePathConfig.getPipelineDefaultWebUiConfigs()).exists()) {
|
||||
try (Stream<Path> paths =
|
||||
Files.walk(Paths.get(runtimePathConfig.getPipelineDefaultWebUiConfigs()))) {
|
||||
List<Path> jsonFiles =
|
||||
paths.filter(Files::isRegularFile)
|
||||
.filter(p -> p.toString().endsWith(".json"))
|
||||
.toList();
|
||||
|
||||
for (Path jsonFile : jsonFiles) {
|
||||
String content = Files.readString(jsonFile, StandardCharsets.UTF_8);
|
||||
pipelineConfigs.add(content);
|
||||
}
|
||||
|
||||
for (String config : pipelineConfigs) {
|
||||
Map<String, Object> jsonContent =
|
||||
new ObjectMapper()
|
||||
.readValue(config, new TypeReference<Map<String, Object>>() {});
|
||||
String name = (String) jsonContent.get("name");
|
||||
if (name == null || name.length() < 1) {
|
||||
String filename =
|
||||
jsonFiles
|
||||
.get(pipelineConfigs.indexOf(config))
|
||||
.getFileName()
|
||||
.toString();
|
||||
name = filename.substring(0, filename.lastIndexOf('.'));
|
||||
}
|
||||
Map<String, String> configWithName = new HashMap<>();
|
||||
configWithName.put("json", config);
|
||||
configWithName.put("name", name);
|
||||
pipelineConfigsWithNames.add(configWithName);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to load pipeline configs", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (pipelineConfigsWithNames.isEmpty()) {
|
||||
Map<String, String> configWithName = new HashMap<>();
|
||||
configWithName.put("json", "");
|
||||
configWithName.put("name", "No preloaded configs found");
|
||||
pipelineConfigsWithNames.add(configWithName);
|
||||
}
|
||||
|
||||
data.setPipelineConfigsWithNames(pipelineConfigsWithNames);
|
||||
data.setPipelineConfigs(pipelineConfigs);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/sign")
|
||||
@Operation(summary = "Get signature form data")
|
||||
public ResponseEntity<SignData> getSignData() {
|
||||
String username = "";
|
||||
if (userService != null) {
|
||||
username = userService.getCurrentUsername();
|
||||
}
|
||||
|
||||
List<SignatureFile> signatures = signatureService.getAvailableSignatures(username);
|
||||
List<FontResource> fonts = getFontNames();
|
||||
|
||||
SignData data = new SignData();
|
||||
data.setSignatures(signatures);
|
||||
data.setFonts(fonts);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/ocr-pdf")
|
||||
@Operation(summary = "Get OCR PDF data")
|
||||
public ResponseEntity<OcrData> getOcrPdfData() {
|
||||
List<String> languages = getAvailableTesseractLanguages();
|
||||
|
||||
OcrData data = new OcrData();
|
||||
data.setLanguages(languages);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
private List<String> getAvailableTesseractLanguages() {
|
||||
String tessdataDir = applicationProperties.getSystem().getTessdataDir();
|
||||
java.io.File[] files = new java.io.File(tessdataDir).listFiles();
|
||||
if (files == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return Arrays.stream(files)
|
||||
.filter(file -> file.getName().endsWith(".traineddata"))
|
||||
.map(file -> file.getName().replace(".traineddata", ""))
|
||||
.filter(lang -> !"osd".equalsIgnoreCase(lang))
|
||||
.sorted()
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<FontResource> getFontNames() {
|
||||
List<FontResource> fontNames = new ArrayList<>();
|
||||
fontNames.addAll(getFontNamesFromLocation("classpath:static/fonts/*.woff2"));
|
||||
fontNames.addAll(
|
||||
getFontNamesFromLocation(
|
||||
"file:"
|
||||
+ InstallationPathConfig.getStaticPath()
|
||||
+ "fonts"
|
||||
+ java.io.File.separator
|
||||
+ "*"));
|
||||
return fontNames;
|
||||
}
|
||||
|
||||
private List<FontResource> getFontNamesFromLocation(String locationPattern) {
|
||||
try {
|
||||
Resource[] resources =
|
||||
GeneralUtils.getResourcesFromLocationPattern(locationPattern, resourceLoader);
|
||||
return Arrays.stream(resources)
|
||||
.map(
|
||||
resource -> {
|
||||
try {
|
||||
String filename = resource.getFilename();
|
||||
if (filename != null) {
|
||||
int lastDotIndex = filename.lastIndexOf('.');
|
||||
if (lastDotIndex != -1) {
|
||||
String name = filename.substring(0, lastDotIndex);
|
||||
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);
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
} catch (Exception e) {
|
||||
throw ExceptionUtils.createRuntimeException(
|
||||
"error.fontDirectoryReadFailed", "Failed to read font directory", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Data classes
|
||||
@Data
|
||||
public static class HomeData {
|
||||
private boolean showSurveyFromDocker;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class LicensesData {
|
||||
private List<Dependency> dependencies;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class PipelineData {
|
||||
private List<Map<String, String>> pipelineConfigsWithNames;
|
||||
private List<String> pipelineConfigs;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class SignData {
|
||||
private List<SignatureFile> signatures;
|
||||
private List<FontResource> fonts;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class OcrData {
|
||||
private List<String> languages;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class FontResource {
|
||||
private String name;
|
||||
private String extension;
|
||||
private String type;
|
||||
|
||||
public FontResource(String name, String extension) {
|
||||
this.name = name;
|
||||
this.extension = extension;
|
||||
this.type = getFormatFromExtension(extension);
|
||||
}
|
||||
|
||||
private static String getFormatFromExtension(String extension) {
|
||||
switch (extension) {
|
||||
case "ttf":
|
||||
return "truetype";
|
||||
case "woff":
|
||||
return "woff";
|
||||
case "woff2":
|
||||
return "woff2";
|
||||
case "eot":
|
||||
return "embedded-opentype";
|
||||
case "svg":
|
||||
return "svg";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -110,14 +110,14 @@ public class ConfigController {
|
||||
}
|
||||
|
||||
@GetMapping("/endpoint-enabled")
|
||||
public ResponseEntity<Boolean> isEndpointEnabled(@RequestParam String endpoint) {
|
||||
public ResponseEntity<Boolean> isEndpointEnabled(@RequestParam(name = "endpoint") String endpoint) {
|
||||
boolean enabled = endpointConfiguration.isEndpointEnabled(endpoint);
|
||||
return ResponseEntity.ok(enabled);
|
||||
}
|
||||
|
||||
@GetMapping("/endpoints-enabled")
|
||||
public ResponseEntity<Map<String, Boolean>> areEndpointsEnabled(
|
||||
@RequestParam String endpoints) {
|
||||
@RequestParam(name = "endpoints") String endpoints) {
|
||||
Map<String, Boolean> result = new HashMap<>();
|
||||
String[] endpointArray = endpoints.split(",");
|
||||
for (String endpoint : endpointArray) {
|
||||
|
@ -0,0 +1,484 @@
|
||||
package stirling.software.proprietary.controller.api;
|
||||
|
||||
import static stirling.software.common.util.ProviderUtils.validateProvider;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.ApplicationProperties.Security;
|
||||
import stirling.software.common.model.ApplicationProperties.Security.OAUTH2;
|
||||
import stirling.software.common.model.ApplicationProperties.Security.OAUTH2.Client;
|
||||
import stirling.software.common.model.ApplicationProperties.Security.SAML2;
|
||||
import stirling.software.common.model.FileInfo;
|
||||
import stirling.software.common.model.enumeration.Role;
|
||||
import stirling.software.common.model.oauth2.GitHubProvider;
|
||||
import stirling.software.common.model.oauth2.GoogleProvider;
|
||||
import stirling.software.common.model.oauth2.KeycloakProvider;
|
||||
import stirling.software.proprietary.audit.AuditEventType;
|
||||
import stirling.software.proprietary.audit.AuditLevel;
|
||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||
import stirling.software.proprietary.model.Team;
|
||||
import stirling.software.proprietary.model.dto.TeamWithUserCountDTO;
|
||||
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||
import stirling.software.proprietary.security.database.repository.SessionRepository;
|
||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||
import stirling.software.proprietary.security.model.Authority;
|
||||
import stirling.software.proprietary.security.model.SessionEntity;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.repository.TeamRepository;
|
||||
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||
import stirling.software.proprietary.security.service.DatabaseService;
|
||||
import stirling.software.proprietary.security.service.TeamService;
|
||||
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/proprietary/ui-data")
|
||||
@Tag(name = "Proprietary UI Data", description = "APIs for React UI data (Proprietary features)")
|
||||
@EnterpriseEndpoint
|
||||
public class ProprietaryUIDataController {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final AuditConfigurationProperties auditConfig;
|
||||
private final SessionPersistentRegistry sessionPersistentRegistry;
|
||||
private final UserRepository userRepository;
|
||||
private final TeamRepository teamRepository;
|
||||
private final SessionRepository sessionRepository;
|
||||
private final DatabaseService databaseService;
|
||||
private final boolean runningEE;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public ProprietaryUIDataController(
|
||||
ApplicationProperties applicationProperties,
|
||||
AuditConfigurationProperties auditConfig,
|
||||
SessionPersistentRegistry sessionPersistentRegistry,
|
||||
UserRepository userRepository,
|
||||
TeamRepository teamRepository,
|
||||
SessionRepository sessionRepository,
|
||||
DatabaseService databaseService,
|
||||
ObjectMapper objectMapper,
|
||||
@Qualifier("runningEE") boolean runningEE) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.auditConfig = auditConfig;
|
||||
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
||||
this.userRepository = userRepository;
|
||||
this.teamRepository = teamRepository;
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.databaseService = databaseService;
|
||||
this.objectMapper = objectMapper;
|
||||
this.runningEE = runningEE;
|
||||
}
|
||||
|
||||
@GetMapping("/audit-dashboard")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@Operation(summary = "Get audit dashboard data")
|
||||
public ResponseEntity<AuditDashboardData> getAuditDashboardData() {
|
||||
AuditDashboardData data = new AuditDashboardData();
|
||||
data.setAuditEnabled(auditConfig.isEnabled());
|
||||
data.setAuditLevel(auditConfig.getAuditLevel());
|
||||
data.setAuditLevelInt(auditConfig.getLevel());
|
||||
data.setRetentionDays(auditConfig.getRetentionDays());
|
||||
data.setAuditLevels(AuditLevel.values());
|
||||
data.setAuditEventTypes(AuditEventType.values());
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/login")
|
||||
@Operation(summary = "Get login page data")
|
||||
public ResponseEntity<LoginData> getLoginData() {
|
||||
LoginData data = new LoginData();
|
||||
Map<String, String> providerList = new HashMap<>();
|
||||
Security securityProps = applicationProperties.getSecurity();
|
||||
OAUTH2 oauth = securityProps.getOauth2();
|
||||
|
||||
if (oauth != null && oauth.getEnabled()) {
|
||||
if (oauth.isSettingsValid()) {
|
||||
String firstChar = String.valueOf(oauth.getProvider().charAt(0));
|
||||
String clientName =
|
||||
oauth.getProvider().replaceFirst(firstChar, firstChar.toUpperCase());
|
||||
providerList.put("/oauth2/authorization/" + oauth.getProvider(), clientName);
|
||||
}
|
||||
|
||||
Client client = oauth.getClient();
|
||||
if (client != null) {
|
||||
GoogleProvider google = client.getGoogle();
|
||||
if (validateProvider(google)) {
|
||||
providerList.put(
|
||||
"/oauth2/authorization/" + google.getName(), google.getClientName());
|
||||
}
|
||||
|
||||
GitHubProvider github = client.getGithub();
|
||||
if (validateProvider(github)) {
|
||||
providerList.put(
|
||||
"/oauth2/authorization/" + github.getName(), github.getClientName());
|
||||
}
|
||||
|
||||
KeycloakProvider keycloak = client.getKeycloak();
|
||||
if (validateProvider(keycloak)) {
|
||||
providerList.put(
|
||||
"/oauth2/authorization/" + keycloak.getName(),
|
||||
keycloak.getClientName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SAML2 saml2 = securityProps.getSaml2();
|
||||
if (securityProps.isSaml2Active()
|
||||
&& applicationProperties.getSystem().getEnableAlphaFunctionality()
|
||||
&& applicationProperties.getPremium().isEnabled()) {
|
||||
String samlIdp = saml2.getProvider();
|
||||
String saml2AuthenticationPath = "/saml2/authenticate/" + saml2.getRegistrationId();
|
||||
|
||||
if (!applicationProperties.getPremium().getProFeatures().isSsoAutoLogin()) {
|
||||
providerList.put(saml2AuthenticationPath, samlIdp + " (SAML 2)");
|
||||
}
|
||||
}
|
||||
|
||||
// Remove null entries
|
||||
providerList
|
||||
.entrySet()
|
||||
.removeIf(entry -> entry.getKey() == null || entry.getValue() == null);
|
||||
|
||||
data.setProviderList(providerList);
|
||||
data.setLoginMethod(securityProps.getLoginMethod());
|
||||
data.setAltLogin(!providerList.isEmpty() && securityProps.isAltLogin());
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/admin-settings")
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@Operation(summary = "Get admin settings data")
|
||||
public ResponseEntity<AdminSettingsData> getAdminSettingsData(Authentication authentication) {
|
||||
List<User> allUsers = userRepository.findAllWithTeam();
|
||||
Iterator<User> iterator = allUsers.iterator();
|
||||
Map<String, String> roleDetails = Role.getAllRoleDetails();
|
||||
|
||||
Map<String, Boolean> userSessions = new HashMap<>();
|
||||
Map<String, Date> userLastRequest = new HashMap<>();
|
||||
int activeUsers = 0;
|
||||
int disabledUsers = 0;
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
User user = iterator.next();
|
||||
if (user != null) {
|
||||
boolean shouldRemove = false;
|
||||
|
||||
// Check if user is an INTERNAL_API_USER
|
||||
for (Authority authority : user.getAuthorities()) {
|
||||
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
|
||||
shouldRemove = true;
|
||||
roleDetails.remove(Role.INTERNAL_API_USER.getRoleId());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user is part of the Internal team
|
||||
if (user.getTeam() != null
|
||||
&& user.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
||||
shouldRemove = true;
|
||||
}
|
||||
|
||||
if (shouldRemove) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Session status and last request time
|
||||
int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval();
|
||||
boolean hasActiveSession = false;
|
||||
Date lastRequest = null;
|
||||
Optional<SessionEntity> latestSession =
|
||||
sessionPersistentRegistry.findLatestSession(user.getUsername());
|
||||
|
||||
if (latestSession.isPresent()) {
|
||||
SessionEntity sessionEntity = latestSession.get();
|
||||
Date lastAccessedTime = sessionEntity.getLastRequest();
|
||||
Instant now = Instant.now();
|
||||
Instant expirationTime =
|
||||
lastAccessedTime
|
||||
.toInstant()
|
||||
.plus(maxInactiveInterval, ChronoUnit.SECONDS);
|
||||
|
||||
if (now.isAfter(expirationTime)) {
|
||||
sessionPersistentRegistry.expireSession(sessionEntity.getSessionId());
|
||||
} else {
|
||||
hasActiveSession = !sessionEntity.isExpired();
|
||||
}
|
||||
lastRequest = sessionEntity.getLastRequest();
|
||||
} else {
|
||||
lastRequest = new Date(0);
|
||||
}
|
||||
|
||||
userSessions.put(user.getUsername(), hasActiveSession);
|
||||
userLastRequest.put(user.getUsername(), lastRequest);
|
||||
|
||||
if (hasActiveSession) activeUsers++;
|
||||
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()
|
||||
.filter(team -> !team.getName().equals(TeamService.INTERNAL_TEAM_NAME))
|
||||
.toList();
|
||||
|
||||
AdminSettingsData data = new AdminSettingsData();
|
||||
data.setUsers(sortedUsers);
|
||||
data.setCurrentUsername(authentication.getName());
|
||||
data.setRoleDetails(roleDetails);
|
||||
data.setUserSessions(userSessions);
|
||||
data.setUserLastRequest(userLastRequest);
|
||||
data.setTotalUsers(allUsers.size());
|
||||
data.setActiveUsers(activeUsers);
|
||||
data.setDisabledUsers(disabledUsers);
|
||||
data.setTeams(allTeams);
|
||||
data.setMaxPaidUsers(applicationProperties.getPremium().getMaxUsers());
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/account")
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@Operation(summary = "Get account page data")
|
||||
public ResponseEntity<AccountData> getAccountData(Authentication authentication) {
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
|
||||
Object principal = authentication.getPrincipal();
|
||||
String username = null;
|
||||
boolean isOAuth2Login = false;
|
||||
boolean isSaml2Login = false;
|
||||
|
||||
if (principal instanceof UserDetails detailsUser) {
|
||||
username = detailsUser.getUsername();
|
||||
} else if (principal instanceof OAuth2User oAuth2User) {
|
||||
username = oAuth2User.getName();
|
||||
isOAuth2Login = true;
|
||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
||||
username = saml2User.name();
|
||||
isSaml2Login = true;
|
||||
}
|
||||
|
||||
if (username == null) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
|
||||
Optional<User> user = userRepository.findByUsernameIgnoreCaseWithSettings(username);
|
||||
if (user.isEmpty()) {
|
||||
return ResponseEntity.status(404).build();
|
||||
}
|
||||
|
||||
String settingsJson;
|
||||
try {
|
||||
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Error converting settings map", e);
|
||||
return ResponseEntity.status(500).build();
|
||||
}
|
||||
|
||||
AccountData data = new AccountData();
|
||||
data.setUsername(username);
|
||||
data.setRole(user.get().getRolesAsString());
|
||||
data.setSettings(settingsJson);
|
||||
data.setChangeCredsFlag(user.get().isFirstLogin());
|
||||
data.setOAuth2Login(isOAuth2Login);
|
||||
data.setSaml2Login(isSaml2Login);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/teams")
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@Operation(summary = "Get teams list data")
|
||||
public ResponseEntity<TeamsData> getTeamsData() {
|
||||
List<TeamWithUserCountDTO> allTeamsWithCounts = teamRepository.findAllTeamsWithUserCount();
|
||||
List<TeamWithUserCountDTO> teamsWithCounts =
|
||||
allTeamsWithCounts.stream()
|
||||
.filter(team -> !team.getName().equals(TeamService.INTERNAL_TEAM_NAME))
|
||||
.toList();
|
||||
|
||||
List<Object[]> teamActivities = sessionRepository.findLatestActivityByTeam();
|
||||
Map<Long, Date> teamLastRequest = new HashMap<>();
|
||||
for (Object[] result : teamActivities) {
|
||||
Long teamId = (Long) result[0];
|
||||
Date lastActivity = (Date) result[1];
|
||||
teamLastRequest.put(teamId, lastActivity);
|
||||
}
|
||||
|
||||
TeamsData data = new TeamsData();
|
||||
data.setTeamsWithCounts(teamsWithCounts);
|
||||
data.setTeamLastRequest(teamLastRequest);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/teams/{id}")
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@Operation(summary = "Get team details data")
|
||||
public ResponseEntity<TeamDetailsData> getTeamDetailsData(@PathVariable("id") Long id) {
|
||||
Team team =
|
||||
teamRepository
|
||||
.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("Team not found"));
|
||||
|
||||
if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
||||
return ResponseEntity.status(403).build();
|
||||
}
|
||||
|
||||
List<User> teamUsers = userRepository.findAllByTeamId(id);
|
||||
List<User> allUsers = userRepository.findAllWithTeam();
|
||||
List<User> availableUsers =
|
||||
allUsers.stream()
|
||||
.filter(
|
||||
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);
|
||||
Map<String, Date> userLastRequest = new HashMap<>();
|
||||
for (Object[] result : userSessions) {
|
||||
String username = (String) result[0];
|
||||
Date lastRequest = (Date) result[1];
|
||||
userLastRequest.put(username, lastRequest);
|
||||
}
|
||||
|
||||
TeamDetailsData data = new TeamDetailsData();
|
||||
data.setTeam(team);
|
||||
data.setTeamUsers(teamUsers);
|
||||
data.setAvailableUsers(availableUsers);
|
||||
data.setUserLastRequest(userLastRequest);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/database")
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@Operation(summary = "Get database management data")
|
||||
public ResponseEntity<DatabaseData> getDatabaseData() {
|
||||
List<FileInfo> backupList = databaseService.getBackupList();
|
||||
String dbVersion = databaseService.getH2Version();
|
||||
boolean isVersionUnknown = "Unknown".equalsIgnoreCase(dbVersion);
|
||||
|
||||
DatabaseData data = new DatabaseData();
|
||||
data.setBackupFiles(backupList);
|
||||
data.setDatabaseVersion(dbVersion);
|
||||
data.setVersionUnknown(isVersionUnknown);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
// Data classes
|
||||
@Data
|
||||
public static class AuditDashboardData {
|
||||
private boolean auditEnabled;
|
||||
private AuditLevel auditLevel;
|
||||
private int auditLevelInt;
|
||||
private int retentionDays;
|
||||
private AuditLevel[] auditLevels;
|
||||
private AuditEventType[] auditEventTypes;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class LoginData {
|
||||
private Map<String, String> providerList;
|
||||
private String loginMethod;
|
||||
private boolean altLogin;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class AdminSettingsData {
|
||||
private List<User> users;
|
||||
private String currentUsername;
|
||||
private Map<String, String> roleDetails;
|
||||
private Map<String, Boolean> userSessions;
|
||||
private Map<String, Date> userLastRequest;
|
||||
private int totalUsers;
|
||||
private int activeUsers;
|
||||
private int disabledUsers;
|
||||
private List<Team> teams;
|
||||
private int maxPaidUsers;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class AccountData {
|
||||
private String username;
|
||||
private String role;
|
||||
private String settings;
|
||||
private boolean changeCredsFlag;
|
||||
private boolean oAuth2Login;
|
||||
private boolean saml2Login;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class TeamsData {
|
||||
private List<TeamWithUserCountDTO> teamsWithCounts;
|
||||
private Map<Long, Date> teamLastRequest;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class TeamDetailsData {
|
||||
private Team team;
|
||||
private List<User> teamUsers;
|
||||
private List<User> availableUsers;
|
||||
private Map<String, Date> userLastRequest;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class DatabaseData {
|
||||
private List<FileInfo> backupFiles;
|
||||
private String databaseVersion;
|
||||
private boolean versionUnknown;
|
||||
}
|
||||
}
|
@ -52,6 +52,44 @@ http {
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
|
||||
# Proxy Swagger UI to backend (including versioned paths)
|
||||
location ~ ^/swagger-ui(.*)$ {
|
||||
proxy_pass ${VITE_API_BASE_URL}/swagger-ui$1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
|
||||
proxy_set_header Connection '';
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
}
|
||||
|
||||
# Proxy API docs to backend (with query parameters and sub-paths)
|
||||
location ~ ^/v3/api-docs(.*)$ {
|
||||
proxy_pass ${VITE_API_BASE_URL}/v3/api-docs$1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
}
|
||||
|
||||
# Proxy v1 API docs to backend (with query parameters and sub-paths)
|
||||
location ~ ^/v1/api-docs(.*)$ {
|
||||
proxy_pass ${VITE_API_BASE_URL}/v1/api-docs$1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
}
|
||||
|
||||
# Cache static assets
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
|
@ -575,6 +575,10 @@
|
||||
"title": "Validate PDF Signature",
|
||||
"desc": "Verify digital signatures and certificates in PDF documents"
|
||||
},
|
||||
"swagger": {
|
||||
"title": "API Documentation",
|
||||
"desc": "View API documentation and test endpoints"
|
||||
},
|
||||
"replaceColorPdf": {
|
||||
"title": "Advanced Colour options",
|
||||
"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"
|
||||
},
|
||||
"swagger": {
|
||||
"title": "API Documentation",
|
||||
"header": "API Documentation",
|
||||
"desc": "View and test the Stirling PDF API endpoints",
|
||||
"tags": "api,documentation,swagger,endpoints,development"
|
||||
},
|
||||
"cookieBanner": {
|
||||
"popUp": {
|
||||
"title": "How we use Cookies",
|
||||
|
@ -575,6 +575,10 @@
|
||||
"title": "Validate PDF Signature",
|
||||
"desc": "Verify digital signatures and certificates in PDF documents"
|
||||
},
|
||||
"swagger": {
|
||||
"title": "API Documentation",
|
||||
"desc": "View API documentation and test endpoints"
|
||||
},
|
||||
"replaceColorPdf": {
|
||||
"title": "Replace and Invert Color",
|
||||
"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"
|
||||
},
|
||||
"swagger": {
|
||||
"title": "API Documentation",
|
||||
"header": "API Documentation",
|
||||
"desc": "View and test the Stirling PDF API endpoints",
|
||||
"tags": "api,documentation,swagger,endpoints,development"
|
||||
},
|
||||
"cookieBanner": {
|
||||
"popUp": {
|
||||
"title": "How we use Cookies",
|
||||
|
@ -2,6 +2,7 @@ import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ContentCutIcon from "@mui/icons-material/ContentCut";
|
||||
import ZoomInMapIcon from "@mui/icons-material/ZoomInMap";
|
||||
import ApiIcon from "@mui/icons-material/Api";
|
||||
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
|
||||
import { Tool, ToolDefinition, BaseToolProps, ToolRegistry } from "../types/tool";
|
||||
|
||||
@ -26,6 +27,15 @@ const toolDefinitions: Record<string, ToolDefinition> = {
|
||||
description: "Reduce PDF file size",
|
||||
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"]
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
|
18
frontend/src/tools/SwaggerUI.tsx
Normal file
18
frontend/src/tools/SwaggerUI.tsx
Normal 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;
|
Loading…
x
Reference in New Issue
Block a user