From 0f55d5741b3dfda617670f92a77c3cd6e462be6d Mon Sep 17 00:00:00 2001 From: tv Date: Sun, 8 Nov 2026 15:36:49 +0000 Subject: dashboard: log out of this browser, and say what can't be The dashboard had no way to log out; only the login page did. It now shows the user beside the connection, and "log out" where the page got in by its session: that ends this browser's session (DELETE /auth/session), other sessions of the user going on, and opens the login page. Where it got in by what it can't end, the browser's password or the proxy's, it says so instead (the token answer now names `login`, how the user proved it). After logging out, a page still let in by the browser's password says so too. A list of a user's sessions, to end one by one, is in future-work.md. --- src/api/dashboard/dashboard.html | 44 +++++++++++++++++++++++++++++++++++++--- src/api/login.rs | 1 + 2 files changed, 42 insertions(+), 3 deletions(-) (limited to 'src/api') diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html index 0f31d7c..cad0bcf 100644 --- a/src/api/dashboard/dashboard.html +++ b/src/api/dashboard/dashboard.html @@ -306,6 +306,8 @@ connecting… + +
@@ -2310,14 +2312,49 @@ async function tokenCall(url, bearer, body) { if (!response.ok) throw new Error(response.status + " " + (answer.error || response.statusText)); return answer; } +// How the page got in: by its session ("session"), which logging out ends, or by what +// it can't end (`login` of `POST /auth/login`): the browser's password ("htpasswd"), the +// proxy's ("header"). +let loggedIn = null; +function showWho() { + const who = document.getElementById("who"); + const by = { htpasswd: " (by this browser's password: close the browser to log out)", + header: " (by your proxy: log out there)" }; + who.textContent = token ? token.user + (by[loggedIn] || "") : ""; + document.getElementById("logout").hidden = !token || loggedIn !== "session"; +} +document.getElementById("logout").addEventListener("click", async () => { + // This browser's session ends; other sessions of the user go on. + await fetch("../auth/session", { method: "DELETE", credentials: "same-origin" }).catch(() => {}); + clearTimeout(tokenTimer); + token = narrowToken = null; + // In still, by what the page can't end: said so; else to the login page. + try { + token = await tokenCall("../auth/login"); + loggedIn = token.login; + showWho(); + obtainTokens(); + } catch (_) { + location.assign("../login"); + } +}); async function obtainTokens() { clearTimeout(tokenTimer); try { // Refreshed as the session (`POST /auth/session`), whose cookie lets the page in; // the first: by that cookie, else by the browser's password or the proxy's. - token = token && token.expiresAt > Date.now() + 60000 - ? await tokenCall("../auth/session", token.token) - : await tokenCall("../auth/session").catch(() => tokenCall("../auth/login")); + if (token && token.expiresAt > Date.now() + 60000) { + token = await tokenCall("../auth/session", token.token); + } else { + try { + token = await tokenCall("../auth/session"); + loggedIn = "session"; + } catch (_) { + token = await tokenCall("../auth/login"); + loggedIn = token.login; + } + } + showWho(); narrowToken = await tokenCall("../auth/refresh", token.token, { limit: READ_MEMBERS }); tokenWhy = null; const life = Math.min(token.expiresAt, narrowToken.expiresAt) - Date.now(); @@ -2330,6 +2367,7 @@ async function obtainTokens() { } catch (error) { token = narrowToken = null; tokenWhy = "no token: " + error.message; + showWho(); tokenTimer = setTimeout(obtainTokens, 30000); } } diff --git a/src/api/login.rs b/src/api/login.rs index 73304e8..4b42178 100644 --- a/src/api/login.rs +++ b/src/api/login.rs @@ -179,6 +179,7 @@ fn answer(state: &AppState, claims: &Claims) -> Result { Ok(Json(json!({ "token": token::issue(key, claims), "user": claims.user, + "login": claims.login, "issuer": claims.issuer, "expiresAt": claims.exp, "sessionExpiresAt": claims.session_exp, -- cgit v1.3.1