From b99e7ddf38233120ea19485968c12c5548e7fa61 Mon Sep 17 00:00:00 2001 From: tv Date: Sat, 7 Nov 2026 22:35:22 +0100 Subject: auth: a login page and browser sessions GET /login without authentication: log in by password or with a token (from a login by key). POST/DELETE /auth/session keep the token as an HttpOnly, SameSite=Strict cookie; the dashboard sends a browser without a user there and renews the session while open. Auth plan M2/M3 (browsers). --- src/api/dashboard/dashboard.html | 11 ++++-- src/api/dashboard/login.html | 82 ++++++++++++++++++++++++++++++++++++++++ src/api/dashboard/mod.rs | 7 ++++ 3 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 src/api/dashboard/login.html (limited to 'src/api/dashboard') diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html index 300f4fe..8146f32 100644 --- a/src/api/dashboard/dashboard.html +++ b/src/api/dashboard/dashboard.html @@ -2276,8 +2276,9 @@ function unapplySubscriptions() { } overlaid.clear(); } -// Tokens: this page's, from logging in on this node (the browser's password for it, or -// the proxy's), and a narrower one, reading the member list only, to send to others. +// Tokens: this page's, from logging in on this node (the browser's password for it, the +// proxy's, or the session the login page began), and a narrower one, reading the member +// list only, to send to others. // Each refreshed half way through its life. let token = null, narrowToken = null, tokenWhy = "not logged in yet", tokenTimer = null; const READ_MEMBERS = [{ path: ["_continuum", "members"], rights: ["read"] }]; @@ -2293,9 +2294,11 @@ async function tokenCall(url, bearer, body) { async function obtainTokens() { clearTimeout(tokenTimer); try { + // Refreshed as the session (`POST /auth/session`), whose cookie lets the page in; + // the first: by the browser's password or the proxy's, else by that cookie. token = token && token.expiresAt > Date.now() + 60000 - ? await tokenCall("../auth/refresh", token.token) - : await tokenCall("../auth/login"); + ? await tokenCall("../auth/session", token.token) + : await tokenCall("../auth/login").catch(() => tokenCall("../auth/session")); narrowToken = await tokenCall("../auth/refresh", token.token, { limit: READ_MEMBERS }); tokenWhy = null; const life = Math.min(token.expiresAt, narrowToken.expiresAt) - Date.now(); diff --git a/src/api/dashboard/login.html b/src/api/dashboard/login.html new file mode 100644 index 0000000..77668ee --- /dev/null +++ b/src/api/dashboard/login.html @@ -0,0 +1,82 @@ + + + + + +continuum: log in + + + +
+

Log in to continuum

+
+ + + +
+

or with a token, from continuum-signed login:

+
+ + +
+ + +
+ + + diff --git a/src/api/dashboard/mod.rs b/src/api/dashboard/mod.rs index 9cb4ea4..24fdacb 100644 --- a/src/api/dashboard/mod.rs +++ b/src/api/dashboard/mod.rs @@ -602,6 +602,13 @@ fn page() -> &'static (String, String) { }) } +/// `GET /login`: a page to log in on, by password or with a token, without +/// authentication (it holds nothing): it keeps a session for the browser +/// (`POST /auth/session`) and opens the dashboard. +pub(crate) async fn login_page() -> Html<&'static str> { + Html(include_str!("login.html")) +} + /// `GET /admin/dashboard`: the page. async fn page_html() -> Html<&'static str> { Html(page().1.as_str()) -- cgit v1.3.1