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

@@ -13,11 +13,12 @@ import (
)
type PartyService struct {
db *sqlx.DB
db *sqlx.DB
audit *AuditService
}
func NewPartyService(db *sqlx.DB) *PartyService {
return &PartyService{db: db}
func NewPartyService(db *sqlx.DB, audit *AuditService) *PartyService {
return &PartyService{db: db, audit: audit}
}
type CreatePartyInput struct {
@@ -79,6 +80,7 @@ func (s *PartyService) Create(ctx context.Context, tenantID, caseID uuid.UUID, u
if err := s.db.GetContext(ctx, &party, "SELECT * FROM parties WHERE id = $1", id); err != nil {
return nil, fmt.Errorf("fetching created party: %w", err)
}
s.audit.Log(ctx, "create", "party", &id, nil, party)
return &party, nil
}
@@ -135,6 +137,7 @@ func (s *PartyService) Update(ctx context.Context, tenantID, partyID uuid.UUID,
if err := s.db.GetContext(ctx, &updated, "SELECT * FROM parties WHERE id = $1", partyID); err != nil {
return nil, fmt.Errorf("fetching updated party: %w", err)
}
s.audit.Log(ctx, "update", "party", &partyID, current, updated)
return &updated, nil
}
@@ -148,5 +151,6 @@ func (s *PartyService) Delete(ctx context.Context, tenantID, partyID uuid.UUID)
if rows == 0 {
return sql.ErrNoRows
}
s.audit.Log(ctx, "delete", "party", &partyID, nil, nil)
return nil
}