src/pages/login.rs
//! Account: who holger says this session is. The passkey session (from the
//! login page, `assets/login.html`) and a pasted bearer land in the same
//! HttpOnly cookie of the console server (A34-38) and ride every call the same
//! way — the rest of the console cannot tell them apart, and holger decides on
//! each call.
use leptos::prelude::*;
use leptos_router::hooks::use_navigate;
use crate::grpc;
/// Paste-a-token login. The token is whatever the server's configured auth
/// methods accept — a static API token or an OIDC/OAuth2 access token issued
/// by the operator's IdP; the console never sees a password and never mints
/// anything itself. Handed to the console server, which keeps it in its HttpOnly
/// cookie (removed by Log out) and sends it on as `authorization: Bearer`.
#[component]
pub fn Login(logged_in: RwSignal<bool>) -> impl IntoView {
crate::webio::set_title("Account");
// The server's view of the current session — rendered above the forms so
// the page doubles as Account.
let whoami = LocalResource::new(move || {
let _ = logged_in.get();
async move { grpc::who_am_i().await.ok() }
});
let token = RwSignal::new(String::new());
let hint: RwSignal<Option<String>> = RwSignal::new(None);
let navigate = use_navigate();
let save = move || {
let t = token.get_untracked();
let t = t.trim();
if t.is_empty() {
hint.set(Some("paste a token first".into()));
return;
}
let (t, navigate) = (t.to_owned(), navigate.clone());
leptos::task::spawn_local(async move {
match crate::session::hold_token(&t).await {
Ok(()) => {
logged_in.set(true);
navigate("/", Default::default());
}
Err(why) => hint.set(Some(why)),
}
});
};
let save_click = save.clone();
// Who is logged in, by the server's session (`crate::session`) — the same
// answer the topbar shows, so /account never offers a login to somebody the
// topbar calls logged in.
let status = LocalResource::new(move || {
let _ = logged_in.get();
async move { crate::session::fetch_status().await }
});
let shown = move || {
let s = status.get().flatten();
crate::session::shown(s.as_ref(), s.as_ref().is_some_and(|s| s.token_held))
};
view! {
<section class="page">
<crate::pages::mine::MineTabs/>
<h2>"Account"</h2>
{move || match shown() {
crate::session::Shown::Session { user, role } => view! {
<dl class="facts">
<dt>"Session"</dt><dd>"logged in with a passkey"</dd>
<dt>"User"</dt><dd>{user}</dd>
<dt>"Role here"</dt><dd>{role}</dd>
</dl>
}.into_any(),
crate::session::Shown::LoggedOut { login } => view! {
<p>"Not logged in. "<a class="btn primary" rel="external" href=login>"Log in with your passkey"</a></p>
}.into_any(),
_ => view! { <span></span> }.into_any(),
}}
// holger's own view of the credential THIS CONSOLE presents — one
// subject for every browser session, so it is labelled as such.
{move || whoami.get().flatten().map(|id| view! {
<h3>"This session"</h3>
{if id.authenticated {
view! { <p>{crate::util::session_words(&id.method, &id.global_role)}</p> }.into_any()
} else {
view! { <p>{format!("You are not signed in. Your role here: {}.", id.global_role)}</p> }.into_any()
}}
<p>{crate::util::access_words(&id.access_words, id.read_gate)}</p>
})}
// The console's own token and SSO forms are a road ONLY on a holger
// without passkeys; on a passkey console they were the "several ways
// in" the owner reported, and they are not offered.
{move || {
let s = shown();
(s.token_forms() && s.offers_login()).then(|| view! {
<h3>"Log in with a token"</h3>
<p class="count measurenote">
"Paste a bearer credential accepted by this server — a static API token, or an \
access token from your identity provider. It is held by this console in a \
cookie no script can read, sent as authorization: Bearer on every call, and \
dropped on Log out. Your role (reader / writer / admin) is the server's mapping, not a \
console setting."
</p>
<div class="toolbar">
<label class="vh" for="token-input">"Bearer token"</label>
<input
id="token-input"
type="password"
placeholder="bearer token…"
prop:value=move || token.get()
on:input=move |ev| token.set(event_target_value(&ev))
on:keydown={
let save = save.clone();
move |ev| { if ev.key() == "Enter" { save(); } }
}
/>
<button on:click={
let save = save_click.clone();
move |_| save()
}>"Log in"</button>
</div>
{move || hint.get().map(|m| view! { <p class="err" role="alert">{m}</p> })}
})
}}
</section>
}
}