diff --git a/crates/zesdex-backend/src/app/runtime/actions/mod.rs b/crates/zesdex-backend/src/app/runtime/actions/mod.rs index 8269cfa..5c0755c 100644 --- a/crates/zesdex-backend/src/app/runtime/actions/mod.rs +++ b/crates/zesdex-backend/src/app/runtime/actions/mod.rs @@ -1665,9 +1665,11 @@ fn save_current_session(state: &AppStateRest) { /// Return: a success message on completion, or an error if the flow fails /// at any step. fn run_oauth_flow(provider: &str) -> anyhow::Result { - use crate::service::oauth::manager::{OAuthConfig, OAuthManager}; - use crate::service::oauth::loopback::LoopbackServer; - use crate::service::oauth::pkce::CodeVerifier; + 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 { @@ -1706,12 +1708,14 @@ fn run_oauth_flow(provider: &str) -> anyhow::Result { let server = LoopbackServer::bind()?; let redirect_uri = server.redirect_uri(); - let verifier = CodeVerifier::new(); - let challenge = verifier.challenge(); - let state_token = hex::encode(sha2::Sha256::digest(rand_bytes(16))); + let token_path = dirs::config_dir() + .unwrap_or_else(|| std::path::PathBuf::from(".")) + .join("zesdex") + .join(format!("oauth_{provider}.json")); - let mut manager = OAuthManager::new(config.clone()); - let auth_url = manager.build_auth_url(&redirect_uri, &state_token, challenge.as_str()); + 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() { tracing::warn!("[oauth] auth_url was empty for provider '{}'", provider); } else if webbrowser::open(&auth_url).is_err() { @@ -1721,24 +1725,12 @@ fn run_oauth_flow(provider: &str) -> anyhow::Result { ); } - let code = server.wait_for_code(120_000, &state_token)?; + let code = server.wait_for_code(120_000, &state)?; - manager.exchange_code(&code, &redirect_uri, verifier.as_str()) + oauth_service + .complete_flow(&config, &redirect_uri, &code, &state) .map_err(|e| anyhow::anyhow!("{e}"))?; - if let Some(ref token) = manager.token { - let token_path = dirs::config_dir() - .unwrap_or_else(|| std::path::PathBuf::from(".")) - .join("zesdex") - .join(format!("oauth_{provider}.json")); - if let Some(parent) = token_path.parent() { - let _ = std::fs::create_dir_all(parent); - } - if let Err(e) = std::fs::write(&token_path, serde_json::to_string_pretty(token).unwrap_or_default()) { - tracing::warn!("[oauth] failed to persist token for '{}': {}", provider, e); - } - } - Ok(format!("Successfully authenticated with {provider}.")) } @@ -1788,26 +1780,6 @@ fn spawn_api_connectivity_check(state: &AppStateRest) { }); } -/// Generate `n` pseudo-random bytes from the system clock mixed with a monotonic -/// counter, providing sufficient unpredictability for a per-flow OAuth state -/// token without a `rand` dependency. -/// -/// Why: avoids pulling in a full RNG crate for the OAuth state token; -/// the counter ensures sequential invocations produce different outputs even -/// within the same clock tick, which is sufficient for a short-lived nonce. -fn rand_bytes(n: usize) -> Vec { - use std::sync::atomic::{AtomicU64, Ordering}; - use std::time::{SystemTime, UNIX_EPOCH}; - static COUNTER: AtomicU64 = AtomicU64::new(0); - let counter = COUNTER.fetch_add(1, Ordering::Relaxed); - let seed = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() as u64; - let base = seed ^ counter; - (0..n).map(|i| ((base >> ((i as u64 % 8) * 8)) ^ (i as u64 * 2_654_435_761)) as u8).collect() -} - #[cfg(test)] mod tests { use super::*;