feat: Zonen als first-class Entity + Domain↔Backend-Verknüpfung sichtbar
* Migration 0012: firewall_zones (id, name UNIQUE, description, builtin),
Seed wan/lan/dmz/mgmt/cluster als builtin. CHECK-Constraints auf
network_interfaces.role + firewall_rules.{src,dst}_zone +
firewall_nat_rules.{in,out}_zone gedroppt — Validation lebt jetzt
app-side (Handler prüft Existenz in firewall_zones).
* Backend: firewall.ZonesRepo (CRUD + Exists + References-Lookup),
/api/v1/firewall/zones, builtin geschützt (Name nicht änderbar,
Delete blockiert), Rename eines Custom-Zone aktuell ohne Cascade
(Handler-Sorge bei Rules/NAT/Networks).
* Handler-Validation in CreateRule/UpdateRule/CreateNAT/UpdateNAT +
NetworksHandler: Zone-Existence-Check pro Mutation, 400 bei Tippfehler.
* Frontend: Firewall-Tab "Zonen" (CRUD mit builtin-Schutz). Networks-
Form lädt Rollen aus /firewall/zones (statt hardcoded Liste); Rules-
und NAT-Forms ziehen die Zone-Auswahl ebenfalls aus der API.
* Domain-Form bekommt Primary-Backend-Picker (Field war im Modell,
fehlte im UI). Backends-Tabelle zeigt umgekehrt welche Domains
darauf zeigen — bidirektionale Sicht ohne Schemaänderung.
* HAProxy-Renderer: safeID-FuncMap escaped Server-Namen mit Whitespace
("Control Master 1" → "Control_Master_1"). Vorher ist haproxy beim
Reload an Spaces im Backend-Namen kaputt gegangen.
* Version 1.0.3 → 1.0.6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,8 +7,8 @@ import DataTable from '../../components/DataTable'
|
||||
import SystemRulesCard from './SystemRules'
|
||||
|
||||
import apiClient, { isEnvelope } from '../../api/client'
|
||||
import type { AddressGroup, AddressObject, FwRule, FwService, ServiceGroup, Zone } from './types'
|
||||
import { ZONES } from './types'
|
||||
import type { AddressGroup, AddressObject, FwRule, FwService, FwZone, ServiceGroup, Zone } from './types'
|
||||
import { ZONES_FALLBACK } from './types'
|
||||
|
||||
interface FormValues {
|
||||
name?: string
|
||||
@@ -63,6 +63,11 @@ async function listSG(): Promise<ServiceGroup[]> {
|
||||
if (!isEnvelope(r.data)) return []
|
||||
return (r.data.data as { service_groups?: ServiceGroup[] }).service_groups ?? []
|
||||
}
|
||||
async function listZones(): Promise<FwZone[]> {
|
||||
const r = await apiClient.get('/firewall/zones')
|
||||
if (!isEnvelope(r.data)) return []
|
||||
return (r.data.data as { zones?: FwZone[] }).zones ?? []
|
||||
}
|
||||
|
||||
function buildPayload(v: FormValues) {
|
||||
const out: Partial<FwRule> = {
|
||||
@@ -91,6 +96,13 @@ export default function RulesTab() {
|
||||
const { data: ags } = useQuery({ queryKey: ['fw', 'addr-grp'], queryFn: listAG })
|
||||
const { data: svs } = useQuery({ queryKey: ['fw', 'svc'], queryFn: listSv })
|
||||
const { data: sgs } = useQuery({ queryKey: ['fw', 'svc-grp'], queryFn: listSG })
|
||||
const { data: zones } = useQuery({ queryKey: ['fw', 'zones'], queryFn: listZones })
|
||||
|
||||
// Picker options: 'any' (special) + every zone the operator has
|
||||
// declared. Fallback to the seed list while the query is loading.
|
||||
const zoneOptions: Zone[] = zones && zones.length > 0
|
||||
? ['any', ...zones.map((z) => z.name)]
|
||||
: ZONES_FALLBACK
|
||||
|
||||
const aoLabel = (id?: number | null) => aos?.find(o => o.id === id)?.name ?? `#${id}`
|
||||
const agLabel = (id?: number | null) => ags?.find(g => g.id === id)?.name ?? `#${id}`
|
||||
@@ -234,7 +246,7 @@ export default function RulesTab() {
|
||||
{(['src', 'dst'] as const).map((side) => (
|
||||
<Space key={side} size="middle" style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'flex-start' }}>
|
||||
<Form.Item label={t(`fw.rule.${side}Zone`)} name={`${side}_zone`} rules={[{ required: true }]}>
|
||||
<Select style={{ width: 120 }} options={ZONES.map(z => ({ value: z, label: z }))} />
|
||||
<Select style={{ width: 140 }} options={zoneOptions.map(z => ({ value: z, label: z }))} />
|
||||
</Form.Item>
|
||||
<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 }))} />
|
||||
|
||||
Reference in New Issue
Block a user