refactor(backend): alihkan run_oauth_flow ke zesdex-iam OAuthServiceImpl

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
asepharyana
2026-07-17 09:08:41 +07:00
co-authored by Claude Sonnet 5
parent ff6a749c11
commit 4ab812d93b
@@ -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<String> {
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<String> {
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<String> {
);
}
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<u8> {
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::*;