133 lines
5.9 KiB
JavaScript
133 lines
5.9 KiB
JavaScript
import { h, PageHeader, Badge, StatusDot, Empty, Card, esc, enc, att_esc, $val, apiFetch, toast, dismissToast, openModal, closeModal, formModal, definePage, hComp, connect, reactive, requestUpdate, Link, createRouter, ToastContainer, render, parseZones } from '/static/hoover/index.js';
|
|
|
|
function addRuleModal(zones, state) {
|
|
openModal((inner, idx) => {
|
|
formModal(inner, 'Add Rich Rule',
|
|
[
|
|
{ label: 'Zone', id: 'rule-zone', tag: 'select', options: zones.map(z => [z, z === zones[0]]) },
|
|
{ label: 'Rule (XML)', id: 'rule-text', tag: 'textarea', placeholder: 'rule family="ipv4" source address="192.168.1.0/24" accept' },
|
|
],
|
|
[
|
|
{ label: 'Cancel', cls: 'btn-outline', action: 'c', handler: () => closeModal(idx) },
|
|
{
|
|
label: 'Add', cls: 'btn-primary', action: 's', handler: async () => {
|
|
const zone = $val('rule-zone');
|
|
const rule = ($val('rule-text') || '').trim();
|
|
if (!zone || !rule) { toast('Zone and rule are required', 'error'); return; }
|
|
const r = await apiFetch('/api/firewall/rich-rules', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ zone, rule }),
|
|
});
|
|
if (r.ok) {
|
|
toast('Rule added', 'success');
|
|
closeModal(idx);
|
|
await load(state);
|
|
} else {
|
|
toast(r.error || 'Failed', 'error');
|
|
}
|
|
},
|
|
},
|
|
],
|
|
);
|
|
});
|
|
}
|
|
|
|
async function load(state) {
|
|
try {
|
|
const r = await apiFetch('/api/firewall/config');
|
|
if (r.ok) state.config = r.data || {};
|
|
else state.error = r.error;
|
|
const zr = await apiFetch('/api/firewall/zones');
|
|
if (zr.ok) state.zones = Object.keys(zr.data?.active || {});
|
|
} catch (e) {
|
|
state.error = String(e);
|
|
}
|
|
state.loading = false;
|
|
}
|
|
|
|
export default definePage({
|
|
init() {
|
|
return { config: {}, loading: true, error: null, zones: [] };
|
|
},
|
|
subscribe: ['firewall'],
|
|
load,
|
|
render(state) {
|
|
if (state.loading) {
|
|
return [
|
|
PageHeader({ title: 'Rules', subtitle: 'Firewall rich rules' }),
|
|
h('div', { class: 'card', key: 'loading' },
|
|
h('div', { class: 'card-body loading' }, 'Loading...'),
|
|
),
|
|
];
|
|
}
|
|
|
|
if (state.error) {
|
|
return [
|
|
PageHeader({ title: 'Rules' }),
|
|
h('div', { class: 'card', key: 'error' },
|
|
h('div', { class: 'card-body error-msg' }, state.error),
|
|
),
|
|
];
|
|
}
|
|
|
|
const cfg = state.config || {};
|
|
const zoneData = cfg.zones || {};
|
|
const zoneRules = {};
|
|
Object.entries(zoneData).forEach(([zname, zcfg]) => {
|
|
const rr = zcfg.rich_rules || [];
|
|
if (rr.length) zoneRules[zname] = rr;
|
|
});
|
|
|
|
const cards = Object.entries(zoneRules).map(([zone, rules]) => {
|
|
return h('div', { class: 'card', key: zone },
|
|
h('div', { class: 'card-header' }, 'Zone: ' + esc(zone)),
|
|
h('div', { class: 'card-body' },
|
|
h('table', { class: 'table' },
|
|
h('thead', null,
|
|
h('tr', null,
|
|
h('th', null, '#'),
|
|
h('th', null, 'Rule'),
|
|
h('th', { style: 'width:80px;' }, 'Action'),
|
|
),
|
|
),
|
|
h('tbody', null,
|
|
(Array.isArray(rules) ? rules : []).map((entry, i) => {
|
|
const ruleId = typeof entry === 'object' ? entry.id : null;
|
|
const ruleText = typeof entry === 'object' ? (entry.rule || '') : String(entry);
|
|
return h('tr', { key: i },
|
|
h('td', { class: 'text-muted' }, i + 1),
|
|
h('td', { style: 'font-family:monospace;font-size:12px;word-break:break-all;' }, esc(ruleText)),
|
|
h('td', null,
|
|
h('button', { class: 'btn btn-sm btn-danger',
|
|
'on:click': async () => {
|
|
if (!confirm('Remove rule: ' + ruleText.substring(0, 40) + '...?')) return;
|
|
const r = await apiFetch('/api/firewall/rich-rules/' + enc(zone) + '/' + enc(ruleId || ''), { method: 'DELETE' });
|
|
if (r.ok) {
|
|
toast('Rule removed', 'success');
|
|
await load(state);
|
|
} else {
|
|
toast(r.error || 'Failed', 'error');
|
|
}
|
|
}}, 'Remove'),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
return [
|
|
PageHeader({
|
|
title: 'Rules',
|
|
subtitle: 'Firewall rich rules',
|
|
actions: h('button', { class: 'btn btn-primary',
|
|
'on:click': () => addRuleModal(state.zones, state) }, 'Add Rule'),
|
|
}),
|
|
...(cards.length ? cards : [Empty({ text: 'No rich rules configured.' })]),
|
|
];
|
|
},
|
|
});
|