From cfc91e0a424163869c64d30e55d55a53f18a3dbf Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Tue, 1 Sep 2026 14:08:32 +0200 Subject: refactor(node): JSON-only control API, Node page absorbs the admin dashboard Remove the node daemon's server-rendered admin UI (GET / and /audit, the _render_* helpers and inline templates) and the `meshbay-node ui` CLI verb. The loopback control API stays; it is now JSON only, ruff-clean, and 453 lines (was 1074). Also drop three never-wired endpoints (/api/config, /api/chat/history, /ws/chat, plus broadcast_chat) and the pointless 18000/tcp firewall profiles. The desktop client's Node page (static/node-page.js) takes over what the dashboard showed, reorganised into six tabs (Overview, Groups, Roster, Peers, Audit, Settings): - Overview: version, node id, QUIC port, hub, index-cache maintenance - Roster: node-wide view with unpin - Peers and Audit: auto-load on open, no Load button - Audit: real usernames and group names (resolved from the roster and node.toml), Previous/Next pagination newest-first, Export CSV of every matching row - Settings: node settings, STUN, ICE, denylist, then Unlink from hub Backend: audit.get_entries gains `offset`; /api/audit and /api/peers resolve ids to names via a new _display_names helper; CSP tightened to default-src 'none' now that no HTML is served. draft-v6 sections 2.11 and 2.12 corrected -- the Node page uses the loopback API, not MNP. One capability is intentionally dropped: browser-based admin on a headless server. The CLI covers every operation there. See docs/refactor-node-ui.md. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01MQCaZnde4Bjjdu84dhSuF5 --- packages/meshbay-node/src/meshbay_node/ui/app.py | 727 +++-------------------- 1 file changed, 76 insertions(+), 651 deletions(-) (limited to 'packages/meshbay-node/src/meshbay_node/ui/app.py') diff --git a/packages/meshbay-node/src/meshbay_node/ui/app.py b/packages/meshbay-node/src/meshbay_node/ui/app.py index 671597f..6fdc78f 100644 --- a/packages/meshbay-node/src/meshbay_node/ui/app.py +++ b/packages/meshbay-node/src/meshbay_node/ui/app.py @@ -1,34 +1,25 @@ """ -MeshBay Node — local administration web UI (localhost:18000). +MeshBay Node — local control API (loopback, default port 18000). -FastAPI app providing: - - Dashboard: node status, connected peers, group overview - - Groups: file listing, shared directory info - - Peers: connected WebRTC/QUIC clients - - Audit log: IP + action log for legal compliance - - API endpoints for all data (JSON) +A JSON-only FastAPI app: node status, groups and roots, roster and denylist, +node settings, connected peers, and the audit log. It is the single control +plane for the node — the `meshbay-node` CLI and the desktop client's Node page +are both clients of it. (Chat is served to browsers over MNP/WebRTC, not here.) -Served only on 127.0.0.1 — not exposed to the network. -Gated by a per-run session token (11.5.3) — printed at daemon startup. +Served only on 127.0.0.1 — never network-exposed — and every request is gated +by a per-run session token (11.5.3) written to `/ui-token`. There is +no server-rendered UI: the Node page ships in the desktop client (see +`docs/refactor-node-ui.md`). """ import asyncio -import base64 -import json import logging -import time -from html import escape -from pathlib import Path -from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, Query -from fastapi.responses import HTMLResponse, JSONResponse +from fastapi import FastAPI, HTTPException, Query +from fastapi.responses import JSONResponse -from meshbay_node import __version__ -from meshbay_node import ops -from meshbay_node.config import DEFAULT_CONFIG_PATH +from meshbay_node import __version__, ops from meshbay_node.indexer.indexer import DirectoryIndexer -from meshbay_common.crypto import generate_gek, wrap_gek_aes -from meshbay_common.join import ROLE_MEMBER, ROLE_OPERATOR log = logging.getLogger(__name__) @@ -50,6 +41,31 @@ def _op(coro): return run() +async def _display_names(state: dict) -> tuple[dict[str, str], dict[str, str]]: + """(user_id -> username, group_id -> name) for rendering ids a human reads. + + Usernames come from the roster (the node's own record); group names from + node.toml. Both are best-effort — a missing entry just leaves the caller + with the raw id to shorten. + """ + users: dict[str, str] = {} + roster = state.get("roster") + if roster: + try: + for ident in await roster.list_identities(): + if ident.get("username"): + users[ident["user_id"]] = ident["username"] + except Exception: + pass + groups: dict[str, str] = {} + config = state.get("config") + if config: + for g in config.groups: + if getattr(g, "id", None): + groups[g.id] = g.name + return users, groups + + def create_ui_app(state: dict) -> FastAPI: app = FastAPI( @@ -62,14 +78,14 @@ def create_ui_app(state: dict) -> FastAPI: @app.middleware("http") async def _require_session_token(request, call_next): """ - Gate the admin UI behind a per-run token (11.5.3). + Gate the control API behind a per-run token (11.5.3). "localhost only" is weaker than it sounds: any process on the machine can reach it, and a page in the operator's browser can reach it too via DNS rebinding. Since this API can re-initialise a group's GEK and read the audit log, an unauthenticated loopback service is a privilege boundary - waiting to be crossed. The token is printed at startup and accepted as - ?t= or the X-MeshBay-Token header. + waiting to be crossed. The token is written to `/ui-token` at + startup and accepted as ?t= or the X-MeshBay-Token header. """ from fastapi.responses import PlainTextResponse @@ -84,25 +100,17 @@ def create_ui_app(state: dict) -> FastAPI: @app.middleware("http") async def _security_headers(request, call_next): """ - Defence in depth behind the escaping fixes for H2. This UI is unauthenticated - on loopback, so script execution here equals full control of the node admin API. - - Note what this does and does not do: the page relies on inline - -""" - - -def _render_audit_page(token: str = "") -> str: - return _AUDIT_HTML.replace("__TOKEN__", json.dumps(token)) - - -_AUDIT_HTML = """ - - - -MeshBay Node — Audit Log - - - - -
-

Audit Log

- - -
- - - -
- -
- - - -
TimeEventUserIPGroupDetail
-
- - -""" -- cgit v1.2.3