import { html, PageHeader, Badge, Empty, ConfirmDelete, renderGuard, enc, esc, $val, definePage, getModel, MultiSelectModal, QuickModal, PendingDot, fwDirty, fwInfo, fwTitle } from '/static/hoover/index.js'; // Services shown by default in the service picker. Everything else is only // visible with the "Show all options" toggle (or while it is already // selected on the zone). const COMMON_SERVICES = [ 'amqp', 'cron', 'docker', 'ftp', 'ftps', 'http', 'https', 'irc', 'ldap', 'mysql', 'nfs', 'ntp', 'postgresql', 'radius', 'rsync', 'sip', 'smtp', 'smtps', 'snmp', 'ssh', 'telnet', 'vnc', 'xmpp', ]; const addZone = QuickModal({ title: 'Add Zone', fields: [ { label: 'Zone name', id: 'zone-name', placeholder: 'e.g. internal' }, { label: 'Target', id: 'zone-target', placeholder: 'default (usually leave as default)' }, ], submit: { url: '/api/firewall/zones', body: () => ({ name: ($val('zone-name') || '').trim(), target: ($val('zone-target') || '').trim() || 'default' }), validate: (b) => !b.name ? 'Zone name required' : null, successMsg: 'Zone created', }, }); export default definePage({ init() { return { firewall: getModel('firewall'), }; }, render(state) { const guard = renderGuard(state.firewall, 'Zones', 'Firewall zones', state.firewall.data?.zones); if (guard) return guard; const fw = fwDirty(state.firewall.data?.pending); // Live zone data (parsed `--list-all-zones`): carries interfaces, // services, target, and masquerade for every defined zone. const liveZones = state.firewall.data?.zones || {}; const zoneDetails = {}; for (const name of Object.keys(liveZones)) { zoneDetails[name] = liveZones[name] || { interfaces: [] }; } const zoneCards = Object.entries(zoneDetails).map(([name, zdata]) => { const z = typeof zdata === 'object' ? zdata : {}; const ifacesArr = Array.isArray(z.interfaces) ? z.interfaces : []; const svcsArr = Array.isArray(z.services) ? z.services : []; const info = fwInfo(fw, name); const ifTitle = fwTitle(fw, name, 'interfaces'); const svcTitle = fwTitle(fw, name, 'services'); const tgtTitle = fwTitle(fw, name, 'target'); return html`

${info.dirty ? PendingDot({}) : ''}${name}

${z.target ? 'Target: ' + esc(z.target) : ''}
Interfaces
${ifacesArr.length ? ifacesArr.map(i => html`<${Badge} text=${esc(i)} />`) : html`None`}
Services
${svcsArr.length ? svcsArr.map(s => html`<${Badge} text=${esc(s)} variant="success" />`) : html`None`}
<${ConfirmDelete} url=${'/api/firewall/zones/' + enc(name)} deleteKey=${name} message=${'Delete zone ' + name + '?'} success=${'Zone ' + name + ' deleted'} label="Delete" />
`; }); const uncovered = Array.isArray(state.firewall.data?.uncovered_interfaces) ? state.firewall.data.uncovered_interfaces : []; const uncoveredBanner = uncovered.length ? html`
Uncovered interfaces
These interfaces are not assigned to any firewall zone, so clients on these segments lose all connectivity, including DHCP. Add each interface to a zone to restore access.
${uncovered.map(i => html`<${Badge} text=${esc(i)} variant="danger" />`)}
` : null; return [ PageHeader({ title: 'Zones', subtitle: 'Firewall zones', actions: html``, }), uncoveredBanner, zoneCards.length ? html`
${zoneCards}
` : Empty({ text: 'No zones configured. Add a zone to get started.' }), ]; }, });