refactor: unify project structure, improve security, and enhance deployment

- Fix WireGuard private key leak in API responses and config updates
- Update systemd service to serve from repo root with adjusted sandbox
- Add CLI flags, idempotency, and dev mode to install.sh
- Extract common utilities to lib/common.py and webui/api/common.py
- Migrate frontend to htmx for simpler, more maintainable UI
- Update docs to reflect current architecture and deployment model
- Vendor htmx dependencies per project requirements
This commit is contained in:
2026-05-25 00:53:32 +00:00
parent 8829ac579d
commit d1ab717c0f
36 changed files with 857 additions and 626 deletions
+19 -19
View File
@@ -26,7 +26,7 @@ class TestParseForwardPorts:
class TestGetActiveZones:
@patch("lib.firewall._run")
@patch("lib.firewall.run")
def test_parses_active_zones(self, mock_run):
mock_run.return_value = "public\n eth0\ninternal\n eth1\n eth2"
result = firewall.get_active_zones()
@@ -35,13 +35,13 @@ class TestGetActiveZones:
"internal": ["eth1", "eth2"],
}
@patch("lib.firewall._run")
@patch("lib.firewall.run")
def test_empty_output(self, mock_run):
mock_run.return_value = ""
result = firewall.get_active_zones()
assert result == {}
@patch("lib.firewall._run")
@patch("lib.firewall.run")
def test_zone_with_no_interfaces(self, mock_run):
mock_run.return_value = "dmz"
result = firewall.get_active_zones()
@@ -49,7 +49,7 @@ class TestGetActiveZones:
class TestGetZoneInfo:
@patch("lib.firewall._run")
@patch("lib.firewall.run")
def test_parses_zone_info(self, mock_run):
mock_run.return_value = (
"target: default\n"
@@ -76,7 +76,7 @@ class TestGetZoneInfo:
class TestGetInterfaces:
@patch("lib.firewall._run")
@patch("lib.firewall.run")
def test_parses_interfaces(self, mock_run):
mock_run.return_value = (
"1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536\n"
@@ -88,7 +88,7 @@ class TestGetInterfaces:
class TestGetRichRules:
@patch("lib.firewall._run")
@patch("lib.firewall.run")
def test_single_rule(self, mock_run):
mock_run.return_value = (
'rule family="ipv4" port protocol="tcp" port="443" accept;'
@@ -96,13 +96,13 @@ class TestGetRichRules:
result = firewall.get_rich_rules("public")
assert len(result) == 1
@patch("lib.firewall._run")
@patch("lib.firewall.run")
def test_empty_rules(self, mock_run):
mock_run.return_value = ""
result = firewall.get_rich_rules("public")
assert result == []
@patch("lib.firewall._run")
@patch("lib.firewall.run")
def test_multiline_rule(self, mock_run):
mock_run.return_value = (
'rule family="ipv4"\n source address="10.0.0.0/24"\n reject;'
@@ -120,14 +120,14 @@ class TestNowIso:
class TestAddForwardPort:
@patch("lib.firewall._run")
@patch("lib.firewall.run")
def test_forward_port_basic(self, mock_run):
mock_run.return_value = ""
firewall.add_forward_port("public", 443, "tcp", toaddr="10.0.0.5", toport=8080)
calls = [c[0][0] for c in mock_run.call_args_list]
assert any("--add-forward-port=" in str(c) for c in calls)
@patch("lib.firewall._run")
@patch("lib.firewall.run")
def test_forward_port_port_only(self, mock_run):
mock_run.return_value = ""
firewall.add_forward_port("public", 80, "tcp", toport=8080)
@@ -229,7 +229,7 @@ class TestConfigGet:
'{"zones": {"public": {"interfaces": ["eth0"], "services": ["http"], "masquerade": true, "target": "DEFAULT"}}}'
)
with patch.object(firewall, "CONFIG_FILE", cfg_file):
result = firewall.config_get()
result = firewall.get_config()
assert result["zones"]["public"]["interfaces"] == ["eth0"]
assert result["zones"]["public"]["services"] == ["http"]
@@ -241,7 +241,7 @@ class TestConfigSet:
patch.object(firewall, "CONFIG_FILE", cfg_file),
patch.object(firewall, "CONFIG_DIR", tmp_path),
):
firewall.config_set({"zones": {"test": {"interfaces": ["eth0"]}}})
firewall.save_config({"zones": {"test": {"interfaces": ["eth0"]}}})
import json as _json
content = _json.loads(cfg_file.read_text())
@@ -249,7 +249,7 @@ class TestConfigSet:
class TestConfigApply:
@patch("lib.firewall.config_get")
@patch("lib.firewall.get_config")
@patch("lib.firewall.save_backup")
@patch("lib.firewall.get_available_zones")
@patch("lib.firewall.create_zone")
@@ -287,7 +287,7 @@ class TestConfigApply:
mock_set_svcs.assert_called_once_with("public", ["http", "https"])
mock_set_mq.assert_called_once_with("public", True)
@patch("lib.firewall.config_get")
@patch("lib.firewall.get_config")
@patch("lib.firewall.save_backup")
@patch("lib.firewall.get_available_zones")
@patch("lib.firewall.create_zone")
@@ -325,7 +325,7 @@ class TestConfigApply:
class TestConfigPending:
@patch("lib.firewall.config_get")
@patch("lib.firewall.get_config")
@patch("lib.firewall.get_state")
def test_detects_interface_drift(self, mock_state, mock_cfg):
mock_cfg.return_value = {
@@ -350,7 +350,7 @@ class TestConfigPending:
assert result["needs_apply"] is True
assert any(c["type"] == "interfaces" for c in result["pending"])
@patch("lib.firewall.config_get")
@patch("lib.firewall.get_config")
@patch("lib.firewall.get_state")
def test_in_sync(self, mock_state, mock_cfg):
mock_cfg.return_value = {
@@ -374,7 +374,7 @@ class TestConfigPending:
result = firewall.config_pending()
assert result["needs_apply"] is False
@patch("lib.firewall.config_get")
@patch("lib.firewall.get_config")
@patch("lib.firewall.get_state")
def test_detects_services_drift(self, mock_state, mock_cfg):
mock_cfg.return_value = {
@@ -398,7 +398,7 @@ class TestConfigPending:
result = firewall.config_pending()
assert any(c["type"] == "services" for c in result["pending"])
@patch("lib.firewall.config_get")
@patch("lib.firewall.get_config")
@patch("lib.firewall.get_state")
def test_detects_unmanaged_zones(self, mock_state, mock_cfg):
mock_cfg.return_value = {"zones": {}}
@@ -416,7 +416,7 @@ class TestConfigPending:
class TestConfigEmptyZones:
@patch("lib.firewall.config_get")
@patch("lib.firewall.get_config")
@patch("lib.firewall.save_backup")
@patch("lib.firewall.get_available_zones")
@patch("lib.firewall.create_zone")