mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-23 16:05:09 +00:00
Compare commits
No commits in common. "a5aed57d9f8f63d4f3e86920288c5a029f5b826c" and "43c5a1970f63b245bf7125ab247d14c85e6ac534" have entirely different histories.
a5aed57d9f
...
43c5a1970f
58
convert_properties_to_utf8.sh
Normal file
58
convert_properties_to_utf8.sh
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Check if iconv is installed
|
||||||
|
if ! command -v iconv &> /dev/null; then
|
||||||
|
echo "Error: iconv is required but not installed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Directory containing property files
|
||||||
|
PROP_DIR="stirling-pdf/src/main/resources"
|
||||||
|
|
||||||
|
# List of files to convert
|
||||||
|
FILES=(
|
||||||
|
"stirling-pdf/src/main/resources/messages_az_AZ.properties"
|
||||||
|
"stirling-pdf/src/main/resources/messages_ca_CA.properties"
|
||||||
|
"stirling-pdf/src/main/resources/messages_cs_CZ.properties"
|
||||||
|
"stirling-pdf/src/main/resources/messages_da_DK.properties"
|
||||||
|
"stirling-pdf/src/main/resources/messages_de_DE.properties"
|
||||||
|
"stirling-pdf/src/main/resources/messages_es_ES.properties"
|
||||||
|
"stirling-pdf/src/main/resources/messages_fr_FR.properties"
|
||||||
|
"stirling-pdf/src/main/resources/messages_ga_IE.properties"
|
||||||
|
"stirling-pdf/src/main/resources/messages_hu_HU.properties"
|
||||||
|
"stirling-pdf/src/main/resources/messages_it_IT.properties"
|
||||||
|
"stirling-pdf/src/main/resources/messages_nl_NL.properties"
|
||||||
|
"stirling-pdf/src/main/resources/messages_no_NB.properties"
|
||||||
|
"stirling-pdf/src/main/resources/messages_pl_PL.properties"
|
||||||
|
"stirling-pdf/src/main/resources/messages_pt_BR.properties"
|
||||||
|
"stirling-pdf/src/main/resources/messages_pt_PT.properties"
|
||||||
|
"stirling-pdf/src/main/resources/messages_ro_RO.properties"
|
||||||
|
"stirling-pdf/src/main/resources/messages_ru_RU.properties"
|
||||||
|
"stirling-pdf/src/main/resources/messages_sk_SK.properties"
|
||||||
|
"stirling-pdf/src/main/resources/messages_sv_SE.properties"
|
||||||
|
"stirling-pdf/src/main/resources/messages_tr_TR.properties"
|
||||||
|
"stirling-pdf/src/main/resources/messages_uk_UA.properties"
|
||||||
|
"stirling-pdf/src/main/resources/messages_vi_VN.properties"
|
||||||
|
)
|
||||||
|
|
||||||
|
for file in "${FILES[@]}"; do
|
||||||
|
echo "Processing $file..."
|
||||||
|
|
||||||
|
# Create a backup of the original file
|
||||||
|
cp "$file" "${file}.bak"
|
||||||
|
|
||||||
|
# Convert from ISO-8859-1 to UTF-8
|
||||||
|
iconv -f ISO-8859-1 -t UTF-8 "${file}.bak" > "$file"
|
||||||
|
|
||||||
|
# Check if conversion was successful
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "Successfully converted $file to UTF-8"
|
||||||
|
# Verify the file is now UTF-8
|
||||||
|
file "$file"
|
||||||
|
else
|
||||||
|
echo "Failed to convert $file, restoring backup"
|
||||||
|
mv "${file}.bak" "$file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "All files processed."
|
@ -6,8 +6,6 @@ import org.aspectj.lang.ProceedingJoinPoint;
|
|||||||
import org.aspectj.lang.annotation.Around;
|
import org.aspectj.lang.annotation.Around;
|
||||||
import org.aspectj.lang.annotation.Aspect;
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
import org.aspectj.lang.reflect.MethodSignature;
|
import org.aspectj.lang.reflect.MethodSignature;
|
||||||
import org.slf4j.MDC;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@ -17,22 +15,16 @@ import org.springframework.web.bind.annotation.PutMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.context.request.RequestContextHolder;
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import stirling.software.common.util.RequestUriUtils;
|
import stirling.software.common.util.RequestUriUtils;
|
||||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||||
import stirling.software.proprietary.service.AuditService;
|
import stirling.software.proprietary.service.AuditService;
|
||||||
|
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,20 +40,12 @@ public class ControllerAuditAspect {
|
|||||||
private final AuditService auditService;
|
private final AuditService auditService;
|
||||||
private final AuditConfigurationProperties auditConfig;
|
private final AuditConfigurationProperties auditConfig;
|
||||||
|
|
||||||
|
|
||||||
@Around("execution(* org.springframework.web.servlet.resource.ResourceHttpRequestHandler.handleRequest(..))")
|
|
||||||
public Object auditStaticResource(ProceedingJoinPoint jp) throws Throwable {
|
|
||||||
log.info("HELLOOOOOOOOOOOOOOOO");
|
|
||||||
return auditController(jp, "GET");
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Intercept all methods with GetMapping annotation
|
* Intercept all methods with GetMapping annotation
|
||||||
*/
|
*/
|
||||||
@Around("@annotation(org.springframework.web.bind.annotation.GetMapping)")
|
@Around("@annotation(org.springframework.web.bind.annotation.GetMapping)")
|
||||||
public Object auditGetMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
public Object auditGetMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||||
return auditController(joinPoint, "GET");
|
return auditControllerMethod(joinPoint, "GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -69,7 +53,7 @@ public class ControllerAuditAspect {
|
|||||||
*/
|
*/
|
||||||
@Around("@annotation(org.springframework.web.bind.annotation.PostMapping)")
|
@Around("@annotation(org.springframework.web.bind.annotation.PostMapping)")
|
||||||
public Object auditPostMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
public Object auditPostMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||||
return auditController(joinPoint, "POST");
|
return auditControllerMethod(joinPoint, "POST");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -77,7 +61,7 @@ public class ControllerAuditAspect {
|
|||||||
*/
|
*/
|
||||||
@Around("@annotation(org.springframework.web.bind.annotation.PutMapping)")
|
@Around("@annotation(org.springframework.web.bind.annotation.PutMapping)")
|
||||||
public Object auditPutMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
public Object auditPutMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||||
return auditController(joinPoint, "PUT");
|
return auditControllerMethod(joinPoint, "PUT");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -85,7 +69,7 @@ public class ControllerAuditAspect {
|
|||||||
*/
|
*/
|
||||||
@Around("@annotation(org.springframework.web.bind.annotation.DeleteMapping)")
|
@Around("@annotation(org.springframework.web.bind.annotation.DeleteMapping)")
|
||||||
public Object auditDeleteMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
public Object auditDeleteMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||||
return auditController(joinPoint, "DELETE");
|
return auditControllerMethod(joinPoint, "DELETE");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -93,163 +77,194 @@ public class ControllerAuditAspect {
|
|||||||
*/
|
*/
|
||||||
@Around("@annotation(org.springframework.web.bind.annotation.PatchMapping)")
|
@Around("@annotation(org.springframework.web.bind.annotation.PatchMapping)")
|
||||||
public Object auditPatchMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
public Object auditPatchMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||||
return auditController(joinPoint, "PATCH");
|
return auditControllerMethod(joinPoint, "PATCH");
|
||||||
}
|
}
|
||||||
|
|
||||||
private Object auditController(ProceedingJoinPoint joinPoint, String httpMethod) throws Throwable {
|
/**
|
||||||
MethodSignature sig = (MethodSignature) joinPoint.getSignature();
|
* Common method to audit controller methods
|
||||||
Method method = sig.getMethod();
|
*/
|
||||||
AuditLevel level = auditConfig.getAuditLevel();
|
private Object auditControllerMethod(ProceedingJoinPoint joinPoint, String httpMethod) throws Throwable {
|
||||||
// OFF below BASIC?
|
// Skip if below STANDARD level (controller auditing is considered STANDARD level)
|
||||||
if (!auditConfig.isLevelEnabled(AuditLevel.BASIC)) {
|
if (!auditConfig.isLevelEnabled(AuditLevel.STANDARD)) {
|
||||||
return joinPoint.proceed();
|
return joinPoint.proceed();
|
||||||
}
|
}
|
||||||
|
|
||||||
// // Opt-out
|
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||||
// if (method.isAnnotationPresent(Audited.class)) {
|
Method method = signature.getMethod();
|
||||||
// return joinPoint.proceed();
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
// Don't audit methods that already have @Audited annotation
|
||||||
|
if (method.isAnnotationPresent(Audited.class)) {
|
||||||
|
return joinPoint.proceed();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the request path
|
||||||
String path = getRequestPath(method, httpMethod);
|
String path = getRequestPath(method, httpMethod);
|
||||||
|
|
||||||
// Skip static GET resources
|
// Skip auditing static resources for GET requests
|
||||||
if ("GET".equals(httpMethod)) {
|
if ("GET".equals(httpMethod)) {
|
||||||
HttpServletRequest maybe = getCurrentRequest();
|
HttpServletRequest request = getCurrentRequest();
|
||||||
if (maybe != null && !RequestUriUtils.isTrackableResource(maybe.getContextPath(), maybe.getRequestURI())) {
|
if (request != null && RequestUriUtils.isStaticResource(request.getContextPath(), request.getRequestURI())) {
|
||||||
return joinPoint.proceed();
|
return joinPoint.proceed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
// Create audit data
|
||||||
HttpServletRequest req = attrs != null ? attrs.getRequest() : null;
|
Map<String, Object> auditData = new HashMap<>();
|
||||||
HttpServletResponse resp = attrs != null ? attrs.getResponse() : null;
|
auditData.put("controller", joinPoint.getTarget().getClass().getSimpleName());
|
||||||
|
auditData.put("method", method.getName());
|
||||||
|
auditData.put("httpMethod", httpMethod);
|
||||||
|
auditData.put("path", path);
|
||||||
|
|
||||||
long start = System.currentTimeMillis();
|
// Add method parameters if at VERBOSE level
|
||||||
Map<String, Object> data = new HashMap<>();
|
if (auditConfig.isLevelEnabled(AuditLevel.VERBOSE)) {
|
||||||
|
Object[] args = joinPoint.getArgs();
|
||||||
|
String[] parameterNames = signature.getParameterNames();
|
||||||
|
|
||||||
// BASIC
|
if (args != null && parameterNames != null) {
|
||||||
if (level.includes(AuditLevel.BASIC)) {
|
IntStream.range(0, args.length)
|
||||||
data.put("timestamp", Instant.now().toString());
|
.forEach(i -> {
|
||||||
data.put("principal", SecurityContextHolder.getContext().getAuthentication().getName());
|
String paramName = i < parameterNames.length ? parameterNames[i] : "arg" + i;
|
||||||
data.put("path", path);
|
auditData.put("arg_" + paramName, args[i]);
|
||||||
data.put("httpMethod", httpMethod);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// STANDARD
|
|
||||||
if (level.includes(AuditLevel.STANDARD) && req != null) {
|
|
||||||
data.put("clientIp", req.getRemoteAddr());
|
|
||||||
data.put("sessionId", req.getSession(false) != null ? req.getSession(false).getId() : null);
|
|
||||||
data.put("requestId", MDC.get("requestId"));
|
|
||||||
|
|
||||||
if ("POST".equalsIgnoreCase(httpMethod)
|
|
||||||
|| "PUT".equalsIgnoreCase(httpMethod)
|
|
||||||
|| "PATCH".equalsIgnoreCase(httpMethod)) {
|
|
||||||
String ct = req.getContentType();
|
|
||||||
if (ct != null && (
|
|
||||||
ct.contains("application/x-www-form-urlencoded") ||
|
|
||||||
ct.contains("multipart/form-data")
|
|
||||||
)) {
|
|
||||||
Map<String,String[]> params = req.getParameterMap();
|
|
||||||
if (!params.isEmpty()) {
|
|
||||||
data.put("formParams", params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// VERBOSE args
|
|
||||||
if (level.includes(AuditLevel.VERBOSE)) {
|
|
||||||
String[] names = sig.getParameterNames();
|
|
||||||
Object[] vals = joinPoint.getArgs();
|
|
||||||
if (names != null && vals != null) {
|
|
||||||
IntStream.range(0, names.length).forEach(i -> data.put("arg_" + names[i], vals[i]));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Object result = null;
|
Object result;
|
||||||
try {
|
try {
|
||||||
|
// Execute the method
|
||||||
result = joinPoint.proceed();
|
result = joinPoint.proceed();
|
||||||
data.put("outcome", "success");
|
|
||||||
|
// Add success status
|
||||||
|
auditData.put("status", "success");
|
||||||
|
|
||||||
|
// Add result if at VERBOSE level
|
||||||
|
if (auditConfig.isLevelEnabled(AuditLevel.VERBOSE) && result != null) {
|
||||||
|
auditData.put("resultType", result.getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
} catch (Throwable ex) {
|
} catch (Throwable ex) {
|
||||||
data.put("outcome", "failure");
|
// Always add failure information
|
||||||
data.put("errorType", ex.getClass().getSimpleName());
|
auditData.put("status", "failure");
|
||||||
data.put("errorMessage", ex.getMessage());
|
auditData.put("errorType", ex.getClass().getName());
|
||||||
|
auditData.put("errorMessage", ex.getMessage());
|
||||||
|
|
||||||
|
// Re-throw the exception
|
||||||
throw ex;
|
throw ex;
|
||||||
} finally {
|
} finally {
|
||||||
// finalize STANDARD
|
// Determine the appropriate audit event type based on the controller package and class name
|
||||||
if (level.includes(AuditLevel.STANDARD)) {
|
AuditEventType eventType = determineAuditEventType(joinPoint.getTarget().getClass(), path, httpMethod);
|
||||||
data.put("latencyMs", System.currentTimeMillis() - start);
|
|
||||||
if (resp != null) data.put("statusCode", resp.getStatus());
|
// Create the audit entry using the enum type
|
||||||
}
|
auditService.audit(eventType, auditData, AuditLevel.STANDARD);
|
||||||
// finalize VERBOSE result
|
|
||||||
if (level.includes(AuditLevel.VERBOSE) && result != null) {
|
|
||||||
data.put("result", result.toString());
|
|
||||||
}
|
|
||||||
AuditEventType type = determineAuditEventType(joinPoint.getTarget().getClass(), path, httpMethod);
|
|
||||||
auditService.audit(type, data, level);
|
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private AuditEventType determineAuditEventType(Class<?> controller, String path, String httpMethod) {
|
/**
|
||||||
String cls = controller.getSimpleName().toLowerCase();
|
* Determines the appropriate audit event type based on the controller's package and class name and HTTP method
|
||||||
String pkg = controller.getPackage().getName().toLowerCase();
|
*/
|
||||||
if ("GET".equals(httpMethod)) return AuditEventType.HTTP_REQUEST;
|
private AuditEventType determineAuditEventType(Class<?> controllerClass, String path, String httpMethod) {
|
||||||
if (cls.contains("user") || cls.contains("auth") || pkg.contains("auth")
|
String className = controllerClass.getSimpleName().toLowerCase();
|
||||||
|| path.startsWith("/user") || path.startsWith("/login")) {
|
String packageName = controllerClass.getPackage().getName().toLowerCase();
|
||||||
|
|
||||||
|
// For GET requests, just use HTTP_REQUEST as they don't process anything
|
||||||
|
if (httpMethod.equals("GET")) {
|
||||||
|
return AuditEventType.HTTP_REQUEST;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For actual processing operations (POST, PUT, DELETE, etc.)
|
||||||
|
|
||||||
|
// User/authentication related controllers
|
||||||
|
if (className.contains("user") || className.contains("auth") ||
|
||||||
|
packageName.contains("security") || packageName.contains("auth") ||
|
||||||
|
path.startsWith("/user") || path.startsWith("/login") ||
|
||||||
|
path.startsWith("/auth") || path.startsWith("/account")) {
|
||||||
return AuditEventType.USER_PROFILE_UPDATE;
|
return AuditEventType.USER_PROFILE_UPDATE;
|
||||||
} else if (cls.contains("admin") || path.startsWith("/admin") || path.startsWith("/settings")) {
|
}
|
||||||
|
|
||||||
|
// Admin related controllers
|
||||||
|
else if (className.contains("admin") || path.startsWith("/admin") ||
|
||||||
|
path.startsWith("/settings") || className.contains("setting") ||
|
||||||
|
className.contains("database") || path.contains("database")) {
|
||||||
return AuditEventType.SETTINGS_CHANGED;
|
return AuditEventType.SETTINGS_CHANGED;
|
||||||
} else if (cls.contains("file") || path.startsWith("/file")
|
}
|
||||||
|| path.matches("(?i).*/(upload|download)/.*")) {
|
|
||||||
|
// File operations - using path prefixes to avoid false matches
|
||||||
|
else if (className.contains("file") ||
|
||||||
|
path.startsWith("/file") ||
|
||||||
|
path.startsWith("/files/") ||
|
||||||
|
path.matches("(?i).*/(upload|download)/.*")) {
|
||||||
return AuditEventType.FILE_OPERATION;
|
return AuditEventType.FILE_OPERATION;
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
// Default to PDF operations for most controllers
|
||||||
|
else {
|
||||||
return AuditEventType.PDF_PROCESS;
|
return AuditEventType.PDF_PROCESS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the request path from the method's annotations
|
||||||
|
*/
|
||||||
private String getRequestPath(Method method, String httpMethod) {
|
private String getRequestPath(Method method, String httpMethod) {
|
||||||
String base = "";
|
// Check class level RequestMapping
|
||||||
RequestMapping cm = method.getDeclaringClass().getAnnotation(RequestMapping.class);
|
String basePath = "";
|
||||||
if (cm != null && cm.value().length > 0) base = cm.value()[0];
|
RequestMapping classMapping = method.getDeclaringClass().getAnnotation(RequestMapping.class);
|
||||||
String mp = "";
|
if (classMapping != null && classMapping.value().length > 0) {
|
||||||
Annotation ann = switch (httpMethod) {
|
basePath = classMapping.value()[0];
|
||||||
case "GET" -> method.getAnnotation(GetMapping.class);
|
}
|
||||||
case "POST" -> method.getAnnotation(PostMapping.class);
|
|
||||||
case "PUT" -> method.getAnnotation(PutMapping.class);
|
// Check method level mapping
|
||||||
case "DELETE" -> method.getAnnotation(DeleteMapping.class);
|
String methodPath = "";
|
||||||
case "PATCH" -> method.getAnnotation(PatchMapping.class);
|
Annotation annotation = null;
|
||||||
default -> null;
|
|
||||||
};
|
switch (httpMethod) {
|
||||||
if (ann instanceof GetMapping gm && gm.value().length > 0) mp = gm.value()[0];
|
case "GET":
|
||||||
if (ann instanceof PostMapping pm && pm.value().length > 0) mp = pm.value()[0];
|
annotation = method.getAnnotation(GetMapping.class);
|
||||||
if (ann instanceof PutMapping pum && pum.value().length > 0) mp = pum.value()[0];
|
if (annotation != null) {
|
||||||
if (ann instanceof DeleteMapping dm && dm.value().length > 0) mp = dm.value()[0];
|
String[] paths = ((GetMapping) annotation).value();
|
||||||
if (ann instanceof PatchMapping pam && pam.value().length > 0) mp = pam.value()[0];
|
if (paths.length > 0) methodPath = paths[0];
|
||||||
return base + mp;
|
}
|
||||||
|
break;
|
||||||
|
case "POST":
|
||||||
|
annotation = method.getAnnotation(PostMapping.class);
|
||||||
|
if (annotation != null) {
|
||||||
|
String[] paths = ((PostMapping) annotation).value();
|
||||||
|
if (paths.length > 0) methodPath = paths[0];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "PUT":
|
||||||
|
annotation = method.getAnnotation(PutMapping.class);
|
||||||
|
if (annotation != null) {
|
||||||
|
String[] paths = ((PutMapping) annotation).value();
|
||||||
|
if (paths.length > 0) methodPath = paths[0];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "DELETE":
|
||||||
|
annotation = method.getAnnotation(DeleteMapping.class);
|
||||||
|
if (annotation != null) {
|
||||||
|
String[] paths = ((DeleteMapping) annotation).value();
|
||||||
|
if (paths.length > 0) methodPath = paths[0];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "PATCH":
|
||||||
|
annotation = method.getAnnotation(PatchMapping.class);
|
||||||
|
if (annotation != null) {
|
||||||
|
String[] paths = ((PatchMapping) annotation).value();
|
||||||
|
if (paths.length > 0) methodPath = paths[0];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine base path and method path
|
||||||
|
return basePath + methodPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current HttpServletRequest from the RequestContextHolder
|
||||||
|
*/
|
||||||
private HttpServletRequest getCurrentRequest() {
|
private HttpServletRequest getCurrentRequest() {
|
||||||
ServletRequestAttributes a = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
return a != null ? a.getRequest() : null;
|
return attributes != null ? attributes.getRequest() : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,45 +1,16 @@
|
|||||||
package stirling.software.proprietary.config;
|
package stirling.software.proprietary.config;
|
||||||
|
|
||||||
import org.slf4j.MDC;
|
|
||||||
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.core.task.TaskDecorator;
|
|
||||||
import org.springframework.scheduling.annotation.EnableAsync;
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableAsync
|
@EnableAsync
|
||||||
public class AsyncConfig {
|
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")
|
@Bean(name = "auditExecutor")
|
||||||
public Executor auditExecutor() {
|
public Executor auditExecutor() {
|
||||||
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
|
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
|
||||||
@ -47,10 +18,6 @@ public class AsyncConfig {
|
|||||||
exec.setMaxPoolSize(8);
|
exec.setMaxPoolSize(8);
|
||||||
exec.setQueueCapacity(1_000);
|
exec.setQueueCapacity(1_000);
|
||||||
exec.setThreadNamePrefix("audit-");
|
exec.setThreadNamePrefix("audit-");
|
||||||
|
|
||||||
// Set the task decorator to propagate MDC context
|
|
||||||
exec.setTaskDecorator(new MDCContextTaskDecorator());
|
|
||||||
|
|
||||||
exec.initialize();
|
exec.initialize();
|
||||||
return exec;
|
return exec;
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,7 @@ package stirling.software.proprietary.config;
|
|||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.core.Ordered;
|
|
||||||
import org.springframework.core.annotation.Order;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.proprietary.audit.AuditLevel;
|
import stirling.software.proprietary.audit.AuditLevel;
|
||||||
@ -18,23 +14,19 @@ import stirling.software.proprietary.audit.AuditLevel;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@Getter
|
@Getter
|
||||||
@Component
|
@Component
|
||||||
@Order(Ordered.HIGHEST_PRECEDENCE+ 10)
|
|
||||||
public class AuditConfigurationProperties {
|
public class AuditConfigurationProperties {
|
||||||
|
|
||||||
private final boolean enabled;
|
private final boolean enabled;
|
||||||
private final int level;
|
private final int level;
|
||||||
private final int retentionDays;
|
private final int retentionDays;
|
||||||
private final String licenseType;
|
|
||||||
|
|
||||||
|
public AuditConfigurationProperties(ApplicationProperties applicationProperties) {
|
||||||
public AuditConfigurationProperties(ApplicationProperties applicationProperties, @Qualifier("license") String licenseType) {
|
|
||||||
ApplicationProperties.Premium.ProFeatures.Audit auditConfig =
|
ApplicationProperties.Premium.ProFeatures.Audit auditConfig =
|
||||||
applicationProperties.getPremium().getProFeatures().getAudit();
|
applicationProperties.getPremium().getProFeatures().getAudit();
|
||||||
|
|
||||||
this.enabled = auditConfig.isEnabled();
|
this.enabled = auditConfig.isEnabled();
|
||||||
this.level = auditConfig.getLevel();
|
this.level = auditConfig.getLevel();
|
||||||
this.retentionDays = auditConfig.getRetentionDays();
|
this.retentionDays = auditConfig.getRetentionDays();
|
||||||
this.licenseType = licenseType;
|
|
||||||
|
|
||||||
log.info("Initialized audit configuration: enabled={}, level={}, retentionDays={}",
|
log.info("Initialized audit configuration: enabled={}, level={}, retentionDays={}",
|
||||||
this.enabled, this.level, this.retentionDays);
|
this.enabled, this.level, this.retentionDays);
|
||||||
|
@ -2,8 +2,6 @@ package stirling.software.proprietary.config;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import org.slf4j.MDC;
|
import org.slf4j.MDC;
|
||||||
import org.springframework.boot.actuate.audit.AuditEvent;
|
import org.springframework.boot.actuate.audit.AuditEvent;
|
||||||
import org.springframework.boot.actuate.audit.AuditEventRepository;
|
import org.springframework.boot.actuate.audit.AuditEventRepository;
|
||||||
@ -22,7 +20,6 @@ import java.util.Map;
|
|||||||
@Component
|
@Component
|
||||||
@Primary
|
@Primary
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
|
||||||
public class CustomAuditEventRepository implements AuditEventRepository {
|
public class CustomAuditEventRepository implements AuditEventRepository {
|
||||||
|
|
||||||
private final PersistentAuditEventRepository repo;
|
private final PersistentAuditEventRepository repo;
|
||||||
@ -44,15 +41,7 @@ public class CustomAuditEventRepository implements AuditEventRepository {
|
|||||||
? Map.of()
|
? Map.of()
|
||||||
: SecretMasker.mask(ev.getData());
|
: SecretMasker.mask(ev.getData());
|
||||||
|
|
||||||
|
|
||||||
if (clean.isEmpty() ||
|
|
||||||
(clean.size() == 1 && clean.containsKey("details"))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String rid = MDC.get("requestId");
|
String rid = MDC.get("requestId");
|
||||||
|
|
||||||
log.info("AuditEvent clean data (JSON): {}",
|
|
||||||
mapper.writeValueAsString(clean));
|
|
||||||
if (rid != null) {
|
if (rid != null) {
|
||||||
clean = new java.util.HashMap<>(clean);
|
clean = new java.util.HashMap<>(clean);
|
||||||
clean.put("requestId", rid);
|
clean.put("requestId", rid);
|
||||||
|
@ -0,0 +1,116 @@
|
|||||||
|
package stirling.software.proprietary.config;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.slf4j.MDC;
|
||||||
|
import org.springframework.boot.actuate.audit.AuditEvent;
|
||||||
|
import org.springframework.boot.actuate.audit.AuditEventRepository;
|
||||||
|
import org.springframework.context.ApplicationListener;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.context.support.ServletRequestHandledEvent;
|
||||||
|
import stirling.software.proprietary.audit.AuditLevel;
|
||||||
|
import stirling.software.proprietary.util.SecretMasker;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class HttpRequestAuditPublisher
|
||||||
|
implements ApplicationListener<ServletRequestHandledEvent> {
|
||||||
|
|
||||||
|
private final AuditEventRepository repo;
|
||||||
|
private final AuditConfigurationProperties auditConfig;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onApplicationEvent(ServletRequestHandledEvent e) {
|
||||||
|
// Skip if audit is disabled or level is OFF
|
||||||
|
if (!auditConfig.isEnabled() || auditConfig.getAuditLevel() == AuditLevel.OFF) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic request information is included at level STANDARD or higher
|
||||||
|
AuditLevel currentLevel = auditConfig.getAuditLevel();
|
||||||
|
boolean isBasicLevel = currentLevel.includes(AuditLevel.BASIC);
|
||||||
|
boolean isStandardLevel = currentLevel.includes(AuditLevel.STANDARD);
|
||||||
|
boolean isVerboseLevel = currentLevel.includes(AuditLevel.VERBOSE);
|
||||||
|
|
||||||
|
// Special case for errors - always log errors at BASIC level
|
||||||
|
boolean isError = e.getStatusCode() >= 400 || e.getFailureCause() != null;
|
||||||
|
|
||||||
|
// Skip non-error requests if below STANDARD level
|
||||||
|
if (!isStandardLevel && !isError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a mutable map to hold all our audit data
|
||||||
|
Map<String, Object> raw = new HashMap<>();
|
||||||
|
|
||||||
|
// Add basic request data from the event (always included)
|
||||||
|
raw.put("method", e.getMethod());
|
||||||
|
raw.put("uri", e.getRequestUrl());
|
||||||
|
raw.put("status", e.getStatusCode());
|
||||||
|
raw.put("latency", e.getProcessingTimeMillis());
|
||||||
|
raw.put("ip", e.getClientAddress());
|
||||||
|
|
||||||
|
// Add standard level data
|
||||||
|
if (isStandardLevel || isError) {
|
||||||
|
raw.put("servlet", e.getServletName());
|
||||||
|
raw.put("sessionId", e.getSessionId());
|
||||||
|
raw.put("requestId", MDC.get("requestId"));
|
||||||
|
raw.put("host", getHostName());
|
||||||
|
raw.put("timestamp", System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for failure information (always included for errors)
|
||||||
|
if (e.getFailureCause() != null) {
|
||||||
|
raw.put("failed", true);
|
||||||
|
raw.put("errorType", e.getFailureCause().getClass().getName());
|
||||||
|
raw.put("errorMessage", e.getFailureCause().getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add additional data from MDC at VERBOSE level
|
||||||
|
if (isVerboseLevel) {
|
||||||
|
addFromMDC(raw, "userAgent");
|
||||||
|
addFromMDC(raw, "referer");
|
||||||
|
addFromMDC(raw, "acceptLanguage");
|
||||||
|
addFromMDC(raw, "contentType");
|
||||||
|
addFromMDC(raw, "userRoles");
|
||||||
|
addFromMDC(raw, "queryParams");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the correct audit level for this event
|
||||||
|
AuditLevel eventLevel = isError ? AuditLevel.BASIC :
|
||||||
|
isVerboseLevel ? AuditLevel.VERBOSE :
|
||||||
|
AuditLevel.STANDARD;
|
||||||
|
|
||||||
|
// Create the audit event
|
||||||
|
repo.add(new AuditEvent(
|
||||||
|
e.getUserName() != null ? e.getUserName() : "anonymous",
|
||||||
|
"HTTP_REQUEST",
|
||||||
|
SecretMasker.mask(raw)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a value from MDC to the audit data map if present
|
||||||
|
*/
|
||||||
|
private void addFromMDC(Map<String, Object> data, String key) {
|
||||||
|
String value = MDC.get(key);
|
||||||
|
if (StringUtils.hasText(value)) {
|
||||||
|
data.put(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the hostname of the current server
|
||||||
|
*/
|
||||||
|
private String getHostName() {
|
||||||
|
try {
|
||||||
|
return InetAddress.getLocalHost().getHostName();
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
return "unknown-host";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,133 @@
|
|||||||
|
package stirling.software.proprietary.controller;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import stirling.software.proprietary.audit.AuditEventType;
|
||||||
|
import stirling.software.proprietary.audit.AuditLevel;
|
||||||
|
import stirling.software.proprietary.audit.Audited;
|
||||||
|
import stirling.software.proprietary.service.AuditService;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example controller showing how to use the audit service.
|
||||||
|
* This is for demonstration purposes only and should be removed in production.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/audit-demo")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AuditExampleController {
|
||||||
|
|
||||||
|
private final AuditService auditService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example using direct AuditService injection
|
||||||
|
*/
|
||||||
|
@GetMapping("/manual/{id}")
|
||||||
|
public String auditManually(@PathVariable String id) {
|
||||||
|
// Create an example audit event manually
|
||||||
|
auditService.audit("EXAMPLE_EVENT", Map.of(
|
||||||
|
"id", id,
|
||||||
|
"timestamp", System.currentTimeMillis(),
|
||||||
|
"action", "view"
|
||||||
|
));
|
||||||
|
|
||||||
|
return "Audit event created for ID: " + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example using @Audited annotation with basic level
|
||||||
|
*/
|
||||||
|
@Audited(type = AuditEventType.USER_PROFILE_UPDATE, level = AuditLevel.BASIC)
|
||||||
|
@PostMapping("/users")
|
||||||
|
public ResponseEntity<Map<String, Object>> createUser(@RequestBody Map<String, Object> user) {
|
||||||
|
// This method is automatically audited with the USER_REGISTRATION type at BASIC level
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("id", "user123");
|
||||||
|
result.put("username", user.get("username"));
|
||||||
|
result.put("created", true);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example using @Audited annotation with file upload at VERBOSE level
|
||||||
|
*/
|
||||||
|
@Audited(type = AuditEventType.FILE_OPERATION, level = AuditLevel.VERBOSE, includeResult = true)
|
||||||
|
@PostMapping("/files/process")
|
||||||
|
public ResponseEntity<Map<String, Object>> processFile(MultipartFile file) {
|
||||||
|
// This method is automatically audited at VERBOSE level
|
||||||
|
// The audit event will include information about the file
|
||||||
|
// And will also include the result because includeResult=true
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("filename", file != null ? file.getOriginalFilename() : "null");
|
||||||
|
result.put("size", file != null ? file.getSize() : 0);
|
||||||
|
result.put("status", "processed");
|
||||||
|
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically audited controller method with GetMapping.
|
||||||
|
* This method does NOT have an @Audited annotation but will still be
|
||||||
|
* automatically audited by the ControllerAuditAspect.
|
||||||
|
*/
|
||||||
|
@GetMapping("/users/{id}")
|
||||||
|
public ResponseEntity<Map<String, Object>> getUser(@PathVariable String id) {
|
||||||
|
// This method will be automatically audited by the ControllerAuditAspect
|
||||||
|
// The audit will include the controller name, method name, and path
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("id", id);
|
||||||
|
result.put("username", "johndoe");
|
||||||
|
result.put("email", "john.doe@example.com");
|
||||||
|
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically audited controller method with PutMapping.
|
||||||
|
*/
|
||||||
|
@PutMapping("/users/{id}")
|
||||||
|
public ResponseEntity<Map<String, Object>> updateUser(
|
||||||
|
@PathVariable String id,
|
||||||
|
@RequestBody Map<String, Object> user) {
|
||||||
|
// This method will be automatically audited by the ControllerAuditAspect
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("id", id);
|
||||||
|
result.put("username", user.get("username"));
|
||||||
|
result.put("updated", true);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically audited controller method with DeleteMapping.
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/users/{id}")
|
||||||
|
public ResponseEntity<Map<String, Object>> deleteUser(@PathVariable String id) {
|
||||||
|
// This method will be automatically audited by the ControllerAuditAspect
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("id", id);
|
||||||
|
result.put("deleted", true);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
}
|
@ -66,10 +66,8 @@ 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 {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package stirling.software.proprietary.util;
|
package stirling.software.proprietary.util;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@ -14,34 +13,22 @@ public final class SecretMasker {
|
|||||||
|
|
||||||
private SecretMasker() {}
|
private SecretMasker() {}
|
||||||
|
|
||||||
public static Object deepMask(Object value) {
|
public static Map<String, Object> mask(Map<String, Object> in) {
|
||||||
if (value instanceof Map<?,?> m) {
|
if (in == null) {
|
||||||
return m.entrySet().stream().collect(Collectors.toMap(
|
return null;
|
||||||
Map.Entry::getKey,
|
|
||||||
e -> deepMaskValue((String)e.getKey(), e.getValue())
|
|
||||||
));
|
|
||||||
} else if (value instanceof List<?> list) {
|
|
||||||
return list.stream()
|
|
||||||
.map(SecretMasker::deepMask)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private static Object deepMaskValue(String key, Object value) {
|
Map<String, Object> result = new HashMap<>(in.size());
|
||||||
if (key != null && SENSITIVE.matcher(key).find()) {
|
|
||||||
return "***REDACTED***";
|
for (Map.Entry<String, Object> entry : in.entrySet()) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
if (key != null && SENSITIVE.matcher(key).find()) {
|
||||||
|
result.put(key, "***REDACTED***");
|
||||||
|
} else {
|
||||||
|
result.put(key, entry.getValue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return deepMask(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Map<String,Object> mask(Map<String,Object> in) {
|
return result;
|
||||||
if (in == null) return null;
|
|
||||||
return in.entrySet().stream()
|
|
||||||
.collect(Collectors.toMap(
|
|
||||||
Map.Entry::getKey,
|
|
||||||
e -> deepMaskValue(e.getKey(), e.getValue())
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -23,6 +23,61 @@ let endDateFilterInput;
|
|||||||
let applyFiltersButton;
|
let applyFiltersButton;
|
||||||
let resetFiltersButton;
|
let resetFiltersButton;
|
||||||
|
|
||||||
|
// Debug logger function
|
||||||
|
let debugEnabled = false; // Set to false by default in production
|
||||||
|
function debugLog(message, data) {
|
||||||
|
if (!debugEnabled) return;
|
||||||
|
|
||||||
|
const console = document.getElementById('debug-console');
|
||||||
|
if (console) {
|
||||||
|
if (console.style.display === 'none' || !console.style.display) {
|
||||||
|
console.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
const time = new Date().toLocaleTimeString();
|
||||||
|
let logMessage = `[${time}] ${message}`;
|
||||||
|
|
||||||
|
if (data !== undefined) {
|
||||||
|
if (typeof data === 'object') {
|
||||||
|
try {
|
||||||
|
logMessage += ': ' + JSON.stringify(data);
|
||||||
|
} catch (e) {
|
||||||
|
logMessage += ': ' + data;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logMessage += ': ' + data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logLine = document.createElement('div');
|
||||||
|
logLine.textContent = logMessage;
|
||||||
|
console.appendChild(logLine);
|
||||||
|
console.scrollTop = console.scrollHeight;
|
||||||
|
|
||||||
|
// Keep only last 100 lines
|
||||||
|
while (console.childNodes.length > 100) {
|
||||||
|
console.removeChild(console.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also log to browser console
|
||||||
|
if (data !== undefined) {
|
||||||
|
window.console.log(message, data);
|
||||||
|
} else {
|
||||||
|
window.console.log(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add keyboard shortcut to toggle debug console
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'F12' && e.ctrlKey) {
|
||||||
|
const console = document.getElementById('debug-console');
|
||||||
|
if (console) {
|
||||||
|
console.style.display = console.style.display === 'none' || !console.style.display ? 'block' : 'none';
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Initialize page
|
// Initialize page
|
||||||
// Theme change listener to redraw charts when theme changes
|
// Theme change listener to redraw charts when theme changes
|
||||||
@ -33,6 +88,7 @@ function setupThemeChangeListener() {
|
|||||||
if (mutation.attributeName === 'data-bs-theme' || mutation.attributeName === 'class') {
|
if (mutation.attributeName === 'data-bs-theme' || mutation.attributeName === 'class') {
|
||||||
// Redraw charts with new theme colors if they exist
|
// Redraw charts with new theme colors if they exist
|
||||||
if (typeChart && userChart && timeChart) {
|
if (typeChart && userChart && timeChart) {
|
||||||
|
debugLog('Theme changed, redrawing charts');
|
||||||
// If we have stats data cached, use it
|
// If we have stats data cached, use it
|
||||||
if (window.cachedStatsData) {
|
if (window.cachedStatsData) {
|
||||||
renderCharts(window.cachedStatsData);
|
renderCharts(window.cachedStatsData);
|
||||||
@ -50,6 +106,8 @@ function setupThemeChangeListener() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
debugLog('Page initialized');
|
||||||
|
|
||||||
// Initialize DOM references
|
// Initialize DOM references
|
||||||
auditTableBody = document.getElementById('auditTableBody');
|
auditTableBody = document.getElementById('auditTableBody');
|
||||||
pageSizeSelect = document.getElementById('pageSizeSelect');
|
pageSizeSelect = document.getElementById('pageSizeSelect');
|
||||||
@ -61,6 +119,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
applyFiltersButton = document.getElementById('applyFilters');
|
applyFiltersButton = document.getElementById('applyFilters');
|
||||||
resetFiltersButton = document.getElementById('resetFilters');
|
resetFiltersButton = document.getElementById('resetFilters');
|
||||||
|
|
||||||
|
// Debug log DOM elements
|
||||||
|
debugLog('DOM elements initialized', {
|
||||||
|
auditTableBody: !!auditTableBody,
|
||||||
|
pageSizeSelect: !!pageSizeSelect
|
||||||
|
});
|
||||||
|
|
||||||
// Load event types for dropdowns
|
// Load event types for dropdowns
|
||||||
loadEventTypes();
|
loadEventTypes();
|
||||||
|
|
||||||
@ -68,6 +132,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
if (auditTableBody) {
|
if (auditTableBody) {
|
||||||
auditTableBody.innerHTML =
|
auditTableBody.innerHTML =
|
||||||
'<tr><td colspan="5" class="text-center"><div class="spinner-border spinner-border-sm" role="status"></div> ' + window.i18n.loading + '</td></tr>';
|
'<tr><td colspan="5" class="text-center"><div class="spinner-border spinner-border-sm" role="status"></div> ' + window.i18n.loading + '</td></tr>';
|
||||||
|
} else {
|
||||||
|
debugLog('ERROR: auditTableBody element not found!');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make a direct API call first to avoid validation issues
|
// Make a direct API call first to avoid validation issues
|
||||||
@ -95,6 +161,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
endDateFilter = endDateFilterInput.value;
|
endDateFilter = endDateFilterInput.value;
|
||||||
currentPage = 0;
|
currentPage = 0;
|
||||||
window.requestedPage = 0;
|
window.requestedPage = 0;
|
||||||
|
debugLog('Applying filters and resetting to page 0');
|
||||||
loadAuditData(0, pageSize);
|
loadAuditData(0, pageSize);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -118,6 +185,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Update UI
|
// Update UI
|
||||||
document.getElementById('currentPage').textContent = '1';
|
document.getElementById('currentPage').textContent = '1';
|
||||||
|
|
||||||
|
debugLog('Resetting filters and going to page 0');
|
||||||
|
|
||||||
// Load data with reset filters
|
// Load data with reset filters
|
||||||
loadAuditData(0, pageSize);
|
loadAuditData(0, pageSize);
|
||||||
});
|
});
|
||||||
@ -151,23 +220,29 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Check this radio button
|
// Check this radio button
|
||||||
radio.checked = true;
|
radio.checked = true;
|
||||||
|
|
||||||
|
debugLog('Radio format selected', radio.value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle export button
|
// Handle export button with debug
|
||||||
exportButton.onclick = function(e) {
|
exportButton.onclick = function(e) {
|
||||||
|
debugLog('Export button clicked');
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
// Get selected format with fallback
|
// Get selected format with fallback
|
||||||
const selectedRadio = document.querySelector('input[name="exportFormat"]:checked');
|
const selectedRadio = document.querySelector('input[name="exportFormat"]:checked');
|
||||||
const exportFormat = selectedRadio ? selectedRadio.value : 'csv';
|
const exportFormat = selectedRadio ? selectedRadio.value : 'csv';
|
||||||
|
|
||||||
|
debugLog('Selected format', exportFormat);
|
||||||
exportAuditData(exportFormat);
|
exportAuditData(exportFormat);
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set up pagination buttons
|
// Set up pagination buttons
|
||||||
document.getElementById('page-first').onclick = function() {
|
document.getElementById('page-first').onclick = function() {
|
||||||
|
debugLog('First page button clicked');
|
||||||
if (currentPage > 0) {
|
if (currentPage > 0) {
|
||||||
goToPage(0);
|
goToPage(0);
|
||||||
}
|
}
|
||||||
@ -175,6 +250,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById('page-prev').onclick = function() {
|
document.getElementById('page-prev').onclick = function() {
|
||||||
|
debugLog('Previous page button clicked');
|
||||||
if (currentPage > 0) {
|
if (currentPage > 0) {
|
||||||
goToPage(currentPage - 1);
|
goToPage(currentPage - 1);
|
||||||
}
|
}
|
||||||
@ -182,6 +258,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById('page-next').onclick = function() {
|
document.getElementById('page-next').onclick = function() {
|
||||||
|
debugLog('Next page button clicked');
|
||||||
if (currentPage < totalPages - 1) {
|
if (currentPage < totalPages - 1) {
|
||||||
goToPage(currentPage + 1);
|
goToPage(currentPage + 1);
|
||||||
}
|
}
|
||||||
@ -189,6 +266,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById('page-last').onclick = function() {
|
document.getElementById('page-last').onclick = function() {
|
||||||
|
debugLog('Last page button clicked');
|
||||||
if (totalPages > 0 && currentPage < totalPages - 1) {
|
if (totalPages > 0 && currentPage < totalPages - 1) {
|
||||||
goToPage(totalPages - 1);
|
goToPage(totalPages - 1);
|
||||||
}
|
}
|
||||||
@ -215,6 +293,13 @@ function loadAuditData(targetPage, realPageSize) {
|
|||||||
const requestedPage = targetPage !== undefined ? targetPage : window.requestedPage || 0;
|
const requestedPage = targetPage !== undefined ? targetPage : window.requestedPage || 0;
|
||||||
realPageSize = realPageSize || pageSize;
|
realPageSize = realPageSize || pageSize;
|
||||||
|
|
||||||
|
debugLog('Loading audit data', {
|
||||||
|
currentPage: currentPage,
|
||||||
|
requestedPage: requestedPage,
|
||||||
|
pageSize: pageSize,
|
||||||
|
realPageSize: realPageSize
|
||||||
|
});
|
||||||
|
|
||||||
showLoading('table-loading');
|
showLoading('table-loading');
|
||||||
|
|
||||||
// Always request page 0 from server, but with increased page size if needed
|
// Always request page 0 from server, but with increased page size if needed
|
||||||
@ -225,6 +310,8 @@ function loadAuditData(targetPage, realPageSize) {
|
|||||||
if (startDateFilter) url += `&startDate=${startDateFilter}`;
|
if (startDateFilter) url += `&startDate=${startDateFilter}`;
|
||||||
if (endDateFilter) url += `&endDate=${endDateFilter}`;
|
if (endDateFilter) url += `&endDate=${endDateFilter}`;
|
||||||
|
|
||||||
|
debugLog('Fetching URL', url);
|
||||||
|
|
||||||
// Update page indicator
|
// Update page indicator
|
||||||
if (document.getElementById('page-indicator')) {
|
if (document.getElementById('page-indicator')) {
|
||||||
document.getElementById('page-indicator').textContent = `Page ${requestedPage + 1} of ?`;
|
document.getElementById('page-indicator').textContent = `Page ${requestedPage + 1} of ?`;
|
||||||
@ -232,10 +319,16 @@ function loadAuditData(targetPage, realPageSize) {
|
|||||||
|
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
|
debugLog('Response received', response.status);
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
debugLog('Data received', {
|
||||||
|
totalPages: data.totalPages,
|
||||||
|
serverPage: data.currentPage,
|
||||||
|
totalElements: data.totalElements,
|
||||||
|
contentSize: data.content.length
|
||||||
|
});
|
||||||
|
|
||||||
// Calculate the correct slice of data to show for the requested page
|
// Calculate the correct slice of data to show for the requested page
|
||||||
let displayContent = data.content;
|
let displayContent = data.content;
|
||||||
@ -248,6 +341,10 @@ function loadAuditData(targetPage, realPageSize) {
|
|||||||
totalPages = calculatedTotalPages;
|
totalPages = calculatedTotalPages;
|
||||||
currentPage = requestedPage; // Use our tracked page, not server's
|
currentPage = requestedPage; // Use our tracked page, not server's
|
||||||
|
|
||||||
|
debugLog('Pagination state updated', {
|
||||||
|
totalPages: totalPages,
|
||||||
|
currentPage: currentPage
|
||||||
|
});
|
||||||
|
|
||||||
// Update UI
|
// Update UI
|
||||||
document.getElementById('currentPage').textContent = currentPage + 1;
|
document.getElementById('currentPage').textContent = currentPage + 1;
|
||||||
@ -268,7 +365,7 @@ function loadAuditData(targetPage, realPageSize) {
|
|||||||
// Restore original page size for next operations
|
// Restore original page size for next operations
|
||||||
if (window.originalPageSize && realPageSize !== window.originalPageSize) {
|
if (window.originalPageSize && realPageSize !== window.originalPageSize) {
|
||||||
pageSize = window.originalPageSize;
|
pageSize = window.originalPageSize;
|
||||||
|
debugLog('Restored original page size', pageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store original page size for recovery
|
// Store original page size for recovery
|
||||||
@ -276,10 +373,10 @@ function loadAuditData(targetPage, realPageSize) {
|
|||||||
|
|
||||||
// Clear busy flag
|
// Clear busy flag
|
||||||
window.paginationBusy = false;
|
window.paginationBusy = false;
|
||||||
|
debugLog('Pagination completed successfully');
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
debugLog('Error loading data', error.message);
|
||||||
if (auditTableBody) {
|
if (auditTableBody) {
|
||||||
auditTableBody.innerHTML = `<tr><td colspan="5" class="text-center">${window.i18n.errorLoading} ${error.message}</td></tr>`;
|
auditTableBody.innerHTML = `<tr><td colspan="5" class="text-center">${window.i18n.errorLoading} ${error.message}</td></tr>`;
|
||||||
}
|
}
|
||||||
@ -341,15 +438,19 @@ function exportAuditData(format) {
|
|||||||
|
|
||||||
// Render table with audit data
|
// Render table with audit data
|
||||||
function renderTable(events) {
|
function renderTable(events) {
|
||||||
|
debugLog('renderTable called with', events ? events.length : 0, 'events');
|
||||||
|
|
||||||
if (!events || events.length === 0) {
|
if (!events || events.length === 0) {
|
||||||
|
debugLog('No events to render');
|
||||||
auditTableBody.innerHTML = '<tr><td colspan="5" class="text-center">' + window.i18n.noEventsFound + '</td></tr>';
|
auditTableBody.innerHTML = '<tr><td colspan="5" class="text-center">' + window.i18n.noEventsFound + '</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
debugLog('Clearing table body');
|
||||||
auditTableBody.innerHTML = '';
|
auditTableBody.innerHTML = '';
|
||||||
|
|
||||||
|
debugLog('Processing events for table');
|
||||||
events.forEach((event, index) => {
|
events.forEach((event, index) => {
|
||||||
try {
|
try {
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
@ -374,11 +475,13 @@ function renderTable(events) {
|
|||||||
|
|
||||||
auditTableBody.appendChild(row);
|
auditTableBody.appendChild(row);
|
||||||
} catch (rowError) {
|
} catch (rowError) {
|
||||||
|
debugLog('Error rendering row ' + index, rowError.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
debugLog('Table rendering complete');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
debugLog('Error in renderTable', e.message);
|
||||||
auditTableBody.innerHTML = '<tr><td colspan="5" class="text-center">' + window.i18n.errorRendering + ' ' + e.message + '</td></tr>';
|
auditTableBody.innerHTML = '<tr><td colspan="5" class="text-center">' + window.i18n.errorRendering + ' ' + e.message + '</td></tr>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -407,24 +510,29 @@ function showEventDetails(event) {
|
|||||||
|
|
||||||
// Direct pagination approach - server seems to be hard-limited to returning 20 items
|
// Direct pagination approach - server seems to be hard-limited to returning 20 items
|
||||||
function goToPage(page) {
|
function goToPage(page) {
|
||||||
|
debugLog('goToPage called with page', page);
|
||||||
|
|
||||||
// Basic validation - totalPages may not be initialized on first load
|
// Basic validation - totalPages may not be initialized on first load
|
||||||
if (page < 0) {
|
if (page < 0) {
|
||||||
|
debugLog('Invalid page', page);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip validation against totalPages on first load
|
// Skip validation against totalPages on first load
|
||||||
if (totalPages > 0 && page >= totalPages) {
|
if (totalPages > 0 && page >= totalPages) {
|
||||||
|
debugLog('Page exceeds total pages', page);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple guard flag
|
// Simple guard flag
|
||||||
if (window.paginationBusy) {
|
if (window.paginationBusy) {
|
||||||
|
debugLog('Pagination busy, ignoring request');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.paginationBusy = true;
|
window.paginationBusy = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
debugLog('Setting page to', page);
|
||||||
|
|
||||||
// Store the requested page for later
|
// Store the requested page for later
|
||||||
window.requestedPage = page;
|
window.requestedPage = page;
|
||||||
@ -436,6 +544,7 @@ function goToPage(page) {
|
|||||||
// Load data with this page
|
// Load data with this page
|
||||||
loadAuditData(page, pageSize);
|
loadAuditData(page, pageSize);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
debugLog('Error in pagination', e.message);
|
||||||
window.paginationBusy = false;
|
window.paginationBusy = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -473,16 +582,9 @@ function renderCharts(data) {
|
|||||||
datasets: [{
|
datasets: [{
|
||||||
label: window.i18n.eventsByType,
|
label: window.i18n.eventsByType,
|
||||||
data: typeValues,
|
data: typeValues,
|
||||||
backgroundColor: colors.chartColors.slice(0, typeLabels.length).map(color => {
|
backgroundColor: colors.chartColors.slice(0, typeLabels.length),
|
||||||
// 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),
|
borderColor: colors.chartColors.slice(0, typeLabels.length),
|
||||||
borderWidth: 2,
|
borderWidth: 1
|
||||||
borderRadius: 4
|
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
@ -495,11 +597,7 @@ function renderCharts(data) {
|
|||||||
font: {
|
font: {
|
||||||
weight: colors.isDarkMode ? 'bold' : 'normal',
|
weight: colors.isDarkMode ? 'bold' : 'normal',
|
||||||
size: 14
|
size: 14
|
||||||
},
|
}
|
||||||
usePointStyle: true,
|
|
||||||
pointStyle: 'rectRounded',
|
|
||||||
boxWidth: 12,
|
|
||||||
boxHeight: 12,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
@ -510,18 +608,11 @@ function renderCharts(data) {
|
|||||||
bodyFont: {
|
bodyFont: {
|
||||||
size: 13
|
size: 13
|
||||||
},
|
},
|
||||||
backgroundColor: colors.isDarkMode ? 'rgba(40, 44, 52, 0.9)' : 'rgba(255, 255, 255, 0.9)',
|
backgroundColor: colors.isDarkMode ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.8)',
|
||||||
titleColor: colors.isDarkMode ? '#ffffff' : '#000000',
|
titleColor: colors.isDarkMode ? '#ffffff' : '#000000',
|
||||||
bodyColor: colors.isDarkMode ? '#ffffff' : '#000000',
|
bodyColor: colors.isDarkMode ? '#ffffff' : '#000000',
|
||||||
borderColor: colors.isDarkMode ? 'rgba(255, 255, 255, 0.5)' : colors.grid,
|
borderColor: colors.grid,
|
||||||
borderWidth: 1,
|
borderWidth: 1
|
||||||
padding: 10,
|
|
||||||
cornerRadius: 6,
|
|
||||||
callbacks: {
|
|
||||||
label: function(context) {
|
|
||||||
return `${context.dataset.label}: ${context.raw}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
@ -532,11 +623,10 @@ function renderCharts(data) {
|
|||||||
font: {
|
font: {
|
||||||
weight: colors.isDarkMode ? 'bold' : 'normal',
|
weight: colors.isDarkMode ? 'bold' : 'normal',
|
||||||
size: 12
|
size: 12
|
||||||
},
|
}
|
||||||
precision: 0 // Only show whole numbers
|
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
color: colors.isDarkMode ? 'rgba(255, 255, 255, 0.1)' : colors.grid
|
color: colors.grid
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
@ -553,25 +643,11 @@ function renderCharts(data) {
|
|||||||
color: colors.text,
|
color: colors.text,
|
||||||
font: {
|
font: {
|
||||||
weight: colors.isDarkMode ? 'bold' : 'normal',
|
weight: colors.isDarkMode ? 'bold' : 'normal',
|
||||||
size: 11
|
size: 12
|
||||||
},
|
}
|
||||||
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: {
|
grid: {
|
||||||
color: colors.isDarkMode ? 'rgba(255, 255, 255, 0.1)' : colors.grid,
|
color: colors.grid
|
||||||
display: false // Hide vertical gridlines for cleaner look
|
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
@ -580,8 +656,7 @@ function renderCharts(data) {
|
|||||||
font: {
|
font: {
|
||||||
weight: colors.isDarkMode ? 'bold' : 'normal',
|
weight: colors.isDarkMode ? 'bold' : 'normal',
|
||||||
size: 14
|
size: 14
|
||||||
},
|
}
|
||||||
padding: {top: 10, bottom: 0}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -602,8 +677,8 @@ function renderCharts(data) {
|
|||||||
label: window.i18n.eventsByUser,
|
label: window.i18n.eventsByUser,
|
||||||
data: userValues,
|
data: userValues,
|
||||||
backgroundColor: colors.chartColors.slice(0, userLabels.length),
|
backgroundColor: colors.chartColors.slice(0, userLabels.length),
|
||||||
borderWidth: 2,
|
borderWidth: 1,
|
||||||
borderColor: colors.isDarkMode ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.5)'
|
borderColor: colors.isDarkMode ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.2)'
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
@ -619,10 +694,6 @@ function renderCharts(data) {
|
|||||||
weight: colors.isDarkMode ? 'bold' : 'normal'
|
weight: colors.isDarkMode ? 'bold' : 'normal'
|
||||||
},
|
},
|
||||||
padding: 15,
|
padding: 15,
|
||||||
usePointStyle: true,
|
|
||||||
pointStyle: 'circle',
|
|
||||||
boxWidth: 10,
|
|
||||||
boxHeight: 10,
|
|
||||||
// Add a box around each label for better contrast in dark mode
|
// Add a box around each label for better contrast in dark mode
|
||||||
generateLabels: function(chart) {
|
generateLabels: function(chart) {
|
||||||
const original = Chart.overrides.pie.plugins.legend.labels.generateLabels;
|
const original = Chart.overrides.pie.plugins.legend.labels.generateLabels;
|
||||||
@ -630,9 +701,8 @@ function renderCharts(data) {
|
|||||||
|
|
||||||
if (colors.isDarkMode) {
|
if (colors.isDarkMode) {
|
||||||
labels.forEach(label => {
|
labels.forEach(label => {
|
||||||
// Enhance contrast for dark mode
|
label.fillStyle = 'rgba(0, 0, 0, 0.7)'; // Dark background for text
|
||||||
label.fillStyle = label.fillStyle; // Keep original fill
|
label.strokeStyle = label.strokeStyle; // Keep original color for border
|
||||||
label.strokeStyle = 'rgba(255, 255, 255, 0.8)'; // White border
|
|
||||||
label.lineWidth = 2; // Thicker border
|
label.lineWidth = 2; // Thicker border
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -649,13 +719,11 @@ function renderCharts(data) {
|
|||||||
bodyFont: {
|
bodyFont: {
|
||||||
size: 13
|
size: 13
|
||||||
},
|
},
|
||||||
backgroundColor: colors.isDarkMode ? 'rgba(40, 44, 52, 0.9)' : 'rgba(255, 255, 255, 0.9)',
|
backgroundColor: colors.isDarkMode ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.8)',
|
||||||
titleColor: colors.isDarkMode ? '#ffffff' : '#000000',
|
titleColor: colors.isDarkMode ? '#ffffff' : '#000000',
|
||||||
bodyColor: colors.isDarkMode ? '#ffffff' : '#000000',
|
bodyColor: colors.isDarkMode ? '#ffffff' : '#000000',
|
||||||
borderColor: colors.isDarkMode ? 'rgba(255, 255, 255, 0.5)' : colors.grid,
|
borderColor: colors.grid,
|
||||||
borderWidth: 1,
|
borderWidth: 1
|
||||||
padding: 10,
|
|
||||||
cornerRadius: 6
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -667,17 +735,6 @@ function renderCharts(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const timeCtx = document.getElementById('timeChart').getContext('2d');
|
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, {
|
timeChart = new Chart(timeCtx, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
@ -685,16 +742,10 @@ function renderCharts(data) {
|
|||||||
datasets: [{
|
datasets: [{
|
||||||
label: window.i18n.eventsOverTime,
|
label: window.i18n.eventsOverTime,
|
||||||
data: timeValues,
|
data: timeValues,
|
||||||
backgroundColor: bgColor,
|
backgroundColor: colors.chartColors[0] + '40', // 40 = 25% opacity
|
||||||
borderColor: borderColor,
|
borderColor: colors.chartColors[0],
|
||||||
borderWidth: 3,
|
tension: 0.1,
|
||||||
tension: 0.2,
|
fill: true
|
||||||
fill: true,
|
|
||||||
pointBackgroundColor: borderColor,
|
|
||||||
pointBorderColor: colors.isDarkMode ? '#fff' : '#000',
|
|
||||||
pointBorderWidth: 2,
|
|
||||||
pointRadius: 5,
|
|
||||||
pointHoverRadius: 7
|
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
@ -707,11 +758,7 @@ function renderCharts(data) {
|
|||||||
font: {
|
font: {
|
||||||
weight: colors.isDarkMode ? 'bold' : 'normal',
|
weight: colors.isDarkMode ? 'bold' : 'normal',
|
||||||
size: 14
|
size: 14
|
||||||
},
|
}
|
||||||
usePointStyle: true,
|
|
||||||
pointStyle: 'line',
|
|
||||||
boxWidth: 50,
|
|
||||||
boxHeight: 3
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
@ -722,24 +769,13 @@ function renderCharts(data) {
|
|||||||
bodyFont: {
|
bodyFont: {
|
||||||
size: 13
|
size: 13
|
||||||
},
|
},
|
||||||
backgroundColor: colors.isDarkMode ? 'rgba(40, 44, 52, 0.9)' : 'rgba(255, 255, 255, 0.9)',
|
backgroundColor: colors.isDarkMode ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.8)',
|
||||||
titleColor: colors.isDarkMode ? '#ffffff' : '#000000',
|
titleColor: colors.isDarkMode ? '#ffffff' : '#000000',
|
||||||
bodyColor: colors.isDarkMode ? '#ffffff' : '#000000',
|
bodyColor: colors.isDarkMode ? '#ffffff' : '#000000',
|
||||||
borderColor: colors.isDarkMode ? 'rgba(255, 255, 255, 0.5)' : colors.grid,
|
borderColor: colors.grid,
|
||||||
borderWidth: 1,
|
borderWidth: 1
|
||||||
padding: 10,
|
|
||||||
cornerRadius: 6,
|
|
||||||
callbacks: {
|
|
||||||
label: function(context) {
|
|
||||||
return `Events: ${context.raw}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
interaction: {
|
|
||||||
intersect: false,
|
|
||||||
mode: 'index'
|
|
||||||
},
|
|
||||||
scales: {
|
scales: {
|
||||||
y: {
|
y: {
|
||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
@ -748,11 +784,10 @@ function renderCharts(data) {
|
|||||||
font: {
|
font: {
|
||||||
weight: colors.isDarkMode ? 'bold' : 'normal',
|
weight: colors.isDarkMode ? 'bold' : 'normal',
|
||||||
size: 12
|
size: 12
|
||||||
},
|
}
|
||||||
precision: 0 // Only show whole numbers
|
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
color: colors.isDarkMode ? 'rgba(255, 255, 255, 0.1)' : colors.grid
|
color: colors.grid
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
@ -770,12 +805,10 @@ function renderCharts(data) {
|
|||||||
font: {
|
font: {
|
||||||
weight: colors.isDarkMode ? 'bold' : 'normal',
|
weight: colors.isDarkMode ? 'bold' : 'normal',
|
||||||
size: 12
|
size: 12
|
||||||
},
|
}
|
||||||
maxRotation: 45,
|
|
||||||
minRotation: 45
|
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
color: colors.isDarkMode ? 'rgba(255, 255, 255, 0.1)' : colors.grid
|
color: colors.grid
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
@ -784,8 +817,7 @@ function renderCharts(data) {
|
|||||||
font: {
|
font: {
|
||||||
weight: colors.isDarkMode ? 'bold' : 'normal',
|
weight: colors.isDarkMode ? 'bold' : 'normal',
|
||||||
size: 14
|
size: 14
|
||||||
},
|
}
|
||||||
padding: {top: 20}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -876,38 +908,20 @@ function getThemeColors() {
|
|||||||
'rgba(255, 255, 255, 0.2)' : // Semi-transparent white for dark mode
|
'rgba(255, 255, 255, 0.2)' : // Semi-transparent white for dark mode
|
||||||
getComputedStyle(document.documentElement).getPropertyValue('--md-sys-color-outline-variant').trim();
|
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 {
|
return {
|
||||||
text: textColor,
|
text: textColor,
|
||||||
grid: gridColor,
|
grid: gridColor,
|
||||||
backgroundColor: getComputedStyle(document.documentElement).getPropertyValue('--md-sys-color-surface-container').trim(),
|
backgroundColor: getComputedStyle(document.documentElement).getPropertyValue('--md-sys-color-surface-container').trim(),
|
||||||
chartColors: isDarkMode ? chartColorsDark : chartColorsLight,
|
chartColors: [
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue('--md-sys-color-primary').trim(),
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue('--md-sys-color-secondary').trim(),
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue('--md-sys-color-tertiary').trim(),
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue('--md-nav-section-color-other').trim(),
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue('--md-nav-section-color-convert').trim(),
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue('--md-nav-section-color-sign').trim(),
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue('--md-nav-section-color-security').trim(),
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue('--md-nav-section-color-convertto').trim(),
|
||||||
|
],
|
||||||
isDarkMode: isDarkMode
|
isDarkMode: isDarkMode
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -358,6 +358,9 @@
|
|||||||
<script th:src="@{/js/thirdParty/jquery.min.js}"></script>
|
<script th:src="@{/js/thirdParty/jquery.min.js}"></script>
|
||||||
<script th:src="@{/js/thirdParty/bootstrap.min.js}"></script>
|
<script th:src="@{/js/thirdParty/bootstrap.min.js}"></script>
|
||||||
|
|
||||||
|
<!-- Debug console for development purposes -->
|
||||||
|
<div id="debug-console"></div>
|
||||||
|
|
||||||
<!-- Internationalization data for JavaScript -->
|
<!-- Internationalization data for JavaScript -->
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
window.i18n = {
|
window.i18n = {
|
||||||
@ -378,5 +381,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Debug console -->
|
||||||
|
<div id="debug-console"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -175,6 +175,7 @@ public class SPDFApplication {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
log.info("Running configs {}", applicationProperties.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setServerPortStatic(String port) {
|
public static void setServerPortStatic(String port) {
|
||||||
@ -207,19 +208,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"};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user