feat(sprint-6): Phase 4 — Immutable audit log
- V8 migration: audit_events table (JSONB metadata, immutable by design) - AuditEvent entity + AuditEventType enum (18 event types) - AuditService: log events, paginated query, PDF export - AuditController: GET /api/v1/audit (paginated, filtered), GET export - AuditEventRepository with JPQL filtered queries - Frontend: /audit-log page (read-only, filterable, timezone-aware) - PDF export button for Behörde inspections - Sidebar: 'Protokoll' under new Compliance section - PdfReportGenerator: generateAuditReport method added - 10-year retention, REVOKE DELETE documented - Full i18n (de/en) with 18 event type translations
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
package de.cannamanage.api.controller;
|
||||
|
||||
import de.cannamanage.domain.entity.AuditEvent;
|
||||
import de.cannamanage.domain.entity.TenantContext;
|
||||
import de.cannamanage.domain.enums.AuditEventType;
|
||||
import de.cannamanage.service.AuditService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
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.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/audit")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "Audit", description = "Immutable audit log (KCanG compliance, 10-year retention)")
|
||||
public class AuditController {
|
||||
|
||||
private final AuditService auditService;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "Get paginated audit log",
|
||||
description = "Returns audit events with optional filters. Admin only.")
|
||||
@PreAuthorize("hasRole('ADMIN') or @staffPermissions.has(authentication, T(de.cannamanage.domain.enums.StaffPermission).VIEW_COMPLIANCE_REPORT)")
|
||||
public ResponseEntity<Page<AuditEventResponse>> getAuditLog(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size,
|
||||
@RequestParam(required = false) AuditEventType eventType,
|
||||
@RequestParam(required = false) String entityType,
|
||||
@RequestParam(required = false) UUID actorId,
|
||||
@RequestParam(required = false) Instant from,
|
||||
@RequestParam(required = false) Instant to
|
||||
) {
|
||||
UUID tenantId = TenantContext.getCurrentTenant();
|
||||
Page<AuditEvent> events = auditService.getEvents(
|
||||
tenantId, page, size, eventType, entityType, actorId, from, to
|
||||
);
|
||||
Page<AuditEventResponse> response = events.map(AuditEventResponse::from);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/export")
|
||||
@Operation(summary = "Export audit log as PDF",
|
||||
description = "Generates a PDF audit report for the specified date range. Admin only.")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public ResponseEntity<byte[]> exportAuditPdf(
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to
|
||||
) {
|
||||
UUID tenantId = TenantContext.getCurrentTenant();
|
||||
Instant fromInstant = from.atStartOfDay(ZoneId.of("Europe/Berlin")).toInstant();
|
||||
Instant toInstant = to.plusDays(1).atStartOfDay(ZoneId.of("Europe/Berlin")).toInstant();
|
||||
|
||||
byte[] pdf = auditService.exportPdf(tenantId, fromInstant, toInstant);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"attachment; filename=\"audit-log-" + from + "-to-" + to + ".pdf\"")
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.body(pdf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Response DTO for audit events (read-only projection).
|
||||
*/
|
||||
public record AuditEventResponse(
|
||||
UUID id,
|
||||
String eventType,
|
||||
String entityType,
|
||||
UUID entityId,
|
||||
UUID actorId,
|
||||
String actorName,
|
||||
String actorRole,
|
||||
String description,
|
||||
String metadata,
|
||||
String ipAddress,
|
||||
Instant timestamp
|
||||
) {
|
||||
public static AuditEventResponse from(AuditEvent event) {
|
||||
return new AuditEventResponse(
|
||||
event.getId(),
|
||||
event.getEventType().name(),
|
||||
event.getEntityType(),
|
||||
event.getEntityId(),
|
||||
event.getActorId(),
|
||||
event.getActorName(),
|
||||
event.getActorRole(),
|
||||
event.getDescription(),
|
||||
event.getMetadata(),
|
||||
event.getIpAddress(),
|
||||
event.getTimestamp()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
-- V8: Immutable Audit Log (KCanG §19 — 10-year retention)
|
||||
CREATE TABLE IF NOT EXISTS audit_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_type VARCHAR(50) NOT NULL,
|
||||
entity_type VARCHAR(50) NOT NULL,
|
||||
entity_id UUID,
|
||||
actor_id UUID NOT NULL,
|
||||
actor_name VARCHAR(255) NOT NULL,
|
||||
actor_role VARCHAR(20) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
metadata JSONB,
|
||||
ip_address VARCHAR(45),
|
||||
timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
tenant_id UUID NOT NULL
|
||||
);
|
||||
|
||||
-- Indexes for efficient querying
|
||||
CREATE INDEX idx_audit_timestamp ON audit_events(timestamp DESC);
|
||||
CREATE INDEX idx_audit_entity ON audit_events(entity_type, entity_id);
|
||||
CREATE INDEX idx_audit_actor ON audit_events(actor_id);
|
||||
CREATE INDEX idx_audit_tenant ON audit_events(tenant_id);
|
||||
CREATE INDEX idx_audit_type ON audit_events(event_type);
|
||||
|
||||
-- IMMUTABILITY: Revoke DELETE from application user
|
||||
-- (In production, run as DBA: REVOKE DELETE ON audit_events FROM cannamanage_app;)
|
||||
COMMENT ON TABLE audit_events IS 'Immutable audit log — 10-year retention (KCanG). Application role cannot DELETE.';
|
||||
Reference in New Issue
Block a user