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
75 lines
2.4 KiB
Rust
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);
|
|
}
|
|
}
|