"""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=``. Visual styling mirrors the Nous Research design system (the ``@nous-research/ui`` package the React dashboard uses): the same ``Collapse`` / ``Rules Compressed`` typeface, amber-on-dark colour tokens (``#170d02`` / ``#ffac02`` / ``#fff``), uppercase + wide-tracking brand chrome, and the inset-bevel button shadow. Fonts are served out of the SPA's ``/fonts/`` directory which the dashboard-auth gate already allowlists pre-auth (see ``_GATE_PUBLIC_PREFIXES`` in ``middleware.py``), so the page renders without needing the React bundle loaded. Test-stable class names: the existing test suite extracts the ``class="provider-btn"`` anchor href to walk the OAuth flow. That class name MUST NOT change without updating ``tests/hermes_cli/test_dashboard_auth_401_reauth.py``. """ 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. # # Single curly braces are placeholders for ``str.format``; CSS curlies # are doubled (``{{`` / ``}}``). _LOGIN_HTML_TEMPLATE = """\ Sign in — Hermes Agent
NousResearch

Sign in

Choose a sign-in method to continue to the Hermes Agent dashboard.

{provider_buttons}
Public bind · Auth required
""" _EMPTY_HTML = """\ Sign-in unavailable — Hermes Agent

Sign-in unavailable

This dashboard is bound to a non-loopback host but no authentication providers are installed.

Install plugins/dashboard-auth-nous (default) or another auth provider, or restart with --insecure to bypass the auth gate (not recommended on untrusted networks).

""" def render_login_html(*, next_path: str = "") -> str: """Return the full HTML for ``GET /login``. ``next_path`` — when set, the post-login landing path the user originally requested. Threaded into each provider button's ``href`` as a ``next=`` query parameter so the OAuth round trip carries it end-to-end. The caller (``routes.login_page``) is responsible for validating ``next_path`` against the same-origin rules before we emit it; we still HTML-escape it as defence in depth. """ providers = list_providers() if not providers: return _EMPTY_HTML if next_path: # URL-encode then HTML-escape. The URL-encode step matches the # gate's ``_safe_next_target`` output shape (also URL-encoded), # so a value that round-tripped from /login?next=... back into # the button href is byte-identical. from urllib.parse import quote next_qs = f"&next={html.escape(quote(next_path, safe=''), quote=True)}" else: next_qs = "" buttons = [] for p in providers: buttons.append( f' ' f'Sign in with {html.escape(p.display_name)}' ) return _LOGIN_HTML_TEMPLATE.format(provider_buttons="\n".join(buttons))