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
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user