refactor(backend): alihkan run_oauth_flow ke zesdex-iam OAuthServiceImpl
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
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
|
/// Return: a success message on completion, or an error if the flow fails
|
||||||
/// at any step.
|
/// at any step.
|
||||||
fn run_oauth_flow(provider: &str) -> anyhow::Result<String> {
|
fn run_oauth_flow(provider: &str) -> anyhow::Result<String> {
|
||||||
use crate::service::oauth::manager::{OAuthConfig, OAuthManager};
|
use zesdex_iam::domain::oauth::OAuthConfig;
|
||||||
use crate::service::oauth::loopback::LoopbackServer;
|
use zesdex_iam::domain::service::OAuthService;
|
||||||
use crate::service::oauth::pkce::CodeVerifier;
|
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 {
|
let config = match provider {
|
||||||
"zen" | "opencode" => OAuthConfig {
|
"zen" | "opencode" => OAuthConfig {
|
||||||
@@ -1706,12 +1708,14 @@ fn run_oauth_flow(provider: &str) -> anyhow::Result<String> {
|
|||||||
let server = LoopbackServer::bind()?;
|
let server = LoopbackServer::bind()?;
|
||||||
let redirect_uri = server.redirect_uri();
|
let redirect_uri = server.redirect_uri();
|
||||||
|
|
||||||
let verifier = CodeVerifier::new();
|
let token_path = dirs::config_dir()
|
||||||
let challenge = verifier.challenge();
|
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||||
let state_token = hex::encode(sha2::Sha256::digest(rand_bytes(16)));
|
.join("zesdex")
|
||||||
|
.join(format!("oauth_{provider}.json"));
|
||||||
|
|
||||||
let mut manager = OAuthManager::new(config.clone());
|
let oauth_service = OAuthServiceImpl::new(FileSystemOAuthRepository::new(), token_path);
|
||||||
let auth_url = manager.build_auth_url(&redirect_uri, &state_token, challenge.as_str());
|
|
||||||
|
let (auth_url, state) = oauth_service.start_flow(&config, &redirect_uri)?;
|
||||||
if auth_url.is_empty() {
|
if auth_url.is_empty() {
|
||||||
tracing::warn!("[oauth] auth_url was empty for provider '{}'", provider);
|
tracing::warn!("[oauth] auth_url was empty for provider '{}'", provider);
|
||||||
} else if webbrowser::open(&auth_url).is_err() {
|
} 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}"))?;
|
.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}."))
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user