From cdba970891433dda54f4aacb9d5dd2d5aa14409f Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Tue, 21 Apr 2026 15:25:16 -0400 Subject: [PATCH] refactor(oauth): brand CLI callback pages to match the Fabro auth theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ephemeral loopback server that finishes `fabro auth login` returned four raw HTML fragments (one literally `

Logged in. You can close this tab.

`, and two that weren't even wrapped in a document). Replace them with a self-contained dark-theme shell that mirrors the redesigned /auth/cli/resume page: inline Fabro logo SVG, dark panel over the atmosphere gradient, mint status dot for success, coral for failure, consistent typography. Shell is fully offline — this process doesn't have /logo.svg or the SPA CSS available, so everything is inlined. Also HTML-escape the oauth error_description before interpolation, and add `white-space: nowrap` to inline in the resume shell so `fabro auth login` never wraps mid-command. Copy alignment: success eyebrow "Signed in" + headline "You're signed in to Fabro"; error eyebrow "Sign-in failed" + headline "CLI sign-in could not continue", with remediation pointing at the exact command. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../tests/it/support/auth_harness.rs | 2 +- lib/crates/fabro-oauth/src/lib.rs | 276 ++++++++++++------ lib/crates/fabro-server/src/auth/cli_flow.rs | 1 + 3 files changed, 190 insertions(+), 89 deletions(-) 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#" + + + + + {title} · Fabro + + + +
+
{FABRO_LOGO_SVG}
+
+
+ {body} +
+
+
+ +"# + ) +} + +fn callback_success_page() -> String { + browser_shell( + "Signed in", + r#" +
+

Signed in

+

You're signed in to Fabro

+
+

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

+

CLI sign-in could not continue

+
+

{detail}

+

Return to your terminal and run fabro auth login again.

+"# + ), + ) +} + // --------------------------------------------------------------------------- // PKCE // --------------------------------------------------------------------------- @@ -300,7 +468,9 @@ pub async fn start_callback_server( if params.state != *expected_state { return ( StatusCode::BAD_REQUEST, - Html("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.", + )), ); } @@ -316,29 +486,7 @@ pub async fn start_callback_server( } return ( StatusCode::BAD_REQUEST, - Html(format!( - r#" - - - -Authorization Failed - - - -
-
-

Authorization Failed

-

{desc}

-
- -"# - )), + Html(callback_error_page(&desc)), ); } @@ -351,7 +499,9 @@ pub async fn start_callback_server( } return ( StatusCode::BAD_REQUEST, - Html("No authorization code received".to_string()), + Html(callback_error_page( + "The identity provider did not return an authorization code.", + )), ); }; @@ -361,33 +511,7 @@ pub async fn start_callback_server( if let Some(tx) = shutdown_tx.lock().unwrap().take() { let _ = tx.send(()); } - ( - StatusCode::OK, - Html( - r#" - - - -Authorization - - - -
-
-

Authorization Successful

-

You can close this tab and return to your terminal.

-
- -"# - .to_string(), - ), - ) + (StatusCode::OK, Html(callback_success_page())) }, ), ); @@ -434,14 +558,9 @@ pub async fn start_callback_server_with_errors( if params.state != *expected_state { return ( StatusCode::BAD_REQUEST, - Html( - r#" - -Authorization Failed -

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#" - - - -Authorization Failed - - -

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#" - -Authorization -

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("Authorization"), - "response should contain updated title: {body}" + body.contains("Signed in · Fabro"), + "response should contain the new title: {body}" ); assert!( - body.contains("Authorization Successful"), - "response should contain success message: {body}" + body.contains("You're signed in to Fabro"), + "response should contain the success headline: {body}" ); } diff --git a/lib/crates/fabro-server/src/auth/cli_flow.rs b/lib/crates/fabro-server/src/auth/cli_flow.rs index a5a6b73e4..20370c680 100644 --- a/lib/crates/fabro-server/src/auth/cli_flow.rs +++ b/lib/crates/fabro-server/src/auth/cli_flow.rs @@ -923,6 +923,7 @@ fn browser_shell(status: StatusCode, title: &str, body: &str) -> Response { border-radius: 0.25rem; background: var(--overlay-2); color: var(--fg-2); + white-space: nowrap; }} .identity {{ display: flex;