From 1671aaf8e89f3fea6514a703ea09334d3f509bc4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Mar 2026 08:24:41 +0000 Subject: [PATCH] fix: break infinite redirect for non-admin users on admin UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: auth_middleware redirected all non-admins (including logged-in ones) to /login, and login_page redirected logged-in users back — a loop. Fix: - auth_middleware now distinguishes unauthenticated (→ /login?next=) from logged-in-but-not-admin (→ /denied), breaking the loop entirely - /denied page's "sign in with a different account" link now goes to /logout first, so clicking it clears the session before the login form appears The login_page auto-redirect for logged-in users is restored, which is required for the Caddy forward_auth flow (deployed apps redirecting through /login?next=). https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH --- server/src/auth.rs | 60 ++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/server/src/auth.rs b/server/src/auth.rs index 838e4f2..900bd1f 100644 --- a/server/src/auth.rs +++ b/server/src/auth.rs @@ -96,28 +96,33 @@ pub async fn auth_middleware( let user_id = current_user_id(&state, request.headers()).await; - let is_admin = match user_id { - None => false, - Some(uid) => { - sqlx::query_scalar::<_, i64>("SELECT is_admin FROM users WHERE id = ?") - .bind(&uid) - .fetch_optional(&state.db) - .await - .unwrap_or(None) - .map(|v| v != 0) - .unwrap_or(false) + let path = request + .uri() + .path_and_query() + .map(|p| p.as_str()) + .unwrap_or("/"); + + let uid = match user_id { + None => { + // Not logged in → send to login with return path. + return Redirect::to(&format!("/login?next={}", safe_path(path))).into_response(); } + Some(uid) => uid, }; + let is_admin = sqlx::query_scalar::<_, i64>("SELECT is_admin FROM users WHERE id = ?") + .bind(&uid) + .fetch_optional(&state.db) + .await + .unwrap_or(None) + .map(|v| v != 0) + .unwrap_or(false); + if is_admin { next.run(request).await } else { - let path = request - .uri() - .path_and_query() - .map(|p| p.as_str()) - .unwrap_or("/"); - Redirect::to(&format!("/login?next={}", safe_path(path))).into_response() + // Logged in but not an admin → access denied, no redirect loop. + Redirect::to("/denied").into_response() } } @@ -243,23 +248,10 @@ pub async fn login_page( headers: HeaderMap, Query(params): Query, ) -> Response { - // Already logged in as an admin → redirect. - if let Some(uid) = current_user_id(&state, &headers).await { - let is_admin = if uid == "bootstrap" { - true - } else { - sqlx::query_scalar::<_, i64>("SELECT is_admin FROM users WHERE id = ?") - .bind(&uid) - .fetch_optional(&state.db) - .await - .unwrap_or(None) - .map(|v| v != 0) - .unwrap_or(false) - }; - if is_admin { - let next = params.next.as_deref().map(|n| safe_redirect(n, &state.domain_suffix)).unwrap_or_else(|| "/".into()); - return Redirect::to(&next).into_response(); - } + // Already logged in → redirect. + if current_user_id(&state, &headers).await.is_some() { + let next = params.next.as_deref().map(|n| safe_redirect(n, &state.domain_suffix)).unwrap_or_else(|| "/".into()); + return Redirect::to(&next).into_response(); } let next = params.next.map(|s| safe_redirect(&s, &state.domain_suffix)).unwrap_or_else(|| "/".into()); Html(login_html(&next, None)).into_response() @@ -361,7 +353,7 @@ a{{color:#818cf8}}

Access denied

You do not have access to {app}.

Contact your administrator to request access.

- ← Sign in with a different account + ← Sign in with a different account "# )) }