37039351be
- wireguard: POST /peers with JSON encoding (was /add-peer) - rules: delete by rule_id in URL path (was JSON body); pass rule objects with id from server; add hx-disable to initial render - nat: port forward delete uses URL path params to match blueprint - nat: masquerade toggle uses native hx-post/hx-vals (was inline fetch) - app.js renderers updated to use URL path deletes for rules and forwards - remove TODO.md
84 lines
3.5 KiB
HTML
84 lines
3.5 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Rules - Vacuum Wall{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="page-header">
|
|
<div>
|
|
<h1>Rich Rules</h1>
|
|
<div class="subtitle">Firewalld rich firewall rules per zone</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mb-4">
|
|
<h3>Add Rule</h3>
|
|
<form hx-post="/api/firewall/rich-rules" hx-swap="none" hx-on::after-request="if(evt.detail.successful){ refreshTable('/api/firewall/config', document.getElementById('rules-container'), renderRules); showSuccessToast('Rule added'); }">
|
|
<div class="inline-form">
|
|
<div class="form-group">
|
|
<label for="rule-zone">Zone</label>
|
|
<select id="rule-zone" name="zone" required>
|
|
<option value="">— Select zone —</option>
|
|
{% for zone in (zones or []) %}
|
|
<option value="{{ zone.get('name', '') }}">{{ zone.get('name', '') }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="rule-text">Rule Expression</label>
|
|
<input type="text" id="rule-text" name="rule" placeholder="e.g., rule family=ipv4 source address=192.168.1.0/24 accept" required style="min-width:420px;">
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">Add Rule</button>
|
|
</div>
|
|
</form>
|
|
<div class="text-muted text-sm mt-2">
|
|
Reference: <a href="https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html" target="_blank" style="color:var(--accent);">firewalld rich language syntax</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="rules-container">
|
|
{% if rules or False %}
|
|
{% for zone_name, zone_rules in rules.items() %}
|
|
<div class="card">
|
|
<h3>Zone: <span style="color:var(--accent);">{{ zone_name or '(default)' }}</span></h3>
|
|
{% if zone_rules %}
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>#</th>
|
|
<th>Rule</th>
|
|
<th style="width:80px;">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for rule in zone_rules %}
|
|
{% set rule_obj = rule if rule is mapping else {'id': None, 'rule': rule} %}
|
|
<tr>
|
|
<td class="text-muted">{{ loop.index }}</td>
|
|
<td hx-disable style="font-family:monospace;font-size:12px;word-break:break-all;">{{ rule_obj.rule }}</td>
|
|
<td>
|
|
<form hx-delete="/api/firewall/rich-rules/{{ zone_name | urlencode }}/{{ rule_obj.id }}" hx-swap="none" hx-confirm="Remove rule {{ rule_obj.rule[:50] }}?" hx-on::after-request="if(evt.detail.successful){ refreshTable('/api/firewall/config', document.getElementById('rules-container'), renderRules); showSuccessToast('Rule removed'); }">
|
|
<button type="submit" class="btn btn-sm btn-danger">Remove</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<div class="text-muted text-sm">No rich rules configured for this zone.</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="card">
|
|
<div class="text-muted text-sm">No rules loaded. Add rules using the form above, or ensure the zones API is providing rule data.</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if not (zones or []) %}
|
|
<div class="card" style="border-color:var(--warning);">
|
|
<div class="text-muted text-sm" style="color:var(--warning);">No zones configured. Create a zone first before adding rich rules.</div>
|
|
</div>
|
|
{% endif %}
|
|
{% endblock %}
|