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:
63
backend/internal/handlers/audit_log.go
Normal file
63
backend/internal/handlers/audit_log.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"mgit.msbls.de/m/KanzlAI-mGMT/internal/auth"
|
||||
"mgit.msbls.de/m/KanzlAI-mGMT/internal/services"
|
||||
)
|
||||
|
||||
type AuditLogHandler struct {
|
||||
svc *services.AuditService
|
||||
}
|
||||
|
||||
func NewAuditLogHandler(svc *services.AuditService) *AuditLogHandler {
|
||||
return &AuditLogHandler{svc: svc}
|
||||
}
|
||||
|
||||
func (h *AuditLogHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, ok := auth.TenantFromContext(r.Context())
|
||||
if !ok {
|
||||
writeError(w, http.StatusForbidden, "missing tenant")
|
||||
return
|
||||
}
|
||||
|
||||
q := r.URL.Query()
|
||||
page, _ := strconv.Atoi(q.Get("page"))
|
||||
limit, _ := strconv.Atoi(q.Get("limit"))
|
||||
|
||||
filter := services.AuditFilter{
|
||||
EntityType: q.Get("entity_type"),
|
||||
From: q.Get("from"),
|
||||
To: q.Get("to"),
|
||||
Page: page,
|
||||
Limit: limit,
|
||||
}
|
||||
|
||||
if idStr := q.Get("entity_id"); idStr != "" {
|
||||
if id, err := uuid.Parse(idStr); err == nil {
|
||||
filter.EntityID = &id
|
||||
}
|
||||
}
|
||||
if idStr := q.Get("user_id"); idStr != "" {
|
||||
if id, err := uuid.Parse(idStr); err == nil {
|
||||
filter.UserID = &id
|
||||
}
|
||||
}
|
||||
|
||||
entries, total, err := h.svc.List(r.Context(), tenantID, filter)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to fetch audit log")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"entries": entries,
|
||||
"total": total,
|
||||
"page": filter.Page,
|
||||
"limit": filter.Limit,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user