From fd55a719589b1ca369b6e34765ca64618e690d56 Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Sat, 28 Mar 2026 22:40:36 -0700 Subject: [PATCH] feat: serve branded login.html with injected window.MUXPLEX_AUTH Replace login_page() stub with handler that: - Reads login.html from _FRONTEND_DIR - Determines username (PAM mode: current user, else empty string) - Builds mode_data with json.dumps({mode, user}) - Injects before - Returns HTMLResponse(html) Move _FRONTEND_DIR definition above routes so login_page() can reference it. Keep app.mount() at bottom and /auth/mode endpoint. Add test: test_get_login_injects_muxplex_auth - Asserts status 200, MUXPLEX_AUTH in text, '"mode"' in text All 45 tests pass. --- muxplex/main.py | 25 +++++++++++++++---------- muxplex/tests/test_api.py | 8 ++++++++ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/muxplex/main.py b/muxplex/main.py index fb2a0da..12070d2 100644 --- a/muxplex/main.py +++ b/muxplex/main.py @@ -9,6 +9,7 @@ Background poll loop reconciles tmux session state every POLL_INTERVAL seconds. import asyncio import contextlib +import json import logging import os import pathlib @@ -266,6 +267,13 @@ class HeartbeatPayload(BaseModel): last_interaction_at: float +# --------------------------------------------------------------------------- +# Frontend directory +# --------------------------------------------------------------------------- + +_FRONTEND_DIR = pathlib.Path(__file__).parent / "frontend" + + # --------------------------------------------------------------------------- # Routes # --------------------------------------------------------------------------- @@ -476,16 +484,14 @@ async def terminal_ws_proxy(websocket: WebSocket) -> None: @app.get("/login", response_class=HTMLResponse) async def login_page(): - """Stub login page — replaced in Phase 2 with branded login.html.""" - return HTMLResponse( - "" - "

muxplex login

" - "
" - "" - "" - "
" - "" + """Serve branded login.html with injected window.MUXPLEX_AUTH containing auth mode and username.""" + html = (_FRONTEND_DIR / "login.html").read_text() + username = pwd.getpwuid(os.getuid()).pw_name if _auth_mode == "pam" else "" + mode_data = json.dumps({"mode": _auth_mode, "user": username}) + html = html.replace( + "", f"" ) + return HTMLResponse(html) @app.post("/login") @@ -545,5 +551,4 @@ async def auth_mode_endpoint(): # Static file serving — MUST come after all API routes (first-match-wins) # --------------------------------------------------------------------------- -_FRONTEND_DIR = pathlib.Path(__file__).parent / "frontend" app.mount("/", StaticFiles(directory=str(_FRONTEND_DIR), html=True), name="frontend") diff --git a/muxplex/tests/test_api.py b/muxplex/tests/test_api.py index 189a6df..0a543ea 100644 --- a/muxplex/tests/test_api.py +++ b/muxplex/tests/test_api.py @@ -707,6 +707,14 @@ def test_get_auth_mode_returns_json(client): assert data["mode"] in ("pam", "password") +def test_get_login_injects_muxplex_auth(client): + """GET /login returns 200 with MUXPLEX_AUTH injected into HTML.""" + response = client.get("/login") + assert response.status_code == 200 + assert "MUXPLEX_AUTH" in response.text + assert '"mode"' in response.text + + # --------------------------------------------------------------------------- # POST /login # ---------------------------------------------------------------------------