mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-05 07:41:39 +00:00
Phase 3, Tasks 3.2 + 3.3 + 3.4. These three pieces are mutually dependent so they land together. middleware.py - gated_auth_middleware engages when app.state.auth_required is True. Allowlists /login, /auth/*, /api/auth/providers, and static asset paths; everything else demands a valid session_at cookie. Verifies by trying every registered provider's verify_session in turn (multi- provider stack); attaches verified Session to request.state.session. Returns 401 JSON for /api/* and 302 -> /login for HTML. ProviderError during verify -> 503. routes.py - APIRouter with: GET /login server-rendered HTML GET /auth/login?provider=N 302 to IDP + PKCE cookie GET /auth/callback?code,state completes login, sets session cookies POST /auth/logout clears cookies + best-effort revoke GET /api/auth/providers public bootstrap endpoint (503 if zero) GET /api/auth/me verified session as JSON (auth-required) login_page.py - Inline-CSS HTML template, no React, no JavaScript. web_server.py - Mounted gated_auth_middleware between host_header and auth_middleware (FastAPI runs middlewares in registration order: host check -> cookie auth -> token auth). auth_middleware short-circuits when auth_required so cookie auth is authoritative in gated mode. Router is included before mount_spa so the catch-all doesn't swallow /login or /auth/*. 17 new behavioural tests; loopback regression harness still green.
104 lines
3.1 KiB
Python
104 lines
3.1 KiB
Python
"""Server-rendered /login page.
|
|
|
|
No React, no JavaScript dependency. Listed providers come from the
|
|
registry; clicking a provider sends a GET to
|
|
``/auth/login?provider=<name>``.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import html
|
|
|
|
from hermes_cli.dashboard_auth import list_providers
|
|
|
|
# Inline minimal CSS. The dashboard's full skin lives in the React
|
|
# bundle, which we deliberately do NOT load here — the login page must
|
|
# not depend on the SPA build being present or on the injected session
|
|
# token.
|
|
_LOGIN_HTML_TEMPLATE = """\
|
|
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Sign in — Hermes Agent</title>
|
|
<style>
|
|
:root {{
|
|
--bg: #0a0a0b;
|
|
--fg: #e5e5e7;
|
|
--accent: #f97316;
|
|
--border: #27272a;
|
|
}}
|
|
html, body {{
|
|
margin: 0; padding: 0; height: 100%;
|
|
background: var(--bg); color: var(--fg);
|
|
font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
}}
|
|
main {{
|
|
max-width: 28rem; margin: 10vh auto; padding: 2rem;
|
|
border: 1px solid var(--border); border-radius: 0.75rem;
|
|
background: rgba(255,255,255,0.02);
|
|
}}
|
|
h1 {{ margin: 0 0 0.5rem; font-size: 1.5rem; }}
|
|
p {{ margin: 0 0 1.5rem; opacity: 0.7; }}
|
|
.provider-list {{ display: grid; gap: 0.75rem; }}
|
|
.provider-btn {{
|
|
display: block; width: 100%; box-sizing: border-box;
|
|
padding: 0.875rem 1rem; text-align: center;
|
|
background: var(--accent); color: #0a0a0b;
|
|
font-weight: 600; font-size: 1rem;
|
|
border-radius: 0.5rem; text-decoration: none;
|
|
border: 0; cursor: pointer;
|
|
}}
|
|
.provider-btn:hover {{ filter: brightness(1.1); }}
|
|
footer {{
|
|
margin-top: 2rem; font-size: 0.875rem;
|
|
opacity: 0.5; text-align: center;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<h1>Sign in to Hermes Agent</h1>
|
|
<p>Choose a sign-in method to continue.</p>
|
|
<div class="provider-list">
|
|
{provider_buttons}
|
|
</div>
|
|
<footer>This dashboard is bound to a non-loopback host.<br>
|
|
Sign-in is required for security.</footer>
|
|
</main>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
_EMPTY_HTML = """\
|
|
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Sign-in unavailable — Hermes Agent</title>
|
|
</head>
|
|
<body><main style="font-family: system-ui; max-width: 36rem; margin: 10vh auto; padding: 2rem;">
|
|
<h1>Sign-in unavailable</h1>
|
|
<p>This dashboard is bound to a non-loopback host but no authentication
|
|
providers are installed.</p>
|
|
<p>Install <code>plugins/dashboard-auth-nous</code> (default) or another
|
|
auth provider, or restart with <code>--insecure</code> to bypass the
|
|
auth gate (not recommended on untrusted networks).</p>
|
|
</main></body></html>
|
|
"""
|
|
|
|
|
|
def render_login_html() -> str:
|
|
"""Return the full HTML for ``GET /login``."""
|
|
providers = list_providers()
|
|
if not providers:
|
|
return _EMPTY_HTML
|
|
|
|
buttons = []
|
|
for p in providers:
|
|
buttons.append(
|
|
f' <a class="provider-btn" '
|
|
f'href="/auth/login?provider={html.escape(p.name, quote=True)}">'
|
|
f'Sign in with {html.escape(p.display_name)}</a>'
|
|
)
|
|
return _LOGIN_HTML_TEMPLATE.format(provider_buttons="\n".join(buttons))
|