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,120 @@
|
||||
package de.cannamanage.domain.entity;
|
||||
|
||||
import de.cannamanage.domain.enums.AuditEventType;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Immutable audit log entry.
|
||||
* No setters for fields post-persist — once written, never changed.
|
||||
* 10-year retention per KCanG compliance requirements.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "audit_events")
|
||||
public class AuditEvent {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column(name = "id", nullable = false, updatable = false)
|
||||
private UUID id;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "event_type", nullable = false, updatable = false, length = 50)
|
||||
private AuditEventType eventType;
|
||||
|
||||
@Column(name = "entity_type", nullable = false, updatable = false, length = 50)
|
||||
private String entityType;
|
||||
|
||||
@Column(name = "entity_id", updatable = false)
|
||||
private UUID entityId;
|
||||
|
||||
@Column(name = "actor_id", nullable = false, updatable = false)
|
||||
private UUID actorId;
|
||||
|
||||
@Column(name = "actor_name", nullable = false, updatable = false)
|
||||
private String actorName;
|
||||
|
||||
@Column(name = "actor_role", nullable = false, updatable = false, length = 20)
|
||||
private String actorRole;
|
||||
|
||||
@Column(name = "description", nullable = false, updatable = false, columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
@Column(name = "metadata", updatable = false, columnDefinition = "jsonb")
|
||||
private String metadata;
|
||||
|
||||
@Column(name = "ip_address", updatable = false, length = 45)
|
||||
private String ipAddress;
|
||||
|
||||
@Column(name = "timestamp", nullable = false, updatable = false)
|
||||
private Instant timestamp;
|
||||
|
||||
@Column(name = "tenant_id", nullable = false, updatable = false)
|
||||
private UUID tenantId;
|
||||
|
||||
protected AuditEvent() {
|
||||
// JPA
|
||||
}
|
||||
|
||||
private AuditEvent(Builder builder) {
|
||||
this.eventType = builder.eventType;
|
||||
this.entityType = builder.entityType;
|
||||
this.entityId = builder.entityId;
|
||||
this.actorId = builder.actorId;
|
||||
this.actorName = builder.actorName;
|
||||
this.actorRole = builder.actorRole;
|
||||
this.description = builder.description;
|
||||
this.metadata = builder.metadata;
|
||||
this.ipAddress = builder.ipAddress;
|
||||
this.timestamp = Instant.now();
|
||||
this.tenantId = builder.tenantId;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
// Read-only getters
|
||||
public UUID getId() { return id; }
|
||||
public AuditEventType getEventType() { return eventType; }
|
||||
public String getEntityType() { return entityType; }
|
||||
public UUID getEntityId() { return entityId; }
|
||||
public UUID getActorId() { return actorId; }
|
||||
public String getActorName() { return actorName; }
|
||||
public String getActorRole() { return actorRole; }
|
||||
public String getDescription() { return description; }
|
||||
public String getMetadata() { return metadata; }
|
||||
public String getIpAddress() { return ipAddress; }
|
||||
public Instant getTimestamp() { return timestamp; }
|
||||
public UUID getTenantId() { return tenantId; }
|
||||
|
||||
public static class Builder {
|
||||
private AuditEventType eventType;
|
||||
private String entityType;
|
||||
private UUID entityId;
|
||||
private UUID actorId;
|
||||
private String actorName;
|
||||
private String actorRole;
|
||||
private String description;
|
||||
private String metadata;
|
||||
private String ipAddress;
|
||||
private UUID tenantId;
|
||||
|
||||
public Builder eventType(AuditEventType eventType) { this.eventType = eventType; return this; }
|
||||
public Builder entityType(String entityType) { this.entityType = entityType; return this; }
|
||||
public Builder entityId(UUID entityId) { this.entityId = entityId; return this; }
|
||||
public Builder actorId(UUID actorId) { this.actorId = actorId; return this; }
|
||||
public Builder actorName(String actorName) { this.actorName = actorName; return this; }
|
||||
public Builder actorRole(String actorRole) { this.actorRole = actorRole; return this; }
|
||||
public Builder description(String description) { this.description = description; return this; }
|
||||
public Builder metadata(String metadata) { this.metadata = metadata; return this; }
|
||||
public Builder ipAddress(String ipAddress) { this.ipAddress = ipAddress; return this; }
|
||||
public Builder tenantId(UUID tenantId) { this.tenantId = tenantId; return this; }
|
||||
|
||||
public AuditEvent build() {
|
||||
return new AuditEvent(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package de.cannamanage.domain.enums;
|
||||
|
||||
/**
|
||||
* All auditable event types in the system.
|
||||
* Immutable audit trail for KCanG compliance (10-year retention).
|
||||
*/
|
||||
public enum AuditEventType {
|
||||
// Distribution events
|
||||
DISTRIBUTION_RECORDED,
|
||||
DISTRIBUTION_VOIDED,
|
||||
|
||||
// Member events
|
||||
MEMBER_CREATED,
|
||||
MEMBER_UPDATED,
|
||||
MEMBER_SUSPENDED,
|
||||
MEMBER_EXPELLED,
|
||||
|
||||
// Stock events
|
||||
BATCH_CREATED,
|
||||
BATCH_RECALLED,
|
||||
|
||||
// Auth events
|
||||
LOGIN_SUCCESS,
|
||||
LOGIN_FAILED,
|
||||
LOGOUT,
|
||||
PASSWORD_CHANGED,
|
||||
|
||||
// Staff events
|
||||
STAFF_INVITED,
|
||||
STAFF_PERMISSIONS_CHANGED,
|
||||
STAFF_REVOKED,
|
||||
|
||||
// Consent events
|
||||
CONSENT_GRANTED,
|
||||
CONSENT_REVOKED,
|
||||
DATA_EXPORTED,
|
||||
DATA_DELETED,
|
||||
|
||||
// Billing events
|
||||
SUBSCRIPTION_STARTED,
|
||||
SUBSCRIPTION_CANCELED,
|
||||
PAYMENT_RECEIVED,
|
||||
PAYMENT_FAILED
|
||||
}
|
||||
Reference in New Issue
Block a user