package httpapi import ( "context" "log" "net" "net/http" "strings" "time" "evobgp/internal/audit" "evobgp/internal/store" ) func (s *Server) registerAuditRoutes(m *http.ServeMux) { m.HandleFunc("GET /audit", s.handleListAudit) } func (s *Server) handleListAudit(w http.ResponseWriter, r *http.Request) { a, ok := authFromContext(r.Context()) if !ok || !s.requirePerm(w, a, "bgp:monitoring:read") { return } cursor := r.URL.Query().Get("cursor") limit := parseLimitQuery(r, 20, 200) filter := store.AuditListFilter{ Action: strings.TrimSpace(r.URL.Query().Get("action")), Severity: strings.TrimSpace(r.URL.Query().Get("severity")), } if filter.Severity != "" && !store.ValidAuditSeverity(filter.Severity) { writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid severity") return } items, next, hasMore, err := s.store.ListAudit(a.TenantID, cursor, limit, filter) if err != nil { writeInternalError(w, "audit_list", err) return } out := make([]map[string]any, 0, len(items)) for _, row := range items { out = append(out, auditEntryJSON(row)) } writeJSON(w, http.StatusOK, map[string]any{"items": out, "next_cursor": next, "has_more": hasMore}) } func auditEntryJSON(row *store.AuditEntry) map[string]any { if row == nil { return map[string]any{} } m := map[string]any{ "id": row.ID, "tenant_id": row.TenantID, "event_id": row.EventID, "source_app": row.SourceApp, "action": row.Action, "severity": row.Severity, "actor_user_id": strPtrOrNull(row.ActorUserID), "actor_email": strPtrOrNull(row.ActorEmail), "actor_name": strPtrOrNull(row.ActorName), "actor_api_key_prefix": strPtrOrNull(row.ActorAPIKeyPrefix), "target_type": strPtrOrNull(row.TargetType), "target_id": strPtrOrNull(row.TargetID), "summary": row.Summary, "details": row.Details, "ip": strPtrOrNull(row.IP), "created_at": row.CreatedAt.UTC().Format(time.RFC3339Nano), "portal_pushed_at": nil, } if row.PortalPushedAt != nil { m["portal_pushed_at"] = row.PortalPushedAt.UTC().Format(time.RFC3339Nano) } if m["details"] == nil { m["details"] = nil } return m } func (s *Server) recordCRUDAudit(r *http.Request, a Auth, action, summary, targetID string, details map[string]any) { if s == nil || s.store == nil { return } in := store.AuditAppendInput{ TenantID: a.TenantID, Action: action, Severity: store.AuditSeverityInfo, TargetType: store.AuditTargetAppResource, TargetID: targetID, Summary: summary, Details: details, IP: clientIP(r), } fillAuditActor(&in, a) entry, err := s.store.AppendAudit(in) if err != nil { log.Printf("httpapi: audit append action=%s: %v", action, err) return } s.pushAuditToPortal(entry) } func fillAuditActor(in *store.AuditAppendInput, a Auth) { if in == nil { return } if a.Kind == AuthKindJWT { in.ActorUserID = strings.TrimSpace(a.UserID) in.ActorEmail = strings.TrimSpace(a.Email) if in.ActorEmail != "" { in.ActorName = in.ActorEmail } return } prefix := actorPrefix(a) in.ActorAPIKeyPrefix = prefix if prefix != "" { in.ActorName = "apikey:" + prefix } } func (s *Server) pushAuditToPortal(entry *store.AuditEntry) { if s == nil || s.auditPusher == nil || entry == nil { return } pusher := s.auditPusher go func() { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() if err := pusher.PushEvent(ctx, entry); err != nil { log.Printf("httpapi: audit portal push event_id=%s: %v", entry.EventID, err) } }() } func clientIP(r *http.Request) string { if r == nil { return "" } if xff := strings.TrimSpace(r.Header.Get("X-Forwarded-For")); xff != "" { parts := strings.Split(xff, ",") if len(parts) > 0 { return strings.TrimSpace(parts[0]) } } if xrip := strings.TrimSpace(r.Header.Get("X-Real-IP")); xrip != "" { return xrip } host, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)) if err != nil { return strings.TrimSpace(r.RemoteAddr) } return host } // initAuditPusher wires portal push when URL and secret are configured. func (s *Server) initAuditPusher(portalURL, ingestSecret string) { base := strings.TrimSpace(portalURL) secret := strings.TrimSpace(ingestSecret) if base == "" || secret == "" { return } s.auditPusher = &audit.PortalPusher{ BaseURL: base, Secret: secret, MarkPushed: func(id string) error { if s.store == nil { return nil } return s.store.MarkAuditPortalPushed(id) }, } }