Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
73 lines
2.1 KiB
Go
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
|
|
})
|
|
}
|