fix(firewall): fehlende onError-Handler + i18n in Rules + NATRules

- 6 Mutations (create/update/del je Rules + NATRules) bekommen onError-Handler
- Deutsche Placeholder-Texte → t() überführt
- Action-Labels (accept/drop/reject) und Kind-Labels (any/object/group/cidr)
  in t() überführt statt hardkodiert anzeigen
- CIDR-Label-Hardkodierung in Rules-Formular behoben
- en/de: fw.rule.actions.*, fw.rule.kinds.*, fw.rule.namePlaceholder,
  fw.nat.namePlaceholder ergänzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-24 12:40:08 +02:00
parent 92eece936d
commit ca1ee64829
8 changed files with 26 additions and 10 deletions

View File

@@ -1 +1 @@
1.1.82 1.1.83

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.82" var version = "1.1.83"
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.82" var version = "1.1.83"
const usage = `edgeguard-ctl — EdgeGuard CLI const usage = `edgeguard-ctl — EdgeGuard CLI

View File

@@ -35,7 +35,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.82" var version = "1.1.83"
const ( const (
// renewTickInterval — how often we re-evaluate expiring certs. // renewTickInterval — how often we re-evaluate expiring certs.

View File

@@ -105,6 +105,10 @@
"hits": "Hits", "hits": "Hits",
"add": "Regel hinzufügen", "edit": "Regel bearbeiten", "add": "Regel hinzufügen", "edit": "Regel bearbeiten",
"deleteConfirm": "Diese Regel wirklich löschen?", "deleteConfirm": "Diese Regel wirklich löschen?",
"namePlaceholder": "z.B. SSH von Office erlauben",
"cidr": "CIDR",
"actions": { "accept": "ACCEPT", "drop": "DROP", "reject": "REJECT" },
"kinds": { "any": "Beliebig", "object": "Objekt", "group": "Gruppe", "cidr": "CIDR" },
"emptyTitle": "Noch keine eigenen Firewall-Regeln.", "emptyTitle": "Noch keine eigenen Firewall-Regeln.",
"emptyDesc": "Die System-Regeln oben halten SSH (rate-limited), HTTPS :443 und Mgmt-UI :3443 immer offen (Anti-Lockout). Eigene Regeln für app-spezifische Inbound-Ports oder zonenübergreifende Forwards anlegen." "emptyDesc": "Die System-Regeln oben halten SSH (rate-limited), HTTPS :443 und Mgmt-UI :3443 immer offen (Anti-Lockout). Eigene Regeln für app-spezifische Inbound-Ports oder zonenübergreifende Forwards anlegen."
}, },
@@ -119,6 +123,7 @@
"comment": "Kommentar", "comment": "Kommentar",
"add": "NAT-Regel hinzufügen", "edit": "NAT-Regel bearbeiten", "add": "NAT-Regel hinzufügen", "edit": "NAT-Regel bearbeiten",
"deleteConfirm": "Diese NAT-Regel wirklich löschen?", "deleteConfirm": "Diese NAT-Regel wirklich löschen?",
"namePlaceholder": "z.B. HTTP zu Web-Backend weiterleiten",
"emptyTitle": "Noch keine NAT-Regeln.", "emptyTitle": "Noch keine NAT-Regeln.",
"emptyDesc": "DNAT (z. B. extern :2030 → intern 10.10.20.12:22 für SSH zu einem internen Host) oder SNAT/MASQUERADE (Internet-Zugang für ein internes Subnetz über die Box-IP)." "emptyDesc": "DNAT (z. B. extern :2030 → intern 10.10.20.12:22 für SSH zu einem internen Host) oder SNAT/MASQUERADE (Internet-Zugang für ein internes Subnetz über die Box-IP)."
}, },

View File

@@ -105,6 +105,10 @@
"hits": "Hits", "hits": "Hits",
"add": "Add rule", "edit": "Edit rule", "add": "Add rule", "edit": "Edit rule",
"deleteConfirm": "Really delete this rule?", "deleteConfirm": "Really delete this rule?",
"namePlaceholder": "e.g. Allow SSH from office",
"cidr": "CIDR",
"actions": { "accept": "ACCEPT", "drop": "DROP", "reject": "REJECT" },
"kinds": { "any": "Any", "object": "Object", "group": "Group", "cidr": "CIDR" },
"emptyTitle": "No custom firewall rules yet.", "emptyTitle": "No custom firewall rules yet.",
"emptyDesc": "The system rules above keep SSH (rate-limited), HTTPS :443 and the mgmt UI :3443 open (anti-lockout). Add custom rules for app-specific inbound ports or cross-zone forwards." "emptyDesc": "The system rules above keep SSH (rate-limited), HTTPS :443 and the mgmt UI :3443 open (anti-lockout). Add custom rules for app-specific inbound ports or cross-zone forwards."
}, },
@@ -119,6 +123,7 @@
"comment": "Comment", "comment": "Comment",
"add": "Add NAT rule", "edit": "Edit NAT rule", "add": "Add NAT rule", "edit": "Edit NAT rule",
"deleteConfirm": "Really delete this NAT rule?", "deleteConfirm": "Really delete this NAT rule?",
"namePlaceholder": "e.g. Forward HTTP to web backend",
"emptyTitle": "No NAT rules yet.", "emptyTitle": "No NAT rules yet.",
"emptyDesc": "DNAT (e.g. external :2030 → internal 10.10.20.12:22 for SSH to an internal host) or SNAT/MASQUERADE (internet access for an internal subnet via the box IP)." "emptyDesc": "DNAT (e.g. external :2030 → internal 10.10.20.12:22 for SSH to an internal host) or SNAT/MASQUERADE (internet access for an internal subnet via the box IP)."
}, },

