package audit import ( "context" "encoding/json" "fmt" "github.com/jackc/pgx/v5/pgxpool" ) type Entry struct { ID int64 `json:"id"` OccurredAt string `json:"occurred_at"` ActorUserID *string `json:"actor_user_id,omitempty"` Action string `json:"action"` TargetType string `json:"target_type"` TargetID *string `json:"target_id,omitempty"` Outcome string `json:"outcome"` Details map[string]any `json:"details"` } type Store struct{ DB *pgxpool.Pool } func (s Store) Record(ctx context.Context, actorUserID *string, action, targetType string, targetID *string, outcome string, details map[string]any) error { data, err := json.Marshal(details) if err != nil { return fmt.Errorf("encode audit details: %w", err) } _, err = s.DB.Exec(ctx, `INSERT INTO bell_audit_log(actor_user_id,action,target_type,target_id,outcome,details) VALUES($1,$2,$3,$4,$5,$6)`, actorUserID, action, targetType, targetID, outcome, data) if err != nil { return fmt.Errorf("append audit entry: %w", err) } return nil } func (s Store) List(ctx context.Context, limit int) ([]Entry, error) { if limit < 1 || limit > 200 { limit = 50 } rows, err := s.DB.Query(ctx, `SELECT id,occurred_at::text,actor_user_id::text,action,target_type,target_id,outcome,details FROM bell_audit_log ORDER BY id DESC LIMIT $1`, limit) if err != nil { return nil, err } defer rows.Close() entries := make([]Entry, 0, limit) for rows.Next() { var e Entry var data []byte if err := rows.Scan(&e.ID, &e.OccurredAt, &e.ActorUserID, &e.Action, &e.TargetType, &e.TargetID, &e.Outcome, &data); err != nil { return nil, err } _ = json.Unmarshal(data, &e.Details) entries = append(entries, e) } return entries, rows.Err() }