mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-23 16:05:09 +00:00
Compare commits
1 Commits
5a2c8b9a16
...
0ee2b765ae
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0ee2b765ae |
46
.github/workflows/PR-Demo-Comment-with-react.yml
vendored
46
.github/workflows/PR-Demo-Comment-with-react.yml
vendored
@ -38,8 +38,7 @@ jobs:
|
|||||||
pr_ref: ${{ steps.get-pr-info.outputs.ref }}
|
pr_ref: ${{ steps.get-pr-info.outputs.ref }}
|
||||||
comment_id: ${{ github.event.comment.id }}
|
comment_id: ${{ github.event.comment.id }}
|
||||||
disable_security: ${{ steps.check-security-flag.outputs.disable_security }}
|
disable_security: ${{ steps.check-security-flag.outputs.disable_security }}
|
||||||
enable_pro: ${{ steps.check-pro-flag.outputs.enable_pro }}
|
|
||||||
enable_enterprise: ${{ steps.check-pro-flag.outputs.enable_enterprise }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||||
@ -99,25 +98,6 @@ jobs:
|
|||||||
echo "disable_security=true" >> $GITHUB_OUTPUT
|
echo "disable_security=true" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Check for pro flag
|
|
||||||
id: check-pro-flag
|
|
||||||
env:
|
|
||||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
|
||||||
run: |
|
|
||||||
if [[ "$COMMENT_BODY" == *"pro"* ]] || [[ "$COMMENT_BODY" == *"premium"* ]]; then
|
|
||||||
echo "pro flags detected in comment"
|
|
||||||
echo "enable_pro=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "enable_enterprise=false" >> $GITHUB_OUTPUT
|
|
||||||
elif [[ "$COMMENT_BODY" == *"enterprise"* ]]; then
|
|
||||||
echo "enterprise flags detected in comment"
|
|
||||||
echo "enable_enterprise=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "enable_pro=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "No pro or enterprise flags detected in comment"
|
|
||||||
echo "enable_pro=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "enable_enterprise=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Add 'in_progress' reaction to comment
|
- name: Add 'in_progress' reaction to comment
|
||||||
id: add-eyes-reaction
|
id: add-eyes-reaction
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||||
@ -220,30 +200,15 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
# Set security settings based on flags
|
# Set security settings based on flags
|
||||||
if [ "${{ needs.check-comment.outputs.disable_security }}" == "false" ]; then
|
if [ "${{ needs.check-comment.outputs.disable_security }}" == "false" ]; then
|
||||||
DISABLE_ADDITIONAL_FEATURES="false"
|
DOCKER_SECURITY="true"
|
||||||
LOGIN_SECURITY="true"
|
LOGIN_SECURITY="true"
|
||||||
SECURITY_STATUS="🔒 Security Enabled"
|
SECURITY_STATUS="🔒 Security Enabled"
|
||||||
else
|
else
|
||||||
DISABLE_ADDITIONAL_FEATURES="true"
|
DOCKER_SECURITY="false"
|
||||||
LOGIN_SECURITY="false"
|
LOGIN_SECURITY="false"
|
||||||
SECURITY_STATUS="Security Disabled"
|
SECURITY_STATUS="Security Disabled"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Set pro/enterprise settings (enterprise implies pro)
|
|
||||||
if [ "${{ needs.check-comment.outputs.enable_enterprise }}" == "true" ]; then
|
|
||||||
PREMIUM_ENABLED="true"
|
|
||||||
PREMIUM_KEY="${{ secrets.ENTERPRISE_KEY }}"
|
|
||||||
PREMIUM_PROFEATURES_AUDIT_ENABLED="true"
|
|
||||||
elif [ "${{ needs.check-comment.outputs.enable_pro }}" == "true" ]; then
|
|
||||||
PREMIUM_ENABLED="true"
|
|
||||||
PREMIUM_KEY="${{ secrets.PREMIUM_KEY }}"
|
|
||||||
PREMIUM_PROFEATURES_AUDIT_ENABLED="true"
|
|
||||||
else
|
|
||||||
PREMIUM_ENABLED="false"
|
|
||||||
PREMIUM_KEY=""
|
|
||||||
PREMIUM_PROFEATURES_AUDIT_ENABLED="false"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# First create the docker-compose content locally
|
# First create the docker-compose content locally
|
||||||
cat > docker-compose.yml << EOF
|
cat > docker-compose.yml << EOF
|
||||||
version: '3.3'
|
version: '3.3'
|
||||||
@ -258,7 +223,7 @@ jobs:
|
|||||||
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/config:/configs:rw
|
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/config:/configs:rw
|
||||||
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/logs:/logs:rw
|
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/logs:/logs:rw
|
||||||
environment:
|
environment:
|
||||||
DISABLE_ADDITIONAL_FEATURES: "${DISABLE_ADDITIONAL_FEATURES}"
|
DISABLE_ADDITIONAL_FEATURES: "${DOCKER_SECURITY}"
|
||||||
SECURITY_ENABLELOGIN: "${LOGIN_SECURITY}"
|
SECURITY_ENABLELOGIN: "${LOGIN_SECURITY}"
|
||||||
SYSTEM_DEFAULTLOCALE: en-GB
|
SYSTEM_DEFAULTLOCALE: en-GB
|
||||||
UI_APPNAME: "Stirling-PDF PR#${{ needs.check-comment.outputs.pr_number }}"
|
UI_APPNAME: "Stirling-PDF PR#${{ needs.check-comment.outputs.pr_number }}"
|
||||||
@ -267,9 +232,6 @@ jobs:
|
|||||||
SYSTEM_MAXFILESIZE: "100"
|
SYSTEM_MAXFILESIZE: "100"
|
||||||
METRICS_ENABLED: "true"
|
METRICS_ENABLED: "true"
|
||||||
SYSTEM_GOOGLEVISIBILITY: "false"
|
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||||
PREMIUM_KEY: "${PREMIUM_KEY}"
|
|
||||||
PREMIUM_ENABLED: "${PREMIUM_ENABLED}"
|
|
||||||
PREMIUM_PROFEATURES_AUDIT_ENABLED: "${PREMIUM_PROFEATURES_AUDIT_ENABLED}"
|
|
||||||
restart: on-failure:5
|
restart: on-failure:5
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
@ -61,16 +61,8 @@ Make sure to place the entry under the correct language section. This helps main
|
|||||||
|
|
||||||
#### Windows command
|
#### Windows command
|
||||||
|
|
||||||
```powershell
|
```ps
|
||||||
python .github/scripts/check_language_properties.py --reference-file stirling-pdf\src\main\resources\messages_en_GB.properties --branch "" --files stirling-pdf\src\main\resources\messages_pl_PL.properties
|
python .github/scripts/check_language_properties.py --reference-file src\main\resources\messages_en_GB.properties --branch "" --files src\main\resources\messages_pl_PL.properties
|
||||||
|
|
||||||
python .github/scripts/check_language_properties.py --reference-file stirling-pdf\src\main\resources\messages_en_GB.properties --branch "" --check-file stirling-pdf\src\main\resources\messages_pl_PL.properties
|
python .github/scripts/check_language_properties.py --reference-file src\main\resources\messages_en_GB.properties --branch "" --check-file src\main\resources\messages_pl_PL.properties
|
||||||
```
|
|
||||||
|
|
||||||
#### Linux command
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 .github/scripts/check_language_properties.py --reference-file stirling-pdf/src/main/resources/messages_en_GB.properties --branch "" --files stirling-pdf/src/main/resources/messages_pl_PL.properties
|
|
||||||
|
|
||||||
python3 .github/scripts/check_language_properties.py --reference-file stirling-pdf/src/main/resources/messages_en_GB.properties --branch "" --check-file stirling-pdf/src/main/resources/messages_pl_PL.properties
|
|
||||||
```
|
```
|
||||||
|
63
build.gradle
63
build.gradle
@ -83,31 +83,6 @@ allprojects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
tasks.register('writeVersion') {
|
|
||||||
def propsFile = file("$projectDir/common/src/main/resources/version.properties")
|
|
||||||
def propsDir = propsFile.parentFile
|
|
||||||
|
|
||||||
doLast {
|
|
||||||
if (propsDir.exists()) {
|
|
||||||
if (propsFile.exists()) {
|
|
||||||
println "File exists: $propsFile"
|
|
||||||
} else {
|
|
||||||
println "$propsFile does not exist. Creating file."
|
|
||||||
propsFile.createNewFile()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
println "Creating directory: $propsDir"
|
|
||||||
propsDir.mkdirs()
|
|
||||||
propsFile.createNewFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
def props = new Properties()
|
|
||||||
props.setProperty("version", version)
|
|
||||||
props.store(propsFile.newWriter(), null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
apply plugin: 'java'
|
apply plugin: 'java'
|
||||||
apply plugin: 'java-library'
|
apply plugin: 'java-library'
|
||||||
@ -170,19 +145,6 @@ subprojects {
|
|||||||
test {
|
test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure all packaging tasks depend on writeVersion from root project
|
|
||||||
tasks.withType(org.springframework.boot.gradle.tasks.bundling.BootJar) {
|
|
||||||
dependsOn(rootProject.tasks.writeVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType(Jar) {
|
|
||||||
dependsOn(rootProject.tasks.writeVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType(org.gradle.api.tasks.bundling.Zip) {
|
|
||||||
dependsOn(rootProject.tasks.writeVersion)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(JavaCompile).configureEach {
|
tasks.withType(JavaCompile).configureEach {
|
||||||
@ -554,9 +516,32 @@ tasks.named("test") {
|
|||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.register('writeVersion') {
|
||||||
|
def propsFile = file("$projectDir/common/src/main/resources/version.properties")
|
||||||
|
def propsDir = propsFile.parentFile
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
if (propsDir.exists()) {
|
||||||
|
if (propsFile.exists()) {
|
||||||
|
println "File exists: $propsFile"
|
||||||
|
} else {
|
||||||
|
println "$propsFile does not exist. Creating file."
|
||||||
|
propsFile.createNewFile()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println "Creating directory: $propsDir"
|
||||||
|
propsDir.mkdirs()
|
||||||
|
propsFile.createNewFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
def props = new Properties()
|
||||||
|
props.setProperty("version", version)
|
||||||
|
props.store(propsFile.newWriter(), null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure all relevant processes depend on writeVersion
|
|
||||||
processResources.dependsOn(writeVersion)
|
processResources.dependsOn(writeVersion)
|
||||||
|
project(':stirling-pdf').tasks.bootJar.dependsOn(writeVersion)
|
||||||
|
|
||||||
tasks.register('printVersion') {
|
tasks.register('printVersion') {
|
||||||
doLast {
|
doLast {
|
||||||
|
@ -439,7 +439,6 @@ public class ApplicationProperties {
|
|||||||
@Data
|
@Data
|
||||||
public static class ProFeatures {
|
public static class ProFeatures {
|
||||||
private boolean ssoAutoLogin;
|
private boolean ssoAutoLogin;
|
||||||
private boolean database;
|
|
||||||
private CustomMetadata customMetadata = new CustomMetadata();
|
private CustomMetadata customMetadata = new CustomMetadata();
|
||||||
private GoogleDrive googleDrive = new GoogleDrive();
|
private GoogleDrive googleDrive = new GoogleDrive();
|
||||||
|
|
||||||
@ -485,14 +484,6 @@ public class ApplicationProperties {
|
|||||||
@Data
|
@Data
|
||||||
public static class EnterpriseFeatures {
|
public static class EnterpriseFeatures {
|
||||||
private PersistentMetrics persistentMetrics = new PersistentMetrics();
|
private PersistentMetrics persistentMetrics = new PersistentMetrics();
|
||||||
private Audit audit = new Audit();
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public static class Audit {
|
|
||||||
private boolean enabled = true;
|
|
||||||
private int level = 2; // 0=OFF, 1=BASIC, 2=STANDARD, 3=VERBOSE
|
|
||||||
private int retentionDays = 90;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class PersistentMetrics {
|
public static class PersistentMetrics {
|
||||||
|
@ -19,7 +19,6 @@ public class RequestUriUtils {
|
|||||||
|| requestURI.endsWith(".svg")
|
|| requestURI.endsWith(".svg")
|
||||||
|| requestURI.endsWith(".png")
|
|| requestURI.endsWith(".png")
|
||||||
|| requestURI.endsWith(".ico")
|
|| requestURI.endsWith(".ico")
|
||||||
|| requestURI.endsWith(".txt")
|
|
||||||
|| requestURI.endsWith(".webmanifest")
|
|| requestURI.endsWith(".webmanifest")
|
||||||
|| requestURI.startsWith(contextPath + "/api/v1/info/status");
|
|| requestURI.startsWith(contextPath + "/api/v1/info/status");
|
||||||
}
|
}
|
||||||
@ -36,7 +35,6 @@ public class RequestUriUtils {
|
|||||||
|| requestURI.endsWith(".png")
|
|| requestURI.endsWith(".png")
|
||||||
|| requestURI.endsWith(".ico")
|
|| requestURI.endsWith(".ico")
|
||||||
|| requestURI.endsWith(".css")
|
|| requestURI.endsWith(".css")
|
||||||
|| requestURI.endsWith(".txt")
|
|
||||||
|| requestURI.endsWith(".map")
|
|| requestURI.endsWith(".map")
|
||||||
|| requestURI.endsWith(".svg")
|
|| requestURI.endsWith(".svg")
|
||||||
|| requestURI.endsWith("popularity.txt")
|
|| requestURI.endsWith("popularity.txt")
|
||||||
|
@ -1,131 +0,0 @@
|
|||||||
package stirling.software.proprietary.audit;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.aspectj.lang.ProceedingJoinPoint;
|
|
||||||
import org.aspectj.lang.annotation.Around;
|
|
||||||
import org.aspectj.lang.annotation.Aspect;
|
|
||||||
import org.aspectj.lang.reflect.MethodSignature;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.context.request.RequestContextHolder;
|
|
||||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
|
||||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
|
||||||
import stirling.software.proprietary.service.AuditService;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Aspect for processing {@link Audited} annotations.
|
|
||||||
*/
|
|
||||||
@Aspect
|
|
||||||
@Component
|
|
||||||
@Slf4j
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class AuditAspect {
|
|
||||||
|
|
||||||
private final AuditService auditService;
|
|
||||||
private final AuditConfigurationProperties auditConfig;
|
|
||||||
|
|
||||||
@Around("@annotation(stirling.software.proprietary.audit.Audited)")
|
|
||||||
public Object auditMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
|
||||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
|
||||||
Method method = signature.getMethod();
|
|
||||||
Audited auditedAnnotation = method.getAnnotation(Audited.class);
|
|
||||||
|
|
||||||
// Fast path: use unified check to determine if we should audit
|
|
||||||
// This avoids all data collection if auditing is disabled
|
|
||||||
if (!AuditUtils.shouldAudit(method, auditConfig)) {
|
|
||||||
return joinPoint.proceed();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only create the map once we know we'll use it
|
|
||||||
Map<String, Object> auditData = AuditUtils.createBaseAuditData(joinPoint, auditedAnnotation.level());
|
|
||||||
|
|
||||||
// Add HTTP information if we're in a web context
|
|
||||||
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
|
||||||
if (attrs != null) {
|
|
||||||
HttpServletRequest req = attrs.getRequest();
|
|
||||||
String path = req.getRequestURI();
|
|
||||||
String httpMethod = req.getMethod();
|
|
||||||
AuditUtils.addHttpData(auditData, httpMethod, path, auditedAnnotation.level());
|
|
||||||
AuditUtils.addFileData(auditData, joinPoint, auditedAnnotation.level());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add arguments if requested and if at VERBOSE level, or if specifically requested
|
|
||||||
boolean includeArgs = auditedAnnotation.includeArgs() &&
|
|
||||||
(auditedAnnotation.level() == AuditLevel.VERBOSE ||
|
|
||||||
auditConfig.getAuditLevel() == AuditLevel.VERBOSE);
|
|
||||||
|
|
||||||
if (includeArgs) {
|
|
||||||
AuditUtils.addMethodArguments(auditData, joinPoint, AuditLevel.VERBOSE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record start time for latency calculation
|
|
||||||
long startTime = System.currentTimeMillis();
|
|
||||||
Object result;
|
|
||||||
try {
|
|
||||||
// Execute the method
|
|
||||||
result = joinPoint.proceed();
|
|
||||||
|
|
||||||
// Add success status
|
|
||||||
auditData.put("status", "success");
|
|
||||||
|
|
||||||
// Add result if requested and if at VERBOSE level
|
|
||||||
boolean includeResult = auditedAnnotation.includeResult() &&
|
|
||||||
(auditedAnnotation.level() == AuditLevel.VERBOSE ||
|
|
||||||
auditConfig.getAuditLevel() == AuditLevel.VERBOSE);
|
|
||||||
|
|
||||||
if (includeResult && result != null) {
|
|
||||||
// Use safe string conversion with size limiting
|
|
||||||
auditData.put("result", AuditUtils.safeToString(result, 1000));
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (Throwable ex) {
|
|
||||||
// Always add failure information regardless of level
|
|
||||||
auditData.put("status", "failure");
|
|
||||||
auditData.put("errorType", ex.getClass().getName());
|
|
||||||
auditData.put("errorMessage", ex.getMessage());
|
|
||||||
|
|
||||||
// Re-throw the exception
|
|
||||||
throw ex;
|
|
||||||
} finally {
|
|
||||||
// Add timing information - use isHttpRequest=false to ensure we get timing for non-HTTP methods
|
|
||||||
HttpServletResponse resp = attrs != null ? attrs.getResponse() : null;
|
|
||||||
boolean isHttpRequest = attrs != null;
|
|
||||||
AuditUtils.addTimingData(auditData, startTime, resp, auditedAnnotation.level(), isHttpRequest);
|
|
||||||
|
|
||||||
// Resolve the event type based on annotation and context
|
|
||||||
String httpMethod = null;
|
|
||||||
String path = null;
|
|
||||||
if (attrs != null) {
|
|
||||||
HttpServletRequest req = attrs.getRequest();
|
|
||||||
httpMethod = req.getMethod();
|
|
||||||
path = req.getRequestURI();
|
|
||||||
}
|
|
||||||
|
|
||||||
AuditEventType eventType = AuditUtils.resolveEventType(
|
|
||||||
method,
|
|
||||||
joinPoint.getTarget().getClass(),
|
|
||||||
path,
|
|
||||||
httpMethod,
|
|
||||||
auditedAnnotation
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if we should use string type instead
|
|
||||||
String typeString = auditedAnnotation.typeString();
|
|
||||||
if (eventType == AuditEventType.HTTP_REQUEST && StringUtils.isNotEmpty(typeString)) {
|
|
||||||
// Use the string type (for backward compatibility)
|
|
||||||
auditService.audit(typeString, auditData, auditedAnnotation.level());
|
|
||||||
} else {
|
|
||||||
// Use the enum type (preferred)
|
|
||||||
auditService.audit(eventType, auditData, auditedAnnotation.level());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,62 +0,0 @@
|
|||||||
package stirling.software.proprietary.audit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Standardized audit event types for the application.
|
|
||||||
*/
|
|
||||||
public enum AuditEventType {
|
|
||||||
// Authentication events - BASIC level
|
|
||||||
USER_LOGIN("User login"),
|
|
||||||
USER_LOGOUT("User logout"),
|
|
||||||
USER_FAILED_LOGIN("Failed login attempt"),
|
|
||||||
|
|
||||||
// User/admin events - BASIC level
|
|
||||||
USER_PROFILE_UPDATE("User or profile operation"),
|
|
||||||
|
|
||||||
// System configuration events - STANDARD level
|
|
||||||
SETTINGS_CHANGED("System or admin settings operation"),
|
|
||||||
|
|
||||||
// File operations - STANDARD level
|
|
||||||
FILE_OPERATION("File operation"),
|
|
||||||
|
|
||||||
// PDF operations - STANDARD level
|
|
||||||
PDF_PROCESS("PDF processing operation"),
|
|
||||||
|
|
||||||
// HTTP requests - STANDARD level
|
|
||||||
HTTP_REQUEST("HTTP request");
|
|
||||||
|
|
||||||
private final String description;
|
|
||||||
|
|
||||||
AuditEventType(String description) {
|
|
||||||
this.description = description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDescription() {
|
|
||||||
return description;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the enum value from a string representation.
|
|
||||||
* Useful for backward compatibility with string-based event types.
|
|
||||||
*
|
|
||||||
* @param type The string representation of the event type
|
|
||||||
* @return The corresponding enum value or null if not found
|
|
||||||
*/
|
|
||||||
public static AuditEventType fromString(String type) {
|
|
||||||
if (type == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return AuditEventType.valueOf(type);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// If the exact enum name doesn't match, try finding a similar one
|
|
||||||
for (AuditEventType eventType : values()) {
|
|
||||||
if (eventType.name().equalsIgnoreCase(type) ||
|
|
||||||
eventType.getDescription().equalsIgnoreCase(type)) {
|
|
||||||
return eventType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
package stirling.software.proprietary.audit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines the different levels of audit logging available in the application.
|
|
||||||
*/
|
|
||||||
public enum AuditLevel {
|
|
||||||
/**
|
|
||||||
* OFF - No audit logging (level 0)
|
|
||||||
* Disables all audit logging except for critical security events
|
|
||||||
*/
|
|
||||||
OFF(0),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* BASIC - Minimal audit logging (level 1)
|
|
||||||
* Includes:
|
|
||||||
* - Authentication events (login, logout, failed logins)
|
|
||||||
* - Password changes
|
|
||||||
* - User/role changes
|
|
||||||
* - System configuration changes
|
|
||||||
*/
|
|
||||||
BASIC(1),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* STANDARD - Standard audit logging (level 2)
|
|
||||||
* Includes everything in BASIC plus:
|
|
||||||
* - All HTTP requests (basic info: URL, method, status)
|
|
||||||
* - File operations (upload, download, process)
|
|
||||||
* - PDF operations (view, edit, etc.)
|
|
||||||
* - User operations
|
|
||||||
*/
|
|
||||||
STANDARD(2),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* VERBOSE - Detailed audit logging (level 3)
|
|
||||||
* Includes everything in STANDARD plus:
|
|
||||||
* - Request headers and parameters
|
|
||||||
* - Method parameters
|
|
||||||
* - Operation results
|
|
||||||
* - Detailed timing information
|
|
||||||
*/
|
|
||||||
VERBOSE(3);
|
|
||||||
|
|
||||||
private final int level;
|
|
||||||
|
|
||||||
AuditLevel(int level) {
|
|
||||||
this.level = level;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getLevel() {
|
|
||||||
return level;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if this audit level includes the specified level
|
|
||||||
* @param otherLevel The level to check against
|
|
||||||
* @return true if this level is equal to or greater than the specified level
|
|
||||||
*/
|
|
||||||
public boolean includes(AuditLevel otherLevel) {
|
|
||||||
return this.level >= otherLevel.level;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an AuditLevel from an integer value
|
|
||||||
* @param level The integer level (0-3)
|
|
||||||
* @return The corresponding AuditLevel
|
|
||||||
*/
|
|
||||||
public static AuditLevel fromInt(int level) {
|
|
||||||
// Ensure level is within valid bounds
|
|
||||||
int boundedLevel = Math.min(Math.max(level, 0), 3);
|
|
||||||
|
|
||||||
for (AuditLevel auditLevel : values()) {
|
|
||||||
if (auditLevel.level == boundedLevel) {
|
|
||||||
return auditLevel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default to STANDARD if somehow we didn't match
|
|
||||||
return STANDARD;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,375 +0,0 @@
|
|||||||
package stirling.software.proprietary.audit;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.aspectj.lang.ProceedingJoinPoint;
|
|
||||||
import org.aspectj.lang.reflect.MethodSignature;
|
|
||||||
import org.slf4j.MDC;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.web.context.request.RequestContextHolder;
|
|
||||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
import stirling.software.common.util.RequestUriUtils;
|
|
||||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.IntStream;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared utilities for audit aspects to ensure consistent behavior
|
|
||||||
* across different audit mechanisms.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class AuditUtils {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a standard audit data map with common attributes based on the current audit level
|
|
||||||
*
|
|
||||||
* @param joinPoint The AspectJ join point
|
|
||||||
* @param auditLevel The current audit level
|
|
||||||
* @return A map with standard audit data
|
|
||||||
*/
|
|
||||||
public static Map<String, Object> createBaseAuditData(ProceedingJoinPoint joinPoint, AuditLevel auditLevel) {
|
|
||||||
Map<String, Object> data = new HashMap<>();
|
|
||||||
|
|
||||||
// Common data for all levels
|
|
||||||
data.put("timestamp", Instant.now().toString());
|
|
||||||
|
|
||||||
// Add principal if available
|
|
||||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
|
||||||
if (auth != null && auth.getName() != null) {
|
|
||||||
data.put("principal", auth.getName());
|
|
||||||
} else {
|
|
||||||
data.put("principal", "system");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add class name and method name only at VERBOSE level
|
|
||||||
if (auditLevel.includes(AuditLevel.VERBOSE)) {
|
|
||||||
data.put("className", joinPoint.getTarget().getClass().getName());
|
|
||||||
data.put("methodName", ((MethodSignature) joinPoint.getSignature()).getMethod().getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add HTTP-specific information to the audit data if available
|
|
||||||
*
|
|
||||||
* @param data The existing audit data map
|
|
||||||
* @param httpMethod The HTTP method (GET, POST, etc.)
|
|
||||||
* @param path The request path
|
|
||||||
* @param auditLevel The current audit level
|
|
||||||
*/
|
|
||||||
public static void addHttpData(Map<String, Object> data, String httpMethod, String path, AuditLevel auditLevel) {
|
|
||||||
if (httpMethod == null || path == null) {
|
|
||||||
return; // Skip if we don't have basic HTTP info
|
|
||||||
}
|
|
||||||
|
|
||||||
// BASIC level HTTP data
|
|
||||||
data.put("httpMethod", httpMethod);
|
|
||||||
data.put("path", path);
|
|
||||||
|
|
||||||
// Get request attributes safely
|
|
||||||
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
|
||||||
if (attrs == null) {
|
|
||||||
return; // No request context available
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpServletRequest req = attrs.getRequest();
|
|
||||||
if (req == null) {
|
|
||||||
return; // No request available
|
|
||||||
}
|
|
||||||
|
|
||||||
// STANDARD level HTTP data
|
|
||||||
if (auditLevel.includes(AuditLevel.STANDARD)) {
|
|
||||||
data.put("clientIp", req.getRemoteAddr());
|
|
||||||
data.put("sessionId", req.getSession(false) != null ? req.getSession(false).getId() : null);
|
|
||||||
data.put("requestId", MDC.get("requestId"));
|
|
||||||
|
|
||||||
// Form data for POST/PUT/PATCH
|
|
||||||
if (("POST".equalsIgnoreCase(httpMethod) ||
|
|
||||||
"PUT".equalsIgnoreCase(httpMethod) ||
|
|
||||||
"PATCH".equalsIgnoreCase(httpMethod)) && req.getContentType() != null) {
|
|
||||||
|
|
||||||
String contentType = req.getContentType();
|
|
||||||
if (contentType.contains("application/x-www-form-urlencoded") ||
|
|
||||||
contentType.contains("multipart/form-data")) {
|
|
||||||
|
|
||||||
Map<String, String[]> params = new HashMap<>(req.getParameterMap());
|
|
||||||
// Remove CSRF token from logged parameters
|
|
||||||
params.remove("_csrf");
|
|
||||||
|
|
||||||
if (!params.isEmpty()) {
|
|
||||||
data.put("formParams", params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add file information to the audit data if available
|
|
||||||
*
|
|
||||||
* @param data The existing audit data map
|
|
||||||
* @param joinPoint The AspectJ join point
|
|
||||||
* @param auditLevel The current audit level
|
|
||||||
*/
|
|
||||||
public static void addFileData(Map<String, Object> data, ProceedingJoinPoint joinPoint, AuditLevel auditLevel) {
|
|
||||||
if (auditLevel.includes(AuditLevel.STANDARD)) {
|
|
||||||
List<MultipartFile> files = Arrays.stream(joinPoint.getArgs())
|
|
||||||
.filter(a -> a instanceof MultipartFile)
|
|
||||||
.map(a -> (MultipartFile)a)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
if (!files.isEmpty()) {
|
|
||||||
List<Map<String,Object>> fileInfos = files.stream().map(f -> {
|
|
||||||
Map<String,Object> m = new HashMap<>();
|
|
||||||
m.put("name", f.getOriginalFilename());
|
|
||||||
m.put("size", f.getSize());
|
|
||||||
m.put("type", f.getContentType());
|
|
||||||
return m;
|
|
||||||
}).collect(Collectors.toList());
|
|
||||||
|
|
||||||
data.put("files", fileInfos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add method arguments to the audit data
|
|
||||||
*
|
|
||||||
* @param data The existing audit data map
|
|
||||||
* @param joinPoint The AspectJ join point
|
|
||||||
* @param auditLevel The current audit level
|
|
||||||
*/
|
|
||||||
public static void addMethodArguments(Map<String, Object> data, ProceedingJoinPoint joinPoint, AuditLevel auditLevel) {
|
|
||||||
if (auditLevel.includes(AuditLevel.VERBOSE)) {
|
|
||||||
MethodSignature sig = (MethodSignature) joinPoint.getSignature();
|
|
||||||
String[] names = sig.getParameterNames();
|
|
||||||
Object[] vals = joinPoint.getArgs();
|
|
||||||
if (names != null && vals != null) {
|
|
||||||
IntStream.range(0, names.length)
|
|
||||||
.forEach(i -> {
|
|
||||||
if (vals[i] != null) {
|
|
||||||
// Convert objects to safe string representation
|
|
||||||
data.put("arg_" + names[i], safeToString(vals[i], 500));
|
|
||||||
} else {
|
|
||||||
data.put("arg_" + names[i], null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Safely convert an object to string with size limiting
|
|
||||||
*
|
|
||||||
* @param obj The object to convert
|
|
||||||
* @param maxLength Maximum length of the resulting string
|
|
||||||
* @return A safe string representation, truncated if needed
|
|
||||||
*/
|
|
||||||
public static String safeToString(Object obj, int maxLength) {
|
|
||||||
if (obj == null) {
|
|
||||||
return "null";
|
|
||||||
}
|
|
||||||
|
|
||||||
String result;
|
|
||||||
try {
|
|
||||||
// Handle common types directly to avoid toString() overhead
|
|
||||||
if (obj instanceof String) {
|
|
||||||
result = (String) obj;
|
|
||||||
} else if (obj instanceof Number || obj instanceof Boolean) {
|
|
||||||
result = obj.toString();
|
|
||||||
} else if (obj instanceof byte[]) {
|
|
||||||
result = "[binary data length=" + ((byte[]) obj).length + "]";
|
|
||||||
} else {
|
|
||||||
// For complex objects, use toString but handle exceptions
|
|
||||||
result = obj.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Truncate if necessary
|
|
||||||
if (result != null && result.length() > maxLength) {
|
|
||||||
return StringUtils.truncate(result, maxLength - 3) + "...";
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (Exception e) {
|
|
||||||
// If toString() fails, return the class name
|
|
||||||
return "[" + obj.getClass().getName() + " - toString() failed]";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if a method should be audited based on config and annotation
|
|
||||||
*
|
|
||||||
* @param method The method to check
|
|
||||||
* @param auditConfig The audit configuration
|
|
||||||
* @return true if the method should be audited
|
|
||||||
*/
|
|
||||||
public static boolean shouldAudit(Method method, AuditConfigurationProperties auditConfig) {
|
|
||||||
// First check if audit is globally enabled - fast path
|
|
||||||
if (!auditConfig.isEnabled()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for annotation override
|
|
||||||
Audited auditedAnnotation = method.getAnnotation(Audited.class);
|
|
||||||
AuditLevel requiredLevel = (auditedAnnotation != null)
|
|
||||||
? auditedAnnotation.level()
|
|
||||||
: AuditLevel.BASIC;
|
|
||||||
|
|
||||||
// Check if the required level is enabled
|
|
||||||
return auditConfig.getAuditLevel().includes(requiredLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add timing and response status data to the audit record
|
|
||||||
*
|
|
||||||
* @param data The audit data to add to
|
|
||||||
* @param startTime The start time in milliseconds
|
|
||||||
* @param response The HTTP response (may be null for non-HTTP methods)
|
|
||||||
* @param level The current audit level
|
|
||||||
* @param isHttpRequest Whether this is an HTTP request (controller) or a regular method call
|
|
||||||
*/
|
|
||||||
public static void addTimingData(Map<String, Object> data, long startTime, HttpServletResponse response, AuditLevel level, boolean isHttpRequest) {
|
|
||||||
if (level.includes(AuditLevel.STANDARD)) {
|
|
||||||
// For HTTP requests, let ControllerAuditAspect handle timing separately
|
|
||||||
// For non-HTTP methods, add execution time here
|
|
||||||
if (!isHttpRequest) {
|
|
||||||
data.put("latencyMs", System.currentTimeMillis() - startTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add HTTP status code if available
|
|
||||||
if (response != null) {
|
|
||||||
try {
|
|
||||||
data.put("statusCode", response.getStatus());
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Ignore - response might be in an inconsistent state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve the event type to use for auditing, considering annotations and context
|
|
||||||
*
|
|
||||||
* @param method The method being audited
|
|
||||||
* @param controller The controller class
|
|
||||||
* @param path The request path (may be null for non-HTTP methods)
|
|
||||||
* @param httpMethod The HTTP method (may be null for non-HTTP methods)
|
|
||||||
* @param annotation The @Audited annotation (may be null)
|
|
||||||
* @return The resolved event type (never null)
|
|
||||||
*/
|
|
||||||
public static AuditEventType resolveEventType(Method method, Class<?> controller, String path, String httpMethod, Audited annotation) {
|
|
||||||
// First check if we have an explicit annotation
|
|
||||||
if (annotation != null && annotation.type() != AuditEventType.HTTP_REQUEST) {
|
|
||||||
return annotation.type();
|
|
||||||
}
|
|
||||||
|
|
||||||
// For HTTP methods, infer based on controller and path
|
|
||||||
if (httpMethod != null && path != null) {
|
|
||||||
String cls = controller.getSimpleName().toLowerCase();
|
|
||||||
String pkg = controller.getPackage().getName().toLowerCase();
|
|
||||||
|
|
||||||
if ("GET".equals(httpMethod)) return AuditEventType.HTTP_REQUEST;
|
|
||||||
|
|
||||||
if (cls.contains("user") || cls.contains("auth") || pkg.contains("auth")
|
|
||||||
|| path.startsWith("/user") || path.startsWith("/login")) {
|
|
||||||
return AuditEventType.USER_PROFILE_UPDATE;
|
|
||||||
} else if (cls.contains("admin") || path.startsWith("/admin") || path.startsWith("/settings")) {
|
|
||||||
return AuditEventType.SETTINGS_CHANGED;
|
|
||||||
} else if (cls.contains("file") || path.startsWith("/file")
|
|
||||||
|| path.matches("(?i).*/(upload|download)/.*")) {
|
|
||||||
return AuditEventType.FILE_OPERATION;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default for non-HTTP methods or when no specific match
|
|
||||||
return AuditEventType.PDF_PROCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine the appropriate audit level to use
|
|
||||||
*
|
|
||||||
* @param method The method to check
|
|
||||||
* @param defaultLevel The default level to use if no annotation present
|
|
||||||
* @param auditConfig The audit configuration
|
|
||||||
* @return The audit level to use
|
|
||||||
*/
|
|
||||||
public static AuditLevel getEffectiveAuditLevel(Method method, AuditLevel defaultLevel, AuditConfigurationProperties auditConfig) {
|
|
||||||
Audited auditedAnnotation = method.getAnnotation(Audited.class);
|
|
||||||
if (auditedAnnotation != null) {
|
|
||||||
// Method has @Audited - use its level
|
|
||||||
return auditedAnnotation.level();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use default level (typically from global config)
|
|
||||||
return defaultLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine the appropriate audit event type to use
|
|
||||||
*
|
|
||||||
* @param method The method being audited
|
|
||||||
* @param controller The controller class
|
|
||||||
* @param path The request path
|
|
||||||
* @param httpMethod The HTTP method
|
|
||||||
* @return The determined audit event type
|
|
||||||
*/
|
|
||||||
public static AuditEventType determineAuditEventType(Method method, Class<?> controller, String path, String httpMethod) {
|
|
||||||
// First check for explicit annotation
|
|
||||||
Audited auditedAnnotation = method.getAnnotation(Audited.class);
|
|
||||||
if (auditedAnnotation != null) {
|
|
||||||
return auditedAnnotation.type();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise infer from controller and path
|
|
||||||
String cls = controller.getSimpleName().toLowerCase();
|
|
||||||
String pkg = controller.getPackage().getName().toLowerCase();
|
|
||||||
|
|
||||||
if ("GET".equals(httpMethod)) return AuditEventType.HTTP_REQUEST;
|
|
||||||
|
|
||||||
if (cls.contains("user") || cls.contains("auth") || pkg.contains("auth")
|
|
||||||
|| path.startsWith("/user") || path.startsWith("/login")) {
|
|
||||||
return AuditEventType.USER_PROFILE_UPDATE;
|
|
||||||
} else if (cls.contains("admin") || path.startsWith("/admin") || path.startsWith("/settings")) {
|
|
||||||
return AuditEventType.SETTINGS_CHANGED;
|
|
||||||
} else if (cls.contains("file") || path.startsWith("/file")
|
|
||||||
|| path.matches("(?i).*/(upload|download)/.*")) {
|
|
||||||
return AuditEventType.FILE_OPERATION;
|
|
||||||
} else {
|
|
||||||
return AuditEventType.PDF_PROCESS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current HTTP request if available
|
|
||||||
*
|
|
||||||
* @return The current request or null if not in a request context
|
|
||||||
*/
|
|
||||||
public static HttpServletRequest getCurrentRequest() {
|
|
||||||
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
|
||||||
return attrs != null ? attrs.getRequest() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a GET request is for a static resource
|
|
||||||
*
|
|
||||||
* @param request The HTTP request
|
|
||||||
* @return true if this is a static resource request
|
|
||||||
*/
|
|
||||||
public static boolean isStaticResourceRequest(HttpServletRequest request) {
|
|
||||||
return request != null && !RequestUriUtils.isTrackableResource(
|
|
||||||
request.getContextPath(), request.getRequestURI());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
package stirling.software.proprietary.audit;
|
|
||||||
|
|
||||||
import java.lang.annotation.ElementType;
|
|
||||||
import java.lang.annotation.Retention;
|
|
||||||
import java.lang.annotation.RetentionPolicy;
|
|
||||||
import java.lang.annotation.Target;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Annotation for methods that should be audited.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* {@code
|
|
||||||
* @Audited(type = AuditEventType.USER_REGISTRATION, level = AuditLevel.BASIC)
|
|
||||||
* public void registerUser(String username) {
|
|
||||||
* // Method implementation
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* For backward compatibility, string-based event types are still supported:
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* {@code
|
|
||||||
* @Audited(typeString = "CUSTOM_EVENT_TYPE", level = AuditLevel.BASIC)
|
|
||||||
* public void customOperation() {
|
|
||||||
* // Method implementation
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
@Target(ElementType.METHOD)
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
public @interface Audited {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The type of audit event using the standardized AuditEventType enum.
|
|
||||||
* This is the preferred way to specify the event type.
|
|
||||||
*
|
|
||||||
* If both type() and typeString() are specified, type() takes precedence.
|
|
||||||
*/
|
|
||||||
AuditEventType type() default AuditEventType.HTTP_REQUEST;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The type of audit event as a string (e.g., "FILE_UPLOAD", "USER_REGISTRATION").
|
|
||||||
* Provided for backward compatibility and custom event types not in the enum.
|
|
||||||
*
|
|
||||||
* If both type() and typeString() are specified, type() takes precedence.
|
|
||||||
*/
|
|
||||||
String typeString() default "";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The audit level at which this event should be logged
|
|
||||||
*/
|
|
||||||
AuditLevel level() default AuditLevel.STANDARD;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Should method arguments be included in the audit event
|
|
||||||
*/
|
|
||||||
boolean includeArgs() default true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Should the method return value be included in the audit event
|
|
||||||
*/
|
|
||||||
boolean includeResult() default false;
|
|
||||||
}
|
|
@ -1,211 +0,0 @@
|
|||||||
package stirling.software.proprietary.audit;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.aspectj.lang.ProceedingJoinPoint;
|
|
||||||
import org.aspectj.lang.annotation.Around;
|
|
||||||
import org.aspectj.lang.annotation.Aspect;
|
|
||||||
import org.aspectj.lang.reflect.MethodSignature;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PatchMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PutMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.context.request.RequestContextHolder;
|
|
||||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
|
||||||
import stirling.software.proprietary.service.AuditService;
|
|
||||||
|
|
||||||
import java.lang.annotation.Annotation;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Aspect for automatically auditing controller methods with web mappings
|
|
||||||
* (GetMapping, PostMapping, etc.)
|
|
||||||
*/
|
|
||||||
@Aspect
|
|
||||||
@Component
|
|
||||||
@Slf4j
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class ControllerAuditAspect {
|
|
||||||
|
|
||||||
private final AuditService auditService;
|
|
||||||
private final AuditConfigurationProperties auditConfig;
|
|
||||||
|
|
||||||
|
|
||||||
@Around("execution(* org.springframework.web.servlet.resource.ResourceHttpRequestHandler.handleRequest(..))")
|
|
||||||
public Object auditStaticResource(ProceedingJoinPoint jp) throws Throwable {
|
|
||||||
return auditController(jp, "GET");
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Intercept all methods with GetMapping annotation
|
|
||||||
*/
|
|
||||||
@Around("@annotation(org.springframework.web.bind.annotation.GetMapping)")
|
|
||||||
public Object auditGetMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
|
||||||
return auditController(joinPoint, "GET");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Intercept all methods with PostMapping annotation
|
|
||||||
*/
|
|
||||||
@Around("@annotation(org.springframework.web.bind.annotation.PostMapping)")
|
|
||||||
public Object auditPostMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
|
||||||
return auditController(joinPoint, "POST");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Intercept all methods with PutMapping annotation
|
|
||||||
*/
|
|
||||||
@Around("@annotation(org.springframework.web.bind.annotation.PutMapping)")
|
|
||||||
public Object auditPutMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
|
||||||
return auditController(joinPoint, "PUT");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Intercept all methods with DeleteMapping annotation
|
|
||||||
*/
|
|
||||||
@Around("@annotation(org.springframework.web.bind.annotation.DeleteMapping)")
|
|
||||||
public Object auditDeleteMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
|
||||||
return auditController(joinPoint, "DELETE");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Intercept all methods with PatchMapping annotation
|
|
||||||
*/
|
|
||||||
@Around("@annotation(org.springframework.web.bind.annotation.PatchMapping)")
|
|
||||||
public Object auditPatchMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
|
||||||
return auditController(joinPoint, "PATCH");
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object auditController(ProceedingJoinPoint joinPoint, String httpMethod) throws Throwable {
|
|
||||||
MethodSignature sig = (MethodSignature) joinPoint.getSignature();
|
|
||||||
Method method = sig.getMethod();
|
|
||||||
|
|
||||||
// Fast path: check if auditing is enabled before doing any work
|
|
||||||
// This avoids all data collection if auditing is disabled
|
|
||||||
if (!AuditUtils.shouldAudit(method, auditConfig)) {
|
|
||||||
return joinPoint.proceed();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if method is explicitly annotated with @Audited
|
|
||||||
Audited auditedAnnotation = method.getAnnotation(Audited.class);
|
|
||||||
AuditLevel level = auditConfig.getAuditLevel();
|
|
||||||
|
|
||||||
// If @Audited annotation is present, respect its level setting
|
|
||||||
if (auditedAnnotation != null) {
|
|
||||||
// Use the level from annotation if it's stricter than global level
|
|
||||||
level = auditedAnnotation.level();
|
|
||||||
}
|
|
||||||
|
|
||||||
String path = getRequestPath(method, httpMethod);
|
|
||||||
|
|
||||||
// Skip static GET resources
|
|
||||||
if ("GET".equals(httpMethod)) {
|
|
||||||
HttpServletRequest maybe = AuditUtils.getCurrentRequest();
|
|
||||||
if (maybe != null && AuditUtils.isStaticResourceRequest(maybe)) {
|
|
||||||
return joinPoint.proceed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
|
||||||
HttpServletRequest req = attrs != null ? attrs.getRequest() : null;
|
|
||||||
HttpServletResponse resp = attrs != null ? attrs.getResponse() : null;
|
|
||||||
|
|
||||||
long start = System.currentTimeMillis();
|
|
||||||
|
|
||||||
// Use AuditUtils to create the base audit data
|
|
||||||
Map<String, Object> data = AuditUtils.createBaseAuditData(joinPoint, level);
|
|
||||||
|
|
||||||
// Add HTTP-specific information
|
|
||||||
AuditUtils.addHttpData(data, httpMethod, path, level);
|
|
||||||
|
|
||||||
// Add file information if present
|
|
||||||
AuditUtils.addFileData(data, joinPoint, level);
|
|
||||||
|
|
||||||
// Add method arguments if at VERBOSE level
|
|
||||||
if (level.includes(AuditLevel.VERBOSE)) {
|
|
||||||
AuditUtils.addMethodArguments(data, joinPoint, level);
|
|
||||||
}
|
|
||||||
|
|
||||||
Object result = null;
|
|
||||||
try {
|
|
||||||
result = joinPoint.proceed();
|
|
||||||
data.put("outcome", "success");
|
|
||||||
} catch (Throwable ex) {
|
|
||||||
data.put("outcome", "failure");
|
|
||||||
data.put("errorType", ex.getClass().getSimpleName());
|
|
||||||
data.put("errorMessage", ex.getMessage());
|
|
||||||
throw ex;
|
|
||||||
} finally {
|
|
||||||
// Handle timing directly for HTTP requests
|
|
||||||
if (level.includes(AuditLevel.STANDARD)) {
|
|
||||||
data.put("latencyMs", System.currentTimeMillis() - start);
|
|
||||||
if (resp != null) data.put("statusCode", resp.getStatus());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call AuditUtils but with isHttpRequest=true to skip additional timing
|
|
||||||
AuditUtils.addTimingData(data, start, resp, level, true);
|
|
||||||
|
|
||||||
// Add result for VERBOSE level
|
|
||||||
if (level.includes(AuditLevel.VERBOSE) && result != null) {
|
|
||||||
// Use safe string conversion with size limiting
|
|
||||||
data.put("result", AuditUtils.safeToString(result, 1000));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the event type using the unified method
|
|
||||||
AuditEventType eventType = AuditUtils.resolveEventType(
|
|
||||||
method,
|
|
||||||
joinPoint.getTarget().getClass(),
|
|
||||||
path,
|
|
||||||
httpMethod,
|
|
||||||
auditedAnnotation
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if we should use string type instead (for backward compatibility)
|
|
||||||
if (auditedAnnotation != null) {
|
|
||||||
String typeString = auditedAnnotation.typeString();
|
|
||||||
if (eventType == AuditEventType.HTTP_REQUEST && StringUtils.isNotEmpty(typeString)) {
|
|
||||||
auditService.audit(typeString, data, level);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the enum type
|
|
||||||
auditService.audit(eventType, data, level);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Using AuditUtils.determineAuditEventType instead
|
|
||||||
|
|
||||||
private String getRequestPath(Method method, String httpMethod) {
|
|
||||||
String base = "";
|
|
||||||
RequestMapping cm = method.getDeclaringClass().getAnnotation(RequestMapping.class);
|
|
||||||
if (cm != null && cm.value().length > 0) base = cm.value()[0];
|
|
||||||
String mp = "";
|
|
||||||
Annotation ann = switch (httpMethod) {
|
|
||||||
case "GET" -> method.getAnnotation(GetMapping.class);
|
|
||||||
case "POST" -> method.getAnnotation(PostMapping.class);
|
|
||||||
case "PUT" -> method.getAnnotation(PutMapping.class);
|
|
||||||
case "DELETE" -> method.getAnnotation(DeleteMapping.class);
|
|
||||||
case "PATCH" -> method.getAnnotation(PatchMapping.class);
|
|
||||||
default -> null;
|
|
||||||
};
|
|
||||||
if (ann instanceof GetMapping gm && gm.value().length > 0) mp = gm.value()[0];
|
|
||||||
if (ann instanceof PostMapping pm && pm.value().length > 0) mp = pm.value()[0];
|
|
||||||
if (ann instanceof PutMapping pum && pum.value().length > 0) mp = pum.value()[0];
|
|
||||||
if (ann instanceof DeleteMapping dm && dm.value().length > 0) mp = dm.value()[0];
|
|
||||||
if (ann instanceof PatchMapping pam && pam.value().length > 0) mp = pam.value()[0];
|
|
||||||
return base + mp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Using AuditUtils.getCurrentRequest instead
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
package stirling.software.proprietary.config;
|
|
||||||
|
|
||||||
import org.slf4j.MDC;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.core.task.TaskDecorator;
|
|
||||||
import org.springframework.scheduling.annotation.EnableAsync;
|
|
||||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@EnableAsync
|
|
||||||
public class AsyncConfig {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MDC context-propagating task decorator
|
|
||||||
* Copies MDC context from the caller thread to the async executor thread
|
|
||||||
*/
|
|
||||||
static class MDCContextTaskDecorator implements TaskDecorator {
|
|
||||||
@Override
|
|
||||||
public Runnable decorate(Runnable runnable) {
|
|
||||||
// Capture the MDC context from the current thread
|
|
||||||
Map<String, String> contextMap = MDC.getCopyOfContextMap();
|
|
||||||
|
|
||||||
return () -> {
|
|
||||||
try {
|
|
||||||
// Set the captured context on the worker thread
|
|
||||||
if (contextMap != null) {
|
|
||||||
MDC.setContextMap(contextMap);
|
|
||||||
}
|
|
||||||
// Execute the task
|
|
||||||
runnable.run();
|
|
||||||
} finally {
|
|
||||||
// Clear the context to prevent memory leaks
|
|
||||||
MDC.clear();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean(name = "auditExecutor")
|
|
||||||
public Executor auditExecutor() {
|
|
||||||
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
|
|
||||||
exec.setCorePoolSize(2);
|
|
||||||
exec.setMaxPoolSize(8);
|
|
||||||
exec.setQueueCapacity(1_000);
|
|
||||||
exec.setThreadNamePrefix("audit-");
|
|
||||||
|
|
||||||
// Set the task decorator to propagate MDC context
|
|
||||||
exec.setTaskDecorator(new MDCContextTaskDecorator());
|
|
||||||
|
|
||||||
exec.initialize();
|
|
||||||
return exec;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
package stirling.software.proprietary.config;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import org.springframework.core.Ordered;
|
|
||||||
import org.springframework.core.annotation.Order;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
|
||||||
import stirling.software.proprietary.audit.AuditLevel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration properties for the audit system.
|
|
||||||
* Reads values from the ApplicationProperties under premium.enterpriseFeatures.audit
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Getter
|
|
||||||
@Component
|
|
||||||
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
|
|
||||||
public class AuditConfigurationProperties {
|
|
||||||
|
|
||||||
private final boolean enabled;
|
|
||||||
private final int level;
|
|
||||||
private final int retentionDays;
|
|
||||||
|
|
||||||
public AuditConfigurationProperties(ApplicationProperties applicationProperties) {
|
|
||||||
ApplicationProperties.Premium.EnterpriseFeatures.Audit auditConfig =
|
|
||||||
applicationProperties.getPremium().getEnterpriseFeatures().getAudit();
|
|
||||||
// Read values directly from configuration
|
|
||||||
this.enabled = auditConfig.isEnabled();
|
|
||||||
|
|
||||||
// Ensure level is within valid bounds (0-3)
|
|
||||||
int configLevel = auditConfig.getLevel();
|
|
||||||
this.level = Math.min(Math.max(configLevel, 0), 3);
|
|
||||||
|
|
||||||
// Retention days (0 means infinite)
|
|
||||||
this.retentionDays = auditConfig.getRetentionDays();
|
|
||||||
|
|
||||||
log.debug("Initialized audit configuration: enabled={}, level={}, retentionDays={} (0=infinite)",
|
|
||||||
this.enabled, this.level, this.retentionDays);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the audit level as an enum
|
|
||||||
* @return The current AuditLevel
|
|
||||||
*/
|
|
||||||
public AuditLevel getAuditLevel() {
|
|
||||||
return AuditLevel.fromInt(level);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the current audit level includes the specified level
|
|
||||||
* @param requiredLevel The level to check against
|
|
||||||
* @return true if auditing is enabled and the current level includes the required level
|
|
||||||
*/
|
|
||||||
public boolean isLevelEnabled(AuditLevel requiredLevel) {
|
|
||||||
return enabled && getAuditLevel().includes(requiredLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the effective retention period in days
|
|
||||||
* @return The number of days to retain audit records, or -1 for infinite retention
|
|
||||||
*/
|
|
||||||
public int getEffectiveRetentionDays() {
|
|
||||||
// 0 means infinite retention
|
|
||||||
return retentionDays <= 0 ? -1 : retentionDays;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package stirling.software.proprietary.config;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
|
||||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration to explicitly enable JPA repositories and scheduling for the audit system.
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
@EnableTransactionManagement
|
|
||||||
@EnableJpaRepositories(basePackages = "stirling.software.proprietary.repository")
|
|
||||||
@EnableScheduling
|
|
||||||
public class AuditJpaConfig {
|
|
||||||
// This configuration enables JPA repositories in the specified package
|
|
||||||
// and enables scheduling for audit cleanup tasks
|
|
||||||
// No additional beans or methods needed
|
|
||||||
}
|
|
@ -1,74 +0,0 @@
|
|||||||
package stirling.software.proprietary.config;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import org.slf4j.MDC;
|
|
||||||
import org.springframework.boot.actuate.audit.AuditEvent;
|
|
||||||
import org.springframework.boot.actuate.audit.AuditEventRepository;
|
|
||||||
import org.springframework.context.annotation.Primary;
|
|
||||||
import org.springframework.scheduling.annotation.Async;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.util.CollectionUtils;
|
|
||||||
import stirling.software.proprietary.model.security.PersistentAuditEvent;
|
|
||||||
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
|
|
||||||
import stirling.software.proprietary.util.SecretMasker;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
@Primary
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Slf4j
|
|
||||||
public class CustomAuditEventRepository implements AuditEventRepository {
|
|
||||||
|
|
||||||
private final PersistentAuditEventRepository repo;
|
|
||||||
private final ObjectMapper mapper;
|
|
||||||
|
|
||||||
/* ── READ side intentionally inert (endpoint disabled) ── */
|
|
||||||
@Override
|
|
||||||
public List<AuditEvent> find(String p, Instant after, String type) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── WRITE side (async) ───────────────────────────────── */
|
|
||||||
@Async("auditExecutor")
|
|
||||||
@Override
|
|
||||||
public void add(AuditEvent ev) {
|
|
||||||
try {
|
|
||||||
Map<String, Object> clean =
|
|
||||||
CollectionUtils.isEmpty(ev.getData())
|
|
||||||
? Map.of()
|
|
||||||
: SecretMasker.mask(ev.getData());
|
|
||||||
|
|
||||||
|
|
||||||
if (clean.isEmpty() ||
|
|
||||||
(clean.size() == 1 && clean.containsKey("details"))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String rid = MDC.get("requestId");
|
|
||||||
|
|
||||||
|
|
||||||
if (rid != null) {
|
|
||||||
clean = new java.util.HashMap<>(clean);
|
|
||||||
clean.put("requestId", rid);
|
|
||||||
}
|
|
||||||
|
|
||||||
String auditEventData = mapper.writeValueAsString(clean);
|
|
||||||
log.debug("AuditEvent data (JSON): {}",auditEventData);
|
|
||||||
|
|
||||||
PersistentAuditEvent ent = PersistentAuditEvent.builder()
|
|
||||||
.principal(ev.getPrincipal())
|
|
||||||
.type(ev.getType())
|
|
||||||
.data(auditEventData)
|
|
||||||
.timestamp(ev.getTimestamp())
|
|
||||||
.build();
|
|
||||||
repo.save(ent);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace(); // fail-open
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,344 +0,0 @@
|
|||||||
package stirling.software.proprietary.controller;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.time.ZoneId;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.springframework.data.domain.Page;
|
|
||||||
import org.springframework.data.domain.PageRequest;
|
|
||||||
import org.springframework.data.domain.Pageable;
|
|
||||||
import org.springframework.data.domain.Sort;
|
|
||||||
import org.springframework.format.annotation.DateTimeFormat;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.stereotype.Controller;
|
|
||||||
import org.springframework.ui.Model;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
|
||||||
import org.springframework.web.bind.annotation.ResponseBody;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import stirling.software.proprietary.audit.AuditEventType;
|
|
||||||
import stirling.software.proprietary.audit.AuditLevel;
|
|
||||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
|
||||||
import stirling.software.proprietary.model.security.PersistentAuditEvent;
|
|
||||||
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
|
|
||||||
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controller for the audit dashboard.
|
|
||||||
* Admin-only access.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Controller
|
|
||||||
@RequestMapping("/audit")
|
|
||||||
@PreAuthorize("hasRole('ADMIN')")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@EnterpriseEndpoint
|
|
||||||
public class AuditDashboardController {
|
|
||||||
|
|
||||||
private final PersistentAuditEventRepository auditRepository;
|
|
||||||
private final AuditConfigurationProperties auditConfig;
|
|
||||||
private final ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display the audit dashboard.
|
|
||||||
*/
|
|
||||||
@GetMapping
|
|
||||||
public String showDashboard(Model model) {
|
|
||||||
model.addAttribute("auditEnabled", auditConfig.isEnabled());
|
|
||||||
model.addAttribute("auditLevel", auditConfig.getAuditLevel());
|
|
||||||
model.addAttribute("auditLevelInt", auditConfig.getLevel());
|
|
||||||
model.addAttribute("retentionDays", auditConfig.getRetentionDays());
|
|
||||||
|
|
||||||
// Add audit level enum values for display
|
|
||||||
model.addAttribute("auditLevels", AuditLevel.values());
|
|
||||||
|
|
||||||
// Add audit event types for the dropdown
|
|
||||||
model.addAttribute("auditEventTypes", AuditEventType.values());
|
|
||||||
|
|
||||||
return "audit/dashboard";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get audit events data for the dashboard tables.
|
|
||||||
*/
|
|
||||||
@GetMapping("/data")
|
|
||||||
@ResponseBody
|
|
||||||
public Map<String, Object> getAuditData(
|
|
||||||
@RequestParam(value = "page", defaultValue = "0") int page,
|
|
||||||
@RequestParam(value = "size", defaultValue = "30") int size,
|
|
||||||
@RequestParam(value = "type", required = false) String type,
|
|
||||||
@RequestParam(value = "principal", required = false) String principal,
|
|
||||||
@RequestParam(value = "startDate", required = false)
|
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
|
||||||
@RequestParam(value = "endDate", required = false)
|
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, HttpServletRequest request) {
|
|
||||||
|
|
||||||
|
|
||||||
Pageable pageable = PageRequest.of(page, size, Sort.by("timestamp").descending());
|
|
||||||
Page<PersistentAuditEvent> events;
|
|
||||||
|
|
||||||
String mode;
|
|
||||||
|
|
||||||
if (type != null && principal != null && startDate != null && endDate != null) {
|
|
||||||
mode = "principal + type + startDate + endDate";
|
|
||||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
events = auditRepository.findByPrincipalAndTypeAndTimestampBetween(principal, type, start, end, pageable);
|
|
||||||
} else if (type != null && principal != null) {
|
|
||||||
mode = "principal + type";
|
|
||||||
events = auditRepository.findByPrincipalAndType(principal, type, pageable);
|
|
||||||
} else if (type != null && startDate != null && endDate != null) {
|
|
||||||
mode = "type + startDate + endDate";
|
|
||||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
events = auditRepository.findByTypeAndTimestampBetween(type, start, end, pageable);
|
|
||||||
} else if (principal != null && startDate != null && endDate != null) {
|
|
||||||
mode = "principal + startDate + endDate";
|
|
||||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
events = auditRepository.findByPrincipalAndTimestampBetween(principal, start, end, pageable);
|
|
||||||
} else if (startDate != null && endDate != null) {
|
|
||||||
mode = "startDate + endDate";
|
|
||||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
events = auditRepository.findByTimestampBetween(start, end, pageable);
|
|
||||||
} else if (type != null) {
|
|
||||||
mode = "type";
|
|
||||||
events = auditRepository.findByType(type, pageable);
|
|
||||||
} else if (principal != null) {
|
|
||||||
mode = "principal";
|
|
||||||
events = auditRepository.findByPrincipal(principal, pageable);
|
|
||||||
} else {
|
|
||||||
mode = "all";
|
|
||||||
events = auditRepository.findAll(pageable);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
List<PersistentAuditEvent> content = events.getContent();
|
|
||||||
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
|
||||||
response.put("content", content);
|
|
||||||
response.put("totalPages", events.getTotalPages());
|
|
||||||
response.put("totalElements", events.getTotalElements());
|
|
||||||
response.put("currentPage", events.getNumber());
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get statistics for charts.
|
|
||||||
*/
|
|
||||||
@GetMapping("/stats")
|
|
||||||
@ResponseBody
|
|
||||||
public Map<String, Object> getAuditStats(
|
|
||||||
@RequestParam(value = "days", defaultValue = "7") int days) {
|
|
||||||
|
|
||||||
// Get events from the last X days
|
|
||||||
Instant startDate = Instant.now().minus(java.time.Duration.ofDays(days));
|
|
||||||
List<PersistentAuditEvent> events = auditRepository.findByTimestampAfter(startDate);
|
|
||||||
|
|
||||||
// Count events by type
|
|
||||||
Map<String, Long> eventsByType = events.stream()
|
|
||||||
.collect(Collectors.groupingBy(PersistentAuditEvent::getType, Collectors.counting()));
|
|
||||||
|
|
||||||
// Count events by principal
|
|
||||||
Map<String, Long> eventsByPrincipal = events.stream()
|
|
||||||
.collect(Collectors.groupingBy(PersistentAuditEvent::getPrincipal, Collectors.counting()));
|
|
||||||
|
|
||||||
// Count events by day
|
|
||||||
Map<String, Long> eventsByDay = events.stream()
|
|
||||||
.collect(Collectors.groupingBy(
|
|
||||||
e -> LocalDateTime.ofInstant(e.getTimestamp(), ZoneId.systemDefault())
|
|
||||||
.format(DateTimeFormatter.ISO_LOCAL_DATE),
|
|
||||||
Collectors.counting()));
|
|
||||||
|
|
||||||
Map<String, Object> stats = new HashMap<>();
|
|
||||||
stats.put("eventsByType", eventsByType);
|
|
||||||
stats.put("eventsByPrincipal", eventsByPrincipal);
|
|
||||||
stats.put("eventsByDay", eventsByDay);
|
|
||||||
stats.put("totalEvents", events.size());
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all unique event types from the database for filtering.
|
|
||||||
*/
|
|
||||||
@GetMapping("/types")
|
|
||||||
@ResponseBody
|
|
||||||
public List<String> getAuditTypes() {
|
|
||||||
// Get distinct event types from the database
|
|
||||||
List<String> dbTypes = auditRepository.findDistinctEventTypes();
|
|
||||||
|
|
||||||
// Include standard enum types in case they're not in the database yet
|
|
||||||
List<String> enumTypes = Arrays.stream(AuditEventType.values())
|
|
||||||
.map(AuditEventType::name)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
// Combine both sources, remove duplicates, and sort
|
|
||||||
Set<String> combinedTypes = new HashSet<>();
|
|
||||||
combinedTypes.addAll(dbTypes);
|
|
||||||
combinedTypes.addAll(enumTypes);
|
|
||||||
|
|
||||||
return combinedTypes.stream().sorted().collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export audit data as CSV.
|
|
||||||
*/
|
|
||||||
@GetMapping("/export")
|
|
||||||
public ResponseEntity<byte[]> exportAuditData(
|
|
||||||
@RequestParam(value = "type", required = false) String type,
|
|
||||||
@RequestParam(value = "principal", required = false) String principal,
|
|
||||||
@RequestParam(value = "startDate", required = false)
|
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
|
||||||
@RequestParam(value = "endDate", required = false)
|
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
|
|
||||||
|
|
||||||
// Get data with same filtering as getAuditData
|
|
||||||
List<PersistentAuditEvent> events;
|
|
||||||
|
|
||||||
if (type != null && principal != null && startDate != null && endDate != null) {
|
|
||||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
events = auditRepository.findAllByPrincipalAndTypeAndTimestampBetweenForExport(
|
|
||||||
principal, type, start, end);
|
|
||||||
} else if (type != null && principal != null) {
|
|
||||||
events = auditRepository.findAllByPrincipalAndTypeForExport(principal, type);
|
|
||||||
} else if (type != null && startDate != null && endDate != null) {
|
|
||||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
events = auditRepository.findAllByTypeAndTimestampBetweenForExport(type, start, end);
|
|
||||||
} else if (principal != null && startDate != null && endDate != null) {
|
|
||||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
events = auditRepository.findAllByPrincipalAndTimestampBetweenForExport(principal, start, end);
|
|
||||||
} else if (startDate != null && endDate != null) {
|
|
||||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
events = auditRepository.findAllByTimestampBetweenForExport(start, end);
|
|
||||||
} else if (type != null) {
|
|
||||||
events = auditRepository.findByTypeForExport(type);
|
|
||||||
} else if (principal != null) {
|
|
||||||
events = auditRepository.findAllByPrincipalForExport(principal);
|
|
||||||
} else {
|
|
||||||
events = auditRepository.findAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to CSV
|
|
||||||
StringBuilder csv = new StringBuilder();
|
|
||||||
csv.append("ID,Principal,Type,Timestamp,Data\n");
|
|
||||||
|
|
||||||
DateTimeFormatter formatter = DateTimeFormatter.ISO_INSTANT;
|
|
||||||
|
|
||||||
for (PersistentAuditEvent event : events) {
|
|
||||||
csv.append(event.getId()).append(",");
|
|
||||||
csv.append(escapeCSV(event.getPrincipal())).append(",");
|
|
||||||
csv.append(escapeCSV(event.getType())).append(",");
|
|
||||||
csv.append(formatter.format(event.getTimestamp())).append(",");
|
|
||||||
csv.append(escapeCSV(event.getData())).append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] csvBytes = csv.toString().getBytes();
|
|
||||||
|
|
||||||
// Set up HTTP headers for download
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
|
||||||
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
|
|
||||||
headers.setContentDispositionFormData("attachment", "audit_export.csv");
|
|
||||||
|
|
||||||
return ResponseEntity.ok()
|
|
||||||
.headers(headers)
|
|
||||||
.body(csvBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export audit data as JSON.
|
|
||||||
*/
|
|
||||||
@GetMapping("/export/json")
|
|
||||||
public ResponseEntity<byte[]> exportAuditDataJson(
|
|
||||||
@RequestParam(value = "type", required = false) String type,
|
|
||||||
@RequestParam(value = "principal", required = false) String principal,
|
|
||||||
@RequestParam(value = "startDate", required = false)
|
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
|
||||||
@RequestParam(value = "endDate", required = false)
|
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
|
|
||||||
|
|
||||||
// Get data with same filtering as getAuditData
|
|
||||||
List<PersistentAuditEvent> events;
|
|
||||||
|
|
||||||
if (type != null && principal != null && startDate != null && endDate != null) {
|
|
||||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
events = auditRepository.findAllByPrincipalAndTypeAndTimestampBetweenForExport(
|
|
||||||
principal, type, start, end);
|
|
||||||
} else if (type != null && principal != null) {
|
|
||||||
events = auditRepository.findAllByPrincipalAndTypeForExport(principal, type);
|
|
||||||
} else if (type != null && startDate != null && endDate != null) {
|
|
||||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
events = auditRepository.findAllByTypeAndTimestampBetweenForExport(type, start, end);
|
|
||||||
} else if (principal != null && startDate != null && endDate != null) {
|
|
||||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
events = auditRepository.findAllByPrincipalAndTimestampBetweenForExport(principal, start, end);
|
|
||||||
} else if (startDate != null && endDate != null) {
|
|
||||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
events = auditRepository.findAllByTimestampBetweenForExport(start, end);
|
|
||||||
} else if (type != null) {
|
|
||||||
events = auditRepository.findByTypeForExport(type);
|
|
||||||
} else if (principal != null) {
|
|
||||||
events = auditRepository.findAllByPrincipalForExport(principal);
|
|
||||||
} else {
|
|
||||||
events = auditRepository.findAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to JSON
|
|
||||||
try {
|
|
||||||
byte[] jsonBytes = objectMapper.writeValueAsBytes(events);
|
|
||||||
|
|
||||||
// Set up HTTP headers for download
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
|
||||||
headers.setContentDispositionFormData("attachment", "audit_export.json");
|
|
||||||
|
|
||||||
return ResponseEntity.ok()
|
|
||||||
.headers(headers)
|
|
||||||
.body(jsonBytes);
|
|
||||||
} catch (JsonProcessingException e) {
|
|
||||||
log.error("Error serializing audit events to JSON", e);
|
|
||||||
return ResponseEntity.internalServerError().build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper method to escape CSV fields.
|
|
||||||
*/
|
|
||||||
private String escapeCSV(String field) {
|
|
||||||
if (field == null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
// Replace double quotes with two double quotes and wrap in quotes
|
|
||||||
return "\"" + field.replace("\"", "\"\"") + "\"";
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
package stirling.software.proprietary.model.security;
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.*;
|
|
||||||
import org.hibernate.annotations.Index;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(
|
|
||||||
name = "audit_events",
|
|
||||||
indexes = {
|
|
||||||
@jakarta.persistence.Index(name = "idx_audit_timestamp", columnList = "timestamp"),
|
|
||||||
@jakarta.persistence.Index(name = "idx_audit_principal", columnList = "principal"),
|
|
||||||
@jakarta.persistence.Index(name = "idx_audit_type", columnList = "type"),
|
|
||||||
@jakarta.persistence.Index(name = "idx_audit_principal_type", columnList = "principal,type"),
|
|
||||||
@jakarta.persistence.Index(name = "idx_audit_type_timestamp", columnList = "type,timestamp")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@Data @Builder @NoArgsConstructor @AllArgsConstructor
|
|
||||||
public class PersistentAuditEvent {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
private String principal;
|
|
||||||
private String type;
|
|
||||||
|
|
||||||
@Lob
|
|
||||||
private String data; // JSON blob
|
|
||||||
|
|
||||||
private Instant timestamp;
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
package stirling.software.proprietary.repository;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.springframework.data.domain.Page;
|
|
||||||
import org.springframework.data.domain.Pageable;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.data.jpa.repository.Modifying;
|
|
||||||
import org.springframework.data.jpa.repository.Query;
|
|
||||||
import org.springframework.data.repository.query.Param;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import stirling.software.proprietary.model.security.PersistentAuditEvent;
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
public interface PersistentAuditEventRepository
|
|
||||||
extends JpaRepository<PersistentAuditEvent, Long> {
|
|
||||||
|
|
||||||
// Basic queries
|
|
||||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%'))")
|
|
||||||
Page<PersistentAuditEvent> findByPrincipal(@Param("principal") String principal, Pageable pageable);
|
|
||||||
Page<PersistentAuditEvent> findByType(String type, Pageable pageable);
|
|
||||||
Page<PersistentAuditEvent> findByTimestampBetween(Instant startDate, Instant endDate, Pageable pageable);
|
|
||||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.type = :type")
|
|
||||||
Page<PersistentAuditEvent> findByPrincipalAndType(@Param("principal") String principal, @Param("type") String type, Pageable pageable);
|
|
||||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.timestamp BETWEEN :startDate AND :endDate")
|
|
||||||
Page<PersistentAuditEvent> findByPrincipalAndTimestampBetween(@Param("principal") String principal, @Param("startDate") Instant startDate, @Param("endDate") Instant endDate, Pageable pageable);
|
|
||||||
Page<PersistentAuditEvent> findByTypeAndTimestampBetween(String type, Instant startDate, Instant endDate, Pageable pageable);
|
|
||||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.type = :type AND e.timestamp BETWEEN :startDate AND :endDate")
|
|
||||||
Page<PersistentAuditEvent> findByPrincipalAndTypeAndTimestampBetween(@Param("principal") String principal, @Param("type") String type, @Param("startDate") Instant startDate, @Param("endDate") Instant endDate, Pageable pageable);
|
|
||||||
|
|
||||||
// Non-paged versions for export
|
|
||||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%'))")
|
|
||||||
List<PersistentAuditEvent> findAllByPrincipalForExport(@Param("principal") String principal);
|
|
||||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE e.type = :type")
|
|
||||||
List<PersistentAuditEvent> findByTypeForExport(@Param("type") String type);
|
|
||||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE e.timestamp BETWEEN :startDate AND :endDate")
|
|
||||||
List<PersistentAuditEvent> findAllByTimestampBetweenForExport(@Param("startDate") Instant startDate, @Param("endDate") Instant endDate);
|
|
||||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE e.timestamp > :startDate")
|
|
||||||
List<PersistentAuditEvent> findByTimestampAfter(@Param("startDate") Instant startDate);
|
|
||||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.type = :type")
|
|
||||||
List<PersistentAuditEvent> findAllByPrincipalAndTypeForExport(@Param("principal") String principal, @Param("type") String type);
|
|
||||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.timestamp BETWEEN :startDate AND :endDate")
|
|
||||||
List<PersistentAuditEvent> findAllByPrincipalAndTimestampBetweenForExport(@Param("principal") String principal, @Param("startDate") Instant startDate, @Param("endDate") Instant endDate);
|
|
||||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE e.type = :type AND e.timestamp BETWEEN :startDate AND :endDate")
|
|
||||||
List<PersistentAuditEvent> findAllByTypeAndTimestampBetweenForExport(@Param("type") String type, @Param("startDate") Instant startDate, @Param("endDate") Instant endDate);
|
|
||||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.type = :type AND e.timestamp BETWEEN :startDate AND :endDate")
|
|
||||||
List<PersistentAuditEvent> findAllByPrincipalAndTypeAndTimestampBetweenForExport(@Param("principal") String principal, @Param("type") String type, @Param("startDate") Instant startDate, @Param("endDate") Instant endDate);
|
|
||||||
|
|
||||||
// Cleanup queries
|
|
||||||
@Query("DELETE FROM PersistentAuditEvent e WHERE e.timestamp < ?1")
|
|
||||||
@Modifying
|
|
||||||
@Transactional
|
|
||||||
int deleteByTimestampBefore(Instant cutoffDate);
|
|
||||||
|
|
||||||
// Find IDs for batch deletion - using JPQL with setMaxResults instead of native query
|
|
||||||
@Query("SELECT e.id FROM PersistentAuditEvent e WHERE e.timestamp < ?1 ORDER BY e.id")
|
|
||||||
List<Long> findIdsForBatchDeletion(Instant cutoffDate, Pageable pageable);
|
|
||||||
|
|
||||||
// Stats queries
|
|
||||||
@Query("SELECT e.type, COUNT(e) FROM PersistentAuditEvent e GROUP BY e.type")
|
|
||||||
List<Object[]> countByType();
|
|
||||||
|
|
||||||
@Query("SELECT e.principal, COUNT(e) FROM PersistentAuditEvent e GROUP BY e.principal")
|
|
||||||
List<Object[]> countByPrincipal();
|
|
||||||
|
|
||||||
// Get distinct event types for filtering
|
|
||||||
@Query("SELECT DISTINCT e.type FROM PersistentAuditEvent e ORDER BY e.type")
|
|
||||||
List<String> findDistinctEventTypes();
|
|
||||||
}
|
|
@ -4,8 +4,6 @@ import jakarta.servlet.ServletException;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.security.authentication.BadCredentialsException;
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
@ -15,16 +13,6 @@ import org.springframework.security.authentication.LockedException;
|
|||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||||
|
|
||||||
import jakarta.servlet.ServletException;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.proprietary.audit.AuditEventType;
|
|
||||||
import stirling.software.proprietary.audit.AuditLevel;
|
|
||||||
import stirling.software.proprietary.audit.Audited;
|
|
||||||
import stirling.software.proprietary.security.model.User;
|
import stirling.software.proprietary.security.model.User;
|
||||||
import stirling.software.proprietary.security.service.LoginAttemptService;
|
import stirling.software.proprietary.security.service.LoginAttemptService;
|
||||||
import stirling.software.proprietary.security.service.UserService;
|
import stirling.software.proprietary.security.service.UserService;
|
||||||
@ -43,7 +31,6 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Audited(type = AuditEventType.USER_FAILED_LOGIN, level = AuditLevel.BASIC)
|
|
||||||
public void onAuthenticationFailure(
|
public void onAuthenticationFailure(
|
||||||
HttpServletRequest request,
|
HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
|
@ -1,11 +1,4 @@
|
|||||||
package stirling.software.proprietary.security;
|
package stirling.software.proprietary.security;
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
|
||||||
import org.springframework.security.web.savedrequest.SavedRequest;
|
|
||||||
|
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
@ -17,9 +10,6 @@ import org.springframework.security.core.Authentication;
|
|||||||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||||
import org.springframework.security.web.savedrequest.SavedRequest;
|
import org.springframework.security.web.savedrequest.SavedRequest;
|
||||||
import stirling.software.common.util.RequestUriUtils;
|
import stirling.software.common.util.RequestUriUtils;
|
||||||
import stirling.software.proprietary.audit.AuditEventType;
|
|
||||||
import stirling.software.proprietary.audit.AuditLevel;
|
|
||||||
import stirling.software.proprietary.audit.Audited;
|
|
||||||
import stirling.software.proprietary.security.service.LoginAttemptService;
|
import stirling.software.proprietary.security.service.LoginAttemptService;
|
||||||
import stirling.software.proprietary.security.service.UserService;
|
import stirling.software.proprietary.security.service.UserService;
|
||||||
|
|
||||||
@ -37,7 +27,6 @@ public class CustomAuthenticationSuccessHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Audited(type = AuditEventType.USER_LOGIN, level = AuditLevel.BASIC)
|
|
||||||
public void onAuthenticationSuccess(
|
public void onAuthenticationSuccess(
|
||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
|
@ -23,9 +23,6 @@ import stirling.software.common.model.ApplicationProperties.Security.OAUTH2;
|
|||||||
import stirling.software.common.model.ApplicationProperties.Security.SAML2;
|
import stirling.software.common.model.ApplicationProperties.Security.SAML2;
|
||||||
import stirling.software.common.model.oauth2.KeycloakProvider;
|
import stirling.software.common.model.oauth2.KeycloakProvider;
|
||||||
import stirling.software.common.util.UrlUtils;
|
import stirling.software.common.util.UrlUtils;
|
||||||
import stirling.software.proprietary.audit.AuditEventType;
|
|
||||||
import stirling.software.proprietary.audit.AuditLevel;
|
|
||||||
import stirling.software.proprietary.audit.Audited;
|
|
||||||
import stirling.software.proprietary.security.saml2.CertificateUtils;
|
import stirling.software.proprietary.security.saml2.CertificateUtils;
|
||||||
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||||
|
|
||||||
@ -40,7 +37,6 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
|||||||
private final AppConfig appConfig;
|
private final AppConfig appConfig;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Audited(type = AuditEventType.USER_LOGOUT, level = AuditLevel.BASIC)
|
|
||||||
public void onLogoutSuccess(
|
public void onLogoutSuccess(
|
||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
@ -62,11 +62,9 @@ public class InitialSecuritySetup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
userService.saveAll(usersWithoutTeam); // batch save
|
userService.saveAll(usersWithoutTeam); // batch save
|
||||||
if(usersWithoutTeam != null && !usersWithoutTeam.isEmpty()) {
|
|
||||||
log.info(
|
log.info(
|
||||||
"Assigned {} user(s) without a team to the default team.", usersWithoutTeam.size());
|
"Assigned {} user(s) without a team to the default team.", usersWithoutTeam.size());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeAdminUser() throws SQLException, UnsupportedProviderException {
|
private void initializeAdminUser() throws SQLException, UnsupportedProviderException {
|
||||||
String initialUsername =
|
String initialUsername =
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
package stirling.software.proprietary.security.config;
|
|
||||||
|
|
||||||
import java.lang.annotation.ElementType;
|
|
||||||
import java.lang.annotation.Retention;
|
|
||||||
import java.lang.annotation.RetentionPolicy;
|
|
||||||
import java.lang.annotation.Target;
|
|
||||||
|
|
||||||
/** Annotation to mark endpoints that require an Enterprise license. */
|
|
||||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
public @interface EnterpriseEndpoint {}
|
|
@ -1,30 +0,0 @@
|
|||||||
package stirling.software.proprietary.security.config;
|
|
||||||
|
|
||||||
import org.aspectj.lang.ProceedingJoinPoint;
|
|
||||||
import org.aspectj.lang.annotation.Around;
|
|
||||||
import org.aspectj.lang.annotation.Aspect;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
|
||||||
|
|
||||||
@Aspect
|
|
||||||
@Component
|
|
||||||
public class EnterpriseEndpointAspect {
|
|
||||||
|
|
||||||
private final boolean runningEE;
|
|
||||||
|
|
||||||
public EnterpriseEndpointAspect(@Qualifier("runningEE") boolean runningEE) {
|
|
||||||
this.runningEE = runningEE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Around(
|
|
||||||
"@annotation(stirling.software.proprietary.security.config.EnterpriseEndpoint) || @within(stirling.software.proprietary.security.config.EnterpriseEndpoint)")
|
|
||||||
public Object checkEnterpriseAccess(ProceedingJoinPoint joinPoint) throws Throwable {
|
|
||||||
if (!runningEE) {
|
|
||||||
throw new ResponseStatusException(
|
|
||||||
HttpStatus.FORBIDDEN, "This endpoint requires an Enterprise license");
|
|
||||||
}
|
|
||||||
return joinPoint.proceed();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,112 +0,0 @@
|
|||||||
package stirling.software.proprietary.service;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import org.springframework.data.domain.PageRequest;
|
|
||||||
import org.springframework.data.domain.Sort;
|
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
|
||||||
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service to periodically clean up old audit events based on retention policy.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class AuditCleanupService {
|
|
||||||
|
|
||||||
private final PersistentAuditEventRepository auditRepository;
|
|
||||||
private final AuditConfigurationProperties auditConfig;
|
|
||||||
|
|
||||||
// Default batch size for deletions
|
|
||||||
private static final int BATCH_SIZE = 10000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scheduled task that runs daily to clean up old audit events.
|
|
||||||
* The retention period is configurable in settings.yml.
|
|
||||||
*/
|
|
||||||
@Scheduled(fixedDelay = 1, initialDelay = 1, timeUnit = TimeUnit.DAYS)
|
|
||||||
public void cleanupOldAuditEvents() {
|
|
||||||
if (!auditConfig.isEnabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int retentionDays = auditConfig.getRetentionDays();
|
|
||||||
if (retentionDays <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Starting audit cleanup for events older than {} days", retentionDays);
|
|
||||||
|
|
||||||
try {
|
|
||||||
Instant cutoffDate = Instant.now().minus(retentionDays, ChronoUnit.DAYS);
|
|
||||||
int totalDeleted = batchDeleteEvents(cutoffDate);
|
|
||||||
log.info("Successfully cleaned up {} audit events older than {}", totalDeleted, cutoffDate);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error cleaning up old audit events", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs batch deletion of events to prevent long-running transactions
|
|
||||||
* and potential database locks.
|
|
||||||
*/
|
|
||||||
private int batchDeleteEvents(Instant cutoffDate) {
|
|
||||||
int totalDeleted = 0;
|
|
||||||
boolean hasMore = true;
|
|
||||||
|
|
||||||
while (hasMore) {
|
|
||||||
// Start a new transaction for each batch
|
|
||||||
List<Long> batchIds = findBatchOfIdsToDelete(cutoffDate);
|
|
||||||
|
|
||||||
if (batchIds.isEmpty()) {
|
|
||||||
hasMore = false;
|
|
||||||
} else {
|
|
||||||
int deleted = deleteBatch(batchIds);
|
|
||||||
totalDeleted += deleted;
|
|
||||||
|
|
||||||
// If we got fewer records than the batch size, we're done
|
|
||||||
if (batchIds.size() < BATCH_SIZE) {
|
|
||||||
hasMore = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalDeleted;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a batch of IDs to delete.
|
|
||||||
*/
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
private List<Long> findBatchOfIdsToDelete(Instant cutoffDate) {
|
|
||||||
PageRequest pageRequest = PageRequest.of(0, BATCH_SIZE, Sort.by("id"));
|
|
||||||
return auditRepository.findIdsForBatchDeletion(cutoffDate, pageRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a batch of events by ID.
|
|
||||||
* Each batch is in its own transaction.
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
private int deleteBatch(List<Long> batchIds) {
|
|
||||||
if (batchIds.isEmpty()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int batchSize = batchIds.size();
|
|
||||||
auditRepository.deleteAllByIdInBatch(batchIds);
|
|
||||||
log.debug("Deleted batch of {} audit events", batchSize);
|
|
||||||
|
|
||||||
return batchSize;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,165 +0,0 @@
|
|||||||
package stirling.software.proprietary.service;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.boot.actuate.audit.AuditEvent;
|
|
||||||
import org.springframework.boot.actuate.audit.AuditEventRepository;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import stirling.software.proprietary.audit.AuditEventType;
|
|
||||||
import stirling.software.proprietary.audit.AuditLevel;
|
|
||||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service for creating manual audit events throughout the application.
|
|
||||||
* This provides easy access to audit functionality in any component.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
public class AuditService {
|
|
||||||
|
|
||||||
private final AuditEventRepository repository;
|
|
||||||
private final AuditConfigurationProperties auditConfig;
|
|
||||||
private final boolean runningEE;
|
|
||||||
|
|
||||||
public AuditService(AuditEventRepository repository,
|
|
||||||
AuditConfigurationProperties auditConfig,
|
|
||||||
@org.springframework.beans.factory.annotation.Qualifier("runningEE") boolean runningEE) {
|
|
||||||
this.repository = repository;
|
|
||||||
this.auditConfig = auditConfig;
|
|
||||||
this.runningEE = runningEE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Record an audit event for the current authenticated user with a specific audit level
|
|
||||||
* using the standardized AuditEventType enum
|
|
||||||
*
|
|
||||||
* @param type The event type from AuditEventType enum
|
|
||||||
* @param data Additional event data (will be automatically sanitized)
|
|
||||||
* @param level The minimum audit level required for this event to be logged
|
|
||||||
*/
|
|
||||||
public void audit(AuditEventType type, Map<String, Object> data, AuditLevel level) {
|
|
||||||
// Skip auditing if this level is not enabled or if not Enterprise edition
|
|
||||||
if (!auditConfig.isEnabled() || !auditConfig.getAuditLevel().includes(level) || !runningEE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String principal = getCurrentUsername();
|
|
||||||
repository.add(new AuditEvent(principal, type.name(), data));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Record an audit event for the current authenticated user with standard level
|
|
||||||
* using the standardized AuditEventType enum
|
|
||||||
*
|
|
||||||
* @param type The event type from AuditEventType enum
|
|
||||||
* @param data Additional event data (will be automatically sanitized)
|
|
||||||
*/
|
|
||||||
public void audit(AuditEventType type, Map<String, Object> data) {
|
|
||||||
// Default to STANDARD level
|
|
||||||
audit(type, data, AuditLevel.STANDARD);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Record an audit event for a specific user with a specific audit level
|
|
||||||
* using the standardized AuditEventType enum
|
|
||||||
*
|
|
||||||
* @param principal The username or system identifier
|
|
||||||
* @param type The event type from AuditEventType enum
|
|
||||||
* @param data Additional event data (will be automatically sanitized)
|
|
||||||
* @param level The minimum audit level required for this event to be logged
|
|
||||||
*/
|
|
||||||
public void audit(String principal, AuditEventType type, Map<String, Object> data, AuditLevel level) {
|
|
||||||
// Skip auditing if this level is not enabled or if not Enterprise edition
|
|
||||||
if (!auditConfig.isLevelEnabled(level) || !runningEE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
repository.add(new AuditEvent(principal, type.name(), data));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Record an audit event for a specific user with standard level
|
|
||||||
* using the standardized AuditEventType enum
|
|
||||||
*
|
|
||||||
* @param principal The username or system identifier
|
|
||||||
* @param type The event type from AuditEventType enum
|
|
||||||
* @param data Additional event data (will be automatically sanitized)
|
|
||||||
*/
|
|
||||||
public void audit(String principal, AuditEventType type, Map<String, Object> data) {
|
|
||||||
// Default to STANDARD level
|
|
||||||
audit(principal, type, data, AuditLevel.STANDARD);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Record an audit event for the current authenticated user with a specific audit level
|
|
||||||
* using a string-based event type (for backward compatibility)
|
|
||||||
*
|
|
||||||
* @param type The event type (e.g., "FILE_UPLOAD", "PASSWORD_CHANGE")
|
|
||||||
* @param data Additional event data (will be automatically sanitized)
|
|
||||||
* @param level The minimum audit level required for this event to be logged
|
|
||||||
*/
|
|
||||||
public void audit(String type, Map<String, Object> data, AuditLevel level) {
|
|
||||||
// Skip auditing if this level is not enabled or if not Enterprise edition
|
|
||||||
if (!auditConfig.isLevelEnabled(level) || !runningEE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String principal = getCurrentUsername();
|
|
||||||
repository.add(new AuditEvent(principal, type, data));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Record an audit event for the current authenticated user with standard level
|
|
||||||
* using a string-based event type (for backward compatibility)
|
|
||||||
*
|
|
||||||
* @param type The event type (e.g., "FILE_UPLOAD", "PASSWORD_CHANGE")
|
|
||||||
* @param data Additional event data (will be automatically sanitized)
|
|
||||||
*/
|
|
||||||
public void audit(String type, Map<String, Object> data) {
|
|
||||||
// Default to STANDARD level
|
|
||||||
audit(type, data, AuditLevel.STANDARD);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Record an audit event for a specific user with a specific audit level
|
|
||||||
* using a string-based event type (for backward compatibility)
|
|
||||||
*
|
|
||||||
* @param principal The username or system identifier
|
|
||||||
* @param type The event type (e.g., "FILE_UPLOAD", "PASSWORD_CHANGE")
|
|
||||||
* @param data Additional event data (will be automatically sanitized)
|
|
||||||
* @param level The minimum audit level required for this event to be logged
|
|
||||||
*/
|
|
||||||
public void audit(String principal, String type, Map<String, Object> data, AuditLevel level) {
|
|
||||||
// Skip auditing if this level is not enabled or if not Enterprise edition
|
|
||||||
if (!auditConfig.isLevelEnabled(level) || !runningEE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
repository.add(new AuditEvent(principal, type, data));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Record an audit event for a specific user with standard level
|
|
||||||
* using a string-based event type (for backward compatibility)
|
|
||||||
*
|
|
||||||
* @param principal The username or system identifier
|
|
||||||
* @param type The event type (e.g., "FILE_UPLOAD", "PASSWORD_CHANGE")
|
|
||||||
* @param data Additional event data (will be automatically sanitized)
|
|
||||||
*/
|
|
||||||
public void audit(String principal, String type, Map<String, Object> data) {
|
|
||||||
// Default to STANDARD level
|
|
||||||
audit(principal, type, data, AuditLevel.STANDARD);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current authenticated username or "system" if none
|
|
||||||
*/
|
|
||||||
private String getCurrentUsername() {
|
|
||||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
|
||||||
return (auth != null && auth.getName() != null) ? auth.getName() : "system";
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
package stirling.software.proprietary.util;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
/** Redacts any map values whose keys match common secret/token patterns. */
|
|
||||||
@Slf4j
|
|
||||||
public final class SecretMasker {
|
|
||||||
|
|
||||||
private static final Pattern SENSITIVE =
|
|
||||||
Pattern.compile("(?i)(password|token|secret|api[_-]?key|authorization|auth|jwt|cred|cert)");
|
|
||||||
|
|
||||||
private SecretMasker() {}
|
|
||||||
|
|
||||||
public static Map<String,Object> mask(Map<String,Object> in) {
|
|
||||||
if (in == null) return null;
|
|
||||||
|
|
||||||
return in.entrySet().stream()
|
|
||||||
.filter(e -> e.getValue() != null)
|
|
||||||
.collect(Collectors.toMap(
|
|
||||||
Map.Entry::getKey,
|
|
||||||
e -> deepMaskValue(e.getKey(), e.getValue())
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Object deepMask(Object value) {
|
|
||||||
if (value instanceof Map<?,?> m) {
|
|
||||||
return m.entrySet().stream()
|
|
||||||
.filter(e -> e.getValue() != null)
|
|
||||||
.collect(Collectors.toMap(
|
|
||||||
Map.Entry::getKey,
|
|
||||||
e -> deepMaskValue((String)e.getKey(), e.getValue())
|
|
||||||
));
|
|
||||||
} else if (value instanceof List<?> list) {
|
|
||||||
return list.stream()
|
|
||||||
.map(SecretMasker::deepMask).toList();
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static Object deepMaskValue(String key, Object value) {
|
|
||||||
if (key != null && SENSITIVE.matcher(key).find()) {
|
|
||||||
return "***REDACTED***";
|
|
||||||
}
|
|
||||||
return deepMask(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,97 +0,0 @@
|
|||||||
package stirling.software.proprietary.web;
|
|
||||||
|
|
||||||
import jakarta.servlet.FilterChain;
|
|
||||||
import jakarta.servlet.ServletException;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.slf4j.MDC;
|
|
||||||
import org.springframework.core.Ordered;
|
|
||||||
import org.springframework.core.annotation.Order;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
|
||||||
import org.springframework.web.util.ContentCachingRequestWrapper;
|
|
||||||
import org.springframework.web.util.ContentCachingResponseWrapper;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Enumeration;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter that stores additional request information for audit purposes
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class AuditWebFilter extends OncePerRequestFilter {
|
|
||||||
|
|
||||||
private static final String USER_AGENT_HEADER = "User-Agent";
|
|
||||||
private static final String REFERER_HEADER = "Referer";
|
|
||||||
private static final String ACCEPT_LANGUAGE_HEADER = "Accept-Language";
|
|
||||||
private static final String CONTENT_TYPE_HEADER = "Content-Type";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doFilterInternal(HttpServletRequest request,
|
|
||||||
HttpServletResponse response,
|
|
||||||
FilterChain filterChain) throws ServletException, IOException {
|
|
||||||
|
|
||||||
// Store key request info in MDC for logging and later audit use
|
|
||||||
try {
|
|
||||||
// Store request headers
|
|
||||||
String userAgent = request.getHeader(USER_AGENT_HEADER);
|
|
||||||
if (userAgent != null) {
|
|
||||||
MDC.put("userAgent", userAgent);
|
|
||||||
}
|
|
||||||
|
|
||||||
String referer = request.getHeader(REFERER_HEADER);
|
|
||||||
if (referer != null) {
|
|
||||||
MDC.put("referer", referer);
|
|
||||||
}
|
|
||||||
|
|
||||||
String acceptLanguage = request.getHeader(ACCEPT_LANGUAGE_HEADER);
|
|
||||||
if (acceptLanguage != null) {
|
|
||||||
MDC.put("acceptLanguage", acceptLanguage);
|
|
||||||
}
|
|
||||||
|
|
||||||
String contentType = request.getHeader(CONTENT_TYPE_HEADER);
|
|
||||||
if (contentType != null) {
|
|
||||||
MDC.put("contentType", contentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store authenticated user roles if available
|
|
||||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
|
||||||
if (auth != null && auth.getAuthorities() != null) {
|
|
||||||
String roles = auth.getAuthorities().stream()
|
|
||||||
.map(a -> a.getAuthority())
|
|
||||||
.reduce((a, b) -> a + "," + b)
|
|
||||||
.orElse("");
|
|
||||||
MDC.put("userRoles", roles);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store query parameters (without values for privacy)
|
|
||||||
Map<String, String[]> parameterMap = request.getParameterMap();
|
|
||||||
if (parameterMap != null && !parameterMap.isEmpty()) {
|
|
||||||
String params = String.join(",", parameterMap.keySet());
|
|
||||||
MDC.put("queryParams", params);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Continue with the filter chain
|
|
||||||
filterChain.doFilter(request, response);
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
// Clear MDC after request is processed
|
|
||||||
MDC.remove("userAgent");
|
|
||||||
MDC.remove("referer");
|
|
||||||
MDC.remove("acceptLanguage");
|
|
||||||
MDC.remove("contentType");
|
|
||||||
MDC.remove("userRoles");
|
|
||||||
MDC.remove("queryParams");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
package stirling.software.proprietary.web;
|
|
||||||
|
|
||||||
import io.github.pixee.security.Newlines;
|
|
||||||
import jakarta.servlet.FilterChain;
|
|
||||||
import jakarta.servlet.ServletException;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.slf4j.MDC;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Guarantees every request carries a stable X-Request-Id; propagates to MDC.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
public class CorrelationIdFilter extends OncePerRequestFilter {
|
|
||||||
|
|
||||||
public static final String HEADER = "X-Request-Id";
|
|
||||||
public static final String MDC_KEY = "requestId";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doFilterInternal(HttpServletRequest req,
|
|
||||||
HttpServletResponse res,
|
|
||||||
FilterChain chain)
|
|
||||||
throws ServletException, IOException {
|
|
||||||
|
|
||||||
try {
|
|
||||||
String id = req.getHeader(HEADER);
|
|
||||||
if (!StringUtils.hasText(id)) {
|
|
||||||
id = UUID.randomUUID().toString();
|
|
||||||
}
|
|
||||||
req.setAttribute(MDC_KEY, id);
|
|
||||||
MDC.put(MDC_KEY, id);
|
|
||||||
res.setHeader(HEADER, Newlines.stripAll(id));
|
|
||||||
|
|
||||||
chain.doFilter(req, res);
|
|
||||||
} finally {
|
|
||||||
MDC.remove(MDC_KEY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,239 +0,0 @@
|
|||||||
.dashboard-card {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
background-color: var(--md-sys-color-surface-container);
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
border: 1px solid var(--md-sys-color-outline-variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
background-color: var(--md-sys-color-surface-container-high);
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-body {
|
|
||||||
background-color: var(--md-sys-color-surface-container);
|
|
||||||
}
|
|
||||||
.stat-card {
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.stat-number {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.stat-label {
|
|
||||||
font-size: 1rem;
|
|
||||||
color: var(--md-sys-color-on-surface-variant);
|
|
||||||
}
|
|
||||||
.chart-container {
|
|
||||||
position: relative;
|
|
||||||
height: 300px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.filter-card {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding: 15px;
|
|
||||||
background-color: var(--md-sys-color-surface-container-low);
|
|
||||||
border: 1px solid var(--md-sys-color-outline-variant);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.loading-overlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: var(--md-sys-color-surface-container-high, rgba(229, 232, 241, 0.8));
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
.level-indicator {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 15px;
|
|
||||||
color: white;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.level-0 {
|
|
||||||
background-color: var(--md-sys-color-error, #dc3545); /* Red */
|
|
||||||
}
|
|
||||||
.level-1 {
|
|
||||||
background-color: var(--md-sys-color-secondary, #fd7e14); /* Orange */
|
|
||||||
}
|
|
||||||
.level-2 {
|
|
||||||
background-color: var(--md-nav-section-color-other, #28a745); /* Green */
|
|
||||||
}
|
|
||||||
.level-3 {
|
|
||||||
background-color: var(--md-sys-color-tertiary, #17a2b8); /* Teal */
|
|
||||||
}
|
|
||||||
/* Custom data table styling */
|
|
||||||
.audit-table {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
border-color: var(--md-sys-color-outline-variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
.audit-table tbody tr {
|
|
||||||
background-color: var(--md-sys-color-surface-container-low);
|
|
||||||
}
|
|
||||||
|
|
||||||
.audit-table tbody tr:nth-child(even) {
|
|
||||||
background-color: var(--md-sys-color-surface-container);
|
|
||||||
}
|
|
||||||
|
|
||||||
.audit-table tbody tr:hover {
|
|
||||||
background-color: var(--md-sys-color-surface-container-high);
|
|
||||||
}
|
|
||||||
.audit-table th {
|
|
||||||
background-color: var(--md-sys-color-surface-container-high);
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 10;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.table-responsive {
|
|
||||||
max-height: 600px;
|
|
||||||
}
|
|
||||||
.pagination-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 15px;
|
|
||||||
padding: 10px 0;
|
|
||||||
border-top: 1px solid var(--md-sys-color-outline-variant);
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination .page-item.active .page-link {
|
|
||||||
background-color: var(--bs-primary);
|
|
||||||
border-color: var(--bs-primary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination .page-link {
|
|
||||||
color: var(--bs-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination .page-link.disabled {
|
|
||||||
pointer-events: none;
|
|
||||||
color: var(--bs-secondary);
|
|
||||||
background-color: var(--bs-light);
|
|
||||||
}
|
|
||||||
.json-viewer {
|
|
||||||
background-color: var(--md-sys-color-surface-container-low);
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 15px;
|
|
||||||
max-height: 350px;
|
|
||||||
overflow-y: auto;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
border: 1px solid var(--md-sys-color-outline-variant);
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Simple, minimal radio styling - no extras */
|
|
||||||
.form-check {
|
|
||||||
padding: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#debug-console {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 400px;
|
|
||||||
height: 200px;
|
|
||||||
background: var(--md-sys-color-surface-container-highest, rgba(0,0,0,0.8));
|
|
||||||
color: var(--md-sys-color-tertiary, #0f0);
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
z-index: 9999;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid var(--md-sys-color-outline);
|
|
||||||
display: none; /* Changed to none by default, enable with key command */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enhanced styling for radio buttons as buttons */
|
|
||||||
label.btn-outline-primary {
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
border-color: var(--md-sys-color-primary);
|
|
||||||
color: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
label.btn-outline-primary.active {
|
|
||||||
background-color: var(--md-sys-color-primary);
|
|
||||||
color: var(--md-sys-color-on-primary);
|
|
||||||
border-color: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
label.btn-outline-primary input[type="radio"] {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal overrides for dark mode */
|
|
||||||
.modal-content {
|
|
||||||
background-color: var(--md-sys-color-surface-container);
|
|
||||||
color: var(--md-sys-color-on-surface);
|
|
||||||
border-color: var(--md-sys-color-outline);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
border-bottom-color: var(--md-sys-color-outline-variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-footer {
|
|
||||||
border-top-color: var(--md-sys-color-outline-variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Improved modal positioning */
|
|
||||||
.modal-dialog-centered {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
min-height: calc(100% - 3.5rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal {
|
|
||||||
z-index: 1050;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Button overrides for theme consistency */
|
|
||||||
.btn-outline-primary {
|
|
||||||
color: var(--md-sys-color-primary);
|
|
||||||
border-color: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline-primary:hover {
|
|
||||||
background-color: var(--md-sys-color-primary);
|
|
||||||
color: var(--md-sys-color-on-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline-secondary {
|
|
||||||
color: var(--md-sys-color-secondary);
|
|
||||||
border-color: var(--md-sys-color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline-secondary:hover {
|
|
||||||
background-color: var(--md-sys-color-secondary);
|
|
||||||
color: var(--md-sys-color-on-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background-color: var(--md-sys-color-primary);
|
|
||||||
color: var(--md-sys-color-on-primary);
|
|
||||||
border-color: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background-color: var(--md-sys-color-secondary);
|
|
||||||
color: var(--md-sys-color-on-secondary);
|
|
||||||
border-color: var(--md-sys-color-secondary);
|
|
||||||
}
|
|
@ -1,999 +0,0 @@
|
|||||||
// Initialize variables
|
|
||||||
let currentPage = 0;
|
|
||||||
let pageSize = 20;
|
|
||||||
let totalPages = 0;
|
|
||||||
let typeFilter = '';
|
|
||||||
let principalFilter = '';
|
|
||||||
let startDateFilter = '';
|
|
||||||
let endDateFilter = '';
|
|
||||||
|
|
||||||
// Charts
|
|
||||||
let typeChart;
|
|
||||||
let userChart;
|
|
||||||
let timeChart;
|
|
||||||
|
|
||||||
// DOM elements - will properly initialize these during page load
|
|
||||||
let auditTableBody;
|
|
||||||
let pageSizeSelect;
|
|
||||||
let typeFilterInput;
|
|
||||||
let exportTypeFilterInput;
|
|
||||||
let principalFilterInput;
|
|
||||||
let startDateFilterInput;
|
|
||||||
let endDateFilterInput;
|
|
||||||
let applyFiltersButton;
|
|
||||||
let resetFiltersButton;
|
|
||||||
|
|
||||||
|
|
||||||
// Initialize page
|
|
||||||
// Theme change listener to redraw charts when theme changes
|
|
||||||
function setupThemeChangeListener() {
|
|
||||||
// Watch for theme changes (usually by a class on body or html element)
|
|
||||||
const observer = new MutationObserver(function(mutations) {
|
|
||||||
mutations.forEach(function(mutation) {
|
|
||||||
if (mutation.attributeName === 'data-bs-theme' || mutation.attributeName === 'class') {
|
|
||||||
// Redraw charts with new theme colors if they exist
|
|
||||||
if (typeChart && userChart && timeChart) {
|
|
||||||
// If we have stats data cached, use it
|
|
||||||
if (window.cachedStatsData) {
|
|
||||||
renderCharts(window.cachedStatsData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Observe the document element for theme changes
|
|
||||||
observer.observe(document.documentElement, { attributes: true });
|
|
||||||
|
|
||||||
// Also observe body for class changes
|
|
||||||
observer.observe(document.body, { attributes: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Initialize DOM references
|
|
||||||
auditTableBody = document.getElementById('auditTableBody');
|
|
||||||
pageSizeSelect = document.getElementById('pageSizeSelect');
|
|
||||||
typeFilterInput = document.getElementById('typeFilter');
|
|
||||||
exportTypeFilterInput = document.getElementById('exportTypeFilter');
|
|
||||||
principalFilterInput = document.getElementById('principalFilter');
|
|
||||||
startDateFilterInput = document.getElementById('startDateFilter');
|
|
||||||
endDateFilterInput = document.getElementById('endDateFilter');
|
|
||||||
applyFiltersButton = document.getElementById('applyFilters');
|
|
||||||
resetFiltersButton = document.getElementById('resetFilters');
|
|
||||||
|
|
||||||
// Load event types for dropdowns
|
|
||||||
loadEventTypes();
|
|
||||||
|
|
||||||
// Show a loading message immediately
|
|
||||||
if (auditTableBody) {
|
|
||||||
auditTableBody.innerHTML =
|
|
||||||
'<tr><td colspan="5" class="text-center"><div class="spinner-border spinner-border-sm" role="status"></div> ' + window.i18n.loading + '</td></tr>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make a direct API call first to avoid validation issues
|
|
||||||
loadAuditData(0, pageSize);
|
|
||||||
|
|
||||||
// Load statistics for dashboard
|
|
||||||
loadStats(7);
|
|
||||||
|
|
||||||
// Setup theme change listener
|
|
||||||
setupThemeChangeListener();
|
|
||||||
|
|
||||||
// Set up event listeners
|
|
||||||
pageSizeSelect.addEventListener('change', function() {
|
|
||||||
pageSize = parseInt(this.value);
|
|
||||||
window.originalPageSize = pageSize;
|
|
||||||
currentPage = 0;
|
|
||||||
window.requestedPage = 0;
|
|
||||||
loadAuditData(0, pageSize);
|
|
||||||
});
|
|
||||||
|
|
||||||
applyFiltersButton.addEventListener('click', function() {
|
|
||||||
typeFilter = typeFilterInput.value.trim();
|
|
||||||
principalFilter = principalFilterInput.value.trim();
|
|
||||||
startDateFilter = startDateFilterInput.value;
|
|
||||||
endDateFilter = endDateFilterInput.value;
|
|
||||||
currentPage = 0;
|
|
||||||
window.requestedPage = 0;
|
|
||||||
loadAuditData(0, pageSize);
|
|
||||||
});
|
|
||||||
|
|
||||||
resetFiltersButton.addEventListener('click', function() {
|
|
||||||
// Reset input fields
|
|
||||||
typeFilterInput.value = '';
|
|
||||||
principalFilterInput.value = '';
|
|
||||||
startDateFilterInput.value = '';
|
|
||||||
endDateFilterInput.value = '';
|
|
||||||
|
|
||||||
// Reset filter variables
|
|
||||||
typeFilter = '';
|
|
||||||
principalFilter = '';
|
|
||||||
startDateFilter = '';
|
|
||||||
endDateFilter = '';
|
|
||||||
|
|
||||||
// Reset page
|
|
||||||
currentPage = 0;
|
|
||||||
window.requestedPage = 0;
|
|
||||||
|
|
||||||
// Update UI
|
|
||||||
document.getElementById('currentPage').textContent = '1';
|
|
||||||
|
|
||||||
// Load data with reset filters
|
|
||||||
loadAuditData(0, pageSize);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset export filters button
|
|
||||||
document.getElementById('resetExportFilters').addEventListener('click', function() {
|
|
||||||
exportTypeFilter.value = '';
|
|
||||||
exportPrincipalFilter.value = '';
|
|
||||||
exportStartDateFilter.value = '';
|
|
||||||
exportEndDateFilter.value = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Make radio buttons behave like toggle buttons
|
|
||||||
const radioLabels = document.querySelectorAll('label.btn-outline-primary');
|
|
||||||
radioLabels.forEach(label => {
|
|
||||||
const radio = label.querySelector('input[type="radio"]');
|
|
||||||
|
|
||||||
if (radio) {
|
|
||||||
// Highlight the checked radio button's label
|
|
||||||
if (radio.checked) {
|
|
||||||
label.classList.add('active');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle clicking on the label
|
|
||||||
label.addEventListener('click', function() {
|
|
||||||
// Remove active class from all labels
|
|
||||||
radioLabels.forEach(l => l.classList.remove('active'));
|
|
||||||
|
|
||||||
// Add active class to this label
|
|
||||||
this.classList.add('active');
|
|
||||||
|
|
||||||
// Check this radio button
|
|
||||||
radio.checked = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle export button
|
|
||||||
exportButton.onclick = function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
// Get selected format with fallback
|
|
||||||
const selectedRadio = document.querySelector('input[name="exportFormat"]:checked');
|
|
||||||
const exportFormat = selectedRadio ? selectedRadio.value : 'csv';
|
|
||||||
exportAuditData(exportFormat);
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set up pagination buttons
|
|
||||||
document.getElementById('page-first').onclick = function() {
|
|
||||||
if (currentPage > 0) {
|
|
||||||
goToPage(0);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById('page-prev').onclick = function() {
|
|
||||||
if (currentPage > 0) {
|
|
||||||
goToPage(currentPage - 1);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById('page-next').onclick = function() {
|
|
||||||
if (currentPage < totalPages - 1) {
|
|
||||||
goToPage(currentPage + 1);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById('page-last').onclick = function() {
|
|
||||||
if (totalPages > 0 && currentPage < totalPages - 1) {
|
|
||||||
goToPage(totalPages - 1);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set up tab change events
|
|
||||||
const tabEls = document.querySelectorAll('button[data-bs-toggle="tab"]');
|
|
||||||
tabEls.forEach(tabEl => {
|
|
||||||
tabEl.addEventListener('shown.bs.tab', function (event) {
|
|
||||||
const targetId = event.target.getAttribute('data-bs-target');
|
|
||||||
if (targetId === '#dashboard') {
|
|
||||||
// Redraw charts when dashboard tab is shown
|
|
||||||
if (typeChart) typeChart.update();
|
|
||||||
if (userChart) userChart.update();
|
|
||||||
if (timeChart) timeChart.update();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load audit data from server
|
|
||||||
function loadAuditData(targetPage, realPageSize) {
|
|
||||||
const requestedPage = targetPage !== undefined ? targetPage : window.requestedPage || 0;
|
|
||||||
realPageSize = realPageSize || pageSize;
|
|
||||||
|
|
||||||
showLoading('table-loading');
|
|
||||||
|
|
||||||
// Always request page 0 from server, but with increased page size if needed
|
|
||||||
let url = `/audit/data?page=${requestedPage}&size=${realPageSize}`;
|
|
||||||
|
|
||||||
if (typeFilter) url += `&type=${encodeURIComponent(typeFilter)}`;
|
|
||||||
if (principalFilter) url += `&principal=${encodeURIComponent(principalFilter)}`;
|
|
||||||
if (startDateFilter) url += `&startDate=${startDateFilter}`;
|
|
||||||
if (endDateFilter) url += `&endDate=${endDateFilter}`;
|
|
||||||
|
|
||||||
// Update page indicator
|
|
||||||
if (document.getElementById('page-indicator')) {
|
|
||||||
document.getElementById('page-indicator').textContent = `Page ${requestedPage + 1} of ?`;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(url)
|
|
||||||
.then(response => {
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
|
|
||||||
|
|
||||||
// Calculate the correct slice of data to show for the requested page
|
|
||||||
let displayContent = data.content;
|
|
||||||
|
|
||||||
// Render the correct slice of data
|
|
||||||
renderTable(displayContent);
|
|
||||||
|
|
||||||
// Calculate total pages based on the actual total elements
|
|
||||||
const calculatedTotalPages = Math.ceil(data.totalElements / realPageSize);
|
|
||||||
totalPages = calculatedTotalPages;
|
|
||||||
currentPage = requestedPage; // Use our tracked page, not server's
|
|
||||||
|
|
||||||
|
|
||||||
// Update UI
|
|
||||||
document.getElementById('currentPage').textContent = currentPage + 1;
|
|
||||||
document.getElementById('totalPages').textContent = totalPages;
|
|
||||||
document.getElementById('totalRecords').textContent = data.totalElements;
|
|
||||||
if (document.getElementById('page-indicator')) {
|
|
||||||
document.getElementById('page-indicator').textContent = `Page ${currentPage + 1} of ${totalPages}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-enable buttons with correct state
|
|
||||||
document.getElementById('page-first').disabled = currentPage === 0;
|
|
||||||
document.getElementById('page-prev').disabled = currentPage === 0;
|
|
||||||
document.getElementById('page-next').disabled = currentPage >= totalPages - 1;
|
|
||||||
document.getElementById('page-last').disabled = currentPage >= totalPages - 1;
|
|
||||||
|
|
||||||
hideLoading('table-loading');
|
|
||||||
|
|
||||||
// Restore original page size for next operations
|
|
||||||
if (window.originalPageSize && realPageSize !== window.originalPageSize) {
|
|
||||||
pageSize = window.originalPageSize;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store original page size for recovery
|
|
||||||
window.originalPageSize = realPageSize;
|
|
||||||
|
|
||||||
// Clear busy flag
|
|
||||||
window.paginationBusy = false;
|
|
||||||
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
|
|
||||||
if (auditTableBody) {
|
|
||||||
auditTableBody.innerHTML = `<tr><td colspan="5" class="text-center">${window.i18n.errorLoading} ${error.message}</td></tr>`;
|
|
||||||
}
|
|
||||||
hideLoading('table-loading');
|
|
||||||
|
|
||||||
// Re-enable buttons
|
|
||||||
document.getElementById('page-first').disabled = false;
|
|
||||||
document.getElementById('page-prev').disabled = false;
|
|
||||||
document.getElementById('page-next').disabled = false;
|
|
||||||
document.getElementById('page-last').disabled = false;
|
|
||||||
|
|
||||||
// Clear busy flag
|
|
||||||
window.paginationBusy = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load statistics for charts
|
|
||||||
function loadStats(days) {
|
|
||||||
showLoading('type-chart-loading');
|
|
||||||
showLoading('user-chart-loading');
|
|
||||||
showLoading('time-chart-loading');
|
|
||||||
|
|
||||||
fetch(`/audit/stats?days=${days}`)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
document.getElementById('total-events').textContent = data.totalEvents;
|
|
||||||
// Cache stats data for theme changes
|
|
||||||
window.cachedStatsData = data;
|
|
||||||
renderCharts(data);
|
|
||||||
hideLoading('type-chart-loading');
|
|
||||||
hideLoading('user-chart-loading');
|
|
||||||
hideLoading('time-chart-loading');
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error loading stats:', error);
|
|
||||||
hideLoading('type-chart-loading');
|
|
||||||
hideLoading('user-chart-loading');
|
|
||||||
hideLoading('time-chart-loading');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export audit data
|
|
||||||
function exportAuditData(format) {
|
|
||||||
const type = exportTypeFilter.value.trim();
|
|
||||||
const principal = exportPrincipalFilter.value.trim();
|
|
||||||
const startDate = exportStartDateFilter.value;
|
|
||||||
const endDate = exportEndDateFilter.value;
|
|
||||||
|
|
||||||
let url = format === 'json' ? '/audit/export/json?' : '/audit/export?';
|
|
||||||
|
|
||||||
if (type) url += `&type=${encodeURIComponent(type)}`;
|
|
||||||
if (principal) url += `&principal=${encodeURIComponent(principal)}`;
|
|
||||||
if (startDate) url += `&startDate=${startDate}`;
|
|
||||||
if (endDate) url += `&endDate=${endDate}`;
|
|
||||||
|
|
||||||
// Trigger download
|
|
||||||
window.location.href = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render table with audit data
|
|
||||||
function renderTable(events) {
|
|
||||||
|
|
||||||
if (!events || events.length === 0) {
|
|
||||||
auditTableBody.innerHTML = '<tr><td colspan="5" class="text-center">' + window.i18n.noEventsFound + '</td></tr>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
auditTableBody.innerHTML = '';
|
|
||||||
|
|
||||||
events.forEach((event, index) => {
|
|
||||||
try {
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
row.innerHTML = `
|
|
||||||
<td>${event.id || 'N/A'}</td>
|
|
||||||
<td>${formatDate(event.timestamp)}</td>
|
|
||||||
<td>${escapeHtml(event.principal || 'N/A')}</td>
|
|
||||||
<td>${escapeHtml(event.type || 'N/A')}</td>
|
|
||||||
<td><button class="btn btn-sm btn-outline-primary view-details">${window.i18n.viewDetails || 'View Details'}</button></td>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Store event data for modal
|
|
||||||
row.dataset.event = JSON.stringify(event);
|
|
||||||
|
|
||||||
// Add click handler for details button
|
|
||||||
const detailsButton = row.querySelector('.view-details');
|
|
||||||
if (detailsButton) {
|
|
||||||
detailsButton.addEventListener('click', function() {
|
|
||||||
showEventDetails(event);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
auditTableBody.appendChild(row);
|
|
||||||
} catch (rowError) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
auditTableBody.innerHTML = '<tr><td colspan="5" class="text-center">' + window.i18n.errorRendering + ' ' + e.message + '</td></tr>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show event details in modal
|
|
||||||
function showEventDetails(event) {
|
|
||||||
// Get modal elements by ID with correct hyphenated IDs from HTML
|
|
||||||
const modalId = document.getElementById('modal-id');
|
|
||||||
const modalPrincipal = document.getElementById('modal-principal');
|
|
||||||
const modalType = document.getElementById('modal-type');
|
|
||||||
const modalTimestamp = document.getElementById('modal-timestamp');
|
|
||||||
const modalData = document.getElementById('modal-data');
|
|
||||||
const eventDetailsModal = document.getElementById('eventDetailsModal');
|
|
||||||
|
|
||||||
// Set modal content
|
|
||||||
if (modalId) modalId.textContent = event.id;
|
|
||||||
if (modalPrincipal) modalPrincipal.textContent = event.principal;
|
|
||||||
if (modalType) modalType.textContent = event.type;
|
|
||||||
if (modalTimestamp) modalTimestamp.textContent = formatDate(event.timestamp);
|
|
||||||
|
|
||||||
// Format JSON data
|
|
||||||
if (modalData) {
|
|
||||||
try {
|
|
||||||
const dataObj = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|
|
||||||
modalData.textContent = JSON.stringify(dataObj, null, 2);
|
|
||||||
} catch (e) {
|
|
||||||
modalData.textContent = event.data || 'No data available';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show the modal
|
|
||||||
if (eventDetailsModal) {
|
|
||||||
const modal = new bootstrap.Modal(eventDetailsModal);
|
|
||||||
modal.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No need for a dynamic pagination renderer anymore as we're using static buttons
|
|
||||||
|
|
||||||
// Direct pagination approach - server seems to be hard-limited to returning 20 items
|
|
||||||
function goToPage(page) {
|
|
||||||
|
|
||||||
// Basic validation - totalPages may not be initialized on first load
|
|
||||||
if (page < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip validation against totalPages on first load
|
|
||||||
if (totalPages > 0 && page >= totalPages) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple guard flag
|
|
||||||
if (window.paginationBusy) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.paginationBusy = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
// Store the requested page for later
|
|
||||||
window.requestedPage = page;
|
|
||||||
currentPage = page;
|
|
||||||
|
|
||||||
// Update UI immediately for user feedback
|
|
||||||
document.getElementById('currentPage').textContent = page + 1;
|
|
||||||
|
|
||||||
// Load data with this page
|
|
||||||
loadAuditData(page, pageSize);
|
|
||||||
} catch (e) {
|
|
||||||
window.paginationBusy = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render charts
|
|
||||||
function renderCharts(data) {
|
|
||||||
// Get theme colors
|
|
||||||
const colors = getThemeColors();
|
|
||||||
|
|
||||||
// Prepare data for charts
|
|
||||||
const typeLabels = Object.keys(data.eventsByType);
|
|
||||||
const typeValues = Object.values(data.eventsByType);
|
|
||||||
|
|
||||||
const userLabels = Object.keys(data.eventsByPrincipal);
|
|
||||||
const userValues = Object.values(data.eventsByPrincipal);
|
|
||||||
|
|
||||||
// Sort days for time chart
|
|
||||||
const timeLabels = Object.keys(data.eventsByDay).sort();
|
|
||||||
const timeValues = timeLabels.map(day => data.eventsByDay[day] || 0);
|
|
||||||
|
|
||||||
// Chart.js global defaults for dark mode compatibility
|
|
||||||
Chart.defaults.color = colors.text;
|
|
||||||
Chart.defaults.borderColor = colors.grid;
|
|
||||||
|
|
||||||
// Type chart
|
|
||||||
if (typeChart) {
|
|
||||||
typeChart.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
const typeCtx = document.getElementById('typeChart').getContext('2d');
|
|
||||||
typeChart = new Chart(typeCtx, {
|
|
||||||
type: 'bar',
|
|
||||||
data: {
|
|
||||||
labels: typeLabels,
|
|
||||||
datasets: [{
|
|
||||||
label: window.i18n.eventsByType,
|
|
||||||
data: typeValues,
|
|
||||||
backgroundColor: colors.chartColors.slice(0, typeLabels.length).map(color => {
|
|
||||||
// Add transparency to the colors
|
|
||||||
if (color.startsWith('rgb(')) {
|
|
||||||
return color.replace('rgb(', 'rgba(').replace(')', ', 0.8)');
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
}),
|
|
||||||
borderColor: colors.chartColors.slice(0, typeLabels.length),
|
|
||||||
borderWidth: 2,
|
|
||||||
borderRadius: 4
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
labels: {
|
|
||||||
color: colors.text,
|
|
||||||
font: {
|
|
||||||
weight: colors.isDarkMode ? 'bold' : 'normal',
|
|
||||||
size: 14
|
|
||||||
},
|
|
||||||
usePointStyle: true,
|
|
||||||
pointStyle: 'rectRounded',
|
|
||||||
boxWidth: 12,
|
|
||||||
boxHeight: 12,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
titleFont: {
|
|
||||||
weight: 'bold',
|
|
||||||
size: 14
|
|
||||||
},
|
|
||||||
bodyFont: {
|
|
||||||
size: 13
|
|
||||||
},
|
|
||||||
backgroundColor: colors.isDarkMode ? 'rgba(40, 44, 52, 0.9)' : 'rgba(255, 255, 255, 0.9)',
|
|
||||||
titleColor: colors.isDarkMode ? '#ffffff' : '#000000',
|
|
||||||
bodyColor: colors.isDarkMode ? '#ffffff' : '#000000',
|
|
||||||
borderColor: colors.isDarkMode ? 'rgba(255, 255, 255, 0.5)' : colors.grid,
|
|
||||||
borderWidth: 1,
|
|
||||||
padding: 10,
|
|
||||||
cornerRadius: 6,
|
|
||||||
callbacks: {
|
|
||||||
label: function(context) {
|
|
||||||
return `${context.dataset.label}: ${context.raw}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
y: {
|
|
||||||
beginAtZero: true,
|
|
||||||
ticks: {
|
|
||||||
color: colors.text,
|
|
||||||
font: {
|
|
||||||
weight: colors.isDarkMode ? 'bold' : 'normal',
|
|
||||||
size: 12
|
|
||||||
},
|
|
||||||
precision: 0 // Only show whole numbers
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
color: colors.isDarkMode ? 'rgba(255, 255, 255, 0.1)' : colors.grid
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Count',
|
|
||||||
color: colors.text,
|
|
||||||
font: {
|
|
||||||
weight: colors.isDarkMode ? 'bold' : 'normal',
|
|
||||||
size: 14
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
x: {
|
|
||||||
ticks: {
|
|
||||||
color: colors.text,
|
|
||||||
font: {
|
|
||||||
weight: colors.isDarkMode ? 'bold' : 'normal',
|
|
||||||
size: 11
|
|
||||||
},
|
|
||||||
callback: function(value, index) {
|
|
||||||
// Get the original label
|
|
||||||
const label = this.getLabelForValue(value);
|
|
||||||
// If the label is too long, truncate it
|
|
||||||
const maxLength = 10;
|
|
||||||
if (label.length > maxLength) {
|
|
||||||
return label.substring(0, maxLength) + '...';
|
|
||||||
}
|
|
||||||
return label;
|
|
||||||
},
|
|
||||||
autoSkip: true,
|
|
||||||
maxRotation: 0,
|
|
||||||
minRotation: 0
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
color: colors.isDarkMode ? 'rgba(255, 255, 255, 0.1)' : colors.grid,
|
|
||||||
display: false // Hide vertical gridlines for cleaner look
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Event Type',
|
|
||||||
color: colors.text,
|
|
||||||
font: {
|
|
||||||
weight: colors.isDarkMode ? 'bold' : 'normal',
|
|
||||||
size: 14
|
|
||||||
},
|
|
||||||
padding: {top: 10, bottom: 0}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// User chart
|
|
||||||
if (userChart) {
|
|
||||||
userChart.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
const userCtx = document.getElementById('userChart').getContext('2d');
|
|
||||||
userChart = new Chart(userCtx, {
|
|
||||||
type: 'pie',
|
|
||||||
data: {
|
|
||||||
labels: userLabels,
|
|
||||||
datasets: [{
|
|
||||||
label: window.i18n.eventsByUser,
|
|
||||||
data: userValues,
|
|
||||||
backgroundColor: colors.chartColors.slice(0, userLabels.length),
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: colors.isDarkMode ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.5)'
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
position: 'right',
|
|
||||||
labels: {
|
|
||||||
color: colors.text,
|
|
||||||
font: {
|
|
||||||
size: colors.isDarkMode ? 14 : 12,
|
|
||||||
weight: colors.isDarkMode ? 'bold' : 'normal'
|
|
||||||
},
|
|
||||||
padding: 15,
|
|
||||||
usePointStyle: true,
|
|
||||||
pointStyle: 'circle',
|
|
||||||
boxWidth: 10,
|
|
||||||
boxHeight: 10,
|
|
||||||
// Add a box around each label for better contrast in dark mode
|
|
||||||
generateLabels: function(chart) {
|
|
||||||
const original = Chart.overrides.pie.plugins.legend.labels.generateLabels;
|
|
||||||
const labels = original.call(this, chart);
|
|
||||||
|
|
||||||
if (colors.isDarkMode) {
|
|
||||||
labels.forEach(label => {
|
|
||||||
// Enhance contrast for dark mode
|
|
||||||
label.fillStyle = label.fillStyle; // Keep original fill
|
|
||||||
label.strokeStyle = 'rgba(255, 255, 255, 0.8)'; // White border
|
|
||||||
label.lineWidth = 2; // Thicker border
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
titleFont: {
|
|
||||||
weight: 'bold',
|
|
||||||
size: 14
|
|
||||||
},
|
|
||||||
bodyFont: {
|
|
||||||
size: 13
|
|
||||||
},
|
|
||||||
backgroundColor: colors.isDarkMode ? 'rgba(40, 44, 52, 0.9)' : 'rgba(255, 255, 255, 0.9)',
|
|
||||||
titleColor: colors.isDarkMode ? '#ffffff' : '#000000',
|
|
||||||
bodyColor: colors.isDarkMode ? '#ffffff' : '#000000',
|
|
||||||
borderColor: colors.isDarkMode ? 'rgba(255, 255, 255, 0.5)' : colors.grid,
|
|
||||||
borderWidth: 1,
|
|
||||||
padding: 10,
|
|
||||||
cornerRadius: 6
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Time chart
|
|
||||||
if (timeChart) {
|
|
||||||
timeChart.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeCtx = document.getElementById('timeChart').getContext('2d');
|
|
||||||
|
|
||||||
// Get first color for line chart with appropriate transparency
|
|
||||||
let bgColor, borderColor;
|
|
||||||
if (colors.isDarkMode) {
|
|
||||||
bgColor = 'rgba(162, 201, 255, 0.3)'; // Light blue with transparency
|
|
||||||
borderColor = 'rgb(162, 201, 255)'; // Light blue solid
|
|
||||||
} else {
|
|
||||||
bgColor = 'rgba(0, 96, 170, 0.2)'; // Dark blue with transparency
|
|
||||||
borderColor = 'rgb(0, 96, 170)'; // Dark blue solid
|
|
||||||
}
|
|
||||||
|
|
||||||
timeChart = new Chart(timeCtx, {
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels: timeLabels,
|
|
||||||
datasets: [{
|
|
||||||
label: window.i18n.eventsOverTime,
|
|
||||||
data: timeValues,
|
|
||||||
backgroundColor: bgColor,
|
|
||||||
borderColor: borderColor,
|
|
||||||
borderWidth: 3,
|
|
||||||
tension: 0.2,
|
|
||||||
fill: true,
|
|
||||||
pointBackgroundColor: borderColor,
|
|
||||||
pointBorderColor: colors.isDarkMode ? '#fff' : '#000',
|
|
||||||
pointBorderWidth: 2,
|
|
||||||
pointRadius: 5,
|
|
||||||
pointHoverRadius: 7
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
labels: {
|
|
||||||
color: colors.text,
|
|
||||||
font: {
|
|
||||||
weight: colors.isDarkMode ? 'bold' : 'normal',
|
|
||||||
size: 14
|
|
||||||
},
|
|
||||||
usePointStyle: true,
|
|
||||||
pointStyle: 'line',
|
|
||||||
boxWidth: 50,
|
|
||||||
boxHeight: 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
titleFont: {
|
|
||||||
weight: 'bold',
|
|
||||||
size: 14
|
|
||||||
},
|
|
||||||
bodyFont: {
|
|
||||||
size: 13
|
|
||||||
},
|
|
||||||
backgroundColor: colors.isDarkMode ? 'rgba(40, 44, 52, 0.9)' : 'rgba(255, 255, 255, 0.9)',
|
|
||||||
titleColor: colors.isDarkMode ? '#ffffff' : '#000000',
|
|
||||||
bodyColor: colors.isDarkMode ? '#ffffff' : '#000000',
|
|
||||||
borderColor: colors.isDarkMode ? 'rgba(255, 255, 255, 0.5)' : colors.grid,
|
|
||||||
borderWidth: 1,
|
|
||||||
padding: 10,
|
|
||||||
cornerRadius: 6,
|
|
||||||
callbacks: {
|
|
||||||
label: function(context) {
|
|
||||||
return `Events: ${context.raw}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
interaction: {
|
|
||||||
intersect: false,
|
|
||||||
mode: 'index'
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
y: {
|
|
||||||
beginAtZero: true,
|
|
||||||
ticks: {
|
|
||||||
color: colors.text,
|
|
||||||
font: {
|
|
||||||
weight: colors.isDarkMode ? 'bold' : 'normal',
|
|
||||||
size: 12
|
|
||||||
},
|
|
||||||
precision: 0 // Only show whole numbers
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
color: colors.isDarkMode ? 'rgba(255, 255, 255, 0.1)' : colors.grid
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Number of Events',
|
|
||||||
color: colors.text,
|
|
||||||
font: {
|
|
||||||
weight: colors.isDarkMode ? 'bold' : 'normal',
|
|
||||||
size: 14
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
x: {
|
|
||||||
ticks: {
|
|
||||||
color: colors.text,
|
|
||||||
font: {
|
|
||||||
weight: colors.isDarkMode ? 'bold' : 'normal',
|
|
||||||
size: 12
|
|
||||||
},
|
|
||||||
maxRotation: 45,
|
|
||||||
minRotation: 45
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
color: colors.isDarkMode ? 'rgba(255, 255, 255, 0.1)' : colors.grid
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Date',
|
|
||||||
color: colors.text,
|
|
||||||
font: {
|
|
||||||
weight: colors.isDarkMode ? 'bold' : 'normal',
|
|
||||||
size: 14
|
|
||||||
},
|
|
||||||
padding: {top: 20}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
function formatDate(timestamp) {
|
|
||||||
const date = new Date(timestamp);
|
|
||||||
return date.toLocaleString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(text) {
|
|
||||||
if (!text) return '';
|
|
||||||
return text
|
|
||||||
.toString()
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/\"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
}
|
|
||||||
|
|
||||||
function showLoading(id) {
|
|
||||||
const loading = document.getElementById(id);
|
|
||||||
if (loading) loading.style.display = 'flex';
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideLoading(id) {
|
|
||||||
const loading = document.getElementById(id);
|
|
||||||
if (loading) loading.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load event types from the server for filter dropdowns
|
|
||||||
function loadEventTypes() {
|
|
||||||
fetch('/audit/types')
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(types => {
|
|
||||||
if (!types || types.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate the type filter dropdowns
|
|
||||||
const typeFilter = document.getElementById('typeFilter');
|
|
||||||
const exportTypeFilter = document.getElementById('exportTypeFilter');
|
|
||||||
|
|
||||||
// Clear existing options except the first one (All event types)
|
|
||||||
while (typeFilter.options.length > 1) {
|
|
||||||
typeFilter.remove(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (exportTypeFilter.options.length > 1) {
|
|
||||||
exportTypeFilter.remove(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new options
|
|
||||||
types.forEach(type => {
|
|
||||||
// Main filter dropdown
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = type;
|
|
||||||
option.textContent = type;
|
|
||||||
typeFilter.appendChild(option);
|
|
||||||
|
|
||||||
// Export filter dropdown
|
|
||||||
const exportOption = document.createElement('option');
|
|
||||||
exportOption.value = type;
|
|
||||||
exportOption.textContent = type;
|
|
||||||
exportTypeFilter.appendChild(exportOption);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error loading event types:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get theme colors for charts
|
|
||||||
function getThemeColors() {
|
|
||||||
const isDarkMode = document.documentElement.getAttribute('data-bs-theme') === 'dark';
|
|
||||||
|
|
||||||
// In dark mode, use higher contrast colors for text
|
|
||||||
const textColor = isDarkMode ?
|
|
||||||
'rgb(255, 255, 255)' : // White for dark mode for maximum contrast
|
|
||||||
getComputedStyle(document.documentElement).getPropertyValue('--md-sys-color-on-surface').trim();
|
|
||||||
|
|
||||||
// Use a more visible grid color in dark mode
|
|
||||||
const gridColor = isDarkMode ?
|
|
||||||
'rgba(255, 255, 255, 0.2)' : // Semi-transparent white for dark mode
|
|
||||||
getComputedStyle(document.documentElement).getPropertyValue('--md-sys-color-outline-variant').trim();
|
|
||||||
|
|
||||||
// Define bright, high-contrast colors for both dark and light modes
|
|
||||||
const chartColorsDark = [
|
|
||||||
'rgb(162, 201, 255)', // Light blue - primary
|
|
||||||
'rgb(193, 194, 248)', // Light purple - tertiary
|
|
||||||
'rgb(255, 180, 171)', // Light red - error
|
|
||||||
'rgb(72, 189, 84)', // Green - other
|
|
||||||
'rgb(25, 177, 212)', // Cyan - convert
|
|
||||||
'rgb(25, 101, 212)', // Blue - sign
|
|
||||||
'rgb(255, 120, 146)', // Pink - security
|
|
||||||
'rgb(104, 220, 149)', // Light green - convertto
|
|
||||||
'rgb(212, 172, 25)', // Yellow - image
|
|
||||||
'rgb(245, 84, 84)', // Red - advance
|
|
||||||
];
|
|
||||||
|
|
||||||
const chartColorsLight = [
|
|
||||||
'rgb(0, 96, 170)', // Blue - primary
|
|
||||||
'rgb(88, 90, 138)', // Purple - tertiary
|
|
||||||
'rgb(186, 26, 26)', // Red - error
|
|
||||||
'rgb(72, 189, 84)', // Green - other
|
|
||||||
'rgb(25, 177, 212)', // Cyan - convert
|
|
||||||
'rgb(25, 101, 212)', // Blue - sign
|
|
||||||
'rgb(255, 120, 146)', // Pink - security
|
|
||||||
'rgb(104, 220, 149)', // Light green - convertto
|
|
||||||
'rgb(212, 172, 25)', // Yellow - image
|
|
||||||
'rgb(245, 84, 84)', // Red - advance
|
|
||||||
];
|
|
||||||
|
|
||||||
return {
|
|
||||||
text: textColor,
|
|
||||||
grid: gridColor,
|
|
||||||
backgroundColor: getComputedStyle(document.documentElement).getPropertyValue('--md-sys-color-surface-container').trim(),
|
|
||||||
chartColors: isDarkMode ? chartColorsDark : chartColorsLight,
|
|
||||||
isDarkMode: isDarkMode
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to generate a palette of colors for charts
|
|
||||||
function getChartColors(count, opacity = 0.6) {
|
|
||||||
try {
|
|
||||||
// Use theme colors first
|
|
||||||
const themeColors = getThemeColors();
|
|
||||||
if (themeColors && themeColors.chartColors && themeColors.chartColors.length > 0) {
|
|
||||||
const result = [];
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
// Get the raw color and add opacity
|
|
||||||
let color = themeColors.chartColors[i % themeColors.chartColors.length];
|
|
||||||
// If it's rgb() format, convert to rgba()
|
|
||||||
if (color.startsWith('rgb(')) {
|
|
||||||
color = color.replace('rgb(', '').replace(')', '');
|
|
||||||
result.push(`rgba(${color}, ${opacity})`);
|
|
||||||
} else {
|
|
||||||
// Just use the color directly
|
|
||||||
result.push(color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Error using theme colors, falling back to default colors', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base colors - a larger palette than the default
|
|
||||||
const colors = [
|
|
||||||
[54, 162, 235], // blue
|
|
||||||
[255, 99, 132], // red
|
|
||||||
[75, 192, 192], // teal
|
|
||||||
[255, 206, 86], // yellow
|
|
||||||
[153, 102, 255], // purple
|
|
||||||
[255, 159, 64], // orange
|
|
||||||
[46, 204, 113], // green
|
|
||||||
[231, 76, 60], // dark red
|
|
||||||
[52, 152, 219], // light blue
|
|
||||||
[155, 89, 182], // violet
|
|
||||||
[241, 196, 15], // dark yellow
|
|
||||||
[26, 188, 156], // turquoise
|
|
||||||
[230, 126, 34], // dark orange
|
|
||||||
[149, 165, 166], // light gray
|
|
||||||
[243, 156, 18], // amber
|
|
||||||
[39, 174, 96], // emerald
|
|
||||||
[211, 84, 0], // dark orange red
|
|
||||||
[22, 160, 133], // green sea
|
|
||||||
[41, 128, 185], // belize hole
|
|
||||||
[142, 68, 173] // wisteria
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = [];
|
|
||||||
|
|
||||||
// Always use the same format regardless of color source
|
|
||||||
if (count > colors.length) {
|
|
||||||
// Generate colors algorithmically for large sets
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
// Generate a color based on position in the hue circle (0-360)
|
|
||||||
const hue = (i * 360 / count) % 360;
|
|
||||||
const sat = 70 + Math.random() * 10; // 70-80%
|
|
||||||
const light = 50 + Math.random() * 10; // 50-60%
|
|
||||||
|
|
||||||
result.push(`hsla(${hue}, ${sat}%, ${light}%, ${opacity})`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Use colors from our palette but also return in hsla format for consistency
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const color = colors[i % colors.length];
|
|
||||||
result.push(`rgba(${color[0]}, ${color[1]}, ${color[2]}, ${opacity})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
# Audit System Help
|
|
||||||
|
|
||||||
## About the Audit System
|
|
||||||
The Stirling PDF audit system records user actions and system events for security monitoring, compliance, and troubleshooting purposes.
|
|
||||||
|
|
||||||
## Audit Levels
|
|
||||||
|
|
||||||
| Level | Name | Description | Use Case |
|
|
||||||
|-------|------|-------------|----------|
|
|
||||||
| 0 | OFF | Minimal auditing, only critical security events | Development environments |
|
|
||||||
| 1 | BASIC | Authentication events, security events, and errors | Production environments with minimal storage |
|
|
||||||
| 2 | STANDARD | All HTTP requests and operations (default) | Normal production use |
|
|
||||||
| 3 | VERBOSE | Detailed information including headers, parameters, and results | Troubleshooting and detailed analysis |
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
Audit settings are configured in the `settings.yml` file under the `premium.proFeatures.audit` section:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
premium:
|
|
||||||
proFeatures:
|
|
||||||
audit:
|
|
||||||
enabled: true # Enable/disable audit logging
|
|
||||||
level: 2 # Audit level (0=OFF, 1=BASIC, 2=STANDARD, 3=VERBOSE)
|
|
||||||
retentionDays: 90 # Number of days to retain audit logs
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Event Types
|
|
||||||
|
|
||||||
### BASIC Events:
|
|
||||||
- USER_LOGIN - User login
|
|
||||||
- USER_LOGOUT - User logout
|
|
||||||
- USER_FAILED_LOGIN - Failed login attempt
|
|
||||||
- USER_PROFILE_UPDATE - User or profile operations
|
|
||||||
|
|
||||||
### STANDARD Events:
|
|
||||||
- HTTP_REQUEST - GET requests for viewing
|
|
||||||
- PDF_PROCESS - PDF processing operations
|
|
||||||
- FILE_OPERATION - File-related operations
|
|
||||||
- SETTINGS_CHANGED - System or admin settings operations
|
|
||||||
|
|
||||||
### VERBOSE Events:
|
|
||||||
- Detailed versions of STANDARD events with parameters and results
|
|
@ -1,250 +0,0 @@
|
|||||||
# Stirling PDF Audit System
|
|
||||||
|
|
||||||
This document provides guidance on how to use the audit system in Stirling PDF.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The audit system provides comprehensive logging of user actions and system events, storing them in a database for later review. This is useful for:
|
|
||||||
|
|
||||||
- Security monitoring
|
|
||||||
- Compliance requirements
|
|
||||||
- User activity tracking
|
|
||||||
- Troubleshooting
|
|
||||||
|
|
||||||
## Audit Levels
|
|
||||||
|
|
||||||
The audit system supports different levels of detail that can be configured in the settings.yml file:
|
|
||||||
|
|
||||||
### Level 0: OFF
|
|
||||||
- Disables all audit logging except for critical security events
|
|
||||||
- Minimal database usage and performance impact
|
|
||||||
- Only recommended for development environments
|
|
||||||
|
|
||||||
### Level 1: BASIC
|
|
||||||
- Authentication events (login, logout, failed logins)
|
|
||||||
- Password changes
|
|
||||||
- User/role changes
|
|
||||||
- System configuration changes
|
|
||||||
- HTTP request errors (status codes >= 400)
|
|
||||||
|
|
||||||
### Level 2: STANDARD (Default)
|
|
||||||
- Everything in BASIC plus:
|
|
||||||
- All HTTP requests (basic info: URL, method, status)
|
|
||||||
- File operations (upload, download, process)
|
|
||||||
- PDF operations (view, edit, etc.)
|
|
||||||
- User operations
|
|
||||||
|
|
||||||
### Level 3: VERBOSE
|
|
||||||
- Everything in STANDARD plus:
|
|
||||||
- Request headers and parameters
|
|
||||||
- Method parameters
|
|
||||||
- Operation results
|
|
||||||
- Detailed timing information
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Audit levels are configured in the settings.yml file under the premium section:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
premium:
|
|
||||||
proFeatures:
|
|
||||||
audit:
|
|
||||||
enabled: true # Enable/disable audit logging
|
|
||||||
level: 2 # Audit level (0=OFF, 1=BASIC, 2=STANDARD, 3=VERBOSE)
|
|
||||||
retentionDays: 90 # Number of days to retain audit logs
|
|
||||||
```
|
|
||||||
|
|
||||||
## Automatic Auditing
|
|
||||||
|
|
||||||
The following events are automatically audited (based on configured level):
|
|
||||||
|
|
||||||
### HTTP Request Auditing
|
|
||||||
All HTTP requests are automatically audited with details based on the configured level:
|
|
||||||
|
|
||||||
- **BASIC level**: Only errors (status code >= 400)
|
|
||||||
- **STANDARD level**: All requests with basic information (URL, method, status code, latency, IP)
|
|
||||||
- **VERBOSE level**: All of the above plus headers, parameters, and detailed timing
|
|
||||||
|
|
||||||
### Controller Method Auditing
|
|
||||||
All controller methods with web mapping annotations are automatically audited:
|
|
||||||
|
|
||||||
- `@GetMapping`
|
|
||||||
- `@PostMapping`
|
|
||||||
- `@PutMapping`
|
|
||||||
- `@DeleteMapping`
|
|
||||||
- `@PatchMapping`
|
|
||||||
|
|
||||||
Methods with these annotations are audited at the **STANDARD** level by default.
|
|
||||||
|
|
||||||
### Security Events
|
|
||||||
The following security events are always audited at the **BASIC** level:
|
|
||||||
|
|
||||||
- Authentication events (login, logout, failed login attempts)
|
|
||||||
- Password changes
|
|
||||||
- User/role changes
|
|
||||||
|
|
||||||
## Manual Auditing
|
|
||||||
|
|
||||||
There are two ways to add audit events from your code:
|
|
||||||
|
|
||||||
### 1. Using AuditService Directly
|
|
||||||
|
|
||||||
Inject the `AuditService` and use it directly:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class MyService {
|
|
||||||
|
|
||||||
private final AuditService auditService;
|
|
||||||
|
|
||||||
public void processPdf(MultipartFile file) {
|
|
||||||
// Process the file...
|
|
||||||
|
|
||||||
// Add an audit event with default level (STANDARD)
|
|
||||||
auditService.audit("PDF_PROCESSED", Map.of(
|
|
||||||
"filename", file.getOriginalFilename(),
|
|
||||||
"size", file.getSize(),
|
|
||||||
"operation", "process"
|
|
||||||
));
|
|
||||||
|
|
||||||
// Or specify an audit level
|
|
||||||
auditService.audit("PDF_PROCESSED_DETAILED", Map.of(
|
|
||||||
"filename", file.getOriginalFilename(),
|
|
||||||
"size", file.getSize(),
|
|
||||||
"operation", "process",
|
|
||||||
"metadata", file.getContentType(),
|
|
||||||
"user", "johndoe"
|
|
||||||
), AuditLevel.VERBOSE);
|
|
||||||
|
|
||||||
// Critical security events should use BASIC level to ensure they're always logged
|
|
||||||
auditService.audit("SECURITY_EVENT", Map.of(
|
|
||||||
"action", "file_access",
|
|
||||||
"resource", file.getOriginalFilename()
|
|
||||||
), AuditLevel.BASIC);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Using the @Audited Annotation
|
|
||||||
|
|
||||||
For simpler auditing, use the `@Audited` annotation on your methods:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Service
|
|
||||||
public class UserService {
|
|
||||||
|
|
||||||
// Basic audit level for important security events
|
|
||||||
@Audited(type = "USER_REGISTRATION", level = AuditLevel.BASIC)
|
|
||||||
public User registerUser(String username, String email) {
|
|
||||||
// Method implementation
|
|
||||||
User user = new User(username, email);
|
|
||||||
// Save user...
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sensitive operations should use BASIC but disable argument logging
|
|
||||||
@Audited(type = "USER_PASSWORD_CHANGE", level = AuditLevel.BASIC, includeArgs = false)
|
|
||||||
public void changePassword(String username, String newPassword) {
|
|
||||||
// Change password implementation
|
|
||||||
// includeArgs=false prevents the password from being included in the audit
|
|
||||||
}
|
|
||||||
|
|
||||||
// Standard level for normal operations (default)
|
|
||||||
@Audited(type = "USER_LOGIN")
|
|
||||||
public boolean login(String username, String password) {
|
|
||||||
// Login implementation
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verbose level for detailed information
|
|
||||||
@Audited(type = "USER_SEARCH", level = AuditLevel.VERBOSE, includeResult = true)
|
|
||||||
public List<User> searchUsers(String query) {
|
|
||||||
// Search implementation
|
|
||||||
// At VERBOSE level, this will include both the query and results
|
|
||||||
return userList;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
With the `@Audited` annotation:
|
|
||||||
- You can specify the audit level using the `level` parameter
|
|
||||||
- Method arguments are automatically included in the audit event (unless `includeArgs = false`)
|
|
||||||
- Return values can be included with `includeResult = true`
|
|
||||||
- Exceptions are automatically captured and included in the audit
|
|
||||||
- The aspect handles all the boilerplate code for you
|
|
||||||
- The annotation respects the configured global audit level
|
|
||||||
|
|
||||||
### 3. Controller Automatic Auditing
|
|
||||||
|
|
||||||
In addition to the manual methods above, all controller methods with web mapping annotations are automatically audited, even without the `@Audited` annotation:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/users")
|
|
||||||
public class UserController {
|
|
||||||
|
|
||||||
// This method will be automatically audited
|
|
||||||
@GetMapping("/{id}")
|
|
||||||
public ResponseEntity<User> getUser(@PathVariable String id) {
|
|
||||||
// Method implementation
|
|
||||||
return ResponseEntity.ok(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This method will be automatically audited
|
|
||||||
@PostMapping
|
|
||||||
public ResponseEntity<User> createUser(@RequestBody User user) {
|
|
||||||
// Method implementation
|
|
||||||
return ResponseEntity.ok(savedUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This method uses @Audited and takes precedence over automatic auditing
|
|
||||||
@Audited(type = "USER_DELETE", level = AuditLevel.BASIC)
|
|
||||||
@DeleteMapping("/{id}")
|
|
||||||
public ResponseEntity<Void> deleteUser(@PathVariable String id) {
|
|
||||||
// Method implementation
|
|
||||||
return ResponseEntity.noContent().build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Important notes about automatic controller auditing:
|
|
||||||
- All controller methods with web mapping annotations are audited at the STANDARD level
|
|
||||||
- If a method already has an @Audited annotation, that takes precedence
|
|
||||||
- The audit event includes controller name, method name, path, and HTTP method
|
|
||||||
- At VERBOSE level, request parameters are also included
|
|
||||||
- Exceptions are automatically captured
|
|
||||||
|
|
||||||
## Common Audit Event Types
|
|
||||||
|
|
||||||
Use consistent event types throughout the application:
|
|
||||||
|
|
||||||
- `FILE_UPLOAD` - When a file is uploaded
|
|
||||||
- `FILE_DOWNLOAD` - When a file is downloaded
|
|
||||||
- `PDF_PROCESS` - When a PDF is processed (split, merged, etc.)
|
|
||||||
- `USER_CREATE` - When a user is created
|
|
||||||
- `USER_UPDATE` - When a user details are updated
|
|
||||||
- `PASSWORD_CHANGE` - When a password is changed
|
|
||||||
- `PERMISSION_CHANGE` - When permissions are modified
|
|
||||||
- `SETTINGS_CHANGE` - When system settings are changed
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
- Sensitive data is automatically masked in audit logs (passwords, API keys, tokens)
|
|
||||||
- Each audit event includes a unique request ID for correlation
|
|
||||||
- Audit events are stored asynchronously to avoid performance impact
|
|
||||||
- The `/auditevents` endpoint is disabled to prevent unauthorized access to audit data
|
|
||||||
|
|
||||||
## Database Storage
|
|
||||||
|
|
||||||
Audit events are stored in the `audit_events` table with the following schema:
|
|
||||||
|
|
||||||
- `id` - Unique identifier
|
|
||||||
- `principal` - The username or system identifier
|
|
||||||
- `type` - The event type
|
|
||||||
- `data` - JSON blob containing event details
|
|
||||||
- `timestamp` - When the event occurred
|
|
||||||
|
|
||||||
## Metrics
|
|
||||||
|
|
||||||
Prometheus metrics are available at `/actuator/prometheus` for monitoring system performance and audit event volume.
|
|
@ -1,383 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
|
||||||
<head>
|
|
||||||
<th:block th:insert="~{fragments/common :: head(title='Audit Dashboard', header='Audit Dashboard')}"></th:block>
|
|
||||||
|
|
||||||
<!-- Include Chart.js for visualizations -->
|
|
||||||
<script th:src="@{/js/thirdParty/chart.umd.min.js}"></script>
|
|
||||||
|
|
||||||
<!-- Include custom CSS -->
|
|
||||||
<link rel="stylesheet" th:href="@{/css/audit-dashboard.css}" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="page-container">
|
|
||||||
<div id="content-wrap">
|
|
||||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
|
||||||
|
|
||||||
<div class="container-fluid mt-4">
|
|
||||||
<h1 class="mb-4" th:text="#{audit.dashboard.title}">Audit Dashboard</h1>
|
|
||||||
|
|
||||||
<!-- System Status Card -->
|
|
||||||
<div class="card dashboard-card mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h2 class="h5 mb-0" th:text="#{audit.dashboard.systemStatus}">Audit System Status</h2>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label" th:text="#{audit.dashboard.status}">Status</div>
|
|
||||||
<div class="stat-number">
|
|
||||||
<span th:if="${auditEnabled}" class="text-success" th:text="#{audit.dashboard.enabled}">Enabled</span>
|
|
||||||
<span th:unless="${auditEnabled}" class="text-danger" th:text="#{audit.dashboard.disabled}">Disabled</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label" th:text="#{audit.dashboard.currentLevel}">Current Level</div>
|
|
||||||
<div class="stat-number">
|
|
||||||
<span th:class="'level-indicator level-' + ${auditLevelInt}" th:text="${auditLevel}">STANDARD</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label" th:text="#{audit.dashboard.retentionPeriod}">Retention Period</div>
|
|
||||||
<div class="stat-number" th:text="${retentionDays} + ' ' + #{audit.dashboard.days}">90 days</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label" th:text="#{audit.dashboard.totalEvents}">Total Events</div>
|
|
||||||
<div class="stat-number" id="total-events">-</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tabs for different sections -->
|
|
||||||
<ul class="nav nav-tabs" id="auditTabs" role="tablist">
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button class="nav-link active" id="dashboard-tab" data-bs-toggle="tab" data-bs-target="#dashboard" type="button" role="tab" aria-controls="dashboard" aria-selected="true" th:text="#{audit.dashboard.tab.dashboard}">Dashboard</button>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button class="nav-link" id="events-tab" data-bs-toggle="tab" data-bs-target="#events" type="button" role="tab" aria-controls="events" aria-selected="false" th:text="#{audit.dashboard.tab.events}">Audit Events</button>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button class="nav-link" id="export-tab" data-bs-toggle="tab" data-bs-target="#export" type="button" role="tab" aria-controls="export" aria-selected="false" th:text="#{audit.dashboard.tab.export}">Export</button>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="tab-content" id="auditTabsContent">
|
|
||||||
<!-- Dashboard Tab -->
|
|
||||||
<div class="tab-pane fade show active" id="dashboard" role="tabpanel" aria-labelledby="dashboard-tab">
|
|
||||||
<div class="row mt-4">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card dashboard-card">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<h3 class="h5 mb-0" th:text="#{audit.dashboard.eventsByType}">Events by Type</h3>
|
|
||||||
<div class="btn-group">
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" onclick="loadStats(7)" th:text="#{audit.dashboard.period.7days}">7 Days</button>
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" onclick="loadStats(30)" th:text="#{audit.dashboard.period.30days}">30 Days</button>
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" onclick="loadStats(90)" th:text="#{audit.dashboard.period.90days}">90 Days</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="chart-container position-relative">
|
|
||||||
<div class="loading-overlay" id="type-chart-loading">
|
|
||||||
<div class="spinner-border text-primary" role="status">
|
|
||||||
<span class="visually-hidden" th:text="#{loading}">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<canvas id="typeChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card dashboard-card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h3 class="h5 mb-0" th:text="#{audit.dashboard.eventsByUser}">Events by User</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="chart-container position-relative">
|
|
||||||
<div class="loading-overlay" id="user-chart-loading">
|
|
||||||
<div class="spinner-border text-primary" role="status">
|
|
||||||
<span class="visually-hidden" th:text="#{loading}">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<canvas id="userChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mt-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card dashboard-card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h3 class="h5 mb-0" th:text="#{audit.dashboard.eventsOverTime}">Events Over Time</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="chart-container position-relative">
|
|
||||||
<div class="loading-overlay" id="time-chart-loading">
|
|
||||||
<div class="spinner-border text-primary" role="status">
|
|
||||||
<span class="visually-hidden" th:text="#{loading}">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<canvas id="timeChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Events Tab -->
|
|
||||||
<div class="tab-pane fade" id="events" role="tabpanel" aria-labelledby="events-tab">
|
|
||||||
<div class="card dashboard-card mt-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h3 class="h5 mb-0" th:text="#{audit.dashboard.auditEvents}">Audit Events</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<!-- Filters -->
|
|
||||||
<div class="card filter-card">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="typeFilter" class="form-label" th:text="#{audit.dashboard.filter.eventType}">Event Type</label>
|
|
||||||
<select class="form-select" id="typeFilter">
|
|
||||||
<option value="" th:text="#{audit.dashboard.filter.allEventTypes}">All event types</option>
|
|
||||||
<!-- Will be populated from API -->
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="principalFilter" class="form-label" th:text="#{audit.dashboard.filter.user}">User</label>
|
|
||||||
<input type="text" class="form-control" id="principalFilter" th:placeholder="#{audit.dashboard.filter.userPlaceholder}" placeholder="Filter by user">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="startDateFilter" class="form-label" th:text="#{audit.dashboard.filter.startDate}">Start Date</label>
|
|
||||||
<input type="date" class="form-control" id="startDateFilter">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="endDateFilter" class="form-label" th:text="#{audit.dashboard.filter.endDate}">End Date</label>
|
|
||||||
<input type="date" class="form-control" id="endDateFilter">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<button id="applyFilters" class="btn btn-primary" th:text="#{audit.dashboard.filter.apply}">Apply Filters</button>
|
|
||||||
<button id="resetFilters" class="btn btn-secondary" th:text="#{reset}">Reset</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Event Table -->
|
|
||||||
<div class="table-responsive position-relative">
|
|
||||||
<div class="loading-overlay" id="table-loading">
|
|
||||||
<div class="spinner-border text-primary" role="status">
|
|
||||||
<span class="visually-hidden" th:text="#{loading}">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<table class="table table-striped table-hover audit-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th th:text="#{audit.dashboard.table.id}">ID</th>
|
|
||||||
<th th:text="#{audit.dashboard.table.time}">Time</th>
|
|
||||||
<th th:text="#{audit.dashboard.table.user}">User</th>
|
|
||||||
<th th:text="#{audit.dashboard.table.type}">Type</th>
|
|
||||||
<th th:text="#{audit.dashboard.table.details}">Details</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="auditTableBody">
|
|
||||||
<!-- Table rows will be populated by JavaScript -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pagination -->
|
|
||||||
<div class="pagination-container">
|
|
||||||
<div>
|
|
||||||
<span th:text="#{audit.dashboard.pagination.show}">Show</span>
|
|
||||||
<select id="pageSizeSelect" class="form-select form-select-sm d-inline-block w-auto mx-2">
|
|
||||||
<option value="10">10</option>
|
|
||||||
<option value="20" selected>20</option>
|
|
||||||
<option value="50">50</option>
|
|
||||||
<option value="100">100</option>
|
|
||||||
</select>
|
|
||||||
<span th:text="#{audit.dashboard.pagination.entries}">entries</span>
|
|
||||||
<span class="mx-3" th:text="#{audit.dashboard.pagination.pageInfo1}">Page </span><span id="currentPage">1</span> <span th:text="#{audit.dashboard.pagination.pageInfo2}">of</span> <span id="totalPages">1</span> (<span th:text="#{audit.dashboard.pagination.totalRecords}">Total records:</span> <span id="totalRecords">0</span>)
|
|
||||||
</div>
|
|
||||||
<nav aria-label="Audit events pagination">
|
|
||||||
<div class="btn-group" role="group" aria-label="Pagination">
|
|
||||||
<button type="button" class="btn btn-outline-primary" id="page-first">«</button>
|
|
||||||
<button type="button" class="btn btn-outline-primary" id="page-prev">‹</button>
|
|
||||||
<span class="btn btn-outline-secondary disabled" id="page-indicator">Page 1 of 1</span>
|
|
||||||
<button type="button" class="btn btn-outline-primary" id="page-next">›</button>
|
|
||||||
<button type="button" class="btn btn-outline-primary" id="page-last">»</button>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Event Details Modal -->
|
|
||||||
<div class="modal fade" id="eventDetailsModal" tabindex="-1" aria-labelledby="eventDetailsModalLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="eventDetailsModalLabel" th:text="#{audit.dashboard.modal.eventDetails}">Event Details</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" th:aria-label="#{close}" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<strong th:text="#{audit.dashboard.modal.id} + ':'">ID:</strong> <span id="modal-id"></span>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<strong th:text="#{audit.dashboard.modal.user} + ':'">User:</strong> <span id="modal-principal"></span>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<strong th:text="#{audit.dashboard.modal.type} + ':'">Type:</strong> <span id="modal-type"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<strong th:text="#{audit.dashboard.modal.time} + ':'">Time:</strong> <span id="modal-timestamp"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<strong th:text="#{audit.dashboard.modal.data} + ':'">Data:</strong>
|
|
||||||
<div class="json-viewer" id="modal-data"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}">Close</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Export Tab -->
|
|
||||||
<div class="tab-pane fade" id="export" role="tabpanel" aria-labelledby="export-tab">
|
|
||||||
<div class="card dashboard-card mt-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h3 class="h5 mb-0" th:text="#{audit.dashboard.export.title}">Export Audit Data</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<!-- Export Filters -->
|
|
||||||
<div class="card filter-card">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="exportTypeFilter" class="form-label" th:text="#{audit.dashboard.filter.eventType}">Event Type</label>
|
|
||||||
<select class="form-select" id="exportTypeFilter">
|
|
||||||
<option value="" th:text="#{audit.dashboard.filter.allEventTypes}">All event types</option>
|
|
||||||
<!-- Will be populated from API -->
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="exportPrincipalFilter" class="form-label" th:text="#{audit.dashboard.filter.user}">User</label>
|
|
||||||
<input type="text" class="form-control" id="exportPrincipalFilter" th:placeholder="#{audit.dashboard.filter.userPlaceholder}" placeholder="Filter by user">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="exportStartDateFilter" class="form-label" th:text="#{audit.dashboard.filter.startDate}">Start Date</label>
|
|
||||||
<input type="date" class="form-control" id="exportStartDateFilter">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="exportEndDateFilter" class="form-label" th:text="#{audit.dashboard.filter.endDate}">End Date</label>
|
|
||||||
<input type="date" class="form-control" id="exportEndDateFilter">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mt-3">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<h5 th:text="#{audit.dashboard.export.format}">Export Format</h5>
|
|
||||||
<div>
|
|
||||||
<label class="btn btn-outline-primary" style="margin-right: 10px;">
|
|
||||||
<input type="radio" name="exportFormat" id="formatCSV" value="csv" checked style="margin-right: 5px;">
|
|
||||||
<span th:text="#{audit.dashboard.export.csv}">CSV (Comma Separated Values)</span>
|
|
||||||
</label>
|
|
||||||
<label class="btn btn-outline-primary">
|
|
||||||
<input type="radio" name="exportFormat" id="formatJSON" value="json" style="margin-right: 5px;">
|
|
||||||
<span th:text="#{audit.dashboard.export.json}">JSON (JavaScript Object Notation)</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<button id="exportButton" class="btn btn-primary mt-4">
|
|
||||||
<i class="bi bi-download"></i> <span th:text="#{audit.dashboard.export.button}">Export Data</span>
|
|
||||||
</button>
|
|
||||||
<button id="resetExportFilters" class="btn btn-secondary mt-4 ms-2">
|
|
||||||
<span th:text="#{audit.dashboard.filter.reset}">Reset Filters</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alert alert-info mt-3">
|
|
||||||
<h5 th:text="#{audit.dashboard.export.infoTitle}">Export Information</h5>
|
|
||||||
<p th:text="#{audit.dashboard.export.infoDesc1}">The export will include all audit events matching the selected filters. For large datasets, the export may take a few moments to generate.</p>
|
|
||||||
<p th:text="#{audit.dashboard.export.infoDesc2}">Exported data will include:</p>
|
|
||||||
<ul>
|
|
||||||
<li th:text="#{audit.dashboard.export.infoItem1}">Event ID</li>
|
|
||||||
<li th:text="#{audit.dashboard.export.infoItem2}">User</li>
|
|
||||||
<li th:text="#{audit.dashboard.export.infoItem3}">Event Type</li>
|
|
||||||
<li th:text="#{audit.dashboard.export.infoItem4}">Timestamp</li>
|
|
||||||
<li th:text="#{audit.dashboard.export.infoItem5}">Event Data</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bootstrap JS is loaded by the common fragments -->
|
|
||||||
<script th:src="@{/js/thirdParty/jquery.min.js}"></script>
|
|
||||||
<script th:src="@{/js/thirdParty/bootstrap.min.js}"></script>
|
|
||||||
|
|
||||||
<!-- Internationalization data for JavaScript -->
|
|
||||||
<script th:inline="javascript">
|
|
||||||
window.i18n = {
|
|
||||||
loading: /*[[#{loading}]]*/ 'Loading...',
|
|
||||||
noEventsFound: /*[[#{audit.dashboard.js.noEventsFound}]]*/ 'No audit events found matching the current filters',
|
|
||||||
errorLoading: /*[[#{audit.dashboard.js.errorLoading}]]*/ 'Error loading data:',
|
|
||||||
errorRendering: /*[[#{audit.dashboard.js.errorRendering}]]*/ 'Error rendering table:',
|
|
||||||
loadingPage: /*[[#{audit.dashboard.js.loadingPage}]]*/ 'Loading page',
|
|
||||||
eventsByType: /*[[#{audit.dashboard.eventsByType}]]*/ 'Events by Type',
|
|
||||||
eventsByUser: /*[[#{audit.dashboard.eventsByUser}]]*/ 'Events by User',
|
|
||||||
eventsOverTime: /*[[#{audit.dashboard.eventsOverTime}]]*/ 'Events Over Time',
|
|
||||||
viewDetails: /*[[#{audit.dashboard.table.viewDetails}]]*/ 'View Details'
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Load custom JavaScript -->
|
|
||||||
<script th:src="@{/js/audit/dashboard.js}"></script>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -19,7 +19,6 @@ package org.apache.pdfbox.examples.signature;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
|
||||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||||
import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
|
import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
|
||||||
import org.bouncycastle.cms.CMSException;
|
import org.bouncycastle.cms.CMSException;
|
||||||
|
@ -30,7 +30,6 @@ import java.security.cert.CertificateException;
|
|||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
|
|
||||||
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
|
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
|
||||||
import org.bouncycastle.cert.jcajce.JcaCertStore;
|
import org.bouncycastle.cert.jcajce.JcaCertStore;
|
||||||
import org.bouncycastle.cms.CMSException;
|
import org.bouncycastle.cms.CMSException;
|
||||||
|
@ -28,7 +28,6 @@ import java.security.MessageDigest;
|
|||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||||
|
@ -27,7 +27,6 @@ import java.security.MessageDigest;
|
|||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.bouncycastle.asn1.ASN1Encodable;
|
import org.bouncycastle.asn1.ASN1Encodable;
|
||||||
import org.bouncycastle.asn1.ASN1EncodableVector;
|
import org.bouncycastle.asn1.ASN1EncodableVector;
|
||||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||||
|
@ -2,7 +2,6 @@ package stirling.software.SPDF.Factories;
|
|||||||
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import stirling.software.common.model.api.misc.HighContrastColorCombination;
|
import stirling.software.common.model.api.misc.HighContrastColorCombination;
|
||||||
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
||||||
import stirling.software.common.util.misc.CustomColorReplaceStrategy;
|
import stirling.software.common.util.misc.CustomColorReplaceStrategy;
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
package stirling.software.SPDF;
|
package stirling.software.SPDF;
|
||||||
|
|
||||||
|
import io.github.pixee.security.SystemCommand;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
import io.github.pixee.security.SystemCommand;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package stirling.software.SPDF;
|
package stirling.software.SPDF;
|
||||||
|
|
||||||
|
import io.github.pixee.security.SystemCommand;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@ -9,20 +12,12 @@ import java.util.Collections;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.core.env.Environment;
|
import org.springframework.core.env.Environment;
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
import io.github.pixee.security.SystemCommand;
|
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import jakarta.annotation.PreDestroy;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.UI.WebBrowser;
|
import stirling.software.SPDF.UI.WebBrowser;
|
||||||
import stirling.software.common.configuration.AppConfig;
|
import stirling.software.common.configuration.AppConfig;
|
||||||
import stirling.software.common.configuration.ConfigInitializer;
|
import stirling.software.common.configuration.ConfigInitializer;
|
||||||
@ -175,6 +170,7 @@ public class SPDFApplication {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
log.info("Running configs {}", applicationProperties.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setServerPortStatic(String port) {
|
public static void setServerPortStatic(String port) {
|
||||||
@ -207,19 +203,20 @@ public class SPDFApplication {
|
|||||||
if (arg.startsWith("--spring.profiles.active=")) {
|
if (arg.startsWith("--spring.profiles.active=")) {
|
||||||
String[] provided = arg.substring(arg.indexOf('=') + 1).split(",");
|
String[] provided = arg.substring(arg.indexOf('=') + 1).split(",");
|
||||||
if (provided.length > 0) {
|
if (provided.length > 0) {
|
||||||
|
log.info("#######0000000000000###############################");
|
||||||
return provided;
|
return provided;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
log.info("######################################");
|
||||||
// 2. Detect if SecurityConfiguration is present on classpath
|
// 2. Detect if SecurityConfiguration is present on classpath
|
||||||
if (isClassPresent(
|
if (isClassPresent(
|
||||||
"stirling.software.proprietary.security.configuration.SecurityConfiguration")) {
|
"stirling.software.proprietary.security.configuration.SecurityConfiguration")) {
|
||||||
log.info("Additional features in jar");
|
log.info("security");
|
||||||
return new String[] {"security"};
|
return new String[] {"security"};
|
||||||
} else {
|
} else {
|
||||||
log.info("Without additional features in jar");
|
log.info("default");
|
||||||
return new String[] {"default"};
|
return new String[] {"default"};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package stirling.software.SPDF.UI.impl;
|
package stirling.software.SPDF.UI.impl;
|
||||||
|
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
import java.awt.AWTException;
|
import java.awt.AWTException;
|
||||||
import java.awt.BorderLayout;
|
import java.awt.BorderLayout;
|
||||||
import java.awt.Frame;
|
import java.awt.Frame;
|
||||||
@ -14,13 +15,16 @@ import java.io.File;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import javax.swing.JFrame;
|
import javax.swing.JFrame;
|
||||||
import javax.swing.JPanel;
|
import javax.swing.JPanel;
|
||||||
import javax.swing.SwingUtilities;
|
import javax.swing.SwingUtilities;
|
||||||
import javax.swing.Timer;
|
import javax.swing.Timer;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import me.friwi.jcefmaven.CefAppBuilder;
|
||||||
|
import me.friwi.jcefmaven.EnumProgress;
|
||||||
|
import me.friwi.jcefmaven.MavenCefAppHandlerAdapter;
|
||||||
|
import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler;
|
||||||
import org.cef.CefApp;
|
import org.cef.CefApp;
|
||||||
import org.cef.CefClient;
|
import org.cef.CefClient;
|
||||||
import org.cef.CefSettings;
|
import org.cef.CefSettings;
|
||||||
@ -32,16 +36,6 @@ import org.cef.handler.CefDownloadHandlerAdapter;
|
|||||||
import org.cef.handler.CefLoadHandlerAdapter;
|
import org.cef.handler.CefLoadHandlerAdapter;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import jakarta.annotation.PreDestroy;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import me.friwi.jcefmaven.CefAppBuilder;
|
|
||||||
import me.friwi.jcefmaven.EnumProgress;
|
|
||||||
import me.friwi.jcefmaven.MavenCefAppHandlerAdapter;
|
|
||||||
import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.UI.WebBrowser;
|
import stirling.software.SPDF.UI.WebBrowser;
|
||||||
import stirling.software.common.configuration.InstallationPathConfig;
|
import stirling.software.common.configuration.InstallationPathConfig;
|
||||||
import stirling.software.common.util.UIScaling;
|
import stirling.software.common.util.UIScaling;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package stirling.software.SPDF.UI.impl;
|
package stirling.software.SPDF.UI.impl;
|
||||||
|
|
||||||
|
import io.github.pixee.security.BoundedLineReader;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@ -7,14 +8,9 @@ import java.io.InputStreamReader;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
|
|
||||||
import io.github.pixee.security.BoundedLineReader;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.common.util.UIScaling;
|
import stirling.software.common.util.UIScaling;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@ -4,7 +4,6 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Scope;
|
import org.springframework.context.annotation.Scope;
|
||||||
|
|
||||||
import stirling.software.common.configuration.interfaces.ShowAdminInterface;
|
import stirling.software.common.configuration.interfaces.ShowAdminInterface;
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
|
||||||
|
@ -1,23 +1,27 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
import org.springframework.web.servlet.ModelAndView;
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
public class CleanUrlInterceptor implements HandlerInterceptor {
|
public class CleanUrlInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
private static final List<String> ALLOWED_PARAMS = Arrays.asList(
|
private static final List<String> ALLOWED_PARAMS =
|
||||||
"lang", "endpoint", "endpoints", "logout", "error", "errorOAuth", "file", "messageType", "infoMessage",
|
Arrays.asList(
|
||||||
"page", "size", "type", "principal", "startDate", "endDate"
|
"lang",
|
||||||
);
|
"endpoint",
|
||||||
|
"endpoints",
|
||||||
|
"logout",
|
||||||
|
"error",
|
||||||
|
"errorOAuth",
|
||||||
|
"file",
|
||||||
|
"messageType",
|
||||||
|
"infoMessage");
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean preHandle(
|
public boolean preHandle(
|
||||||
|
@ -5,12 +5,9 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@ -5,7 +5,7 @@ import java.util.HashSet;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
@ -17,8 +17,6 @@ import org.springframework.web.method.HandlerMethod;
|
|||||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class EndpointInspector implements ApplicationListener<ContextRefreshedEvent> {
|
public class EndpointInspector implements ApplicationListener<ContextRefreshedEvent> {
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@ -1,17 +1,13 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
import stirling.software.common.configuration.RuntimePathConfig;
|
import stirling.software.common.configuration.RuntimePathConfig;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@ -1,22 +1,17 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
import io.micrometer.common.util.StringUtils;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.core.Ordered;
|
import org.springframework.core.Ordered;
|
||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
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.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import io.micrometer.common.util.StringUtils;
|
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.common.util.GeneralUtils;
|
import stirling.software.common.util.GeneralUtils;
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.servlet.LocaleResolver;
|
import org.springframework.web.servlet.LocaleResolver;
|
||||||
@ -9,9 +9,6 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
|||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
|
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
|
||||||
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
|
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import stirling.software.common.configuration.InstallationPathConfig;
|
|
||||||
|
|
||||||
import ch.qos.logback.core.PropertyDefinerBase;
|
import ch.qos.logback.core.PropertyDefinerBase;
|
||||||
|
import stirling.software.common.configuration.InstallationPathConfig;
|
||||||
|
|
||||||
public class LogbackPropertyLoader extends PropertyDefinerBase {
|
public class LogbackPropertyLoader extends PropertyDefinerBase {
|
||||||
@Override
|
@Override
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
import io.micrometer.core.instrument.Meter;
|
import io.micrometer.core.instrument.Meter;
|
||||||
import io.micrometer.core.instrument.config.MeterFilter;
|
import io.micrometer.core.instrument.config.MeterFilter;
|
||||||
import io.micrometer.core.instrument.config.MeterFilterReply;
|
import io.micrometer.core.instrument.config.MeterFilterReply;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class MetricsConfig {
|
public class MetricsConfig {
|
||||||
|
@ -1,21 +1,16 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
|
||||||
|
|
||||||
import io.micrometer.core.instrument.Counter;
|
import io.micrometer.core.instrument.Counter;
|
||||||
import io.micrometer.core.instrument.MeterRegistry;
|
import io.micrometer.core.instrument.MeterRegistry;
|
||||||
|
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
import java.io.IOException;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
import stirling.software.common.util.RequestUriUtils;
|
import stirling.software.common.util.RequestUriUtils;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.models.Components;
|
import io.swagger.v3.oas.models.Components;
|
||||||
import io.swagger.v3.oas.models.OpenAPI;
|
import io.swagger.v3.oas.models.OpenAPI;
|
||||||
import io.swagger.v3.oas.models.info.Contact;
|
import io.swagger.v3.oas.models.info.Contact;
|
||||||
@ -10,9 +7,9 @@ import io.swagger.v3.oas.models.info.Info;
|
|||||||
import io.swagger.v3.oas.models.info.License;
|
import io.swagger.v3.oas.models.info.License;
|
||||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
import org.springframework.context.ApplicationListener;
|
import org.springframework.context.ApplicationListener;
|
||||||
import org.springframework.context.event.ContextRefreshedEvent;
|
import org.springframework.context.event.ContextRefreshedEvent;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
import stirling.software.common.configuration.InstallationPathConfig;
|
import stirling.software.common.configuration.InstallationPathConfig;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@ -1,21 +1,16 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Hidden;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.service.LanguageService;
|
import stirling.software.SPDF.service.LanguageService;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.pdfbox.cos.COSName;
|
import org.apache.pdfbox.cos.COSName;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
||||||
@ -12,12 +14,6 @@ import org.apache.pdfbox.pdmodel.encryption.PDEncryption;
|
|||||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
|
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
|
||||||
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
import stirling.software.common.model.api.PDFFile;
|
import stirling.software.common.model.api.PDFFile;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.pdfbox.multipdf.LayerUtility;
|
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
@ -15,12 +17,6 @@ import org.springframework.web.bind.annotation.ModelAttribute;
|
|||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.general.CropPdfForm;
|
import stirling.software.SPDF.model.api.general.CropPdfForm;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
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 java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
|
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
|
||||||
@ -20,16 +25,6 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||||||
import org.springframework.web.bind.annotation.ResponseBody;
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
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.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.EditTableOfContentsRequest;
|
import stirling.software.SPDF.model.api.EditTableOfContentsRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -10,7 +12,8 @@ import java.util.ArrayList;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.pdfbox.multipdf.PDFMergerUtility;
|
import org.apache.pdfbox.multipdf.PDFMergerUtility;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
||||||
@ -26,13 +29,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.general.MergePdfsRequest;
|
import stirling.software.SPDF.model.api.general.MergePdfsRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.GeneralUtils;
|
import stirling.software.common.util.GeneralUtils;
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.pdfbox.multipdf.LayerUtility;
|
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
@ -17,13 +20,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest;
|
import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
@ -1,20 +1,16 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.service.PdfImageRemovalService;
|
import stirling.software.SPDF.service.PdfImageRemovalService;
|
||||||
import stirling.software.common.model.api.PDFFile;
|
import stirling.software.common.model.api.PDFFile;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -8,7 +11,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.pdfbox.Loader;
|
import org.apache.pdfbox.Loader;
|
||||||
import org.apache.pdfbox.multipdf.Overlay;
|
import org.apache.pdfbox.multipdf.Overlay;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
@ -19,13 +22,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.general.OverlayPdfsRequest;
|
import stirling.software.SPDF.model.api.general.OverlayPdfsRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.GeneralUtils;
|
import stirling.software.common.util.GeneralUtils;
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@ -13,14 +17,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.SortTypes;
|
import stirling.software.SPDF.model.SortTypes;
|
||||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||||
import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
|
import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.apache.pdfbox.pdmodel.PDPageTree;
|
import org.apache.pdfbox.pdmodel.PDPageTree;
|
||||||
@ -11,13 +14,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.general.RotatePDFRequest;
|
import stirling.software.SPDF.model.api.general.RotatePDFRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.pdfbox.multipdf.LayerUtility;
|
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
@ -18,13 +21,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.general.ScalePagesRequest;
|
import stirling.software.SPDF.model.api.general.ScalePagesRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
@ -10,12 +12,6 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Hidden;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||||
import stirling.software.common.configuration.InstallationPathConfig;
|
import stirling.software.common.configuration.InstallationPathConfig;
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@ -9,7 +12,8 @@ import java.util.List;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@ -19,14 +23,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@ -7,7 +10,12 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
|
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
|
||||||
@ -19,18 +27,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
|
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
|
||||||
import stirling.software.common.model.PdfMetadata;
|
import stirling.software.common.model.PdfMetadata;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@ -8,7 +11,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.pdfbox.multipdf.LayerUtility;
|
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
@ -24,13 +27,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest;
|
import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@ -16,14 +20,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
|
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.GeneralUtils;
|
import stirling.software.common.util.GeneralUtils;
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.awt.geom.AffineTransform;
|
import java.awt.geom.AffineTransform;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.pdfbox.multipdf.LayerUtility;
|
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
@ -15,12 +17,6 @@ import org.springframework.web.bind.annotation.ModelAttribute;
|
|||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
import stirling.software.common.model.api.PDFFile;
|
import stirling.software.common.model.api.PDFFile;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@ -12,14 +16,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.common.configuration.RuntimePathConfig;
|
import stirling.software.common.configuration.RuntimePathConfig;
|
||||||
import stirling.software.common.model.api.converters.EmlToPdfRequest;
|
import stirling.software.common.model.api.converters.EmlToPdfRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
@ -1,18 +1,15 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
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.model.api.converters.HTMLToPdfRequest;
|
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -10,7 +13,8 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
@ -22,14 +26,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
|
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
|
||||||
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
|
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.commonmark.Extension;
|
import org.commonmark.Extension;
|
||||||
import org.commonmark.ext.gfm.tables.TableBlock;
|
import org.commonmark.ext.gfm.tables.TableBlock;
|
||||||
import org.commonmark.ext.gfm.tables.TablesExtension;
|
import org.commonmark.ext.gfm.tables.TablesExtension;
|
||||||
@ -16,13 +19,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
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.model.api.GeneralFile;
|
import stirling.software.common.model.api.GeneralFile;
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@ -7,7 +10,7 @@ import java.nio.file.Path;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.commons.io.FilenameUtils;
|
import org.apache.commons.io.FilenameUtils;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@ -16,13 +19,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
import stirling.software.common.configuration.RuntimePathConfig;
|
import stirling.software.common.configuration.RuntimePathConfig;
|
||||||
import stirling.software.common.model.api.GeneralFile;
|
import stirling.software.common.model.api.GeneralFile;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import stirling.software.common.model.api.PDFFile;
|
import stirling.software.common.model.api.PDFFile;
|
||||||
import stirling.software.common.util.PDFToFile;
|
import stirling.software.common.util.PDFToFile;
|
||||||
|
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.text.PDFTextStripper;
|
import org.apache.pdfbox.text.PDFTextStripper;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@ -11,13 +14,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.converters.PdfToPresentationRequest;
|
import stirling.software.SPDF.model.api.converters.PdfToPresentationRequest;
|
||||||
import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest;
|
import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest;
|
||||||
import stirling.software.SPDF.model.api.converters.PdfToWordRequest;
|
import stirling.software.SPDF.model.api.converters.PdfToWordRequest;
|
||||||
|
@ -1,57 +1,16 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
import java.awt.Color;
|
import io.github.pixee.security.Filenames;
|
||||||
import java.io.ByteArrayOutputStream;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.TimeZone;
|
|
||||||
|
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.apache.pdfbox.Loader;
|
|
||||||
import org.apache.pdfbox.cos.COSArray;
|
|
||||||
import org.apache.pdfbox.cos.COSBase;
|
|
||||||
import org.apache.pdfbox.cos.COSDictionary;
|
|
||||||
import org.apache.pdfbox.cos.COSName;
|
|
||||||
import org.apache.pdfbox.pdfwriter.compress.CompressParameters;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDResources;
|
|
||||||
import org.apache.pdfbox.pdmodel.common.PDMetadata;
|
|
||||||
import org.apache.pdfbox.pdmodel.common.PDStream;
|
|
||||||
import org.apache.pdfbox.pdmodel.font.PDFont;
|
|
||||||
import org.apache.pdfbox.pdmodel.font.PDFontDescriptor;
|
|
||||||
import org.apache.pdfbox.pdmodel.font.PDTrueTypeFont;
|
|
||||||
import org.apache.pdfbox.pdmodel.font.PDType0Font;
|
|
||||||
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
|
|
||||||
import org.apache.pdfbox.pdmodel.graphics.color.PDOutputIntent;
|
|
||||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
|
||||||
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
|
|
||||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
|
||||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
|
|
||||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationTextMarkup;
|
|
||||||
import org.apache.pdfbox.pdmodel.interactive.viewerpreferences.PDViewerPreferences;
|
|
||||||
import org.apache.xmpbox.XMPMetadata;
|
|
||||||
import org.apache.xmpbox.schema.AdobePDFSchema;
|
|
||||||
import org.apache.xmpbox.schema.DublinCoreSchema;
|
|
||||||
import org.apache.xmpbox.schema.PDFAIdentificationSchema;
|
|
||||||
import org.apache.xmpbox.schema.XMPBasicSchema;
|
|
||||||
import org.apache.xmpbox.xml.DomXmpParser;
|
|
||||||
import org.apache.xmpbox.xml.XmpSerializer;
|
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
@ -59,13 +18,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.converters.PdfToPdfARequest;
|
import stirling.software.SPDF.model.api.converters.PdfToPdfARequest;
|
||||||
import stirling.software.common.util.ProcessExecutor;
|
import stirling.software.common.util.ProcessExecutor;
|
||||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||||
@ -104,121 +56,20 @@ public class ConvertPDFToPDFA {
|
|||||||
: originalFileName;
|
: originalFileName;
|
||||||
|
|
||||||
Path tempInputFile = null;
|
Path tempInputFile = null;
|
||||||
|
Path tempOutputDir = null;
|
||||||
byte[] fileBytes;
|
byte[] fileBytes;
|
||||||
Path loPdfPath = null; // Used for LibreOffice conversion output
|
|
||||||
File preProcessedFile = null;
|
|
||||||
int pdfaPart = 2;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Save uploaded file to temp location
|
// Save uploaded file to temp location
|
||||||
tempInputFile = Files.createTempFile("input_", ".pdf");
|
tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||||
inputFile.transferTo(tempInputFile);
|
inputFile.transferTo(tempInputFile);
|
||||||
|
|
||||||
// Branch conversion based on desired output PDF/A format
|
|
||||||
if ("pdfa".equals(outputFormat)) {
|
|
||||||
preProcessedFile = tempInputFile.toFile();
|
|
||||||
} else {
|
|
||||||
pdfaPart = 1;
|
|
||||||
preProcessedFile = preProcessHighlights(tempInputFile.toFile());
|
|
||||||
}
|
|
||||||
Set<String> missingFonts = new HashSet<>();
|
|
||||||
boolean needImgs = false;
|
|
||||||
try (PDDocument doc = Loader.loadPDF(preProcessedFile)) {
|
|
||||||
missingFonts = findUnembeddedFontNames(doc);
|
|
||||||
needImgs = (pdfaPart == 1) && hasTransparentImages(doc);
|
|
||||||
if (!missingFonts.isEmpty() || needImgs) {
|
|
||||||
// Run LibreOffice conversion to get flattened images and embedded fonts
|
|
||||||
loPdfPath = runLibreOfficeConversion(preProcessedFile.toPath(), pdfaPart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fileBytes =
|
|
||||||
convertToPdfA(
|
|
||||||
preProcessedFile.toPath(), loPdfPath, pdfaPart, missingFonts, needImgs);
|
|
||||||
|
|
||||||
String outputFilename = baseFileName + "_PDFA.pdf";
|
|
||||||
|
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
|
||||||
fileBytes, outputFilename, MediaType.APPLICATION_PDF);
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
// Clean up temporary files
|
|
||||||
if (tempInputFile != null) {
|
|
||||||
Files.deleteIfExists(tempInputFile);
|
|
||||||
}
|
|
||||||
if (loPdfPath != null && loPdfPath.getParent() != null) {
|
|
||||||
FileUtils.deleteDirectory(loPdfPath.getParent().toFile());
|
|
||||||
}
|
|
||||||
if (preProcessedFile != null) {
|
|
||||||
Files.deleteIfExists(preProcessedFile.toPath());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Merge fonts & flattened images from loPdfPath into basePdfPath, then run the standard
|
|
||||||
* PDFBox/A pipeline.
|
|
||||||
*
|
|
||||||
* @param basePdfPath Path to the original (or highlight‐preprocessed) PDF
|
|
||||||
* @param loPdfPath Path to the LibreOffice–flattened PDF/A, or null if not used
|
|
||||||
* @param pdfaPart 1 (PDF/A-1B) or 2 (PDF/A-2B)
|
|
||||||
* @return the final PDF/A bytes
|
|
||||||
*/
|
|
||||||
private byte[] convertToPdfA(
|
|
||||||
Path basePdfPath,
|
|
||||||
Path loPdfPath,
|
|
||||||
int pdfaPart,
|
|
||||||
Set<String> missingFonts,
|
|
||||||
boolean importImages)
|
|
||||||
throws Exception {
|
|
||||||
try (PDDocument baseDoc = Loader.loadPDF(basePdfPath.toFile())) {
|
|
||||||
|
|
||||||
if (loPdfPath != null) {
|
|
||||||
try (PDDocument loDoc = Loader.loadPDF(loPdfPath.toFile())) {
|
|
||||||
if (!missingFonts.isEmpty()) {
|
|
||||||
embedMissingFonts(loDoc, baseDoc, missingFonts);
|
|
||||||
}
|
|
||||||
if (importImages) {
|
|
||||||
importFlattenedImages(loDoc, baseDoc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return processWithPDFBox(baseDoc, pdfaPart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] processWithPDFBox(PDDocument document, int pdfaPart) throws Exception {
|
|
||||||
|
|
||||||
removeElementsForPdfA(document, pdfaPart);
|
|
||||||
|
|
||||||
mergeAndAddXmpMetadata(document, pdfaPart);
|
|
||||||
|
|
||||||
addICCProfileIfNotPresent(document);
|
|
||||||
|
|
||||||
// Mark the document as PDF/A
|
|
||||||
PDDocumentCatalog catalog = document.getDocumentCatalog();
|
|
||||||
catalog.setMetadata(
|
|
||||||
document.getDocumentCatalog().getMetadata()); // Ensure metadata is linked
|
|
||||||
catalog.setViewerPreferences(
|
|
||||||
new PDViewerPreferences(catalog.getCOSObject())); // PDF/A best practice
|
|
||||||
document.getDocument().setVersion(pdfaPart == 1 ? 1.4f : 1.7f);
|
|
||||||
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
if (pdfaPart == 1) {
|
|
||||||
document.save(baos, CompressParameters.NO_COMPRESSION);
|
|
||||||
} else {
|
|
||||||
document.save(baos);
|
|
||||||
}
|
|
||||||
|
|
||||||
return baos.toByteArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Path runLibreOfficeConversion(Path tempInputFile, int pdfaPart) throws Exception {
|
|
||||||
// Create temp output directory
|
// Create temp output directory
|
||||||
Path tempOutputDir = Files.createTempDirectory("output_");
|
tempOutputDir = Files.createTempDirectory("output_");
|
||||||
|
|
||||||
// Determine PDF/A filter based on requested format
|
// Determine PDF/A filter based on requested format
|
||||||
String pdfFilter =
|
String pdfFilter =
|
||||||
pdfaPart == 2
|
"pdfa".equals(outputFormat)
|
||||||
? "pdf:writer_pdf_Export:{\"SelectPdfVersion\":{\"type\":\"long\",\"value\":\"2\"}}"
|
? "pdf:writer_pdf_Export:{\"SelectPdfVersion\":{\"type\":\"long\",\"value\":\"2\"}}"
|
||||||
: "pdf:writer_pdf_Export:{\"SelectPdfVersion\":{\"type\":\"long\",\"value\":\"1\"}}";
|
: "pdf:writer_pdf_Export:{\"SelectPdfVersion\":{\"type\":\"long\",\"value\":\"1\"}}";
|
||||||
|
|
||||||
@ -248,454 +99,24 @@ public class ConvertPDFToPDFA {
|
|||||||
File[] outputFiles = tempOutputDir.toFile().listFiles();
|
File[] outputFiles = tempOutputDir.toFile().listFiles();
|
||||||
if (outputFiles == null || outputFiles.length != 1) {
|
if (outputFiles == null || outputFiles.length != 1) {
|
||||||
throw new RuntimeException(
|
throw new RuntimeException(
|
||||||
"Expected one output PDF, found "
|
"Expected exactly one output file but found "
|
||||||
+ (outputFiles == null ? "none" : outputFiles.length));
|
+ (outputFiles == null ? "none" : outputFiles.length));
|
||||||
}
|
}
|
||||||
return outputFiles[0].toPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void embedMissingFonts(PDDocument loDoc, PDDocument baseDoc, Set<String> missingFonts)
|
fileBytes = FileUtils.readFileToByteArray(outputFiles[0]);
|
||||||
throws IOException {
|
String outputFilename = baseFileName + "_PDFA.pdf";
|
||||||
List<PDPage> loPages = new ArrayList<>();
|
|
||||||
loDoc.getPages().forEach(loPages::add);
|
|
||||||
List<PDPage> basePages = new ArrayList<>();
|
|
||||||
baseDoc.getPages().forEach(basePages::add);
|
|
||||||
|
|
||||||
for (int i = 0; i < loPages.size(); i++) {
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
PDResources loRes = loPages.get(i).getResources();
|
fileBytes, outputFilename, MediaType.APPLICATION_PDF);
|
||||||
PDResources baseRes = basePages.get(i).getResources();
|
|
||||||
|
|
||||||
for (COSName fontKey : loRes.getFontNames()) {
|
} finally {
|
||||||
PDFont loFont = loRes.getFont(fontKey);
|
// Clean up temporary files
|
||||||
if (loFont == null) continue;
|
if (tempInputFile != null) {
|
||||||
|
Files.deleteIfExists(tempInputFile);
|
||||||
String psName = loFont.getName();
|
|
||||||
if (!missingFonts.contains(psName)) continue;
|
|
||||||
|
|
||||||
PDFontDescriptor desc = loFont.getFontDescriptor();
|
|
||||||
if (desc == null) continue;
|
|
||||||
|
|
||||||
PDStream fontStream = null;
|
|
||||||
if (desc.getFontFile() != null) {
|
|
||||||
fontStream = desc.getFontFile();
|
|
||||||
} else if (desc.getFontFile2() != null) {
|
|
||||||
fontStream = desc.getFontFile2();
|
|
||||||
} else if (desc.getFontFile3() != null) {
|
|
||||||
fontStream = desc.getFontFile3();
|
|
||||||
}
|
}
|
||||||
if (fontStream == null) continue;
|
if (tempOutputDir != null) {
|
||||||
|
FileUtils.deleteDirectory(tempOutputDir.toFile());
|
||||||
try (InputStream in = fontStream.createInputStream()) {
|
|
||||||
PDFont newFont = null;
|
|
||||||
try {
|
|
||||||
newFont = PDType0Font.load(baseDoc, in, false);
|
|
||||||
} catch (IOException e1) {
|
|
||||||
try {
|
|
||||||
newFont = PDTrueTypeFont.load(baseDoc, in, null);
|
|
||||||
} catch (IOException | IllegalArgumentException e2) {
|
|
||||||
log.error("Could not embed font {}: {}", psName, e2.getMessage());
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (newFont != null) {
|
|
||||||
baseRes.put(fontKey, newFont);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Set<String> findUnembeddedFontNames(PDDocument doc) throws IOException {
|
|
||||||
Set<String> missing = new HashSet<>();
|
|
||||||
for (PDPage page : doc.getPages()) {
|
|
||||||
PDResources res = page.getResources();
|
|
||||||
for (COSName name : res.getFontNames()) {
|
|
||||||
PDFont font = res.getFont(name);
|
|
||||||
if (font != null && !font.isEmbedded()) {
|
|
||||||
missing.add(font.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return missing;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void importFlattenedImages(PDDocument loDoc, PDDocument baseDoc) throws IOException {
|
|
||||||
List<PDPage> loPages = new ArrayList<>();
|
|
||||||
loDoc.getPages().forEach(loPages::add);
|
|
||||||
List<PDPage> basePages = new ArrayList<>();
|
|
||||||
baseDoc.getPages().forEach(basePages::add);
|
|
||||||
|
|
||||||
for (int i = 0; i < loPages.size(); i++) {
|
|
||||||
PDPage loPage = loPages.get(i);
|
|
||||||
PDPage basePage = basePages.get(i);
|
|
||||||
|
|
||||||
PDResources loRes = loPage.getResources();
|
|
||||||
PDResources baseRes = basePage.getResources();
|
|
||||||
Set<COSName> toReplace = detectTransparentXObjects(basePage);
|
|
||||||
|
|
||||||
for (COSName name : toReplace) {
|
|
||||||
PDXObject loXo = loRes.getXObject(name);
|
|
||||||
if (!(loXo instanceof PDImageXObject img)) continue;
|
|
||||||
|
|
||||||
PDImageXObject newImg = LosslessFactory.createFromImage(baseDoc, img.getImage());
|
|
||||||
|
|
||||||
// replace the resource under the same name
|
|
||||||
baseRes.put(name, newImg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Set<COSName> detectTransparentXObjects(PDPage page) {
|
|
||||||
Set<COSName> transparentObjects = new HashSet<>();
|
|
||||||
PDResources res = page.getResources();
|
|
||||||
if (res == null) return transparentObjects;
|
|
||||||
|
|
||||||
for (COSName name : res.getXObjectNames()) {
|
|
||||||
try {
|
|
||||||
PDXObject xo = res.getXObject(name);
|
|
||||||
if (xo instanceof PDImageXObject img) {
|
|
||||||
COSDictionary d = img.getCOSObject();
|
|
||||||
if (d.containsKey(COSName.SMASK)
|
|
||||||
|| isTransparencyGroup(d)
|
|
||||||
|| d.getBoolean(COSName.INTERPOLATE, false)) {
|
|
||||||
transparentObjects.add(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
log.error("Error processing XObject {}: {}", name.getName(), ioe.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return transparentObjects;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isTransparencyGroup(COSDictionary dict) {
|
|
||||||
COSBase g = dict.getDictionaryObject(COSName.GROUP);
|
|
||||||
return g instanceof COSDictionary gd
|
|
||||||
&& COSName.TRANSPARENCY.equals(gd.getCOSName(COSName.S));
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasTransparentImages(PDDocument doc) {
|
|
||||||
for (PDPage page : doc.getPages()) {
|
|
||||||
PDResources res = page.getResources();
|
|
||||||
if (res == null) continue;
|
|
||||||
for (COSName name : res.getXObjectNames()) {
|
|
||||||
try {
|
|
||||||
PDXObject xo = res.getXObject(name);
|
|
||||||
if (xo instanceof PDImageXObject img) {
|
|
||||||
COSDictionary dict = img.getCOSObject();
|
|
||||||
if (dict.containsKey(COSName.SMASK)) return true;
|
|
||||||
COSBase g = dict.getDictionaryObject(COSName.GROUP);
|
|
||||||
if (g instanceof COSDictionary gd
|
|
||||||
&& COSName.TRANSPARENCY.equals(gd.getCOSName(COSName.S))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (dict.getBoolean(COSName.INTERPOLATE, false)) return true;
|
|
||||||
}
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
log.error("Error processing XObject {}: {}", name.getName(), ioe.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sanitizePdfA(COSBase base, PDResources resources, int pdfaPart) {
|
|
||||||
if (base instanceof COSDictionary dict) {
|
|
||||||
if (pdfaPart == 1) {
|
|
||||||
// Remove transparency-related elements
|
|
||||||
COSBase group = dict.getDictionaryObject(COSName.GROUP);
|
|
||||||
if (group instanceof COSDictionary gDict
|
|
||||||
&& COSName.TRANSPARENCY.equals(gDict.getCOSName(COSName.S))) {
|
|
||||||
dict.removeItem(COSName.GROUP);
|
|
||||||
}
|
|
||||||
|
|
||||||
dict.removeItem(COSName.SMASK);
|
|
||||||
// Transparency blending constants (/CA, /ca) — disallowed in PDF/A-1
|
|
||||||
dict.removeItem(COSName.CA);
|
|
||||||
dict.removeItem(COSName.getPDFName("ca"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interpolation (non-deterministic image scaling) — required to be false
|
|
||||||
if (dict.containsKey(COSName.INTERPOLATE)
|
|
||||||
&& dict.getBoolean(COSName.INTERPOLATE, true)) {
|
|
||||||
dict.setBoolean(COSName.INTERPOLATE, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove common forbidden features (for PDF/A 1 and 2)
|
|
||||||
dict.removeItem(COSName.JAVA_SCRIPT);
|
|
||||||
dict.removeItem(COSName.getPDFName("JS"));
|
|
||||||
dict.removeItem(COSName.getPDFName("RichMedia"));
|
|
||||||
dict.removeItem(COSName.getPDFName("Movie"));
|
|
||||||
dict.removeItem(COSName.getPDFName("Sound"));
|
|
||||||
dict.removeItem(COSName.getPDFName("Launch"));
|
|
||||||
dict.removeItem(COSName.URI);
|
|
||||||
dict.removeItem(COSName.getPDFName("GoToR"));
|
|
||||||
dict.removeItem(COSName.EMBEDDED_FILES);
|
|
||||||
dict.removeItem(COSName.FILESPEC);
|
|
||||||
|
|
||||||
// Recurse through all entries in the dictionary
|
|
||||||
for (Map.Entry<COSName, COSBase> entry : dict.entrySet()) {
|
|
||||||
sanitizePdfA(entry.getValue(), resources, pdfaPart);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (base instanceof COSArray arr) {
|
|
||||||
// Recursively sanitize each item in the array
|
|
||||||
for (COSBase item : arr) {
|
|
||||||
sanitizePdfA(item, resources, pdfaPart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeElementsForPdfA(PDDocument doc, int pdfaPart) {
|
|
||||||
|
|
||||||
if (pdfaPart == 1) {
|
|
||||||
// Remove Optional Content (Layers) - not allowed in PDF/A-1
|
|
||||||
doc.getDocumentCatalog().getCOSObject().removeItem(COSName.getPDFName("OCProperties"));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (PDPage page : doc.getPages()) {
|
|
||||||
if (pdfaPart == 1) {
|
|
||||||
page.setAnnotations(Collections.emptyList());
|
|
||||||
}
|
|
||||||
PDResources res = page.getResources();
|
|
||||||
// Clean page-level dictionary
|
|
||||||
sanitizePdfA(page.getCOSObject(), res, pdfaPart);
|
|
||||||
|
|
||||||
// sanitize each Form XObject
|
|
||||||
if (res != null) {
|
|
||||||
for (COSName name : res.getXObjectNames()) {
|
|
||||||
try {
|
|
||||||
PDXObject xo = res.getXObject(name);
|
|
||||||
if (xo instanceof PDFormXObject form) {
|
|
||||||
sanitizePdfA(form.getCOSObject(), res, pdfaPart);
|
|
||||||
} else if (xo instanceof PDImageXObject img) {
|
|
||||||
sanitizePdfA(img.getCOSObject(), res, pdfaPart);
|
|
||||||
}
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
log.error("Cannot load XObject {}: {}", name.getName(), ioe.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Embbeds the XMP metadata required for PDF/A compliance. */
|
|
||||||
private void mergeAndAddXmpMetadata(PDDocument document, int pdfaPart) throws Exception {
|
|
||||||
PDMetadata existingMetadata = document.getDocumentCatalog().getMetadata();
|
|
||||||
XMPMetadata xmp;
|
|
||||||
|
|
||||||
// Load existing XMP if available
|
|
||||||
if (existingMetadata != null) {
|
|
||||||
try (InputStream xmpStream = existingMetadata.createInputStream()) {
|
|
||||||
DomXmpParser parser = new DomXmpParser();
|
|
||||||
parser.setStrictParsing(false);
|
|
||||||
xmp = parser.parse(xmpStream);
|
|
||||||
} catch (Exception e) {
|
|
||||||
xmp = XMPMetadata.createXMPMetadata();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
xmp = XMPMetadata.createXMPMetadata();
|
|
||||||
}
|
|
||||||
|
|
||||||
PDDocumentInformation docInfo = document.getDocumentInformation();
|
|
||||||
if (docInfo == null) {
|
|
||||||
docInfo = new PDDocumentInformation();
|
|
||||||
}
|
|
||||||
|
|
||||||
String originalCreator = Optional.ofNullable(docInfo.getCreator()).orElse("Unknown");
|
|
||||||
String originalProducer = Optional.ofNullable(docInfo.getProducer()).orElse("Unknown");
|
|
||||||
|
|
||||||
// Only keep the original creator so it can match xmp creator tool for compliance
|
|
||||||
DublinCoreSchema dcSchema = xmp.getDublinCoreSchema();
|
|
||||||
if (dcSchema != null) {
|
|
||||||
List<String> existingCreators = dcSchema.getCreators();
|
|
||||||
if (existingCreators != null) {
|
|
||||||
for (String creator : new ArrayList<>(existingCreators)) {
|
|
||||||
dcSchema.removeCreator(creator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dcSchema = xmp.createAndAddDublinCoreSchema();
|
|
||||||
}
|
|
||||||
dcSchema.addCreator(originalCreator);
|
|
||||||
|
|
||||||
PDFAIdentificationSchema pdfaSchema =
|
|
||||||
(PDFAIdentificationSchema) xmp.getSchema(PDFAIdentificationSchema.class);
|
|
||||||
if (pdfaSchema == null) {
|
|
||||||
pdfaSchema = xmp.createAndAddPDFAIdentificationSchema();
|
|
||||||
}
|
|
||||||
pdfaSchema.setPart(pdfaPart);
|
|
||||||
pdfaSchema.setConformance("B");
|
|
||||||
|
|
||||||
XMPBasicSchema xmpBasicSchema = xmp.getXMPBasicSchema();
|
|
||||||
if (xmpBasicSchema == null) {
|
|
||||||
xmpBasicSchema = xmp.createAndAddXMPBasicSchema();
|
|
||||||
}
|
|
||||||
|
|
||||||
AdobePDFSchema adobePdfSchema = xmp.getAdobePDFSchema();
|
|
||||||
if (adobePdfSchema == null) {
|
|
||||||
adobePdfSchema = xmp.createAndAddAdobePDFSchema();
|
|
||||||
}
|
|
||||||
|
|
||||||
docInfo.setCreator(originalCreator);
|
|
||||||
xmpBasicSchema.setCreatorTool(originalCreator);
|
|
||||||
|
|
||||||
docInfo.setProducer(originalProducer);
|
|
||||||
adobePdfSchema.setProducer(originalProducer);
|
|
||||||
|
|
||||||
String originalAuthor = docInfo.getAuthor();
|
|
||||||
if (originalAuthor != null && !originalAuthor.isBlank()) {
|
|
||||||
docInfo.setAuthor(null);
|
|
||||||
// If the author is set, we keep it in the XMP metadata
|
|
||||||
if (!originalCreator.equals(originalAuthor)) {
|
|
||||||
dcSchema.addCreator(originalAuthor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String title = docInfo.getTitle();
|
|
||||||
if (title != null && !title.isBlank()) {
|
|
||||||
dcSchema.setTitle(title);
|
|
||||||
}
|
|
||||||
String subject = docInfo.getSubject();
|
|
||||||
if (subject != null && !subject.isBlank()) {
|
|
||||||
dcSchema.addSubject(subject);
|
|
||||||
}
|
|
||||||
String keywords = docInfo.getKeywords();
|
|
||||||
if (keywords != null && !keywords.isBlank()) {
|
|
||||||
adobePdfSchema.setKeywords(keywords);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set creation and modification dates
|
|
||||||
Calendar now = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
|
|
||||||
Calendar originalCreationDate = docInfo.getCreationDate();
|
|
||||||
if (originalCreationDate == null) {
|
|
||||||
originalCreationDate = now;
|
|
||||||
}
|
|
||||||
docInfo.setCreationDate(originalCreationDate);
|
|
||||||
xmpBasicSchema.setCreateDate(originalCreationDate);
|
|
||||||
|
|
||||||
docInfo.setModificationDate(now);
|
|
||||||
xmpBasicSchema.setModifyDate(now);
|
|
||||||
xmpBasicSchema.setMetadataDate(now);
|
|
||||||
|
|
||||||
// Serialize the created metadata so it can be attached to the existent metadata
|
|
||||||
ByteArrayOutputStream xmpOut = new ByteArrayOutputStream();
|
|
||||||
new XmpSerializer().serialize(xmp, xmpOut, true);
|
|
||||||
|
|
||||||
PDMetadata newMetadata = new PDMetadata(document);
|
|
||||||
newMetadata.importXMPMetadata(xmpOut.toByteArray());
|
|
||||||
document.getDocumentCatalog().setMetadata(newMetadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addICCProfileIfNotPresent(PDDocument document) throws Exception {
|
|
||||||
if (document.getDocumentCatalog().getOutputIntents().isEmpty()) {
|
|
||||||
try (InputStream colorProfile = getClass().getResourceAsStream("/icc/sRGB2014.icc")) {
|
|
||||||
PDOutputIntent outputIntent = new PDOutputIntent(document, colorProfile);
|
|
||||||
outputIntent.setInfo("sRGB IEC61966-2.1");
|
|
||||||
outputIntent.setOutputCondition("sRGB IEC61966-2.1");
|
|
||||||
outputIntent.setOutputConditionIdentifier("sRGB IEC61966-2.1");
|
|
||||||
outputIntent.setRegistryName("http://www.color.org");
|
|
||||||
document.getDocumentCatalog().addOutputIntent(outputIntent);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Failed to load ICC profile: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private File preProcessHighlights(File inputPdf) throws Exception {
|
|
||||||
|
|
||||||
try (PDDocument document = Loader.loadPDF(inputPdf)) {
|
|
||||||
|
|
||||||
for (PDPage page : document.getPages()) {
|
|
||||||
// Retrieve the annotations on the page.
|
|
||||||
List<PDAnnotation> annotations = page.getAnnotations();
|
|
||||||
for (PDAnnotation annot : annotations) {
|
|
||||||
// Process only highlight annotations.
|
|
||||||
if ("Highlight".equals(annot.getSubtype())
|
|
||||||
&& annot instanceof PDAnnotationTextMarkup highlight) {
|
|
||||||
// Create a new appearance stream with the same bounding box.
|
|
||||||
float[] colorComponents =
|
|
||||||
highlight.getColor() != null
|
|
||||||
? highlight.getColor().getComponents()
|
|
||||||
: new float[] {1f, 1f, 0f};
|
|
||||||
Color highlightColor =
|
|
||||||
new Color(
|
|
||||||
colorComponents[0], colorComponents[1], colorComponents[2]);
|
|
||||||
|
|
||||||
float[] quadPoints = highlight.getQuadPoints();
|
|
||||||
if (quadPoints != null) {
|
|
||||||
try (PDPageContentStream cs =
|
|
||||||
new PDPageContentStream(
|
|
||||||
document,
|
|
||||||
page,
|
|
||||||
PDPageContentStream.AppendMode.PREPEND,
|
|
||||||
true,
|
|
||||||
true)) {
|
|
||||||
|
|
||||||
cs.setStrokingColor(highlightColor);
|
|
||||||
cs.setLineWidth(0.05f);
|
|
||||||
float spacing = 2f;
|
|
||||||
// Draw diagonal lines across the highlight area to simulate
|
|
||||||
// transparency.
|
|
||||||
for (int i = 0; i < quadPoints.length; i += 8) {
|
|
||||||
float minX =
|
|
||||||
Math.min(
|
|
||||||
Math.min(quadPoints[i], quadPoints[i + 2]),
|
|
||||||
Math.min(quadPoints[i + 4], quadPoints[i + 6]));
|
|
||||||
float maxX =
|
|
||||||
Math.max(
|
|
||||||
Math.max(quadPoints[i], quadPoints[i + 2]),
|
|
||||||
Math.max(quadPoints[i + 4], quadPoints[i + 6]));
|
|
||||||
float minY =
|
|
||||||
Math.min(
|
|
||||||
Math.min(quadPoints[i + 1], quadPoints[i + 3]),
|
|
||||||
Math.min(quadPoints[i + 5], quadPoints[i + 7]));
|
|
||||||
float maxY =
|
|
||||||
Math.max(
|
|
||||||
Math.max(quadPoints[i + 1], quadPoints[i + 3]),
|
|
||||||
Math.max(quadPoints[i + 5], quadPoints[i + 7]));
|
|
||||||
|
|
||||||
float width = maxX - minX;
|
|
||||||
float height = maxY - minY;
|
|
||||||
|
|
||||||
for (float y = minY; y <= maxY; y += spacing) {
|
|
||||||
float len = Math.min(width, maxY - y);
|
|
||||||
cs.moveTo(minX, y);
|
|
||||||
cs.lineTo(minX + len, y + len);
|
|
||||||
}
|
|
||||||
for (float x = minX + spacing; x <= maxX; x += spacing) {
|
|
||||||
float len = Math.min(maxX - x, height);
|
|
||||||
cs.moveTo(x, minY);
|
|
||||||
cs.lineTo(x + len, minY + len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cs.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
page.getAnnotations().remove(highlight);
|
|
||||||
COSDictionary pageDict = page.getCOSObject();
|
|
||||||
|
|
||||||
if (pageDict.containsKey(COSName.GROUP)) {
|
|
||||||
COSDictionary groupDict =
|
|
||||||
(COSDictionary) pageDict.getDictionaryObject(COSName.GROUP);
|
|
||||||
|
|
||||||
if (groupDict != null) {
|
|
||||||
if (COSName.TRANSPARENCY
|
|
||||||
.getName()
|
|
||||||
.equalsIgnoreCase(groupDict.getNameAsString(COSName.S))) {
|
|
||||||
pageDict.removeItem(COSName.GROUP);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Save the modified document to a temporary file.
|
|
||||||
File preProcessedFile = Files.createTempFile("preprocessed_", ".pdf").toFile();
|
|
||||||
document.save(preProcessedFile);
|
|
||||||
return preProcessedFile;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,20 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
|
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
|
||||||
import stirling.software.common.configuration.RuntimePathConfig;
|
import stirling.software.common.configuration.RuntimePathConfig;
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
@ -9,7 +11,8 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.csv.CSVFormat;
|
import org.apache.commons.csv.CSVFormat;
|
||||||
import org.apache.commons.csv.QuoteMode;
|
import org.apache.commons.csv.QuoteMode;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
@ -21,17 +24,9 @@ import org.springframework.web.bind.annotation.ModelAttribute;
|
|||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||||
import stirling.software.SPDF.pdf.FlexibleCSVWriter;
|
import stirling.software.SPDF.pdf.FlexibleCSVWriter;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
|
||||||
import technology.tabula.ObjectExtractor;
|
import technology.tabula.ObjectExtractor;
|
||||||
import technology.tabula.Page;
|
import technology.tabula.Page;
|
||||||
import technology.tabula.Table;
|
import technology.tabula.Table;
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
package stirling.software.SPDF.controller.api.filters;
|
package stirling.software.SPDF.controller.api.filters;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
@ -11,13 +14,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.PDFComparisonAndCount;
|
import stirling.software.SPDF.model.api.PDFComparisonAndCount;
|
||||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||||
import stirling.software.SPDF.model.api.filter.ContainsTextRequest;
|
import stirling.software.SPDF.model.api.filter.ContainsTextRequest;
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.text.PDFTextStripper;
|
import org.apache.pdfbox.text.PDFTextStripper;
|
||||||
import org.apache.pdfbox.text.TextPosition;
|
import org.apache.pdfbox.text.TextPosition;
|
||||||
@ -14,14 +18,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest;
|
import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import com.google.zxing.*;
|
||||||
|
import com.google.zxing.common.HybridBinarizer;
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.awt.image.DataBufferByte;
|
import java.awt.image.DataBufferByte;
|
||||||
import java.awt.image.DataBufferInt;
|
import java.awt.image.DataBufferInt;
|
||||||
@ -13,7 +18,8 @@ import java.util.List;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@ -23,17 +29,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import com.google.zxing.*;
|
|
||||||
import com.google.zxing.common.HybridBinarizer;
|
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest;
|
import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -7,7 +10,8 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.apache.pdfbox.pdmodel.PDPageTree;
|
import org.apache.pdfbox.pdmodel.PDPageTree;
|
||||||
@ -21,14 +25,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest;
|
import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.PdfUtils;
|
import stirling.software.common.util.PdfUtils;
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
@ -17,14 +20,17 @@ import java.util.Iterator;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
import javax.imageio.IIOImage;
|
import javax.imageio.IIOImage;
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import javax.imageio.ImageWriteParam;
|
import javax.imageio.ImageWriteParam;
|
||||||
import javax.imageio.ImageWriter;
|
import javax.imageio.ImageWriter;
|
||||||
import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
|
import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
|
||||||
import javax.imageio.stream.ImageOutputStream;
|
import javax.imageio.stream.ImageOutputStream;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.pdfbox.cos.COSName;
|
import org.apache.pdfbox.cos.COSName;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
@ -38,17 +44,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||||
import stirling.software.SPDF.model.api.misc.OptimizePdfRequest;
|
import stirling.software.SPDF.model.api.misc.OptimizePdfRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.pdfbox.cos.*;
|
import org.apache.pdfbox.cos.*;
|
||||||
import org.apache.pdfbox.io.IOUtils;
|
import org.apache.pdfbox.io.IOUtils;
|
||||||
import org.apache.pdfbox.pdfwriter.compress.CompressParameters;
|
import org.apache.pdfbox.pdfwriter.compress.CompressParameters;
|
||||||
@ -17,13 +20,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.common.model.api.PDFFile;
|
import stirling.software.common.model.api.PDFFile;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -10,9 +12,9 @@ import java.util.Arrays;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||||
@ -23,13 +25,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
|
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.CheckProgramInstall;
|
import stirling.software.common.util.CheckProgramInstall;
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.awt.image.RenderedImage;
|
import java.awt.image.RenderedImage;
|
||||||
@ -17,9 +20,9 @@ import java.util.concurrent.Future;
|
|||||||
import java.util.zip.Deflater;
|
import java.util.zip.Deflater;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.pdfbox.cos.COSName;
|
import org.apache.pdfbox.cos.COSName;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
@ -31,14 +34,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.PDFExtractImagesRequest;
|
import stirling.software.SPDF.model.api.PDFExtractImagesRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.ImageProcessingUtils;
|
import stirling.software.common.util.ImageProcessingUtils;
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
import java.awt.Graphics2D;
|
import java.awt.Graphics2D;
|
||||||
import java.awt.RenderingHints;
|
import java.awt.RenderingHints;
|
||||||
@ -8,7 +12,8 @@ import java.awt.image.BufferedImage;
|
|||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
@ -23,16 +28,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.FakeScanRequest;
|
import stirling.software.SPDF.model.api.misc.FakeScanRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
@ -17,14 +21,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.FlattenRequest;
|
import stirling.software.SPDF.model.api.misc.FlattenRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.pdfbox.cos.COSName;
|
import org.apache.pdfbox.cos.COSName;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
||||||
@ -14,14 +18,6 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.web.bind.WebDataBinder;
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.MetadataRequest;
|
import stirling.software.SPDF.model.api.misc.MetadataRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import io.github.pixee.security.BoundedLineReader;
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@ -7,9 +11,9 @@ import java.nio.file.Path;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.pdfbox.multipdf.PDFMergerUtility;
|
import org.apache.pdfbox.multipdf.PDFMergerUtility;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
@ -22,15 +26,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.BoundedLineReader;
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
|
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
@ -9,14 +13,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.OverlayImageRequest;
|
import stirling.software.SPDF.model.api.misc.OverlayImageRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.PdfUtils;
|
import stirling.software.common.util.PdfUtils;
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
@ -17,13 +20,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.AddPageNumbersRequest;
|
import stirling.software.SPDF.model.api.misc.AddPageNumbersRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.GeneralUtils;
|
import stirling.software.common.util.GeneralUtils;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user