// Package dns provides CRUD against dns_zones, dns_records and the // single-row dns_settings table. Renderer in internal/unbound consumes // the same data to emit /etc/edgeguard/unbound/edgeguard.conf. package dns import ( "context" "errors" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "git.netcell-it.de/projekte/edgeguard-native/internal/models" ) var ( ErrZoneNotFound = errors.New("dns zone not found") ErrRecordNotFound = errors.New("dns record not found") ) type Repo struct { Pool *pgxpool.Pool } func New(pool *pgxpool.Pool) *Repo { return &Repo{Pool: pool} } // ── Zones ────────────────────────────────────────────────────── const zoneSelect = ` SELECT id, name, zone_type, description, managed_by, forward_to, active, created_at, updated_at FROM dns_zones ` func (r *Repo) ListZones(ctx context.Context) ([]models.DNSZone, error) { rows, err := r.Pool.Query(ctx, zoneSelect+" ORDER BY name") if err != nil { return nil, err } defer rows.Close() out := make([]models.DNSZone, 0, 8) for rows.Next() { z, err := scanZone(rows) if err != nil { return nil, err } out = append(out, *z) } return out, rows.Err() } func (r *Repo) GetZone(ctx context.Context, id int64) (*models.DNSZone, error) { row := r.Pool.QueryRow(ctx, zoneSelect+" WHERE id = $1", id) z, err := scanZone(row) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, ErrZoneNotFound } return nil, err } return z, nil } func (r *Repo) CreateZone(ctx context.Context, z models.DNSZone) (*models.DNSZone, error) { if z.ManagedBy == "" { z.ManagedBy = "user" } row := r.Pool.QueryRow(ctx, ` INSERT INTO dns_zones (name, zone_type, description, managed_by, forward_to, active) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, name, zone_type, description, managed_by, forward_to, active, created_at, updated_at`, z.Name, z.ZoneType, z.Description, z.ManagedBy, z.ForwardTo, z.Active) return scanZone(row) } func (r *Repo) UpdateZone(ctx context.Context, id int64, z models.DNSZone) (*models.DNSZone, error) { row := r.Pool.QueryRow(ctx, ` UPDATE dns_zones SET name=$1, zone_type=$2, description=$3, forward_to=$4, active=$5, updated_at=NOW() WHERE id=$6 RETURNING id, name, zone_type, description, managed_by, forward_to, active, created_at, updated_at`, z.Name, z.ZoneType, z.Description, z.ForwardTo, z.Active, id) out, err := scanZone(row) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, ErrZoneNotFound } return nil, err } return out, nil } func (r *Repo) DeleteZone(ctx context.Context, id int64) error { tag, err := r.Pool.Exec(ctx, `DELETE FROM dns_zones WHERE id=$1`, id) if err != nil { return err } if tag.RowsAffected() == 0 { return ErrZoneNotFound } return nil } // ── Records ──────────────────────────────────────────────────── const recordSelect = ` SELECT id, zone_id, name, record_type, value, ttl, active, created_at, updated_at FROM dns_records ` func (r *Repo) ListRecordsForZone(ctx context.Context, zoneID int64) ([]models.DNSRecord, error) { rows, err := r.Pool.Query(ctx, recordSelect+" WHERE zone_id=$1 ORDER BY name, record_type", zoneID) if err != nil { return nil, err } defer rows.Close() out := make([]models.DNSRecord, 0, 8) for rows.Next() { rec, err := scanRecord(rows) if err != nil { return nil, err } out = append(out, *rec) } return out, rows.Err() } func (r *Repo) ListAllRecords(ctx context.Context) ([]models.DNSRecord, error) { rows, err := r.Pool.Query(ctx, recordSelect+" ORDER BY zone_id, name") if err != nil { return nil, err } defer rows.Close() out := make([]models.DNSRecord, 0, 16) for rows.Next() { rec, err := scanRecord(rows) if err != nil { return nil, err } out = append(out, *rec) } return out, rows.Err() } func (r *Repo) GetRecord(ctx context.Context, id int64) (*models.DNSRecord, error) { row := r.Pool.QueryRow(ctx, recordSelect+" WHERE id=$1", id) rec, err := scanRecord(row) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, ErrRecordNotFound } return nil, err } return rec, nil } func (r *Repo) CreateRecord(ctx context.Context, rec models.DNSRecord) (*models.DNSRecord, error) { if rec.TTL == 0 { rec.TTL = 300 } row := r.Pool.QueryRow(ctx, ` INSERT INTO dns_records (zone_id, name, record_type, value, ttl, active) VALUES ($1,$2,$3,$4,$5,$6) RETURNING id, zone_id, name, record_type, value, ttl, active, created_at, updated_at`, rec.ZoneID, rec.Name, rec.RecordType, rec.Value, rec.TTL, rec.Active) return scanRecord(row) } func (r *Repo) UpdateRecord(ctx context.Context, id int64, rec models.DNSRecord) (*models.DNSRecord, error) { row := r.Pool.QueryRow(ctx, ` UPDATE dns_records SET name=$1, record_type=$2, value=$3, ttl=$4, active=$5, updated_at=NOW() WHERE id=$6 RETURNING id, zone_id, name, record_type, value, ttl, active, created_at, updated_at`, rec.Name, rec.RecordType, rec.Value, rec.TTL, rec.Active, id) out, err := scanRecord(row) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, ErrRecordNotFound } return nil, err } return out, nil } func (r *Repo) DeleteRecord(ctx context.Context, id int64) error { tag, err := r.Pool.Exec(ctx, `DELETE FROM dns_records WHERE id=$1`, id) if err != nil { return err } if tag.RowsAffected() == 0 { return ErrRecordNotFound } return nil } // ── Settings (single row, id=1) ──────────────────────────────── func (r *Repo) GetSettings(ctx context.Context) (*models.DNSSettings, error) { row := r.Pool.QueryRow(ctx, ` SELECT id, listen_addresses, listen_port, upstream_forwards, access_acl, dnssec, qname_minimisation, cache_min_ttl, cache_max_ttl, updated_at FROM dns_settings WHERE id=1`) var s models.DNSSettings if err := row.Scan(&s.ID, &s.ListenAddresses, &s.ListenPort, &s.UpstreamForwards, &s.AccessACL, &s.DNSSEC, &s.QNameMinimisation, &s.CacheMinTTL, &s.CacheMaxTTL, &s.UpdatedAt); err != nil { return nil, err } return &s, nil } func (r *Repo) UpdateSettings(ctx context.Context, s models.DNSSettings) (*models.DNSSettings, error) { row := r.Pool.QueryRow(ctx, ` UPDATE dns_settings SET listen_addresses=$1, listen_port=$2, upstream_forwards=$3, access_acl=$4, dnssec=$5, qname_minimisation=$6, cache_min_ttl=$7, cache_max_ttl=$8, updated_at=NOW() WHERE id=1 RETURNING id, listen_addresses, listen_port, upstream_forwards, access_acl, dnssec, qname_minimisation, cache_min_ttl, cache_max_ttl, updated_at`, s.ListenAddresses, s.ListenPort, s.UpstreamForwards, s.AccessACL, s.DNSSEC, s.QNameMinimisation, s.CacheMinTTL, s.CacheMaxTTL) var out models.DNSSettings if err := row.Scan(&out.ID, &out.ListenAddresses, &out.ListenPort, &out.UpstreamForwards, &out.AccessACL, &out.DNSSEC, &out.QNameMinimisation, &out.CacheMinTTL, &out.CacheMaxTTL, &out.UpdatedAt); err != nil { return nil, err } return &out, nil } // ── scan helpers ─────────────────────────────────────────────── func scanZone(row interface{ Scan(...any) error }) (*models.DNSZone, error) { var z models.DNSZone if err := row.Scan( &z.ID, &z.Name, &z.ZoneType, &z.Description, &z.ManagedBy, &z.ForwardTo, &z.Active, &z.CreatedAt, &z.UpdatedAt, ); err != nil { return nil, err } return &z, nil } func scanRecord(row interface{ Scan(...any) error }) (*models.DNSRecord, error) { var rec models.DNSRecord if err := row.Scan( &rec.ID, &rec.ZoneID, &rec.Name, &rec.RecordType, &rec.Value, &rec.TTL, &rec.Active, &rec.CreatedAt, &rec.UpdatedAt, ); err != nil { return nil, err } return &rec, nil }