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
+32
-25
@@ -1,13 +1,32 @@
|
||||
//! REST client for the tavolo backend. Same-origin requests carry the
|
||||
//! session cookie automatically.
|
||||
use crate::auth;
|
||||
use crate::model::*;
|
||||
use gloo_net::http::Request;
|
||||
use gloo_net::http::{Request, Response};
|
||||
|
||||
fn server_error(status: u16) -> String {
|
||||
format!("server returned {status}")
|
||||
}
|
||||
|
||||
/// Map a failed response to an error. A `401` additionally starts the
|
||||
/// login flow: the session is gone (or the caller was never logged in),
|
||||
/// and the user is returned to this page after logging in.
|
||||
fn check(resp: &Response) -> Result<(), String> {
|
||||
if resp.status() == 401 {
|
||||
auth::redirect_to_login();
|
||||
return Err("login required".to_string());
|
||||
}
|
||||
if !resp.ok() {
|
||||
return Err(server_error(resp.status()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch the current user; `None` when unauthenticated (401).
|
||||
///
|
||||
/// Deliberately bypasses [`check`]: an unauthenticated answer is how the
|
||||
/// app renders its logged-out state, so it must not trigger the login
|
||||
/// redirect.
|
||||
pub async fn me() -> Result<Option<User>, String> {
|
||||
let resp = Request::get("/api/me")
|
||||
.send()
|
||||
@@ -28,9 +47,7 @@ pub async fn auth_providers() -> Result<Vec<AuthProvider>, String> {
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
if !resp.ok() {
|
||||
return Err(server_error(resp.status()));
|
||||
}
|
||||
check(&resp)?;
|
||||
let page: AuthProvidersPage = resp.json().await.map_err(|e| e.to_string())?;
|
||||
Ok(page.providers)
|
||||
}
|
||||
@@ -41,9 +58,7 @@ pub async fn game_types() -> Result<Vec<GameTypeInfo>, String> {
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
if !resp.ok() {
|
||||
return Err(server_error(resp.status()));
|
||||
}
|
||||
check(&resp)?;
|
||||
let page: GameTypesPage = resp.json().await.map_err(|e| e.to_string())?;
|
||||
Ok(page.results)
|
||||
}
|
||||
@@ -58,9 +73,7 @@ pub async fn create_game(game_type: &str, options: serde_json::Value) -> Result<
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
if !resp.ok() {
|
||||
return Err(server_error(resp.status()));
|
||||
}
|
||||
check(&resp)?;
|
||||
resp.json().await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -71,6 +84,10 @@ pub async fn join_game(code: &str) -> Result<GameView, String> {
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
if resp.status() == 401 {
|
||||
auth::redirect_to_login();
|
||||
return Err("login required".to_string());
|
||||
}
|
||||
if !resp.ok() {
|
||||
let body: serde_json::Value = resp.json().await.unwrap_or_default();
|
||||
let message = body
|
||||
@@ -94,9 +111,7 @@ pub async fn join_preview(code: &str) -> Result<Option<JoinPreview>, String> {
|
||||
if resp.status() == 404 {
|
||||
return Ok(None);
|
||||
}
|
||||
if !resp.ok() {
|
||||
return Err(server_error(resp.status()));
|
||||
}
|
||||
check(&resp)?;
|
||||
resp.json().await.map(Some).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -135,9 +150,7 @@ pub async fn my_games() -> Result<Vec<MyGame>, String> {
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
if !resp.ok() {
|
||||
return Err(server_error(resp.status()));
|
||||
}
|
||||
check(&resp)?;
|
||||
let page: MyGamesPage = resp.json().await.map_err(|e| e.to_string())?;
|
||||
Ok(page.results)
|
||||
}
|
||||
@@ -148,9 +161,7 @@ pub async fn my_matches(cursor: Option<&str>) -> Result<MatchesPage, String> {
|
||||
None => "/api/me/matches?limit=10".to_string(),
|
||||
};
|
||||
let resp = Request::get(&url).send().await.map_err(|e| e.to_string())?;
|
||||
if !resp.ok() {
|
||||
return Err(server_error(resp.status()));
|
||||
}
|
||||
check(&resp)?;
|
||||
resp.json().await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -160,9 +171,7 @@ pub async fn my_ratings() -> Result<RatingsPage, String> {
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
if !resp.ok() {
|
||||
return Err(server_error(resp.status()));
|
||||
}
|
||||
check(&resp)?;
|
||||
resp.json().await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -173,8 +182,6 @@ pub async fn leaderboard(game_type: &str) -> Result<LeaderboardPage, String> {
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
if !resp.ok() {
|
||||
return Err(server_error(resp.status()));
|
||||
}
|
||||
check(&resp)?;
|
||||
resp.json().await.map_err(|e| e.to_string())
|
||||
}
|
||||
Reference in new issue
Block a user