mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-17 13:52:14 +01:00
Add audit system, invite links, and usage analytics (#4749)
# Description of Changes New Features Audit System: Complete audit logging with dashboard, event tracking, and export capabilities Invite Links: Secure invite system with email notifications and expiration Usage Analytics: Endpoint usage statistics and visualization License Management: User counting with grandfathering and license enforcement ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: James Brunton <jbrunton96@gmail.com>
This commit is contained in:
@@ -368,6 +368,10 @@ public class ApplicationProperties {
|
||||
private TempFileManagement tempFileManagement = new TempFileManagement();
|
||||
private DatabaseBackup databaseBackup = new DatabaseBackup();
|
||||
private List<String> corsAllowedOrigins = new ArrayList<>();
|
||||
private String
|
||||
frontendUrl; // Base URL for frontend (used for invite links, etc.). If not set,
|
||||
|
||||
// falls back to backend URL.
|
||||
|
||||
public boolean isAnalyticsEnabled() {
|
||||
return this.getEnableAnalytics() != null && this.getEnableAnalytics();
|
||||
@@ -556,6 +560,7 @@ public class ApplicationProperties {
|
||||
public static class Mail {
|
||||
private boolean enabled;
|
||||
private boolean enableInvites = false;
|
||||
private int inviteLinkExpiryHours = 72; // Default: 72 hours (3 days)
|
||||
private String host;
|
||||
private int port;
|
||||
private String username;
|
||||
|
||||
@@ -43,26 +43,45 @@ public class JarPathUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the path to the restart-helper.jar file Expected to be in the same directory as the main
|
||||
* JAR
|
||||
* Gets the path to the restart-helper.jar file. Checks multiple possible locations: 1. Same
|
||||
* directory as the main JAR (production deployment) 2. ./build/libs/restart-helper.jar
|
||||
* (development build) 3. app/common/build/libs/restart-helper.jar (multi-module build)
|
||||
*
|
||||
* @return Path to restart-helper.jar, or null if not found
|
||||
*/
|
||||
public static Path restartHelperJar() {
|
||||
Path appJar = currentJar();
|
||||
if (appJar == null) {
|
||||
return null;
|
||||
|
||||
// Define possible locations to check (in order of preference)
|
||||
Path[] possibleLocations = new Path[4];
|
||||
|
||||
// Location 1: Same directory as main JAR (production)
|
||||
if (appJar != null) {
|
||||
possibleLocations[0] = appJar.getParent().resolve("restart-helper.jar");
|
||||
}
|
||||
|
||||
Path helperJar = appJar.getParent().resolve("restart-helper.jar");
|
||||
// Location 2: ./build/libs/ (development build)
|
||||
possibleLocations[1] = Paths.get("build", "libs", "restart-helper.jar").toAbsolutePath();
|
||||
|
||||
if (Files.isRegularFile(helperJar)) {
|
||||
log.debug("Restart helper JAR located at: {}", helperJar);
|
||||
return helperJar;
|
||||
} else {
|
||||
log.warn("Restart helper JAR not found at: {}", helperJar);
|
||||
return null;
|
||||
// Location 3: app/common/build/libs/ (multi-module build)
|
||||
possibleLocations[2] =
|
||||
Paths.get("app", "common", "build", "libs", "restart-helper.jar").toAbsolutePath();
|
||||
|
||||
// Location 4: Current working directory
|
||||
possibleLocations[3] = Paths.get("restart-helper.jar").toAbsolutePath();
|
||||
|
||||
// Check each location
|
||||
for (Path location : possibleLocations) {
|
||||
if (location != null && Files.isRegularFile(location)) {
|
||||
log.info("Restart helper JAR found at: {}", location);
|
||||
return location;
|
||||
} else if (location != null) {
|
||||
log.debug("Restart helper JAR not found at: {}", location);
|
||||
}
|
||||
}
|
||||
|
||||
log.warn("Restart helper JAR not found in any expected location");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -76,6 +76,8 @@ public class RequestUriUtils {
|
||||
|| trimmedUri.startsWith("/api/v1/auth/refresh")
|
||||
|| trimmedUri.startsWith("/api/v1/auth/logout")
|
||||
|| trimmedUri.startsWith("/v1/api-docs")
|
||||
|| trimmedUri.startsWith("/api/v1/invite/validate")
|
||||
|| trimmedUri.startsWith("/api/v1/invite/accept")
|
||||
|| trimmedUri.contains("/v1/api-docs");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.util.Arrays;
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
@@ -135,6 +136,17 @@ public class YamlHelper {
|
||||
} else if ("true".equals(newValue) || "false".equals(newValue)) {
|
||||
newValueNode =
|
||||
new ScalarNode(Tag.BOOL, String.valueOf(newValue), ScalarStyle.PLAIN);
|
||||
} else if (newValue instanceof Map<?, ?> map) {
|
||||
// Handle Map objects - convert to MappingNode
|
||||
List<NodeTuple> mapTuples = new ArrayList<>();
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
ScalarNode mapKeyNode =
|
||||
new ScalarNode(
|
||||
Tag.STR, String.valueOf(entry.getKey()), ScalarStyle.PLAIN);
|
||||
Node mapValueNode = convertValueToNode(entry.getValue());
|
||||
mapTuples.add(new NodeTuple(mapKeyNode, mapValueNode));
|
||||
}
|
||||
newValueNode = new MappingNode(Tag.MAP, mapTuples, FlowStyle.BLOCK);
|
||||
} else if (newValue instanceof List<?> list) {
|
||||
List<Node> sequenceNodes = new ArrayList<>();
|
||||
for (Object item : list) {
|
||||
@@ -458,6 +470,43 @@ public class YamlHelper {
|
||||
return isInteger(object) || isShort(object) || isByte(object) || isLong(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Java value to a YAML Node.
|
||||
*
|
||||
* @param value The value to convert.
|
||||
* @return The corresponding YAML Node.
|
||||
*/
|
||||
private Node convertValueToNode(Object value) {
|
||||
if (value == null) {
|
||||
return new ScalarNode(Tag.NULL, "null", ScalarStyle.PLAIN);
|
||||
} else if (isAnyInteger(value)) {
|
||||
return new ScalarNode(Tag.INT, String.valueOf(value), ScalarStyle.PLAIN);
|
||||
} else if (isFloat(value)) {
|
||||
Object floatValue = Float.valueOf(String.valueOf(value));
|
||||
return new ScalarNode(Tag.FLOAT, String.valueOf(floatValue), ScalarStyle.PLAIN);
|
||||
} else if (value instanceof Boolean || "true".equals(value) || "false".equals(value)) {
|
||||
return new ScalarNode(Tag.BOOL, String.valueOf(value), ScalarStyle.PLAIN);
|
||||
} else if (value instanceof Map<?, ?> map) {
|
||||
// Recursively handle nested maps
|
||||
List<NodeTuple> mapTuples = new ArrayList<>();
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
ScalarNode mapKeyNode =
|
||||
new ScalarNode(Tag.STR, String.valueOf(entry.getKey()), ScalarStyle.PLAIN);
|
||||
Node mapValueNode = convertValueToNode(entry.getValue());
|
||||
mapTuples.add(new NodeTuple(mapKeyNode, mapValueNode));
|
||||
}
|
||||
return new MappingNode(Tag.MAP, mapTuples, FlowStyle.BLOCK);
|
||||
} else if (value instanceof List<?> list) {
|
||||
List<Node> sequenceNodes = new ArrayList<>();
|
||||
for (Object item : list) {
|
||||
sequenceNodes.add(convertValueToNode(item));
|
||||
}
|
||||
return new SequenceNode(Tag.SEQ, sequenceNodes, FlowStyle.FLOW);
|
||||
} else {
|
||||
return new ScalarNode(Tag.STR, String.valueOf(value), ScalarStyle.PLAIN);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies comments from an old node to a new one.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user