use chrono::{DateTime, Utc}; fn format_scope_for_display(scope: Option<&str>) -> String { let scope = scope.unwrap_or(""); if scope.is_empty() || scope.contains("atproto") || scope.contains("transition:generic") { return "access your account".to_string(); } let parts: Vec<&str> = scope.split_whitespace().collect(); let friendly: Vec<&str> = parts .iter() .filter_map(|s| { match *s { "atproto" | "transition:generic" | "transition:chat.bsky" => None, "read" => Some("read your data"), "write" => Some("write data"), other => Some(other), } }) .collect(); if friendly.is_empty() { "access your account".to_string() } else { friendly.join(", ") } } fn base_styles() -> &'static str { r#" :root { --bg-primary: #fafafa; --bg-secondary: #f9f9f9; --bg-card: #ffffff; --bg-input: #ffffff; --text-primary: #333333; --text-secondary: #666666; --text-muted: #999999; --border-color: #dddddd; --border-color-light: #cccccc; --accent: #0066cc; --accent-hover: #0052a3; --success-bg: #dfd; --success-border: #8c8; --success-text: #060; --error-bg: #fee; --error-border: #fcc; --error-text: #c00; } @media (prefers-color-scheme: dark) { :root { --bg-primary: #1a1a1a; --bg-secondary: #242424; --bg-card: #2a2a2a; --bg-input: #333333; --text-primary: #e0e0e0; --text-secondary: #a0a0a0; --text-muted: #707070; --border-color: #404040; --border-color-light: #505050; --accent: #4da6ff; --accent-hover: #7abbff; --success-bg: #1a3d1a; --success-border: #2d5a2d; --success-text: #7bc67b; --error-bg: #3d1a1a; --error-border: #5a2d2d; --error-text: #ff7b7b; } } * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg-primary); color: var(--text-primary); min-height: 100vh; line-height: 1.5; } .container { max-width: 400px; margin: 4rem auto; padding: 2rem; } h1 { margin: 0 0 0.5rem 0; font-weight: 600; } .subtitle { color: var(--text-secondary); margin: 0 0 2rem 0; } .subtitle strong { color: var(--text-primary); } .client-info { background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem; } .client-info .client-name { font-weight: 500; color: var(--text-primary); display: block; margin-bottom: 0.25rem; } .client-info .scope { color: var(--text-secondary); font-size: 0.875rem; } .error-banner { background: var(--error-bg); border: 1px solid var(--error-border); color: var(--error-text); border-radius: 4px; padding: 0.75rem; margin-bottom: 1rem; } .form-group { margin-bottom: 1rem; } label { display: block; font-size: 0.875rem; font-weight: 500; margin-bottom: 0.25rem; } input[type="text"], input[type="email"], input[type="password"] { width: 100%; padding: 0.75rem; border: 1px solid var(--border-color-light); border-radius: 4px; font-size: 1rem; color: var(--text-primary); background: var(--bg-input); } input[type="text"]:focus, input[type="email"]:focus, input[type="password"]:focus { outline: none; border-color: var(--accent); } input[type="text"]::placeholder, input[type="email"]::placeholder, input[type="password"]::placeholder { color: var(--text-muted); } .checkbox-group { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1.5rem; } .checkbox-group input[type="checkbox"] { width: 1rem; height: 1rem; accent-color: var(--accent); } .checkbox-group label { margin-bottom: 0; font-weight: normal; color: var(--text-secondary); cursor: pointer; } .buttons { display: flex; gap: 0.75rem; } .btn { flex: 1; padding: 0.75rem; border-radius: 4px; font-size: 1rem; cursor: pointer; border: none; text-align: center; text-decoration: none; } .btn-primary { background: var(--accent); color: white; } .btn-primary:hover { background: var(--accent-hover); } .btn-primary:disabled { opacity: 0.6; cursor: not-allowed; } .btn-secondary { background: transparent; color: var(--accent); border: 1px solid var(--accent); } .btn-secondary:hover { background: var(--accent); color: white; } .footer { text-align: center; margin-top: 1.5rem; font-size: 0.75rem; color: var(--text-muted); } .accounts { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem; } .account-item { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 1rem; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 8px; cursor: pointer; transition: border-color 0.15s, box-shadow 0.15s; text-align: left; } .account-item:hover { border-color: var(--accent); box-shadow: 0 2px 8px rgba(77, 166, 255, 0.15); } .account-info { display: flex; flex-direction: column; gap: 0.25rem; flex: 1; min-width: 0; } .account-info .handle { font-weight: 500; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .account-info .did { font-size: 0.75rem; color: var(--text-muted); font-family: monospace; overflow: hidden; text-overflow: ellipsis; } .chevron { color: var(--text-muted); font-size: 1.25rem; flex-shrink: 0; margin-left: 0.5rem; } .divider { height: 1px; background: var(--border-color); margin: 1rem 0; } .new-account-link { display: block; text-align: center; color: var(--accent); text-decoration: none; font-size: 0.875rem; } .new-account-link:hover { text-decoration: underline; } .help-text { text-align: center; margin-top: 1rem; font-size: 0.875rem; color: var(--text-secondary); } .icon { font-size: 3rem; margin-bottom: 1rem; } .error-code { background: var(--error-bg); border: 1px solid var(--error-border); color: var(--error-text); padding: 0.5rem 1rem; border-radius: 4px; font-family: monospace; display: inline-block; margin-bottom: 1rem; } .success-icon { width: 3rem; height: 3rem; border-radius: 50%; background: var(--success-bg); border: 1px solid var(--success-border); color: var(--success-text); display: flex; align-items: center; justify-content: center; font-size: 1.5rem; margin: 0 auto 1rem; } .text-center { text-align: center; } .code-input { letter-spacing: 0.5em; text-align: center; font-size: 1.5rem; font-family: monospace; } "# } pub fn login_page( client_id: &str, client_name: Option<&str>, scope: Option<&str>, request_uri: &str, error_message: Option<&str>, login_hint: Option<&str>, ) -> String { let client_display = client_name.unwrap_or(client_id); let scope_display = format_scope_for_display(scope); let error_html = error_message .map(|msg| format!(r#"
"#, html_escape(msg))) .unwrap_or_default(); let login_hint_value = login_hint.unwrap_or(""); format!( r#"Sign in to continue to {client_display}
By signing in, you agree to share your account information with this application.
Choose an account to continue to {client_display}
{subtitle}
{error_html}Code expires in 10 minutes.
{description}
{client_display} has been granted access to your account.
You can close this window and return to the application.