package gateway import ( "context" "net/http" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/auth" ) // auditClaims records an admin write with the caller identity read from the // request context (populated by auth.Authorize). It is the who/what/when/result // audit trail required for admin operations; the "主代理代审" user-facing note is // the frontend's job, not the gateway's. func (s *Server) auditClaims(r *http.Request, what, result string) { who := "anonymous" if claims, ok := auth.ClaimsFromContext(r.Context()); ok { who = claims.Sub + ":" + string(claims.Role) } s.audit(r.Context(), who, what, result) } // AuditEvent records an external (non-HTTP) audit event with an explicit caller // identity, so programmatic sinks outside the request path (for example the // channel bridge's blacklist rejection hook, design/16 §4.6 §6) can append to the // same who/what/when/result trail. who is a free-form actor label ("channel" or // "bridge:telegram"); what is the operation and result its outcome. It is safe for // concurrent use. func (s *Server) AuditEvent(who, what, result string) { s.audit(context.Background(), who, what, result) } // audit emits one structured audit record on the gateway logger and persists it to // the configured AuditStore (falling back to an in-memory store when none is // injected). A persist failure is logged but never blocks the admin write, so a // broken audit store cannot take down the audit trail's primary job. func (s *Server) audit(ctx context.Context, who, what, result string) { s.log.Info("admin audit", "who", who, "what", what, "when", time.Now().UTC().Format(time.RFC3339), "result", result, ) if s.obs != nil && s.obs.audit != nil { if err := s.obs.audit.Append(ctx, who, what, result, time.Now()); err != nil { s.log.Error("admin audit persist", "err", err, "what", what) } } }