feat(dashboard): login-ul cu token, dezactivat implicit
Accesul la /echo trece deja prin `tailscale serve` (tailnet only), deci formularul de token era o a doua autentificare peste una existentă. `DASHBOARD_AUTH` nesetat => `_check_dashboard_cookie` trece mereu: paginile .html nu mai redirectează la /echo/login, POST-urile /api/* nu mai dau 401, iar /echo/login sare direct la destinație. Codul de login rămâne intact — `DASHBOARD_AUTH=on` în dashboard/.env îl reactivează neschimbat. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188YmDTUAzVvZDVh8JL8xSa
This commit is contained in:
@@ -128,11 +128,11 @@ source .venv/bin/activate && pip install -r requirements.txt
|
||||
|
||||
*Notă istorică:* `memory/` era symlink la repo-ul legacy Clawdbot; consolidat în echo-core în migrația OpenClaw (2026-04).
|
||||
|
||||
**Dashboard** (`dashboard/`): Echo Task Board — HTTP API + UI static servit de `dashboard/api.py` pe portul 8088, de obicei în spatele unui reverse proxy la `/echo/`. Logica endpoint-urilor în mixin-uri `dashboard/handlers/*.py`; path-uri centralizate în `dashboard/constants.py`. Template systemd user unit la `dashboard/echo-taskboard.service`. `workspace.html` e hub-ul unificat de proiecte (fostul ralph.html + workspace.html); `/echo/ralph.html` → 302 redirect la `/echo/workspace.html`. Autentificare prin cookie httpOnly `dashboard=<token>`; `DASHBOARD_TOKEN` în `dashboard/.env`.
|
||||
**Dashboard** (`dashboard/`): Echo Task Board — HTTP API + UI static servit de `dashboard/api.py` pe portul 8088, de obicei în spatele unui reverse proxy la `/echo/`. Logica endpoint-urilor în mixin-uri `dashboard/handlers/*.py`; path-uri centralizate în `dashboard/constants.py`. Template systemd user unit la `dashboard/echo-taskboard.service`. `workspace.html` e hub-ul unificat de proiecte (fostul ralph.html + workspace.html); `/echo/ralph.html` → 302 redirect la `/echo/workspace.html`. Autentificarea e **dezactivată implicit** (acces doar prin tailnet); se reactivează cu `DASHBOARD_AUTH=on` în `dashboard/.env`.
|
||||
|
||||
## Dashboard — Note arhitecturale
|
||||
|
||||
**Cookie auth:** httpOnly cookie `dashboard=...`; SameSite=Strict; Path=/echo/. EventSource SSE trimite cookie-ul automat. `DASHBOARD_TOKEN` din `dashboard/.env` — setează o dată, restart service. Resetare: schimbă valoarea + restart.
|
||||
**Cookie auth (off by default):** `DASHBOARD_AUTH` nesetat ⇒ `_check_dashboard_cookie` trece mereu, `/echo/login` redirectează direct la dashboard, POST-urile `/api/*` nu mai cer cookie. Motiv: `tailscale serve` expune `/echo` doar în tailnet, deci autentificarea era dublată. Cu `DASHBOARD_AUTH=on` în `dashboard/.env` revine login-ul: httpOnly cookie `dashboard=...`; SameSite=Strict; Path=/echo/; EventSource SSE trimite cookie-ul automat; `DASHBOARD_TOKEN` din `dashboard/.env` — setează o dată, restart service. Resetare: schimbă valoarea + restart.
|
||||
|
||||
**jsonlock helper (`src/jsonlock.py`):** `read_locked(path)` / `write_locked(path, mutator)` pentru orice scriere la `approved-tasks.json`, `sessions/*.json`. Lock pe sidecar `<path>.lock` (inode stabil chiar și după os.replace). Ordine canonică lock-uri: alfabetic după filename. Re-entrant (threading.local refcount). Expune și `LockTimeoutError`.
|
||||
|
||||
|
||||
@@ -7,6 +7,11 @@ This mixin provides:
|
||||
- _check_dashboard_cookie — used by the global POST middleware (and the
|
||||
SSE GET endpoint) to gate access.
|
||||
|
||||
Authentication is **disabled by default** — the dashboard is only reachable
|
||||
over the Tailscale tailnet, which already authenticates the caller, so the
|
||||
extra token prompt added nothing. Set `DASHBOARD_AUTH=on` in `dashboard/.env`
|
||||
to re-enable the token login (everything below still works unchanged).
|
||||
|
||||
`DASHBOARD_TOKEN` is read once from `dashboard/.env` (loaded into
|
||||
`os.environ` by `dashboard/constants.py` at import time). When the token is
|
||||
not configured we generate a random one at startup, stash it in-process,
|
||||
@@ -34,6 +39,17 @@ _COOKIE_PATH = "/echo/"
|
||||
_DASHBOARD_TOKEN: str | None = None
|
||||
|
||||
|
||||
def _auth_enabled() -> bool:
|
||||
"""True only when `DASHBOARD_AUTH` is explicitly turned on.
|
||||
|
||||
Off by default: access control is delegated to Tailscale. When off,
|
||||
`_check_dashboard_cookie` always passes and `/echo/login` is never shown.
|
||||
"""
|
||||
return os.environ.get("DASHBOARD_AUTH", "").strip().lower() in {
|
||||
"on", "1", "true", "yes",
|
||||
}
|
||||
|
||||
|
||||
def _get_dashboard_token() -> str:
|
||||
"""Return the dashboard token (cached). Generates a random one if absent.
|
||||
|
||||
@@ -79,7 +95,12 @@ class AuthHandlers:
|
||||
|
||||
# ── helpers ────────────────────────────────────────────────────────
|
||||
def _check_dashboard_cookie(self) -> bool:
|
||||
"""Return True if the request carries a valid `dashboard` cookie."""
|
||||
"""Return True if the request carries a valid `dashboard` cookie.
|
||||
|
||||
Always True when auth is disabled (the default) — see `_auth_enabled`.
|
||||
"""
|
||||
if not _auth_enabled():
|
||||
return True
|
||||
raw = self.headers.get("Cookie", "") or ""
|
||||
cookies = _parse_cookie_header(raw)
|
||||
provided = cookies.get(_COOKIE_NAME, "")
|
||||
@@ -114,6 +135,15 @@ class AuthHandlers:
|
||||
SameSite=Strict; Path=/echo/ so it scopes to the dashboard reverse
|
||||
proxy mount.
|
||||
"""
|
||||
if not _auth_enabled():
|
||||
# Nothing to log into — bounce straight to the dashboard.
|
||||
self.send_response(302)
|
||||
self.send_header("Location", "/echo/workspace.html")
|
||||
self.send_header("Content-Length", "0")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
# Accept JSON body too (login.html might POST JSON in Lane B2)
|
||||
ctype = (self.headers.get("Content-Type", "") or "").lower()
|
||||
if "application/json" in ctype:
|
||||
|
||||
Reference in New Issue
Block a user