Files
llmgw/internal/control/adminapi/audit.go
T

78 lines
1.9 KiB
Go

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
}