- Add session_id to refresh tokens and enforce it during validation,
preventing stolen refresh tokens from being usable without the
originating browser session
- Set router.isAuthenticated via auth:login event after successful
login (previously only set at page load)
- Add console.warn logging to WS message parse/handler errors
- Improve _refreshPromise error handling in token refresh flow
- Document rate limiter in-memory limitation and CSP connect-src
same-origin requirement
- Add 3 tests for session-bound refresh token validation
Session binding was bypassable: if the X-Session-Id header was absent,
validate_token skipped the check entirely, allowing a stolen JWT to be
used without the originating session.
Server-side: reject 401 early in Flask middleware and daemon WebSocket
handler when X-Session-Id is missing, before calling validate_token.
Updated validate_token to always enforce session_id matching for access
tokens (refresh tokens are unaffected as they carry no session_id claim).
Frontend: removed dead if (stored.session_id) guards in api.js since
the header is now always required. Added X-Session-Id to logout request
headers and always store session_id on login/refresh.
webui/static/hoover/api.js
Guard retryRes.json() with .catch(() => null) so non-JSON
responses (e.g. nginx 502/503) don't throw and lose the
actual status code. Falls back to 'HTTP <status>' error string.
lib/db_sqlite.py
Replace unsafe sql.split(';') loop with conn.executescript()
which properly handles semicolons inside string literals.
webui/server.py
Add _AUTH_PERSONAL set and _is_personal_auth() so personal
auth operations (session, password, logout, webauthn creds)
skip subsystem permission checks. Users with only firewall:read
can now manage their own credentials without needing auth:rw.
Apply ruff line-wrapping formatting to docs and test files.
Clarify auth middleware: extract user_permissions once before
subsystem check, removing conditional variable scoping.
- update-vendor.sh now creates webui/vendor symlinks (htm.js)
- install.sh calls update-vendor.sh after package install
- Add vendor/.empty and webui/vendor/.empty as directory placeholders in git
Replace the legacy top-level management key with a unified paths-based
model. Each domain now contains a paths map where each entry defines its
own backend, auth, headers, and flags (is_management, is_websocket).
- Add _migrate_config() to auto-migrate legacy formats on first load
- Remove set_management_proxy() and POST_NGINX_MANAGEMENT endpoint
- Update server_block.conf template to iterate paths with per-location auth
- Update daemon handler, API blueprint, state collector, and install script
- Add server config generation tests for paths, WebSocket, auth inheritance
- Update frontend proxy page to display per-path rows with flags
The SPA uses hash-based routing, so the catch-all route was dead code.
Replace with explicit routes for / and /vendor/<path>, plus a 404
handler that returns JSON for /api/ paths and empty HTML otherwise.
Phase 1-4: Networkd subsystem
- lib/network.py: systemd-networkd config renderer (.network INI files)
with full schema support: [Match], [Link], [Network], [Address], [Route],
[DHCPv4], [DHCPv6] sections. One Address/=DNS= line per value per spec.
Route sections use #N suffix per systemd.syntax(7).
- lib/network.py: generate_network_files() with 50-<name>.network prefix
and stale file cleanup
- lib/network.py: collect_upstream_dns() filters local/private DNS
- lib/network.py: infer_dhcp_ranges() and infer_zones() helpers
- daemon/handlers/network.py: routes for GET/POST /network/interfaces
and full apply with DNS upstream sync to dnsmasq
- webui/api/network.py: Flask blueprint for /api/network/* endpoints
- webui/api: interfaces page updated with IP config inline editing
- lib/state.py: networkd collector using parse_networkctl_status()
- system/sudoers.d/vacuum-walld: networkctl + systemd-network rules
- system/systemd/vacuum-walld.service: ReadWritePaths for /etc/systemd/network
- install.sh: ACME email now optional, configured from WebUI
- lib/acme.py: get_email() falls back to declarative config
Phase 5: Code review fixes
- daemon/server.py: path params now win over JSON body and query params
in request body merge (prevents config save name override)
- daemon/server.py: remove dead 'import re'
- daemon/handlers/network.py: replace Path.mkdir() with sudo mkdir
for /etc/systemd/network (ProtectSystem=strict compatibility)
- system/sudoers.d/vacuum-walld: pin systemctl to specific commands
(reload/is-active dnsmasq instead of wildcard)
- system/sudoers.d/vacuum-walld: restore !requiretty and section comment
- lib/network.py: remove unused _MANAGEMENT_PORTS constant
- webui/api/network.py: remove redundant body[\name\] = name in save_interface
Tests: 332 passing (110 new/updated), ruff clean
Add comprehensive docstrings to firewall, DHCP, proxy, wireguard, certs,
and logs API endpoints. Document parameters, return values, and error cases
for the documentation system.
- Add daemon/ module with aiohttp server, sync client, and handler registry
- Add daemon/handlers/ for privileged operations (acme, dnsmasq, firewall, logs, nginx, wireguard)
- Add system/acme-deploy.py, vacuum-walld sudoers and systemd service
- Update API routes to use daemon client instead of lib/ directly
- Update lib/, tests/, and webui/ for new architecture
- Update docs and deployment scripts
- 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
- 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
New two-step config flow: POST /config saves desired state to
config/firewall/config.json, GET /config/pending diffs against live
firewalld state, POST /config/apply synchronizes live state. Adds target
normalization helpers and full test coverage for config CRUD and pending
diff logic.
- dashboard.html: Fix zones, leases, wg_status, cert key names, add services var
- server.py: Pass services to dashboard template via _get_service_status()
- lib/acme.py: Fix dead third date format (%Y%m%d%H%M%z) using astimezone(UTC)
- lib/wireguard.py: Add -- separator to cp command to match sudoers rule
- lib/nginx.py: Replace shallow dict.copy() with {**...} for DEFAULT_SSL
- AGENTS.md: Update test count 149 -> 154
- docs/api.md: Rename cert field expiry -> expires_at