Auditing support (#3739)

# Description of Changes

This pull request introduces a comprehensive auditing system to the
application, along with minor updates to existing utilities and
dependencies. The most significant changes include the addition of
audit-related classes and enums, updates to the `ApplicationProperties`
model to support auditing configuration, and enhancements to utility
methods for handling static and trackable resources.

### Audit System Implementation:

* **Audit Aspect for Method Annotations**: Added `AuditAspect` to
process the new `@Audited` annotation, enabling detailed logging of
method execution, HTTP requests, and operation results based on
configurable audit levels.
(`proprietary/src/main/java/stirling/software/proprietary/audit/AuditAspect.java`)
* **Audit Event Types**: Introduced `AuditEventType` enum to define
standardized event types for auditing, such as authentication events,
file operations, and HTTP requests.
(`proprietary/src/main/java/stirling/software/proprietary/audit/AuditEventType.java`)
* **Audit Levels**: Added `AuditLevel` enum to define different levels
of audit logging (OFF, BASIC, STANDARD, VERBOSE), providing granular
control over the amount of data logged.
(`proprietary/src/main/java/stirling/software/proprietary/audit/AuditLevel.java`)

### Application Properties Update:

* **Audit Configuration in `ProFeatures`**: Updated the `ProFeatures`
class in `ApplicationProperties` to include support for auditing with
configurable retention days, levels, and enablement flags.
(`common/src/main/java/stirling/software/common/model/ApplicationProperties.java`)

### Utility Enhancements:

* **Static and Trackable Resource Handling**: Extended `RequestUriUtils`
methods (`isStaticResource` and `isTrackableResource`) to recognize
`.txt` files as valid static and trackable resources.
(`common/src/main/java/stirling/software/common/util/RequestUriUtils.java`)
[[1]](diffhunk://#diff-de3599037908683f2cd8f170939547612c6fc2203e9207eb4d7966508f92bbcbR22)
[[2]](diffhunk://#diff-de3599037908683f2cd8f170939547612c6fc2203e9207eb4d7966508f92bbcbR39)

### Dependency Update:

* **Spring Validation Starter**: Added `spring-boot-starter-validation`
to project dependencies to support validation mechanisms required for
auditing features. (`proprietary/build.gradle`)


Dashboard WIP

![image](https://github.com/user-attachments/assets/20d86809-63b0-44d6-82d3-bdce2ac77aa3)


![image](https://github.com/user-attachments/assets/53a5ba69-71ab-4247-9a66-7ef86e462b13)

![image](https://github.com/user-attachments/assets/9a53eaed-ebc7-463c-81da-8b1c140f8a8c)


---

## 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/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/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/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/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: a <a>
Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>
This commit is contained in:
Anthony Stirling
2025-06-18 13:11:36 +01:00
committed by GitHub
co-authored by a pixeebot[bot]
parent ee41dc11c2
commit 552f2ced4d
38 changed files with 4168 additions and 17 deletions
@@ -0,0 +1,42 @@
# 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
@@ -0,0 +1,250 @@
# 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.
@@ -0,0 +1,383 @@
<!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">&laquo;</button>
<button type="button" class="btn btn-outline-primary" id="page-prev">&lsaquo;</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">&rsaquo;</button>
<button type="button" class="btn btn-outline-primary" id="page-last">&raquo;</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>