package storage import ( "context" "database/sql" "fmt" "strings" "time" ) func (s *Store) AppendAudit(ctx context.Context, input AppendAuditInput) (AuditEvent, error) { const operation = "append audit event" if err := s.checkOpen(operation); err != nil { return AuditEvent{}, err } return appendAudit(ctx, s.sql, operation, input, canonicalTime(s.now())) } func appendAudit(ctx context.Context, q execer, operation string, input AppendAuditInput, occurredAt time.Time) (AuditEvent, error) { if err := requireValue("actor", input.Actor); err != nil { return AuditEvent{}, invalid(operation, err) } if err := requireValue("action", input.Action); err != nil { return AuditEvent{}, invalid(operation, err) } if err := requireValue("subject", input.Subject); err != nil { return AuditEvent{}, invalid(operation, err) } address := auditAddress(ctx, input.Address) detail := input.Detail if detail == "" { detail = "{}" } result, err := q.ExecContext(ctx, ` INSERT INTO audit_log (actor, action, subject, occurred_at, detail, address) VALUES (?, ?, ?, ?, ?, ?)`, input.Actor, input.Action, input.Subject, timeToInt(occurredAt), detail, nullAddress(address)) if err != nil { return AuditEvent{}, classify(operation, err) } id, err := result.LastInsertId() if err != nil { return AuditEvent{}, classify(operation+" id", err) } return AuditEvent{ID: id, Actor: input.Actor, Action: input.Action, Subject: input.Subject, OccurredAt: occurredAt, Detail: detail, Address: address}, nil } func (s *Store) ListAudit(ctx context.Context, filter AuditFilter) ([]AuditEvent, error) { const operation = "list audit events" if err := s.checkOpen(operation); err != nil { return nil, err } limit := filter.Limit if limit == 0 { limit = 100 } if limit < 1 || limit > 1000 { return nil, invalid(operation, fmt.Errorf("limit must be between 1 and 1000")) } query := strings.Builder{} query.WriteString(`SELECT id, actor, action, subject, occurred_at, detail, address FROM audit_log WHERE 1 = 1`) var args []any if filter.Actor != "" { query.WriteString(` AND actor = ?`) args = append(args, filter.Actor) } if filter.Action != "" { query.WriteString(` AND action = ?`) args = append(args, filter.Action) } if filter.Subject != "" { query.WriteString(` AND subject = ?`) args = append(args, filter.Subject) } if filter.After != nil { query.WriteString(` AND occurred_at >= ?`) args = append(args, timeToInt(*filter.After)) } if filter.Before != nil { query.WriteString(` AND occurred_at < ?`) args = append(args, timeToInt(*filter.Before)) } query.WriteString(` ORDER BY occurred_at DESC, id DESC LIMIT ?`) args = append(args, limit) rows, err := s.sql.QueryContext(ctx, query.String(), args...) if err != nil { return nil, classify(operation, err) } defer rows.Close() var events []AuditEvent for rows.Next() { var event AuditEvent var occurredAt int64 var address sql.NullString if err := rows.Scan(&event.ID, &event.Actor, &event.Action, &event.Subject, &occurredAt, &event.Detail, &address); err != nil { return nil, classify(operation, err) } event.OccurredAt = intToTime(occurredAt) event.Address = scanAddress(address) events = append(events, event) } if err := rows.Err(); err != nil { return nil, classify(operation, err) } return events, nil }