// Package audit 提供登录、登出、关键管理动作等敏感事件的审计记录器。 // Recorder 接口与具体存储解耦,user 模块只依赖接口;NewPostgresRecorder // 是默认实现,把每条 Entry 转成 audit_logs 行写入。 // // IP 解析:调用方通常直接传 r.RemoteAddr(host:port 形式),Record 内部 // 用 net.SplitHostPort + netip.ParseAddr 拆解。解析失败时不阻断主流程, // 只把 ip 列写为 NULL,避免审计写失败影响业务。 package audit import ( "context" "encoding/json" "net" "net/netip" "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" ) // Recorder 是审计记录的对外契约。Record 失败时返回 errs.AppError, // 调用方决定是否吞错(user 模块通常吞错并仅记日志)。 type Recorder interface { Record(ctx context.Context, e Entry) error } // Entry 描述一条待写入的审计事件。UserID 为 nil 表示无登录上下文(如系统 // 维护任务);Action 为 dot.notation 字符串("user.login" / "user.logout" // 等),TargetType/TargetID 描述事件指向的对象。 type Entry struct { UserID *uuid.UUID Action string TargetType string TargetID string IP string Metadata map[string]any } type pgRecorder struct { q *auditsqlc.Queries } // NewPostgresRecorder 用现有 pgxpool 构造 Recorder 实现。 func NewPostgresRecorder(pool *pgxpool.Pool) Recorder { return &pgRecorder{q: auditsqlc.New(pool)} } // Record 把 Entry 落地为 audit_logs 一行。 func (r *pgRecorder) Record(ctx context.Context, e Entry) error { var meta []byte if e.Metadata != nil { b, err := json.Marshal(e.Metadata) if err != nil { return errs.Wrap(err, errs.CodeInternal, "marshal audit metadata") } meta = b } var ip *netip.Addr if e.IP != "" { host, _, err := net.SplitHostPort(e.IP) if err != nil { host = e.IP } if a, err := netip.ParseAddr(host); err == nil { ip = &a } } if err := r.q.InsertAuditLog(ctx, auditsqlc.InsertAuditLogParams{ UserID: pgUUIDFromPtr(e.UserID), Action: e.Action, TargetType: ptr(e.TargetType), TargetID: ptr(e.TargetID), Ip: ip, Metadata: meta, }); err != nil { return errs.Wrap(err, errs.CodeInternal, "insert audit") } return nil } // pgUUIDFromPtr 把可空的领域 *uuid.UUID 转为 sqlc 期望的 pgtype.UUID。 // nil 映射为 Valid=false(DB NULL),非 nil 映射为 Valid=true + Bytes。 func pgUUIDFromPtr(u *uuid.UUID) pgtype.UUID { if u == nil { return pgtype.UUID{Valid: false} } return pgtype.UUID{Bytes: *u, Valid: true} } // ptr 把空字符串映射为 nil,使数据库列写入 NULL;其它字符串原样取地址。 func ptr(s string) *string { if s == "" { return nil } return &s }