View File

@@ -69,6 +69,7 @@ export default function NATRulesTab() {
message.success(t('common.save')); setCreating(false); form.resetFields() message.success(t('common.save')); setCreating(false); form.resetFields()
void qc.invalidateQueries({ queryKey: ['fw', 'nat'] }) void qc.invalidateQueries({ queryKey: ['fw', 'nat'] })
}, },
onError: (e: Error) => message.error(e.message),
}) })
const update = useMutation({ const update = useMutation({
mutationFn: async ({ id, v }: { id: number; v: FormValues }) => { await apiClient.put(`/firewall/nat-rules/${id}`, v) }, mutationFn: async ({ id, v }: { id: number; v: FormValues }) => { await apiClient.put(`/firewall/nat-rules/${id}`, v) },
@@ -76,10 +77,12 @@ export default function NATRulesTab() {
message.success(t('common.save')); setEditing(null); form.resetFields() message.success(t('common.save')); setEditing(null); form.resetFields()
void qc.invalidateQueries({ queryKey: ['fw', 'nat'] }) void qc.invalidateQueries({ queryKey: ['fw', 'nat'] })
}, },
onError: (e: Error) => message.error(e.message),
}) })
const del = useMutation({ const del = useMutation({
mutationFn: async (id: number) => { await apiClient.delete(`/firewall/nat-rules/${id}`) }, mutationFn: async (id: number) => { await apiClient.delete(`/firewall/nat-rules/${id}`) },
onSuccess: () => { void qc.invalidateQueries({ queryKey: ['fw', 'nat'] }) }, onSuccess: () => { void qc.invalidateQueries({ queryKey: ['fw', 'nat'] }) },
onError: (e: Error) => message.error(e.message),
}) })
const quickToggle = useMutation({ const quickToggle = useMutation({
mutationFn: async ({ id, row, checked }: { id: number; row: NATRule; checked: boolean }) => { mutationFn: async ({ id, row, checked }: { id: number; row: NATRule; checked: boolean }) => {
@@ -190,7 +193,7 @@ export default function NATRulesTab() {
onFinish={(v) => { if (editing) update.mutate({ id: editing.id, v }); else create.mutate(v) }} onFinish={(v) => { if (editing) update.mutate({ id: editing.id, v }); else create.mutate(v) }}
> >
<Form.Item label={t('fw.nat.name')} name="name"> <Form.Item label={t('fw.nat.name')} name="name">
<Input placeholder="Forward HTTP zu Web-Backend" /> <Input placeholder={t('fw.nat.namePlaceholder')} />
</Form.Item> </Form.Item>
<Space size="middle"> <Space size="middle">
<Form.Item label={t('fw.nat.priority')} name="priority" rules={[{ required: true }]}> <Form.Item label={t('fw.nat.priority')} name="priority" rules={[{ required: true }]}>

View File

@@ -158,6 +158,7 @@ export default function RulesTab() {
message.success(t('common.save')); setCreating(false); form.resetFields() message.success(t('common.save')); setCreating(false); form.resetFields()
void qc.invalidateQueries({ queryKey: ['fw', 'rules'] }) void qc.invalidateQueries({ queryKey: ['fw', 'rules'] })
}, },
onError: (e: Error) => message.error(e.message),
}) })
const update = useMutation({ const update = useMutation({
mutationFn: async ({ id, v }: { id: number; v: FormValues }) => { await apiClient.put(`/firewall/rules/${id}`, buildPayload(v)) }, mutationFn: async ({ id, v }: { id: number; v: FormValues }) => { await apiClient.put(`/firewall/rules/${id}`, buildPayload(v)) },
@@ -165,10 +166,12 @@ export default function RulesTab() {
message.success(t('common.save')); setEditing(null); form.resetFields() message.success(t('common.save')); setEditing(null); form.resetFields()
void qc.invalidateQueries({ queryKey: ['fw', 'rules'] }) void qc.invalidateQueries({ queryKey: ['fw', 'rules'] })
}, },
onError: (e: Error) => message.error(e.message),
}) })
const del = useMutation({ const del = useMutation({
mutationFn: async (id: number) => { await apiClient.delete(`/firewall/rules/${id}`) }, mutationFn: async (id: number) => { await apiClient.delete(`/firewall/rules/${id}`) },
onSuccess: () => { void qc.invalidateQueries({ queryKey: ['fw', 'rules'] }) }, onSuccess: () => { void qc.invalidateQueries({ queryKey: ['fw', 'rules'] }) },
onError: (e: Error) => message.error(e.message),
}) })
const quickToggle = useMutation({ const quickToggle = useMutation({
mutationFn: async ({ id, row, checked }: { id: number; row: FwRule; checked: boolean }) => { mutationFn: async ({ id, row, checked }: { id: number; row: FwRule; checked: boolean }) => {
@@ -295,7 +298,7 @@ export default function RulesTab() {
onFinish={(v) => { if (editing) update.mutate({ id: editing.id, v }); else create.mutate(v) }} onFinish={(v) => { if (editing) update.mutate({ id: editing.id, v }); else create.mutate(v) }}
> >
<Form.Item label={t('fw.rule.name')} name="name"> <Form.Item label={t('fw.rule.name')} name="name">
<Input placeholder="SSH von Office" /> <Input placeholder={t('fw.rule.namePlaceholder')} />
</Form.Item> </Form.Item>
<Space size="middle" className="flex-wrap"> <Space size="middle" className="flex-wrap">
@@ -303,7 +306,7 @@ export default function RulesTab() {
<InputNumber min={0} max={9999} /> <InputNumber min={0} max={9999} />
</Form.Item> </Form.Item>
<Form.Item label={t('fw.rule.action')} name="action" rules={[{ required: true }]}> <Form.Item label={t('fw.rule.action')} name="action" rules={[{ required: true }]}>
<Select style={{ width: 140 }} options={(['accept','drop','reject'] as const).map(a => ({ value: a, label: a }))} /> <Select style={{ width: 140 }} options={(['accept','drop','reject'] as const).map(a => ({ value: a, label: t(`fw.rule.actions.${a}`) }))} />
</Form.Item> </Form.Item>
<Form.Item label={t('fw.rule.enabled')} name="enabled" valuePropName="checked"> <Form.Item label={t('fw.rule.enabled')} name="enabled" valuePropName="checked">
<Switch /> <Switch />
@@ -319,7 +322,7 @@ export default function RulesTab() {
<Select style={{ width: 140 }} options={zoneOptions.map(z => ({ value: z, label: z }))} /> <Select style={{ width: 140 }} options={zoneOptions.map(z => ({ value: z, label: z }))} />
</Form.Item> </Form.Item>
<Form.Item label={t(`fw.rule.${side}Kind`)} name={`${side}_kind`} rules={[{ required: true }]}> <Form.Item label={t(`fw.rule.${side}Kind`)} name={`${side}_kind`} rules={[{ required: true }]}>
<Select style={{ width: 120 }} options={(['any','object','group','cidr'] as const).map(k => ({ value: k, label: k }))} /> <Select style={{ width: 120 }} options={(['any','object','group','cidr'] as const).map(k => ({ value: k, label: t(`fw.rule.kinds.${k}`) }))} />
</Form.Item> </Form.Item>
<Form.Item noStyle shouldUpdate={(p, c) => p[`${side}_kind`] !== c[`${side}_kind`]}> <Form.Item noStyle shouldUpdate={(p, c) => p[`${side}_kind`] !== c[`${side}_kind`]}>
{({ getFieldValue }) => { {({ getFieldValue }) => {
@@ -337,7 +340,7 @@ export default function RulesTab() {
</Form.Item> </Form.Item>
} }
if (k === 'cidr') { if (k === 'cidr') {
return <Form.Item label="CIDR" name={`${side}_cidr`} rules={[{ required: true }]}> return <Form.Item label={t('fw.rule.kinds.cidr')} name={`${side}_cidr`} rules={[{ required: true }]}>
<Input placeholder="10.0.0.0/24" style={{ width: 200 }} /> <Input placeholder="10.0.0.0/24" style={{ width: 200 }} />
</Form.Item> </Form.Item>
} }
@@ -349,7 +352,7 @@ export default function RulesTab() {
<Space size="middle" className="flex-wrap"> <Space size="middle" className="flex-wrap">
<Form.Item label={t('fw.rule.serviceKind')} name="service_kind" rules={[{ required: true }]}> <Form.Item label={t('fw.rule.serviceKind')} name="service_kind" rules={[{ required: true }]}>
<Select style={{ width: 120 }} options={(['any','object','group'] as const).map(k => ({ value: k, label: k }))} /> <Select style={{ width: 120 }} options={(['any','object','group'] as const).map(k => ({ value: k, label: t(`fw.rule.kinds.${k}`) }))} />
</Form.Item> </Form.Item>
<Form.Item noStyle shouldUpdate={(p, c) => p.service_kind !== c.service_kind}> <Form.Item noStyle shouldUpdate={(p, c) => p.service_kind !== c.service_kind}>
{({ getFieldValue }) => { {({ getFieldValue }) => {