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:
@@ -9,8 +9,10 @@ import (
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
userIDKey contextKey = "user_id"
|
||||
tenantIDKey contextKey = "tenant_id"
|
||||
userIDKey contextKey = "user_id"
|
||||
tenantIDKey contextKey = "tenant_id"
|
||||
ipKey contextKey = "ip_address"
|
||||
userAgentKey contextKey = "user_agent"
|
||||
)
|
||||
|
||||
func ContextWithUserID(ctx context.Context, userID uuid.UUID) context.Context {
|
||||
@@ -30,3 +32,23 @@ func TenantFromContext(ctx context.Context) (uuid.UUID, bool) {
|
||||
id, ok := ctx.Value(tenantIDKey).(uuid.UUID)
|
||||
return id, ok
|
||||
}
|
||||
|
||||
func ContextWithRequestInfo(ctx context.Context, ip, userAgent string) context.Context {
|
||||
ctx = context.WithValue(ctx, ipKey, ip)
|
||||
ctx = context.WithValue(ctx, userAgentKey, userAgent)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func IPFromContext(ctx context.Context) *string {
|
||||
if v, ok := ctx.Value(ipKey).(string); ok && v != "" {
|
||||
return &v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func UserAgentFromContext(ctx context.Context) *string {
|
||||
if v, ok := ctx.Value(userAgentKey).(string); ok && v != "" {
|
||||
return &v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -46,6 +46,13 @@ func (m *Middleware) RequireAuth(next http.Handler) http.Handler {
|
||||
}
|
||||
ctx = ContextWithTenantID(ctx, tenantID)
|
||||
|
||||
// Capture IP and user-agent for audit logging
|
||||
ip := r.Header.Get("X-Forwarded-For")
|
||||
if ip == "" {
|
||||
ip = r.RemoteAddr
|
||||
}
|
||||
ctx = ContextWithRequestInfo(ctx, ip, r.UserAgent())
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user