diff --git a/lib/crates/fabro-cli/tests/it/support/auth_harness.rs b/lib/crates/fabro-cli/tests/it/support/auth_harness.rs index c7ee9aa7e..b907cf442 100644 --- a/lib/crates/fabro-cli/tests/it/support/auth_harness.rs +++ b/lib/crates/fabro-cli/tests/it/support/auth_harness.rs @@ -428,7 +428,7 @@ async fn drive_browser_flow(browser_url: &str) { return; } assert!( - status == reqwest::StatusCode::BAD_REQUEST && body.contains("Login failed"), + status == reqwest::StatusCode::BAD_REQUEST && body.contains("Sign-in failed"), "browser flow failed with {status}\n{body}" ); } diff --git a/lib/crates/fabro-oauth/src/lib.rs b/lib/crates/fabro-oauth/src/lib.rs index 8cbbc09b2..b7e531b97 100644 --- a/lib/crates/fabro-oauth/src/lib.rs +++ b/lib/crates/fabro-oauth/src/lib.rs @@ -11,6 +11,174 @@ use sha2::{Digest, Sha256}; use tokio::net::TcpListener; use tokio::sync::oneshot; +// --------------------------------------------------------------------------- +// Browser shell for CLI callback responses +// --------------------------------------------------------------------------- + +const FABRO_LOGO_SVG: &str = r##""##; + +fn html_escape(value: &str) -> String { + value + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} + +/// Renders the Fabro-branded shell used for every response the CLI's +/// ephemeral loopback server emits during `fabro auth login`. Fully +/// self-contained: no external assets, no fonts, inline SVG logo. +fn browser_shell(title: &str, body: &str) -> String { + format!( + r#" + +
+ + +Signed in
+You can close this tab and return to your terminal.
+"#, + ) +} + +fn callback_error_page(detail: &str) -> String { + let detail = html_escape(detail); + browser_shell( + "Sign-in failed", + &format!( + r#" +Sign-in failed
+{detail}
+Return to your terminal and run fabro auth login again.
{desc}
-You can close this tab and return to your terminal.
-State mismatch
-"# - .to_string(), - ), + Html(callback_error_page( + "The sign-in state did not match. This usually means the browser session was reused across attempts.", + )), ); } @@ -460,18 +579,7 @@ pub async fn start_callback_server_with_errors( } return ( StatusCode::BAD_REQUEST, - Html(format!( - r#" - - - -Login failed: {error_description}
- -"# - )), + Html(callback_error_page(&error_description)), ); } @@ -487,7 +595,9 @@ pub async fn start_callback_server_with_errors( } return ( StatusCode::BAD_REQUEST, - Html("No authorization code received".to_string()), + Html(callback_error_page( + "The identity provider did not return an authorization code.", + )), ); }; @@ -497,17 +607,7 @@ pub async fn start_callback_server_with_errors( if let Some(tx) = route_shutdown_tx.lock().unwrap().take() { let _ = tx.send(()); } - ( - StatusCode::OK, - Html( - r#" - -Logged in. You can close this tab.
-"# - .to_string(), - ), - ) + (StatusCode::OK, Html(callback_success_page())) }), ); @@ -1044,12 +1144,12 @@ mod tests { let body = resp.text().await.unwrap(); assert!( - body.contains("