feat(firewall): Regel duplizieren + CIDR-Validierung (v1.1.139)

- Duplicate-Button (CopyOutlined): klont Regel mit priority+1, disabled=true, Name "(copy)"
- CIDR-Felder: Pattern-Validator für IPv4/IPv6-CIDR-Notation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-28 20:36:25 +02:00
parent dc2c1b7471
commit d89569017d
7 changed files with 46 additions and 7 deletions

View File

@@ -1 +1 @@
1.1.138
1.1.139

View File

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

View File

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

View File

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

View File

@@ -109,6 +109,9 @@
"cidr": "CIDR",
"actions": { "accept": "ACCEPT", "drop": "DROP", "reject": "REJECT" },
"kinds": { "any": "Beliebig", "object": "Objekt", "group": "Gruppe", "cidr": "CIDR" },
"duplicate": "Regel duplizieren",
"duplicated": "Regel dupliziert (deaktiviert, Priorität +1).",
"cidrInvalid": "Gültiges CIDR eingeben (z.B. 10.0.0.0/24)",
"moveUp": "Nach oben (höhere Priorität)",
"moveDown": "Nach unten (niedrigere Priorität)",
"emptyTitle": "Noch keine eigenen Firewall-Regeln.",

View File

@@ -109,6 +109,9 @@
"cidr": "CIDR",
"actions": { "accept": "ACCEPT", "drop": "DROP", "reject": "REJECT" },
"kinds": { "any": "Any", "object": "Object", "group": "Group", "cidr": "CIDR" },
"duplicate": "Duplicate rule",
"duplicated": "Rule duplicated (disabled, priority +1).",
"cidrInvalid": "Enter a valid CIDR (e.g. 10.0.0.0/24)",
"moveUp": "Move up (higher priority)",
"moveDown": "Move down (lower priority)",
"emptyTitle": "No custom firewall rules yet.",

View File

@@ -7,7 +7,7 @@ import type { ColumnsType } from 'antd/es/table'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import {
ArrowDownOutlined, ArrowUpOutlined, EyeOutlined, FireOutlined, PlusOutlined,
ArrowDownOutlined, ArrowUpOutlined, CopyOutlined, EyeOutlined, FireOutlined, PlusOutlined,
} from '@ant-design/icons'
const { Text } = Typography
@@ -233,6 +233,23 @@ export default function RulesTab() {
onError: (e: Error) => message.error(e.message),
})
const duplicate = useMutation({
mutationFn: async (r: FwRule) => {
const { id: _id, created_at: _ca, updated_at: _ua, ...rest } = r as FwRule & { created_at?: unknown; updated_at?: unknown }
await apiClient.post('/firewall/rules', {
...rest,
name: r.name ? `${r.name} (copy)` : undefined,
priority: r.priority + 1,
enabled: false,
})
},
onSuccess: () => {
message.success(t('fw.rule.duplicated'))
void qc.invalidateQueries({ queryKey: ['fw', 'rules'] })
},
onError: (e: Error) => message.error(e.message),
})
const editFromRow = (r: FwRule) => {
setEditing(r)
form.setFieldsValue({
@@ -376,7 +393,7 @@ export default function RulesTab() {
},
},
{
title: '', key: 'actions', width: 100,
title: '', key: 'actions', width: 120,
render: (_, row) => (
<Space size={4}>
<Tooltip title={isViewer ? t('auth.viewerBadge') : t('common.edit')}>
@@ -384,6 +401,15 @@ export default function RulesTab() {
{t('common.edit')}
</Button>
</Tooltip>
<Tooltip title={isViewer ? t('auth.viewerBadge') : t('fw.rule.duplicate')}>
<Button
size="small"
icon={<CopyOutlined />}
disabled={isViewer}
loading={duplicate.isPending && duplicate.variables?.id === row.id}
onClick={() => duplicate.mutate(row)}
/>
</Tooltip>
{isViewer ? (
<Tooltip title={t('auth.viewerBadge')}>
<Button size="small" danger disabled>{t('common.delete')}</Button>
@@ -536,7 +562,14 @@ export default function RulesTab() {
</Form.Item>
}
if (k === 'cidr') {
return <Form.Item label={t('fw.rule.kinds.cidr')} name={`${side}_cidr`} rules={[{ required: true }]}>
return <Form.Item
label={t('fw.rule.kinds.cidr')}
name={`${side}_cidr`}
rules={[
{ required: true },
{ pattern: /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$|^[0-9a-fA-F:]+\/\d{1,3}$/, message: t('fw.rule.cidrInvalid') },
]}
>
<Input placeholder="10.0.0.0/24" style={{ width: 200 }} />
</Form.Item>
}