aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/dashboard.md7
-rw-r--r--docs/openapi.yaml1
-rw-r--r--src/api/dashboard/dashboard.html44
-rw-r--r--src/api/login.rs1
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,