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:
10 files changed
+299
-92
No files matched your search
+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 || {
|
||||
|
||||
Reference in new issue
Block a user