package repository import ( "context" "encoding/json" "fmt" "github.com/jackc/pgx/v5/pgxpool" "github.com/geo-platform/tenant-api/internal/ops/domain" ) type AuditRepository struct { pool *pgxpool.Pool } func NewAuditRepository(pool *pgxpool.Pool) *AuditRepository { return &AuditRepository{pool: pool} } func (r *AuditRepository) Append(ctx context.Context, e domain.AuditEvent) error { var metaBytes []byte if e.Metadata != nil { b, err := json.Marshal(e.Metadata) if err != nil { return fmt.Errorf("marshal audit metadata: %w", err) } metaBytes = b } var ( operatorName = nullableString(e.OperatorName) targetType = nullableString(e.TargetType) targetID = nullableString(e.TargetID) ip = nullableString(e.IP) ipRegion = nullableString(e.IPRegion) ua = nullableString(e.UserAgent) reqID = nullableString(e.RequestID) ) _, err := r.pool.Exec(ctx, ` INSERT INTO ops.audit_logs (operator_id, operator_name, action, target_type, target_id, metadata, ip, ip_region, user_agent, request_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`, e.OperatorID, operatorName, e.Action, targetType, targetID, metaBytes, ip, ipRegion, ua, reqID) return err } func (r *AuditRepository) List(ctx context.Context, f domain.AuditLogFilter) ([]domain.AuditLog, int64, error) { args := []any{} where := "1=1" if f.OperatorID != nil { args = append(args, *f.OperatorID) where += fmt.Sprintf(" AND operator_id = $%d", len(args)) } if f.Action != "" { args = append(args, f.Action) where += fmt.Sprintf(" AND action = $%d", len(args)) } if f.StartAt != nil { args = append(args, *f.StartAt) where += fmt.Sprintf(" AND created_at >= $%d", len(args)) } if f.EndAt != nil { args = append(args, *f.EndAt) where += fmt.Sprintf(" AND created_at < $%d", len(args)) } var total int64 if err := r.pool.QueryRow(ctx, "SELECT COUNT(*) FROM ops.audit_logs WHERE "+where, args...).Scan(&total); err != nil { return nil, 0, err } limit := f.Limit if limit <= 0 || limit > 500 { limit = 50 } offset := f.Offset if offset < 0 { offset = 0 } args = append(args, limit, offset) limitArg := len(args) - 1 offsetArg := len(args) rows, err := r.pool.Query(ctx, fmt.Sprintf(` SELECT id, operator_id, operator_name, action, target_type, target_id, metadata, ip, ip_region, user_agent, request_id, created_at FROM ops.audit_logs WHERE %s ORDER BY created_at DESC, id DESC LIMIT $%d OFFSET $%d`, where, limitArg, offsetArg), args...) if err != nil { return nil, 0, err } defer rows.Close() out := make([]domain.AuditLog, 0, limit) for rows.Next() { var ( log domain.AuditLog metaRaw []byte ) if err := rows.Scan( &log.ID, &log.OperatorID, &log.OperatorName, &log.Action, &log.TargetType, &log.TargetID, &metaRaw, &log.IP, &log.IPRegion, &log.UserAgent, &log.RequestID, &log.CreatedAt, ); err != nil { return nil, 0, err } if len(metaRaw) > 0 { if err := json.Unmarshal(metaRaw, &log.Metadata); err != nil { return nil, 0, fmt.Errorf("decode audit metadata: %w", err) } } out = append(out, log) } return out, total, rows.Err() } func nullableString(s string) *string { if s == "" { return nil } return &s }