fix: break infinite redirect for non-admin users on admin UI

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=<app-url>).

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
This commit is contained in:
Claude 2026-03-23 08:24:41 +00:00
parent 812c81104a
commit 1671aaf8e8
No known key found for this signature in database

View file

@ -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)
}
};
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()
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 {
// Logged in but not an admin → access denied, no redirect loop.
Redirect::to("/denied").into_response()
}
}
@ -243,24 +248,11 @@ pub async fn login_page(
headers: HeaderMap,
Query(params): Query<NextParam>,
) -> 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 {
// 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}}</style></head><body>
<h1>Access denied</h1>
<p>You do not have access to <strong>{app}</strong>.</p>
<p>Contact your administrator to request access.</p>
<a href="/login"> Sign in with a different account</a>
<a href="/logout"> Sign in with a different account</a>
</div></body></html>"#
))
}