You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
// reader.go 提供审计日志的只读查询路径(spec §11 步骤4)。
|
||||
//
|
||||
// audit.Recorder 是 write-only 门面;Reader 是与之对称的 read 门面,供 admin
|
||||
// 审计查看器(GET /api/v1/admin/audit-logs)与保留期清理 runner 使用。pgReader
|
||||
// 复用 auditsqlc 生成的 List/Count/Delete 查询,并在 pgtype <-> 领域类型间转换。
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
auditsqlc "github.com/yan1h/agent-coding-workflow/internal/audit/sqlc"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
|
||||
)
|
||||
|
||||
// LogEntry 是一条已落库审计记录的只读领域视图。区别于写入用的 Entry:含 ID/
|
||||
// 时间戳,IP 以字符串形式返回。
|
||||
type LogEntry struct {
|
||||
ID int64
|
||||
UserID *uuid.UUID
|
||||
Action string
|
||||
TargetType string
|
||||
TargetID string
|
||||
IP string
|
||||
Metadata map[string]any
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// AuditFilter 是 List/Count 的可选过滤条件。零值字段表示不过滤。Limit<=0 时由
|
||||
// Reader 兜底为默认页大小;Limit 上限由 Reader 钳制。
|
||||
type AuditFilter struct {
|
||||
UserID *uuid.UUID
|
||||
Action string // 精确匹配
|
||||
ActionPrefix string // 前缀匹配(搜索用)
|
||||
TargetType string
|
||||
TargetID string
|
||||
From *time.Time
|
||||
To *time.Time
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
const (
|
||||
// DefaultAuditPageSize 是未指定 Limit 时的默认页大小。
|
||||
DefaultAuditPageSize = 50
|
||||
// MaxAuditPageSize 是单页返回的硬上限,避免一次拉全表。
|
||||
MaxAuditPageSize = 500
|
||||
)
|
||||
|
||||
// Reader 是审计读取的对外契约:分页列表 + 总数 + 保留期清理。
|
||||
type Reader interface {
|
||||
List(ctx context.Context, f AuditFilter) ([]LogEntry, int, error)
|
||||
PurgeBefore(ctx context.Context, before time.Time) (int64, error)
|
||||
}
|
||||
|
||||
type pgReader struct {
|
||||
q *auditsqlc.Queries
|
||||
}
|
||||
|
||||
// NewPostgresReader 用现有 pgxpool 构造 Reader 实现。
|
||||
func NewPostgresReader(pool *pgxpool.Pool) Reader {
|
||||
return &pgReader{q: auditsqlc.New(pool)}
|
||||
}
|
||||
|
||||
// List 返回符合过滤条件的审计记录页 + 满足条件的总数(用于分页)。
|
||||
func (r *pgReader) List(ctx context.Context, f AuditFilter) ([]LogEntry, int, error) {
|
||||
limit := f.Limit
|
||||
if limit <= 0 {
|
||||
limit = DefaultAuditPageSize
|
||||
}
|
||||
if limit > MaxAuditPageSize {
|
||||
limit = MaxAuditPageSize
|
||||
}
|
||||
offset := f.Offset
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
rows, err := r.q.ListAuditLog(ctx, auditsqlc.ListAuditLogParams{
|
||||
Limit: int32(limit),
|
||||
Offset: int32(offset),
|
||||
UserID: pgUUIDFromPtr(f.UserID),
|
||||
Action: ptr(f.Action),
|
||||
ActionPrefix: ptr(f.ActionPrefix),
|
||||
TargetType: ptr(f.TargetType),
|
||||
TargetID: ptr(f.TargetID),
|
||||
FromTs: tsFromPtr(f.From),
|
||||
ToTs: tsFromPtr(f.To),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, errs.Wrap(err, errs.CodeInternal, "list audit logs")
|
||||
}
|
||||
|
||||
total, err := r.q.CountAuditLog(ctx, auditsqlc.CountAuditLogParams{
|
||||
UserID: pgUUIDFromPtr(f.UserID),
|
||||
Action: ptr(f.Action),
|
||||
ActionPrefix: ptr(f.ActionPrefix),
|
||||
TargetType: ptr(f.TargetType),
|
||||
TargetID: ptr(f.TargetID),
|
||||
FromTs: tsFromPtr(f.From),
|
||||
ToTs: tsFromPtr(f.To),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, errs.Wrap(err, errs.CodeInternal, "count audit logs")
|
||||
}
|
||||
|
||||
out := make([]LogEntry, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, rowToEntry(row))
|
||||
}
|
||||
return out, int(total), nil
|
||||
}
|
||||
|
||||
// PurgeBefore 删除 created_at < before 的全部审计记录,返回删除行数。保留期 runner 调用。
|
||||
func (r *pgReader) PurgeBefore(ctx context.Context, before time.Time) (int64, error) {
|
||||
n, err := r.q.DeleteAuditLogsBefore(ctx, toPgTimestamptz(before))
|
||||
if err != nil {
|
||||
return 0, errs.Wrap(err, errs.CodeInternal, "purge audit logs")
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func rowToEntry(row auditsqlc.AuditLog) LogEntry {
|
||||
e := LogEntry{
|
||||
ID: row.ID,
|
||||
Action: row.Action,
|
||||
CreatedAt: row.CreatedAt.Time,
|
||||
}
|
||||
if row.UserID.Valid {
|
||||
u := uuid.UUID(row.UserID.Bytes)
|
||||
e.UserID = &u
|
||||
}
|
||||
if row.TargetType != nil {
|
||||
e.TargetType = *row.TargetType
|
||||
}
|
||||
if row.TargetID != nil {
|
||||
e.TargetID = *row.TargetID
|
||||
}
|
||||
if row.Ip != nil {
|
||||
e.IP = row.Ip.String()
|
||||
}
|
||||
if len(row.Metadata) > 0 {
|
||||
_ = json.Unmarshal(row.Metadata, &e.Metadata)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// tsFromPtr 把 *time.Time 转为 pgtype.Timestamptz;nil 映射为 NULL(不过滤)。
|
||||
func tsFromPtr(t *time.Time) pgtype.Timestamptz {
|
||||
if t == nil {
|
||||
return pgtype.Timestamptz{Valid: false}
|
||||
}
|
||||
return pgtype.Timestamptz{Time: *t, Valid: true}
|
||||
}
|
||||
|
||||
// toPgTimestamptz 把非空 time.Time 转为有效 Timestamptz。
|
||||
func toPgTimestamptz(t time.Time) pgtype.Timestamptz {
|
||||
return pgtype.Timestamptz{Time: t, Valid: true}
|
||||
}
|
||||
Reference in New Issue
Block a user