Files
tavolo/web/src/auth.rs
T
woggioni 99aa81d682 Redirect to login on 401 and return afterwards
The SPA now sends the user to a new /login page whenever an API call
answers 401, remembering the page they were on. The OIDC callback always
lands back on the configured post-login redirect, so the saved page is
restored client-side once a session exists.

- auth: save/restore the return path in sessionStorage; defer the
  navigation so the failing request can finish first
- api: centralize 401 handling (me() still treats 401 as logged out)
- login page + shared login card, extracted from the lobby
- guard async signal writes so an in-flight request cannot touch a page
  unmounted by the redirect
2026-10-08 21:52:51 +00:00

75 lines
2.4 KiB
Rust

//! Client-side login redirect handling.
//!
//! The OIDC callback always returns the browser to the configured
//! post-login redirect (usually `/`), and the backend's login flow has no
//! per-request `next` support. So when the API answers `401`, the page the
//! user is on is saved in `sessionStorage` and restored once a session
//! exists again.
use sycamore_router::navigate_replace;
/// `sessionStorage` key holding the page to return to after login.
const RETURN_TO_KEY: &str = "tavolo.login.return_to";
fn session_storage() -> Option<web_sys::Storage> {
web_sys::window()?.session_storage().ok().flatten()
}
/// The current page (path plus query), the URL to come back to after
/// login.
pub fn current_path() -> String {
let Some(location) = web_sys::window().map(|w| w.location()) else {
return String::new();
};
format!(
"{}{}",
location.pathname().unwrap_or_default(),
location.search().unwrap_or_default()
)
}
/// Whether a path is a page worth returning to: not the login flow
/// itself, and not an absolute/protocol-relative URL.
fn is_restorable(path: &str) -> bool {
path.starts_with('/')
&& !path.starts_with("//")
&& !path.starts_with("/auth/")
&& !path.starts_with("/api/")
&& path != "/login"
}
/// Handle an expired session: remember the current page and go to the
/// login page.
///
/// The navigation is deferred to a timer so the caller can finish handling
/// the failed response (e.g. set an error) before its page is unmounted.
pub fn redirect_to_login() {
let path = current_path();
if is_restorable(&path) {
if let Some(storage) = session_storage() {
let _ = storage.set_item(RETURN_TO_KEY, &path);
}
}
gloo_timers::callback::Timeout::new(0, move || {
// Several in-flight requests may fail at once; only the first
// takes us to the login page.
if current_path() != "/login" {
navigate_replace("/login");
}
})
.forget();
}
/// The page saved by [`redirect_to_login`], if any.
pub fn saved_return_to() -> Option<String> {
let storage = session_storage()?;
let path = storage.get_item(RETURN_TO_KEY).ok().flatten()?;
is_restorable(&path).then_some(path)
}
/// Drop the saved page, e.g. after it has been restored.
pub fn clear_return_to() {
if let Some(storage) = session_storage() {
let _ = storage.remove_item(RETURN_TO_KEY);
}
}