summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/ui
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/ui')
-rw-r--r--packages/meshbay-node/src/meshbay_node/ui/app.py68
1 files changed, 68 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/ui/app.py b/packages/meshbay-node/src/meshbay_node/ui/app.py
index 28654df..b78f78d 100644
--- a/packages/meshbay-node/src/meshbay_node/ui/app.py
+++ b/packages/meshbay-node/src/meshbay_node/ui/app.py
@@ -23,6 +23,7 @@ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Query
from fastapi.responses import HTMLResponse, JSONResponse
from meshbay_node import __version__
+from meshbay_node.config import DEFAULT_CONFIG_PATH
from meshbay_common.crypto import generate_gek, wrap_gek_aes
from meshbay_common.join import ROLE_MEMBER, ROLE_OPERATOR
@@ -127,6 +128,73 @@ def create_ui_app(state: dict) -> FastAPI:
})
return {"groups": result}
+ @app.post("/api/groups/attach")
+ async def attach_group(payload: dict):
+ """
+ Write a new [[groups]] block into node.toml.
+
+ The name-to-id lookup happens here because this process is the one logged
+ into the hub. Nothing is created on the hub: the group already exists,
+ this only tells the node to host it.
+ """
+ name = (payload.get("name") or "").strip()
+ shared_dir = (payload.get("shared_dir") or "").strip()
+ if not name or not shared_dir:
+ return JSONResponse({"error": "name and shared_dir are required"}, 400)
+
+ config = state.get("config")
+ if not config:
+ return JSONResponse({"error": "No config loaded"}, 503)
+
+ hub = state.get("hub")
+ if not hub or not hub._session:
+ return JSONResponse({"error": "Hub not connected"}, 503)
+ try:
+ mine = await hub.list_my_groups()
+ except Exception as e:
+ return JSONResponse({"error": f"Could not list groups: {e}"}, 502)
+
+ match = [g for g in mine if g["id"] == name or g["name"] == name]
+ if not match:
+ return JSONResponse({
+ "error": f"No group of yours is called {name!r}",
+ "available": [{"name": g["name"], "id": g["id"]} for g in mine],
+ }, 404)
+ if len(match) > 1:
+ return JSONResponse({
+ "error": f"Several of your groups are called {name!r} — use the id",
+ "available": [{"name": g["name"], "id": g["id"]} for g in match],
+ }, 409)
+ group = match[0]
+
+ if any(g.id == group["id"] for g in config.groups):
+ return JSONResponse(
+ {"error": f"{group['name']!r} is already hosted by this node"}, 409)
+
+ path = Path(shared_dir).expanduser()
+ try:
+ path.mkdir(parents=True, exist_ok=True)
+ except OSError as e:
+ return JSONResponse({"error": f"Cannot create {path}: {e}"}, 400)
+
+ # Appended as text rather than re-serialised: node.toml is hand-written
+ # and full of comments explaining decisions, and a round trip through a
+ # TOML writer would throw all of that away.
+ conf_path = Path(state.get("config_path") or DEFAULT_CONFIG_PATH)
+ block = (f'\n[[groups]]\n'
+ f'id = "{group["id"]}"\n'
+ f'name = "{group["name"]}"\n'
+ f'shared_dir = "{path}"\n'
+ f'visibility = "{group.get("visibility", "private")}"\n')
+ try:
+ with conf_path.open("a") as f:
+ f.write(block)
+ except OSError as e:
+ return JSONResponse({"error": f"Cannot write {conf_path}: {e}"}, 500)
+
+ return {"group_id": group["id"], "name": group["name"],
+ "shared_dir": str(path), "config": str(conf_path)}
+
@app.get("/api/groups/{group_id}/files")
async def api_group_files(group_id: str):
groups_ctx = state.get("groups_ctx", {})