// Package waf implements CRUD for per-domain WAF policies (waf_configs). package waf import ( "context" "errors" "time" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "git.netcell-it.de/projekte/edgeguard-native/internal/models" ) var ErrNotFound = errors.New("waf config not found") type Repo struct { Pool *pgxpool.Pool } func New(pool *pgxpool.Pool) *Repo { return &Repo{Pool: pool} } const baseSelect = ` SELECT id, domain_id, enabled, mode, paranoia_level, rule_exclusions, trusted_proxies, custom_rules, updated_at FROM waf_configs ` func scan(row pgx.Row) (*models.WafConfig, error) { var c models.WafConfig err := row.Scan( &c.ID, &c.DomainID, &c.Enabled, &c.Mode, &c.ParanoiaLevel, &c.RuleExclusions, &c.TrustedProxies, &c.CustomRules, &c.UpdatedAt, ) if err != nil { return nil, err } return &c, nil } // List returns all WAF configs ordered by domain_id. func (r *Repo) List(ctx context.Context) ([]models.WafConfig, error) { rows, err := r.Pool.Query(ctx, baseSelect+" ORDER BY domain_id ASC") if err != nil { return nil, err } defer rows.Close() out := make([]models.WafConfig, 0, 16) for rows.Next() { c, err := scan(rows) if err != nil { return nil, err } out = append(out, *c) } return out, rows.Err() } // GetByDomain returns the WAF config for a domain, or ErrNotFound. func (r *Repo) GetByDomain(ctx context.Context, domainID int64) (*models.WafConfig, error) { row := r.Pool.QueryRow(ctx, baseSelect+" WHERE domain_id = $1", domainID) c, err := scan(row) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, ErrNotFound } return nil, err } return c, nil } // Upsert inserts or updates the WAF config for a domain. // Returns the resulting row. func (r *Repo) Upsert(ctx context.Context, c models.WafConfig) (*models.WafConfig, error) { c.UpdatedAt = time.Now() row := r.Pool.QueryRow(ctx, ` INSERT INTO waf_configs (domain_id, enabled, mode, paranoia_level, rule_exclusions, trusted_proxies, custom_rules, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT (domain_id) DO UPDATE SET enabled = EXCLUDED.enabled, mode = EXCLUDED.mode, paranoia_level = EXCLUDED.paranoia_level, rule_exclusions = EXCLUDED.rule_exclusions, trusted_proxies = EXCLUDED.trusted_proxies, custom_rules = EXCLUDED.custom_rules, updated_at = EXCLUDED.updated_at RETURNING id, domain_id, enabled, mode, paranoia_level, rule_exclusions, trusted_proxies, custom_rules, updated_at `, c.DomainID, c.Enabled, c.Mode, c.ParanoiaLevel, c.RuleExclusions, c.TrustedProxies, c.CustomRules, c.UpdatedAt, ) return scan(row) } // ListEnabled returns only configs with enabled=true (used by the WAF agent). func (r *Repo) ListEnabled(ctx context.Context) ([]models.WafConfig, error) { rows, err := r.Pool.Query(ctx, baseSelect+" WHERE enabled = true ORDER BY domain_id ASC") if err != nil { return nil, err } defer rows.Close() out := make([]models.WafConfig, 0, 8) for rows.Next() { c, err := scan(rows) if err != nil { return nil, err } out = append(out, *c) } return out, rows.Err() }