package adminapi import ( "context" "net/http" "strconv" "time" "gitea.mynodes.xyz/ilker/llmgw/internal/store" ) // AuditReader lists recorded changes. type AuditReader interface { AuditEvents(ctx context.Context, f store.AuditFilter) ([]store.AuditEvent, error) } // SetAudit enables GET /api/v1/audit. func (a *API) SetAudit(r AuditReader) { a.audit = r } // audit answers GET /api/v1/audit?since=24h&actor=&contains=&limit=. func (a *API) auditEvents(w http.ResponseWriter, r *http.Request) { if a.audit == nil { writeError(w, http.StatusNotImplemented, "this control plane keeps no audit log: it has no store") return } q := r.URL.Query() since := 7 * 24 * time.Hour if s := q.Get("since"); s != "" { d, err := parseSince(s) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } since = d } limit, _ := strconv.Atoi(q.Get("limit")) ev, err := a.audit.AuditEvents(r.Context(), store.AuditFilter{ Since: time.Now().Add(-since), Actor: q.Get("actor"), Contains: q.Get("contains"), Limit: limit, }) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if ev == nil { ev = []store.AuditEvent{} } writeJSON(w, http.StatusOK, ev) } // parseSince reads 90m, 24h, 7d. func parseSince(s string) (time.Duration, error) { if n, ok := cutSuffix(s, "d"); ok { days, err := strconv.Atoi(n) if err != nil || days < 0 { return 0, errBadSince(s) } return time.Duration(days) * 24 * time.Hour, nil } d, err := time.ParseDuration(s) if err != nil || d < 0 { return 0, errBadSince(s) } return d, nil } type errBadSince string func (e errBadSince) Error() string { return "since " + string(e) + " is not a duration like 90m, 24h or 7d" } func cutSuffix(s, suf string) (string, bool) { if len(s) > len(suf) && s[len(s)-len(suf):] == suf { return s[:len(s)-len(suf)], true } return "", false }