Files
llmgw/internal/store/audit.go
T

73 lines
2.1 KiB
Go

package store
import (
"context"
"database/sql"
"strings"
"time"
)
// AuditEvent is one change: who made it, through what, to what, and how it
// ended. Detail never holds a secret; the writer takes them out.
type AuditEvent struct {
ID int64 `json:"id"`
At time.Time `json:"at"`
Actor string `json:"actor"`
Source string `json:"source"` // api, web
Action string `json:"action"` // "POST /api/v1/models", "POST /admin/users/{id}/suspend"
Target string `json:"target,omitempty"`
Detail string `json:"detail,omitempty"`
Status int `json:"status"`
}
// WriteAudit records one change.
func (s *DB) WriteAudit(ctx context.Context, e AuditEvent) error {
if e.At.IsZero() {
e.At = time.Now()
}
_, err := s.db.ExecContext(ctx, `INSERT INTO audit_events (ts, actor, source, action, target, detail, status)
VALUES (?, ?, ?, ?, ?, ?, ?)`, millis(e.At), e.Actor, e.Source, e.Action, e.Target, e.Detail, e.Status)
return err
}
// AuditFilter narrows a listing; empty fields match everything.
type AuditFilter struct {
Since, Until time.Time
Actor string
// Contains matches the action or the target.
Contains string
Limit int
}
// AuditEvents lists changes, the newest first.
func (s *DB) AuditEvents(ctx context.Context, f AuditFilter) ([]AuditEvent, error) {
q := `SELECT id, ts, actor, source, action, target, detail, status FROM audit_events WHERE ts >= ?`
args := []any{millis(f.Since)}
if !f.Until.IsZero() {
q += ` AND ts < ?`
args = append(args, millis(f.Until))
}
if f.Actor != "" {
q += ` AND actor = ?`
args = append(args, f.Actor)
}
if f.Contains != "" {
q += ` AND (LOWER(action) LIKE ? OR LOWER(target) LIKE ?)`
like := "%" + strings.ToLower(f.Contains) + "%"
args = append(args, like, like)
}
limit := f.Limit
if limit <= 0 || limit > 10000 {
limit = 200
}
q += ` ORDER BY ts DESC, id DESC LIMIT ?`
args = append(args, limit)
return queryAll(ctx, s.db, q, args, func(rows *sql.Rows) (AuditEvent, error) {
var e AuditEvent
var ts int64
err := rows.Scan(&e.ID, &ts, &e.Actor, &e.Source, &e.Action, &e.Target, &e.Detail, &e.Status)
e.At = fromMillis(ts)
return e, err
})
}