feat: append-only audit trail for all mutations (P0)

- Database: kanzlai.audit_log table with RLS, append-only policies
  (no UPDATE/DELETE), indexes for entity, user, and time queries
- Backend: AuditService.Log() with context-based tenant/user/IP/UA
  extraction, wired into all 7 services (case, deadline, appointment,
  document, note, party, tenant)
- API: GET /api/audit-log with entity_type, entity_id, user_id,
  from/to date, and pagination filters
- Frontend: Protokoll tab on case detail page with chronological
  audit entries, diff preview, and pagination

Required by § 50 BRAO and DSGVO Art. 5(2).
This commit is contained in:
m
2026-03-30 11:02:28 +02:00
parent 82878dffd5
commit b36247dfb9
17 changed files with 533 additions and 37 deletions

View File

@@ -12,11 +12,12 @@ import (
)
type AppointmentService struct {
db *sqlx.DB
db *sqlx.DB
audit *AuditService
}
func NewAppointmentService(db *sqlx.DB) *AppointmentService {
return &AppointmentService{db: db}
func NewAppointmentService(db *sqlx.DB, audit *AuditService) *AppointmentService {
return &AppointmentService{db: db, audit: audit}
}
type AppointmentFilter struct {
@@ -86,6 +87,7 @@ func (s *AppointmentService) Create(ctx context.Context, a *models.Appointment)
if err != nil {
return fmt.Errorf("creating appointment: %w", err)
}
s.audit.Log(ctx, "create", "appointment", &a.ID, nil, a)
return nil
}
@@ -116,6 +118,7 @@ func (s *AppointmentService) Update(ctx context.Context, a *models.Appointment)
if rows == 0 {
return fmt.Errorf("appointment not found")
}
s.audit.Log(ctx, "update", "appointment", &a.ID, nil, a)
return nil
}
@@ -131,5 +134,6 @@ func (s *AppointmentService) Delete(ctx context.Context, tenantID, id uuid.UUID)
if rows == 0 {
return fmt.Errorf("appointment not found")
}
s.audit.Log(ctx, "delete", "appointment", &id, nil, nil)
return nil
}