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:
@@ -18,10 +18,11 @@ const documentBucket = "kanzlai-documents"
|
||||
type DocumentService struct {
|
||||
db *sqlx.DB
|
||||
storage *StorageClient
|
||||
audit *AuditService
|
||||
}
|
||||
|
||||
func NewDocumentService(db *sqlx.DB, storage *StorageClient) *DocumentService {
|
||||
return &DocumentService{db: db, storage: storage}
|
||||
func NewDocumentService(db *sqlx.DB, storage *StorageClient, audit *AuditService) *DocumentService {
|
||||
return &DocumentService{db: db, storage: storage, audit: audit}
|
||||
}
|
||||
|
||||
type CreateDocumentInput struct {
|
||||
@@ -97,6 +98,7 @@ func (s *DocumentService) Create(ctx context.Context, tenantID, caseID, userID u
|
||||
if err := s.db.GetContext(ctx, &doc, "SELECT * FROM documents WHERE id = $1", id); err != nil {
|
||||
return nil, fmt.Errorf("fetching created document: %w", err)
|
||||
}
|
||||
s.audit.Log(ctx, "create", "document", &id, nil, doc)
|
||||
return &doc, nil
|
||||
}
|
||||
|
||||
@@ -151,6 +153,7 @@ func (s *DocumentService) Delete(ctx context.Context, tenantID, docID, userID uu
|
||||
// Log case event
|
||||
createEvent(ctx, s.db, tenantID, doc.CaseID, userID, "document_deleted",
|
||||
fmt.Sprintf("Document deleted: %s", doc.Title), nil)
|
||||
s.audit.Log(ctx, "delete", "document", &docID, doc, nil)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user