Files
edgeguard-native/internal/services/domains/domains.go
Debian f1f7df74f7 feat(crowdsec): UI-Schalter „vertrauenswürdiges Admin-Panel" → gerenderte CrowdSec-Whitelist — v1.3.19
Pro-Domain-Flag crowdsec_trusted (Migration 0048). Ist es gesetzt, rendert der
neue crowdsec-whitelist-Generator den Hostname host-genau in
/etc/crowdsec/parsers/s02-enrich/edgeguard-admin-hosts-whitelist.yaml
(evt.Parsed.http_host) und reloadet crowdsec. Löst das Problem, dass Admin-SPAs
(viele /api/-Requests pro Aktion) http-crawl-non_statics triggern und die
Admin-IP bannen — jetzt im Frontend steuerbar statt manueller Node-Datei.

- Generator internal/crowdsec/whitelist.go (configgen.Generator, no-op ohne
  CrowdSec), registriert in edgeguard-ctl render-config + in den Domains-Reloader
  komponiert (Domain-Edit → Whitelist re-render). Aus der replizierten DB
  gerendert → überlebt Node-Neuaufbau (Ersatz für die manuelle Node-Datei).
- postinst: sudoers reload crowdsec + edgeguard-owned Whitelist-Datei anlegen
  (dir root-owned → Generator überschreibt nur die vorab-chownte Datei) +
  Initial-Render.
- UI: Switch „Vertrauenswürdiges Admin-Panel" im Domain-Detail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 22:23:30 +02:00

153 lines
4.6 KiB
Go

// Package domains implements CRUD against the `domains` table.
package domains
import (
"context"
"errors"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
)
var ErrNotFound = errors.New("domain not found")
type Repo struct {
Pool *pgxpool.Pool
}
func New(pool *pgxpool.Pool) *Repo { return &Repo{Pool: pool} }
const baseSelect = `
SELECT id, name, active, primary_backend_id, http_to_https,
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
maintenance_mode, maintenance_message, www_redirect,
rate_limit_rps, max_body_kb, disable_h3,
notes, redirect_to, crowdsec_trusted, created_at, updated_at
FROM domains
`
func (r *Repo) List(ctx context.Context) ([]models.Domain, error) {
rows, err := r.Pool.Query(ctx, baseSelect+" ORDER BY name ASC")
if err != nil {
return nil, err
}
defer rows.Close()
out := make([]models.Domain, 0, 16)
for rows.Next() {
d, err := scanDomain(rows)
if err != nil {
return nil, err
}
out = append(out, *d)
}
return out, rows.Err()
}
func (r *Repo) Get(ctx context.Context, id int64) (*models.Domain, error) {
row := r.Pool.QueryRow(ctx, baseSelect+" WHERE id = $1", id)
d, err := scanDomain(row)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
return d, nil
}
func (r *Repo) Create(ctx context.Context, d models.Domain) (*models.Domain, error) {
if d.HSTSMaxAge == 0 {
d.HSTSMaxAge = 31536000
}
row := r.Pool.QueryRow(ctx, `
INSERT INTO domains (name, active, primary_backend_id, http_to_https,
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
maintenance_mode, maintenance_message, www_redirect,
rate_limit_rps, max_body_kb, disable_h3, notes, redirect_to,
crowdsec_trusted)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
RETURNING id, name, active, primary_backend_id, http_to_https,
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
maintenance_mode, maintenance_message, www_redirect,
rate_limit_rps, max_body_kb, disable_h3,
notes, redirect_to, crowdsec_trusted, created_at, updated_at`,
d.Name, d.Active, d.PrimaryBackendID, d.HTTPToHTTPS,
d.HSTSEnabled, d.HSTSMaxAge, d.HSTSSubdomains, d.HSTSPreload,
d.MaintenanceMode, d.MaintenanceMessage, d.WWWRedirect,
d.RateLimitRPS, d.MaxBodyKB, d.DisableH3, d.Notes, d.RedirectTo,
d.CrowdSecTrusted)
return scanDomain(row)
}
func (r *Repo) Update(ctx context.Context, id int64, d models.Domain) (*models.Domain, error) {
if d.HSTSMaxAge == 0 {
d.HSTSMaxAge = 31536000
}
row := r.Pool.QueryRow(ctx, `
UPDATE domains SET
name = $1,
active = $2,
primary_backend_id = $3,
http_to_https = $4,
hsts_enabled = $5,
hsts_max_age = $6,
hsts_subdomains = $7,
hsts_preload = $8,
maintenance_mode = $9,
maintenance_message = $10,
www_redirect = $11,
rate_limit_rps = $12,
max_body_kb = $13,
disable_h3 = $14,
notes = $15,
redirect_to = $16,
crowdsec_trusted = $17,
updated_at = NOW()
WHERE id = $18
RETURNING id, name, active, primary_backend_id, http_to_https,
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
maintenance_mode, maintenance_message, www_redirect,
rate_limit_rps, max_body_kb, disable_h3,
notes, redirect_to, crowdsec_trusted, created_at, updated_at`,
d.Name, d.Active, d.PrimaryBackendID, d.HTTPToHTTPS,
d.HSTSEnabled, d.HSTSMaxAge, d.HSTSSubdomains, d.HSTSPreload,
d.MaintenanceMode, d.MaintenanceMessage, d.WWWRedirect,
d.RateLimitRPS, d.MaxBodyKB, d.DisableH3, d.Notes, d.RedirectTo,
d.CrowdSecTrusted, id)
out, err := scanDomain(row)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
return out, nil
}
func (r *Repo) Delete(ctx context.Context, id int64) error {
tag, err := r.Pool.Exec(ctx, `DELETE FROM domains WHERE id = $1`, id)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return ErrNotFound
}
return nil
}
func scanDomain(row interface{ Scan(...any) error }) (*models.Domain, error) {
var d models.Domain
if err := row.Scan(
&d.ID, &d.Name, &d.Active, &d.PrimaryBackendID, &d.HTTPToHTTPS,
&d.HSTSEnabled, &d.HSTSMaxAge, &d.HSTSSubdomains, &d.HSTSPreload,
&d.MaintenanceMode, &d.MaintenanceMessage, &d.WWWRedirect,
&d.RateLimitRPS, &d.MaxBodyKB, &d.DisableH3,
&d.Notes, &d.RedirectTo, &d.CrowdSecTrusted, &d.CreatedAt, &d.UpdatedAt,
); err != nil {
return nil, err
}
return &d, nil
}