diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditDashboardController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditDashboardController.java deleted file mode 100644 index 16f860e96..000000000 --- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditDashboardController.java +++ /dev/null @@ -1,396 +0,0 @@ -package stirling.software.proprietary.controller.api; - -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import org.springdoc.core.annotations.ParameterObject; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import stirling.software.proprietary.audit.AuditEventType; -import stirling.software.proprietary.model.api.audit.AuditDataRequest; -import stirling.software.proprietary.model.api.audit.AuditDataResponse; -import stirling.software.proprietary.model.api.audit.AuditExportRequest; -import stirling.software.proprietary.model.api.audit.AuditStatsResponse; -import stirling.software.proprietary.model.security.PersistentAuditEvent; -import stirling.software.proprietary.repository.PersistentAuditEventRepository; -import stirling.software.proprietary.security.config.EnterpriseEndpoint; - -/** REST endpoints for the audit dashboard. */ -@Slf4j -@RestController -@RequestMapping("/api/v1/audit") -@PreAuthorize("hasRole('ROLE_ADMIN')") -@RequiredArgsConstructor -@EnterpriseEndpoint -@Tag(name = "Audit", description = "Only Enterprise - Audit related operations") -public class AuditDashboardController { - - private final PersistentAuditEventRepository auditRepository; - private final ObjectMapper objectMapper; - - /** Get audit events data for the dashboard tables. */ - @GetMapping("/data") - @Operation(summary = "Get audit events data") - public AuditDataResponse getAuditData(@ParameterObject AuditDataRequest request) { - - Pageable pageable = - PageRequest.of( - request.getPage(), request.getSize(), Sort.by("timestamp").descending()); - Page events; - - String type = request.getType(); - String principal = request.getPrincipal(); - LocalDate startDate = request.getStartDate(); - LocalDate endDate = request.getEndDate(); - - if (type != null && principal != null && startDate != null && endDate != null) { - Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant(); - Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant(); - events = - auditRepository.findByPrincipalAndTypeAndTimestampBetween( - principal, type, start, end, pageable); - } else if (type != null && principal != null) { - events = auditRepository.findByPrincipalAndType(principal, type, pageable); - } else if (type != null && startDate != null && endDate != null) { - Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant(); - Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant(); - events = auditRepository.findByTypeAndTimestampBetween(type, start, end, pageable); - } else if (principal != null && startDate != null && endDate != null) { - Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant(); - Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant(); - events = - auditRepository.findByPrincipalAndTimestampBetween( - principal, start, end, pageable); - } else if (startDate != null && endDate != null) { - Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant(); - Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant(); - events = auditRepository.findByTimestampBetween(start, end, pageable); - } else if (type != null) { - events = auditRepository.findByType(type, pageable); - } else if (principal != null) { - events = auditRepository.findByPrincipal(principal, pageable); - } else { - events = auditRepository.findAll(pageable); - } - - // Logging - List content = events.getContent(); - - return new AuditDataResponse( - content, events.getTotalPages(), events.getTotalElements(), events.getNumber()); - } - - /** Get statistics for charts (last X days). Existing behavior preserved. */ - @GetMapping("/stats") - @Operation(summary = "Get audit statistics for the last N days") - public AuditStatsResponse getAuditStats( - @Schema( - description = "Number of days to look back for audit events", - example = "7", - required = true) - @RequestParam(value = "days", defaultValue = "7") - int days) { - - // Get events from the last X days - Instant startDate = Instant.now().minus(java.time.Duration.ofDays(days)); - List events = auditRepository.findByTimestampAfter(startDate); - - // Count events by type - Map eventsByType = - events.stream() - .collect( - Collectors.groupingBy( - PersistentAuditEvent::getType, Collectors.counting())); - - // Count events by principal - Map eventsByPrincipal = - events.stream() - .collect( - Collectors.groupingBy( - PersistentAuditEvent::getPrincipal, Collectors.counting())); - - // Count events by day - Map eventsByDay = - events.stream() - .collect( - Collectors.groupingBy( - e -> - LocalDateTime.ofInstant( - e.getTimestamp(), - ZoneId.systemDefault()) - .format(DateTimeFormatter.ISO_LOCAL_DATE), - Collectors.counting())); - - return new AuditStatsResponse(eventsByType, eventsByPrincipal, eventsByDay, events.size()); - } - - // /** Advanced statistics using repository aggregations, with explicit date range. */ - // @GetMapping("/stats/range") - // @Operation(summary = "Get audit statistics for a date range (aggregated in DB)") - // public Map getAuditStatsRange(@ParameterObject AuditDateExportRequest - // request) { - - // LocalDate startDate = request.getStartDate(); - // LocalDate endDate = request.getEndDate(); - // Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant(); - // Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant(); - - // Map byType = toStringLongMap(auditRepository.countByTypeBetween(start, - // end)); - // Map byPrincipal = - // toStringLongMap(auditRepository.countByPrincipalBetween(start, end)); - - // Map byDay = new HashMap<>(); - // for (Object[] row : auditRepository.histogramByDayBetween(start, end)) { - // int y = ((Number) row[0]).intValue(); - // int m = ((Number) row[1]).intValue(); - // int d = ((Number) row[2]).intValue(); - // long count = ((Number) row[3]).longValue(); - // String key = String.format("%04d-%02d-%02d", y, m, d); - // byDay.put(key, count); - // } - - // Map byHour = new HashMap<>(); - // for (Object[] row : auditRepository.histogramByHourBetween(start, end)) { - // int hour = ((Number) row[0]).intValue(); - // long count = ((Number) row[1]).longValue(); - // byHour.put(String.format("%02d:00", hour), count); - // } - - // Map payload = new HashMap<>(); - // payload.put("byType", byType); - // payload.put("byPrincipal", byPrincipal); - // payload.put("byDay", byDay); - // payload.put("byHour", byHour); - // return payload; - // } - - /** Get all unique event types from the database for filtering. */ - @GetMapping("/types") - @Operation(summary = "Get all unique audit event types") - public List getAuditTypes() { - // Get distinct event types from the database - List dbTypes = auditRepository.findDistinctEventTypes(); - - // Include standard enum types in case they're not in the database yet - List enumTypes = - Arrays.stream(AuditEventType.values()) - .map(AuditEventType::name) - .collect(Collectors.toList()); - - // Combine both sources, remove duplicates, and sort - Set combinedTypes = new HashSet<>(); - combinedTypes.addAll(dbTypes); - combinedTypes.addAll(enumTypes); - - return combinedTypes.stream().sorted().collect(Collectors.toList()); - } - - /** Export audit data as CSV. */ - @GetMapping("/export/csv") - @Operation(summary = "Export audit data as CSV") - public ResponseEntity exportAuditData(@ParameterObject AuditExportRequest request) { - - List events = getAuditEventsByCriteria(request); - - // Convert to CSV - StringBuilder csv = new StringBuilder(); - csv.append("ID,Principal,Type,Timestamp,Data\n"); - - DateTimeFormatter formatter = DateTimeFormatter.ISO_INSTANT; - - for (PersistentAuditEvent event : events) { - csv.append(event.getId()).append(","); - csv.append(escapeCSV(event.getPrincipal())).append(","); - csv.append(escapeCSV(event.getType())).append(","); - csv.append(formatter.format(event.getTimestamp())).append(","); - csv.append(escapeCSV(event.getData())).append("\n"); - } - - byte[] csvBytes = csv.toString().getBytes(); - - // Set up HTTP headers for download - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); - headers.setContentDispositionFormData("attachment", "audit_export.csv"); - - return ResponseEntity.ok().headers(headers).body(csvBytes); - } - - /** Export audit data as JSON. */ - @GetMapping("/export/json") - @Operation(summary = "Export audit data as JSON") - public ResponseEntity exportAuditDataJson(@ParameterObject AuditExportRequest request) { - - List events = getAuditEventsByCriteria(request); - - // Convert to JSON - try { - byte[] jsonBytes = objectMapper.writeValueAsBytes(events); - - // Set up HTTP headers for download - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - headers.setContentDispositionFormData("attachment", "audit_export.json"); - - return ResponseEntity.ok().headers(headers).body(jsonBytes); - } catch (JsonProcessingException e) { - log.error("Error serializing audit events to JSON", e); - return ResponseEntity.internalServerError().build(); - } - } - - // /** Get all unique principals. */ - // @GetMapping("/principals") - // @Operation(summary = "Get all distinct principals") - // public List getPrincipals() { - // return auditRepository.findDistinctPrincipals(); - // } - - // /** Get principals by event type. */ - // @GetMapping("/types/{type}/principals") - // @Operation(summary = "Get distinct principals for a given type") - // public List getPrincipalsByType(@PathVariable("type") String type) { - // return auditRepository.findDistinctPrincipalsByType(type); - // } - - // /** Latest helpers */ - // @GetMapping("/latest") - // @Operation(summary = "Get the latest audit event, optionally filtered by type or principal") - // public ResponseEntity getLatest( - // @RequestParam(value = "type", required = false) String type, - // @RequestParam(value = "principal", required = false) String principal) { - // if (type != null) { - // return auditRepository - // .findTopByTypeOrderByTimestampDesc(type) - // .map(ResponseEntity::ok) - // .orElse(ResponseEntity.noContent().build()); - // } else if (principal != null) { - // return auditRepository - // .findTopByPrincipalOrderByTimestampDesc(principal) - // .map(ResponseEntity::ok) - // .orElse(ResponseEntity.noContent().build()); - // } - // return auditRepository - // .findTopByOrderByTimestampDesc() - // .map(ResponseEntity::ok) - // .orElse(ResponseEntity.noContent().build()); - // } - - /** Cleanup endpoints data before a certain date */ - @DeleteMapping("/cleanup/before") - @Operation( - summary = "Cleanup audit events before a certain date", - description = "Deletes all audit events before the specified date.") - public Map cleanupBefore( - @RequestParam(value = "date", required = true) - @Schema( - description = "The cutoff date for cleanup", - example = "2025-01-01", - format = "date") - @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) - LocalDate date) { - if (date != null && !date.isAfter(LocalDate.now())) { - Instant cutoff = date.atStartOfDay(ZoneId.systemDefault()).toInstant(); - int deleted = auditRepository.deleteByTimestampBefore(cutoff); - return Map.of("deleted", deleted, "cutoffDate", date.toString()); - } - return Map.of( - "error", - "Invalid date format. Use ISO date format (YYYY-MM-DD). Date must be in the past."); - } - - // // ===== Helpers ===== - - // private Map toStringLongMap(List rows) { - // Map map = new HashMap<>(); - // for (Object[] row : rows) { - // String key = String.valueOf(row[0]); - // long val = ((Number) row[1]).longValue(); - // map.put(key, val); - // } - // return map; - // } - - /** Helper method to escape CSV fields. */ - private String escapeCSV(String field) { - if (field == null) { - return ""; - } - // Replace double quotes with two double quotes and wrap in quotes - return "\"" + field.replace("\"", "\"\"") + "\""; - } - - private List getAuditEventsByCriteria(AuditExportRequest request) { - String type = request.getType(); - String principal = request.getPrincipal(); - LocalDate startDate = request.getStartDate(); - LocalDate endDate = request.getEndDate(); - - // Get data with same filtering as getAuditData - List events; - - if (type != null && principal != null && startDate != null && endDate != null) { - Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant(); - Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant(); - events = - auditRepository.findAllByPrincipalAndTypeAndTimestampBetweenForExport( - principal, type, start, end); - } else if (type != null && principal != null) { - events = auditRepository.findAllByPrincipalAndTypeForExport(principal, type); - } else if (type != null && startDate != null && endDate != null) { - Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant(); - Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant(); - events = auditRepository.findAllByTypeAndTimestampBetweenForExport(type, start, end); - } else if (principal != null && startDate != null && endDate != null) { - Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant(); - Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant(); - events = - auditRepository.findAllByPrincipalAndTimestampBetweenForExport( - principal, start, end); - } else if (startDate != null && endDate != null) { - Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant(); - Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant(); - events = auditRepository.findAllByTimestampBetweenForExport(start, end); - } else if (type != null) { - events = auditRepository.findByTypeForExport(type); - } else if (principal != null) { - events = auditRepository.findAllByPrincipalForExport(principal); - } else { - events = auditRepository.findAll(); - } - return events; - } -}