aboutsummaryrefslogtreecommitdiffstats
path: root/src/api
diff options
context:
space:
mode:
Diffstat (limited to 'src/api')
-rw-r--r--src/api/dashboard/dashboard.html17
-rw-r--r--src/api/dashboard/login.html18
-rw-r--r--src/api/login.rs26
3 files changed, 52 insertions, 9 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index fa4d436..17ff44b 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -2058,7 +2058,7 @@ if (handOver) {
// The fragment that opens another node's dashboard (`id`'s) as this page is now
// (`handOver`). What is in focus stays, but `id` itself: its page is its own. Also this
// node's own page, reloaded for a new version (`id` null).
-function handOff(id) {
+function handOff(id, handed) {
const settings = {};
try {
for (let i = 0; i < localStorage.length; i++) {
@@ -2076,6 +2076,9 @@ function handOff(id) {
legend: !document.getElementById("legends").hidden,
figures: [...open],
document: [...documentOpen],
+ // A token reading only, for five minutes: the other node's login page begins a
+ // session with it (`login.html`), where the browser has none there.
+ token: handed || undefined,
}));
}
// `[x, y]` turned by `angle`.
@@ -2446,11 +2449,19 @@ addEventListener("keydown", event => {
// opened in this page; else its halo flashes red, larger than usual, and "no dashboard"
// rises from it in the same red, both fading out. This node's own: here already.
const FLASH_MS = 1200;
-function openDashboard(id) {
+const READ_ALL = [{ path: [], rights: ["read"] }, { path: ["_continuum"], rights: ["read"] }];
+async function openDashboard(id) {
const member = state.graph.members[id];
if (!member || member.self) return;
if (member.dashboard) {
- location.assign(member.dashboard.split("#")[0] + handOff(id));
+ let handed = null;
+ if (token) {
+ try {
+ handed = (await tokenCall("../auth/refresh", token.token,
+ { limit: READ_ALL, lifetime: 300 })).token;
+ } catch (_) { /* the other dashboard asks for a login then */ }
+ }
+ location.assign(member.dashboard.split("#")[0] + handOff(id, handed));
return;
}
const layer = document.getElementById("flashes");
diff --git a/src/api/dashboard/login.html b/src/api/dashboard/login.html
index 77668ee..e09d31c 100644
--- a/src/api/dashboard/login.html
+++ b/src/api/dashboard/login.html
@@ -73,6 +73,24 @@ document.getElementById("token").addEventListener("submit", async event => {
why.textContent = "";
try { await begin(event.target.token.value.trim()); } catch (error) { failed(error); }
});
+// Opened from another member's dashboard (`handOff`), sent here for want of a session:
+// its token, reading only, begins one; the rest goes on to the dashboard.
+(async () => {
+ const HAND_OFF = "#continuum=";
+ if (!location.hash.startsWith(HAND_OFF)) return;
+ const fragment = location.hash;
+ try { history.replaceState(null, "", location.pathname + location.search); } catch (_) {}
+ let handed;
+ try { handed = JSON.parse(decodeURIComponent(fragment.slice(HAND_OFF.length))); } catch (_) { return; }
+ if (!handed || typeof handed.token !== "string") return;
+ try {
+ await call("auth/session", "POST", "Bearer " + handed.token);
+ delete handed.token;
+ location.replace("admin/dashboard" + HAND_OFF + encodeURIComponent(JSON.stringify(handed)));
+ } catch (error) {
+ why.textContent = "The token handed over isn't taken (" + error.message + "): log in.";
+ }
+})();
document.getElementById("out").addEventListener("click", async () => {
await fetch("auth/session", { method: "DELETE", credentials: "same-origin" }).catch(() => {});
why.textContent = "Logged out of this browser.";
diff --git a/src/api/login.rs b/src/api/login.rs
index 1907bac..3b4ee32 100644
--- a/src/api/login.rs
+++ b/src/api/login.rs
@@ -146,6 +146,9 @@ pub(crate) async fn bearer(
pub(crate) struct LimitRequest {
#[serde(default)]
limit: Option<Vec<CapRequest>>,
+ /// A shorter life than `--token-lifetime`, in seconds: a token to hand over, say.
+ #[serde(default)]
+ lifetime: Option<u64>,
}
impl LimitRequest {
@@ -209,7 +212,8 @@ pub(crate) async fn login(
Some("log in with a password or through the proxy; a token is refreshed at /auth/refresh".to_owned()),
));
}
- let limit = body.map(|Json(body)| body).unwrap_or_default().limit()?;
+ let body = body.map(|Json(body)| body).unwrap_or_default();
+ let limit = body.limit()?;
let (swarm, now) = issuing(&state).await?;
{
let replica = state.doc.read().await;
@@ -222,11 +226,17 @@ pub(crate) async fn login(
));
}
}
- let claims = new_login(&state, swarm, now, &user.0, via.0, limit);
+ let claims = new_login(&state, swarm, now, &user.0, via.0, limit, body.lifetime);
info!(user = %user.0, login = via.0, session = %claims.session, "Logged in");
answer(&state, &claims)
}
+/// How long a token lives: `--token-lifetime`, or less if `asked` (seconds) is less.
+fn lifetime_ms(state: &AppState, asked: Option<u64>) -> u64 {
+ let lifetime = state.token_lifetime.as_millis() as u64;
+ asked.map_or(lifetime, |secs| secs.saturating_mul(1000).min(lifetime))
+}
+
/// The claims of a new login of `user`, by `login`, now: a session of its own.
fn new_login(
state: &AppState,
@@ -235,6 +245,7 @@ fn new_login(
user: &str,
login: &str,
limit: Option<Vec<Capability>>,
+ lifetime: Option<u64>,
) -> Claims {
let session_exp = now.saturating_add(state.session_lifetime.as_millis() as u64);
Claims {
@@ -245,7 +256,7 @@ fn new_login(
login: login.to_owned(),
iat: now,
exp: now
- .saturating_add(state.token_lifetime.as_millis() as u64)
+ .saturating_add(lifetime_ms(state, lifetime))
.min(session_exp),
session: token::new_session(),
session_exp,
@@ -292,6 +303,7 @@ pub(crate) async fn login_key(
) -> Result<Response, ApiError> {
let limit = LimitRequest {
limit: request.limit,
+ lifetime: None,
}
.limit()?;
let (swarm, now) = issuing(&state).await?;
@@ -322,7 +334,7 @@ pub(crate) async fn login_key(
ApiError(StatusCode::UNAUTHORIZED, Some(why))
})?;
}
- let claims = new_login(&state, swarm, now, &request.user, "key", limit);
+ let claims = new_login(&state, swarm, now, &request.user, "key", limit, None);
info!(user = %request.user, login = "key", session = %claims.session, "Logged in");
answer(&state, &claims)
}
@@ -396,7 +408,9 @@ async fn refreshed(
Some("the login ended: log in again".to_owned()),
));
}
- let limit = match body.map(|Json(body)| body).unwrap_or_default().limit()? {
+ let body = body.map(|Json(body)| body).unwrap_or_default();
+ let lifetime = body.lifetime;
+ let limit = match body.limit()? {
None => earlier.limit.clone(),
Some(limit) => {
if earlier
@@ -418,7 +432,7 @@ async fn refreshed(
issuer: state.local_id().to_owned(),
iat: now,
exp: now
- .saturating_add(state.token_lifetime.as_millis() as u64)
+ .saturating_add(lifetime_ms(state, lifetime))
.min(earlier.session_exp),
limit,
..(**earlier).clone()