diff options
| -rw-r--r-- | docs/dashboard.md | 7 | ||||
| -rw-r--r-- | docs/openapi.yaml | 1 | ||||
| -rw-r--r-- | src/api/dashboard/dashboard.html | 44 | ||||
| -rw-r--r-- | src/api/login.rs | 1 |
4 files changed, 48 insertions, 5 deletions
diff --git a/docs/dashboard.md b/docs/dashboard.md index 1a0ae86..7f64034 100644 --- a/docs/dashboard.md +++ b/docs/dashboard.md @@ -21,8 +21,11 @@ dashboard without one is sent to. There, a user logs in with its password [`access.md`](access.md), Tokens), or any other. The page keeps a session for the browser, a cookie no script reads and no other site's page sends, renewed while the dashboard is open; it ends with the token's lifetime, or with "Log out of this -browser". The page and the login page run their own script only (a content security -policy naming it by its hash), and no other page may frame them. The API listens on localhost unless `--bind-addr` says otherwise; +browser" on the login page, or "log out" on the dashboard, which ends this browser's +session only. Where the page got in by what it can't end, the browser's password or +the proxy's, it says so beside the user's name. The page and the login page run their +own script only (a content security policy naming it by its hash), and no other page +may frame them. The API listens on localhost unless `--bind-addr` says otherwise; to look at a node on another machine, forward the port (`ssh -L 3000:localhost:3000 <machine>`) or put a TLS proxy in front ([`operations.md`](operations.md), Behind a reverse proxy). diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 13d8a65..2ebf7fc 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1980,6 +1980,7 @@ components: properties: token: { type: string, description: '`ct1.…`: send it as `Authorization: Bearer <token>`.' } user: { type: string } + login: { type: string, description: 'How the user proved it, at the login the session began with: `htpasswd`, `header`, `key`, `token`.' } issuer: { type: string, description: The node that signed it. } expiresAt: { type: integer, description: Unix ms. } sessionExpiresAt: { type: integer, description: Unix ms. } 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 @@ <span id="node" class="id"></span> <span id="clock-warning"></span> <span id="connection"><span id="dot"></span><span id="connection-text">connecting…</span></span> + <span id="who" class="gloss"></span> + <button id="logout" class="toggle" type="button" hidden>log out</button> </header> <main> <section id="map"> @@ -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<Response, ApiError> { Ok(Json(json!({ "token": token::issue(key, claims), "user": claims.user, + "login": claims.login, "issuer": claims.issuer, "expiresAt": claims.exp, "sessionExpiresAt": claims.session_exp, |
