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 }) }