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:
1 parent
43d5a6e2f9
commit
99aa81d682
10 files changed
+299
-92
No files matched your search
@@ -105,18 +105,29 @@ pub fn HistoryPage() -> View {
|
||||
spawn_local(async move {
|
||||
match api::my_matches(next.as_deref()).await {
|
||||
Ok(p) => {
|
||||
// The page can unmount (e.g. the login redirect) while
|
||||
// the request is in flight.
|
||||
if !page.is_alive() {
|
||||
return;
|
||||
}
|
||||
cursor.set(p.next_cursor.clone());
|
||||
rows.update(|acc| acc.extend(p.results.iter().cloned()));
|
||||
page.set(Some(p));
|
||||
}
|
||||
Err(e) => error.set(Some(e)),
|
||||
Err(e) => {
|
||||
if error.is_alive() {
|
||||
error.set(Some(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
spawn_local(async move {
|
||||
if let Ok(types) = api::game_types().await {
|
||||
game_types.set(types);
|
||||
if game_types.is_alive() {
|
||||
game_types.set(types);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -124,11 +135,18 @@ pub fn HistoryPage() -> View {
|
||||
spawn_local(async move {
|
||||
match api::my_matches(None).await {
|
||||
Ok(p) => {
|
||||
if !page.is_alive() {
|
||||
return;
|
||||
}
|
||||
cursor.set(p.next_cursor.clone());
|
||||
rows.set(p.results.clone());
|
||||
page.set(Some(p));
|
||||
}
|
||||
Err(e) => error.set(Some(e)),
|
||||
Err(e) => {
|
||||
if error.is_alive() {
|
||||
error.set(Some(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+25
-62
@@ -13,21 +13,13 @@ use sycamore_router::navigate;
|
||||
|
||||
use crate::api;
|
||||
use crate::components::card::card_back;
|
||||
use crate::components::login::login_card;
|
||||
use crate::components::toast::toast;
|
||||
use crate::model::{
|
||||
fallback_game_types, is_individual_game, AuthProvider, GameTypeInfo, JoinPreview,
|
||||
MyGame, OptionField, OptionKind, PlayerRating, User,
|
||||
};
|
||||
|
||||
/// Human-readable label for a provider name ("google" -> "Google").
|
||||
fn provider_label(name: &str) -> String {
|
||||
let mut chars = name.chars();
|
||||
match chars.next() {
|
||||
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn json_to_string(value: &serde_json::Value) -> String {
|
||||
match value {
|
||||
serde_json::Value::String(s) => s.clone(),
|
||||
@@ -386,18 +378,30 @@ pub fn LobbyPage() -> View {
|
||||
if me.is_some() {
|
||||
spawn_local(async move {
|
||||
if let Ok(p) = api::my_ratings().await {
|
||||
ratings.set(p.results);
|
||||
if ratings.is_alive() {
|
||||
ratings.set(p.results);
|
||||
}
|
||||
}
|
||||
if let Ok(games) = api::my_games().await {
|
||||
my_games.set(games);
|
||||
if my_games.is_alive() {
|
||||
my_games.set(games);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
user.set(Some(me));
|
||||
// The page may have been unmounted (e.g. by the login
|
||||
// redirect triggered while this request was in flight).
|
||||
if user.is_alive() {
|
||||
user.set(Some(me));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error.set(Some(e));
|
||||
user.set(Some(None));
|
||||
if error.is_alive() {
|
||||
error.set(Some(e));
|
||||
}
|
||||
if user.is_alive() {
|
||||
user.set(Some(None));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -405,6 +409,9 @@ pub fn LobbyPage() -> View {
|
||||
spawn_local(async move {
|
||||
match api::game_types().await {
|
||||
Ok(types) if !types.is_empty() => {
|
||||
if !game_types.is_alive() {
|
||||
return;
|
||||
}
|
||||
selected_game.set(types[0].id.clone());
|
||||
game_types.set(types);
|
||||
}
|
||||
@@ -414,7 +421,9 @@ pub fn LobbyPage() -> View {
|
||||
|
||||
spawn_local(async move {
|
||||
if let Ok(list) = api::auth_providers().await {
|
||||
auth_providers.set(list);
|
||||
if auth_providers.is_alive() {
|
||||
auth_providers.set(list);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -470,53 +479,7 @@ pub fn LobbyPage() -> View {
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(None) => view! {
|
||||
div(class="status-card") {
|
||||
div(class="card") {
|
||||
div(class="card-body p-4") {
|
||||
h1(class="h3 mb-3") {
|
||||
span(class="text-warning") { "♠" }
|
||||
" tavolo"
|
||||
}
|
||||
p(class="text-secondary") { "Log in with your account to play." }
|
||||
// rel="external" opts out of the SPA router's click
|
||||
// interception: these must be full page navigations
|
||||
// to the backend's OIDC login endpoints.
|
||||
(move || {
|
||||
let providers = auth_providers.get_clone();
|
||||
if providers.is_empty() {
|
||||
// Providers could not be fetched: the
|
||||
// generic route redirects to the first
|
||||
// configured provider.
|
||||
view! {
|
||||
a(
|
||||
class="btn btn-warning btn-lg w-100",
|
||||
href="/auth/login",
|
||||
rel="external",
|
||||
) {
|
||||
"Log in"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
view! {
|
||||
div(class="d-grid gap-2") {
|
||||
(providers.iter().cloned().map(|p| view! {
|
||||
a(
|
||||
class="btn btn-warning btn-lg w-100",
|
||||
href=p.login_url,
|
||||
rel="external",
|
||||
) {
|
||||
"Log in with " (provider_label(&p.name))
|
||||
}
|
||||
}).collect::<Vec<_>>())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(None) => view! { (login_card(auth_providers)) },
|
||||
Some(Some(_me)) => {
|
||||
view! {
|
||||
(crate::components::nav::app_nav("/", move || {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
//! Login page, shown after the API answered `401` (see [`crate::auth`]):
|
||||
//! the session expired, or the visitor tried to open a page that needs an
|
||||
//! account.
|
||||
//!
|
||||
//! The backend's OIDC callback lands the SPA back on the configured
|
||||
//! post-login redirect; [`crate::auth`] then returns the user to the page
|
||||
//! they were on before this one.
|
||||
use sycamore::prelude::*;
|
||||
use sycamore_router::navigate_replace;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
|
||||
use crate::api;
|
||||
use crate::auth;
|
||||
use crate::components::login::login_card;
|
||||
use crate::model::AuthProvider;
|
||||
|
||||
#[component]
|
||||
pub fn LoginPage() -> View {
|
||||
let providers = create_signal(Vec::<AuthProvider>::new());
|
||||
|
||||
spawn_local(async move {
|
||||
// Already signed in: no business here — return to where the user
|
||||
// was headed, or the lobby.
|
||||
if matches!(api::me().await, Ok(Some(_))) {
|
||||
let target = auth::saved_return_to().unwrap_or_else(|| "/".to_string());
|
||||
auth::clear_return_to();
|
||||
navigate_replace(&target);
|
||||
return;
|
||||
}
|
||||
if let Ok(list) = api::auth_providers().await {
|
||||
// The page can unmount while the fetch is in flight.
|
||||
if providers.is_alive() {
|
||||
providers.set(list);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
(login_card(providers))
|
||||
}
|
||||
}
|
||||
@@ -4,3 +4,4 @@ pub mod game;
|
||||
pub mod history;
|
||||
pub mod leaderboard;
|
||||
pub mod lobby;
|
||||
pub mod login;
|
||||
Reference in new issue
Block a user