//! OAuth PKCE flow — browser-based login for API providers. use tracing::{info, warn}; /// Run a browser-based OAuth PKCE flow for the given provider. /// /// Flow: look up config by provider name ("zen"/"opencode", "openai", /// or a custom provider via env vars) → bind a loopback server → generate /// a PKCE code verifier and challenge → build the authorisation URL → /// wait for the redirect code on the loopback server (with a 120s timeout) /// → exchange the code for a token → save the token to /// `~/.config/zesdex/oauth_{provider}.json`. /// /// Why: the `webbrowser::open` call is currently commented out; the user /// must open the auth URL manually until that line is reinstated. /// /// Return: a success message on completion, or an error if the flow fails /// at any step. pub(super) fn run_oauth_flow(provider: &str) -> anyhow::Result { info!(provider = provider, "starting OAuth flow"); use zesdex_iam::domain::oauth::OAuthConfig; use zesdex_iam::domain::service::OAuthService; use zesdex_iam::application::oauth_service::OAuthServiceImpl; use zesdex_iam::infrastructure::persistence::oauth_repo::FileSystemOAuthRepository; use zesdex_iam::infrastructure::oauth_loopback::LoopbackServer; let config = match provider { "zen" | "opencode" => OAuthConfig { auth_url: "https://opencode.ai/zen/oauth/authorize".to_string(), token_url: "https://opencode.ai/zen/oauth/token".to_string(), client_id: std::env::var("ZEN_CLIENT_ID") .unwrap_or_else(|_| "zesdex".to_string()), client_secret: std::env::var("ZEN_CLIENT_SECRET").ok(), scopes: vec![ "openid".to_string(), "profile".to_string(), "email".to_string(), ], }, "openai" => OAuthConfig { auth_url: "https://auth0.openai.com/authorize".to_string(), token_url: "https://auth0.openai.com/oauth/token".to_string(), client_id: std::env::var("OPENAI_CLIENT_ID") .unwrap_or_else(|_| "zesdex".to_string()), client_secret: std::env::var("OPENAI_CLIENT_SECRET").ok(), scopes: vec![ "openid".to_string(), "profile".to_string(), "email".to_string(), ], }, other => { let auth_url = std::env::var(format!("{}_AUTH_URL", other.to_uppercase())) .map_err(|_| { anyhow::anyhow!( "unknown provider '{}'. Set {}_AUTH_URL env var.", other, other.to_uppercase() ) })?; let token_url = std::env::var(format!("{}_TOKEN_URL", other.to_uppercase())) .map_err(|_| anyhow::anyhow!("{}_TOKEN_URL not set", other.to_uppercase()))?; let client_id = std::env::var(format!("{}_CLIENT_ID", other.to_uppercase())) .unwrap_or_else(|_| "zesdex".to_string()); OAuthConfig { auth_url, token_url, client_id, client_secret: std::env::var(format!("{}_CLIENT_SECRET", other.to_uppercase())) .ok(), scopes: vec![ "openid".to_string(), "profile".to_string(), "email".to_string(), ], } } }; let server = LoopbackServer::bind()?; let redirect_uri = server.redirect_uri(); let token_path = dirs::config_dir() .unwrap_or_else(|| std::path::PathBuf::from(".")) .join("zesdex") .join(format!("oauth_{provider}.json")); let oauth_service = OAuthServiceImpl::new(FileSystemOAuthRepository::new(), token_path); let (auth_url, state) = oauth_service.start_flow(&config, &redirect_uri)?; if auth_url.is_empty() { warn!("OAuth auth_url was empty for provider '{}'", provider); } else if webbrowser::open(&auth_url).is_err() { warn!( "OAuth could not open browser for '{}'; user must open URL manually:\n{}", provider, auth_url ); } info!(provider = provider, "waiting for OAuth redirect"); let code = server.wait_for_code(120_000, &state)?; oauth_service .complete_flow(&config, &redirect_uri, &code, &state) .map_err(|e| anyhow::anyhow!("{e}"))?; info!(provider = provider, "OAuth flow completed"); Ok(format!("Successfully authenticated with {provider}.")) }