fix(routing+backends): Pfad-Präfix-Validierung + Warnung bei leerem Backend-Pool (v1.1.141)

- RoutingRules: path_prefix Pattern-Validator (muss mit / beginnen) + destroyOnHidden
- Backends: Alert-Banner wenn aktives Backend 0 Server hat (→ HAProxy 503)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-28 21:25:55 +02:00
parent 9dee904169
commit 40414d472b
8 changed files with 31 additions and 5 deletions

View File

@@ -1 +1 @@
1.1.140 1.1.141

View File

@@ -60,7 +60,7 @@ import (
usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users" usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users"
) )
var version = "1.1.140" var version = "1.1.141"
func main() { func main() {
addr := os.Getenv("EDGEGUARD_API_ADDR") addr := os.Getenv("EDGEGUARD_API_ADDR")

View File

@@ -11,7 +11,7 @@ import (
"git.netcell-it.de/projekte/edgeguard-native/internal/services/setup" "git.netcell-it.de/projekte/edgeguard-native/internal/services/setup"
) )
var version = "1.1.140" var version = "1.1.141"
const usage = `edgeguard-ctl — EdgeGuard CLI const usage = `edgeguard-ctl — EdgeGuard CLI

View File

@@ -41,7 +41,7 @@ import (
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts" "git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
) )
var version = "1.1.140" var version = "1.1.141"
const ( const (
// renewTickInterval — how often we re-evaluate expiring certs. // renewTickInterval — how often we re-evaluate expiring certs.

View File

@@ -380,6 +380,8 @@
"forceHttp1Hint": "Deaktiviert HTTP/2 (h2) auf der Backend-Verbindung — HAProxy handelt nur noch HTTP/1.1 aus. Nötig für Backends die kein h2 unterstützen (z. B. ältere nginx-Konfigurationen ohne h2-Modul, Legacy-Apps).", "forceHttp1Hint": "Deaktiviert HTTP/2 (h2) auf der Backend-Verbindung — HAProxy handelt nur noch HTTP/1.1 aus. Nötig für Backends die kein h2 unterstützen (z. B. ältere nginx-Konfigurationen ohne h2-Modul, Legacy-Apps).",
"servers": "Server", "servers": "Server",
"noServers": "kein Server", "noServers": "kein Server",
"noServersWarning": "Ein oder mehrere aktive Backends haben keine Server konfiguriert.",
"noServersWarningDesc": "HAProxy liefert für Traffic auf diese Backends 503. Backend öffnen (oranger Tag) und mindestens einen Server hinzufügen.",
"nServers": "{{n}} Server", "nServers": "{{n}} Server",
"serversIn": "Server in „{{name}}\"", "serversIn": "Server in „{{name}}\"",
"serverHintCreate": "Speichern legt nur den Pool an. Server kommen im nächsten Schritt — Pool öffnen → „Server hinzufügen\".", "serverHintCreate": "Speichern legt nur den Pool an. Server kommen im nächsten Schritt — Pool öffnen → „Server hinzufügen\".",
@@ -415,6 +417,8 @@
"editRule": "Regel bearbeiten", "editRule": "Regel bearbeiten",
"domain": "Domain", "domain": "Domain",
"pathPrefix": "Pfad-Präfix", "pathPrefix": "Pfad-Präfix",
"pathPrefixHint": "Muss mit / beginnen. HAProxy matcht als Präfix.",
"pathPrefixInvalid": "Pfad muss mit / beginnen und darf nur gültige URL-Zeichen enthalten",
"backend": "Backend", "backend": "Backend",
"priority": "Priorität", "priority": "Priorität",
"active": "Aktiv", "active": "Aktiv",

View File

@@ -380,6 +380,8 @@
"forceHttp1Hint": "Disables HTTP/2 (h2) on the backend connection — HAProxy negotiates HTTP/1.1 only. Required for backends that don't support h2 (e.g. older nginx configs without the h2 module, legacy apps).", "forceHttp1Hint": "Disables HTTP/2 (h2) on the backend connection — HAProxy negotiates HTTP/1.1 only. Required for backends that don't support h2 (e.g. older nginx configs without the h2 module, legacy apps).",
"servers": "Servers", "servers": "Servers",
"noServers": "no server", "noServers": "no server",
"noServersWarning": "One or more active backends have no servers configured.",
"noServersWarningDesc": "HAProxy will return 503 for any traffic routed to these backends. Open the backend (orange tag) and add at least one server.",
"nServers": "{{n}} servers", "nServers": "{{n}} servers",
"serversIn": "Servers in “{{name}}”", "serversIn": "Servers in “{{name}}”",
"serverHintCreate": "Saving creates the pool only. Add servers in the next step — open the pool and click “Add server”.", "serverHintCreate": "Saving creates the pool only. Add servers in the next step — open the pool and click “Add server”.",
@@ -415,6 +417,8 @@
"editRule": "Edit rule", "editRule": "Edit rule",
"domain": "Domain", "domain": "Domain",
"pathPrefix": "Path prefix", "pathPrefix": "Path prefix",
"pathPrefixHint": "Must start with /. HAProxy uses prefix matching.",
"pathPrefixInvalid": "Path must start with / and contain only valid URL characters",
"backend": "Backend", "backend": "Backend",
"priority": "Priority", "priority": "Priority",
"active": "Active", "active": "Active",

View File

@@ -329,6 +329,15 @@ export default function BackendsPage() {
</Tag> </Tag>
)} )}
/> />
{serverCounts && (data ?? []).some(b => b.active && (serverCounts[b.id] ?? 0) === 0) && (
<Alert
type="warning"
showIcon
className="mb-12"
message={t('backends.noServersWarning')}
description={t('backends.noServersWarningDesc')}
/>
)}
<DataTable <DataTable
rowKey="id" rowKey="id"
loading={isLoading} loading={isLoading}

View File

@@ -270,6 +270,7 @@ export default function RoutingRulesPage() {
onCancel={() => { setEditing(null); setCreating(false) }} onCancel={() => { setEditing(null); setCreating(false) }}
onOk={() => { void form.submit() }} onOk={() => { void form.submit() }}
confirmLoading={create.isPending || update.isPending} confirmLoading={create.isPending || update.isPending}
destroyOnHidden
> >
<Form <Form
form={form} form={form}
@@ -285,7 +286,15 @@ export default function RoutingRulesPage() {
placeholder={t('routing.selectDomain')} placeholder={t('routing.selectDomain')}
/> />
</Form.Item> </Form.Item>
<Form.Item label={t('routing.pathPrefix')} name="path_prefix" rules={[{ required: true }]}> <Form.Item
label={t('routing.pathPrefix')}
name="path_prefix"
extra={t('routing.pathPrefixHint')}
rules={[
{ required: true },
{ pattern: /^\/[a-zA-Z0-9\-._~!$&'()*+,;=:@/%]*$/, message: t('routing.pathPrefixInvalid') },
]}
>
<Input placeholder="/" /> <Input placeholder="/" />
</Form.Item> </Form.Item>
<Form.Item label={t('routing.backend')} name="backend_id" rules={[{ required: true }]}> <Form.Item label={t('routing.backend')} name="backend_id" rules={[{ required: true }]}>