diff --git a/lib/crates/fabro-server/src/auth/browser_shell.rs b/lib/crates/fabro-server/src/auth/browser_shell.rs
new file mode 100644
index 000000000..a83841b54
--- /dev/null
+++ b/lib/crates/fabro-server/src/auth/browser_shell.rs
@@ -0,0 +1,196 @@
+//! Shared HTML shell for browser-facing auth pages.
+//!
+//! Both the web sign-in flow and the CLI auth flow render styled status pages
+//! (errors, confirmations) directly from the server. This module provides the
+//! single dark-themed shell those pages share so they stay visually consistent.
+
+use axum::http::{HeaderValue, StatusCode, header};
+use axum::response::{Html, IntoResponse, Response};
+
+/// Wrap `body` in the standard Fabro auth page chrome (logo, panel, dark
+/// atmosphere) and return it as an HTML response with `status`.
+pub(crate) fn browser_shell(status: StatusCode, title: &str, body: &str) -> Response {
+ let html = format!(
+ r#"
+
+
+
+
+ {title} · Fabro
+
+
+
+
+
+
+
+
+
+
+ {body}
+
+
+
+
+"#,
+ );
+ let mut response = (status, Html(html)).into_response();
+ response.headers_mut().insert(
+ header::CONTENT_TYPE,
+ HeaderValue::from_static("text/html; charset=utf-8"),
+ );
+ response.headers_mut().insert(
+ "x-content-type-options",
+ HeaderValue::from_static("nosniff"),
+ );
+ response
+ .headers_mut()
+ .insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
+ response
+}
diff --git a/lib/crates/fabro-server/src/auth/cli_flow.rs b/lib/crates/fabro-server/src/auth/cli_flow.rs
index e86edf92f..c2e79dd3f 100644
--- a/lib/crates/fabro-server/src/auth/cli_flow.rs
+++ b/lib/crates/fabro-server/src/auth/cli_flow.rs
@@ -9,7 +9,7 @@ use std::sync::Arc;
use axum::extract::rejection::JsonRejection;
use axum::extract::{Query, State};
use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
-use axum::response::{Html, IntoResponse, Redirect, Response};
+use axum::response::{IntoResponse, Redirect, Response};
use axum::routing::{get, post};
use axum::{Extension, Json, Router};
use base64::Engine;
@@ -26,6 +26,7 @@ use sha2::{Digest, Sha256};
use tracing::{info, warn};
use url::{Host, Url};
+use crate::auth::browser_shell::browser_shell;
use crate::auth::{self, AuthCode, ConsumeOutcome, JwtSubject, RefreshToken};
use crate::jwt_auth::{AuthMode, ConfiguredAuth};
use crate::server::AppState;
@@ -775,191 +776,6 @@ fn html_escape(value: &str) -> String {
.replace('\'', "'")
}
-fn browser_shell(status: StatusCode, title: &str, body: &str) -> Response {
- let html = format!(
- r#"
-
-
-
-
- {title} · Fabro
-
-
-
-
-
-
-
-
-
-
- {body}
-
-
-
-
-"#,
- );
- let mut response = (status, Html(html)).into_response();
- response.headers_mut().insert(
- header::CONTENT_TYPE,
- HeaderValue::from_static("text/html; charset=utf-8"),
- );
- response.headers_mut().insert(
- "x-content-type-options",
- HeaderValue::from_static("nosniff"),
- );
- response
- .headers_mut()
- .insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
- response
-}
-
fn cli_login_confirmation_page(session: &SessionCookie) -> Response {
let login = html_escape(&session.login);
let display_name = if session.name.trim().is_empty() {
diff --git a/lib/crates/fabro-server/src/auth/mod.rs b/lib/crates/fabro-server/src/auth/mod.rs
index 709efe7b8..31d3ef083 100644
--- a/lib/crates/fabro-server/src/auth/mod.rs
+++ b/lib/crates/fabro-server/src/auth/mod.rs
@@ -1,9 +1,11 @@
+mod browser_shell;
mod cli_flow;
mod github_endpoints;
mod jwt;
mod keys;
mod translate;
+pub(crate) use browser_shell::browser_shell;
pub(crate) use cli_flow::web_routes;
pub(crate) use fabro_store::{AuthCode, ConsumeOutcome, RefreshToken};
pub use github_endpoints::GithubEndpoints;
diff --git a/lib/crates/fabro-server/src/web_auth.rs b/lib/crates/fabro-server/src/web_auth.rs
index 29439dd8d..4b1f991bd 100644
--- a/lib/crates/fabro-server/src/web_auth.rs
+++ b/lib/crates/fabro-server/src/web_auth.rs
@@ -2,7 +2,7 @@ use std::sync::Arc;
use axum::extract::{Query, State};
use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
-use axum::response::{Html, IntoResponse, Redirect, Response};
+use axum::response::{IntoResponse, Redirect, Response};
use axum::routing::{get, post};
use axum::{Extension, Json, Router};
use cookie::time::Duration;
@@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::{debug, error, info, warn};
-use crate::auth::GithubEndpoints;
+use crate::auth::{GithubEndpoints, browser_shell};
use crate::jwt_auth::{
AuthMode, AuthenticatedService, AuthenticatedSubject, auth_method_name, dev_token_matches,
};
@@ -204,26 +204,21 @@ fn json_response(status: StatusCode, body: serde_json::Value) -> Response {
(status, Json(body)).into_response()
}
-fn static_error_page(body: &'static str) -> Response {
- let mut response = (
+fn static_error_page(message: &'static str) -> Response {
+ browser_shell(
StatusCode::BAD_REQUEST,
- Html(format!(
- "