diff options
Diffstat (limited to 'crates/atuin-client/src/auth.rs')
| -rw-r--r-- | crates/atuin-client/src/auth.rs | 230 |
1 files changed, 0 insertions, 230 deletions
diff --git a/crates/atuin-client/src/auth.rs b/crates/atuin-client/src/auth.rs deleted file mode 100644 index 1031c11f..00000000 --- a/crates/atuin-client/src/auth.rs +++ /dev/null @@ -1,230 +0,0 @@ -use async_trait::async_trait; -use eyre::{Context, Result, bail}; -use reqwest::{Url, header::USER_AGENT}; - -use atuin_common::{ - api::{ATUIN_CARGO_VERSION, ATUIN_HEADER_VERSION, ChangePasswordRequest, LoginRequest}, - tls::ensure_crypto_provider, -}; - -use crate::settings::Settings; - -static APP_USER_AGENT: &str = concat!("atuin/", env!("CARGO_PKG_VERSION")); - -/// Result of an auth operation that may require 2FA. -pub enum AuthResponse { - /// Operation succeeded; for login/register, contains the session token. - /// `auth_type` indicates the kind of token: `Some("hub")` for Hub API - /// tokens (prefixed `atapi_`), `Some("cli")` for legacy CLI session - /// tokens. `None` when the server didn't include the field (old servers). - Success { - session: String, - auth_type: Option<String>, - }, - /// Two-factor authentication is required; the caller should prompt for a - /// TOTP code and retry with it. - TwoFactorRequired, -} - -/// Result of a mutating account operation that may require 2FA. -pub enum MutateResponse { - /// Operation completed successfully. - Success, - /// Two-factor authentication is required; the caller should prompt for a - /// TOTP code and retry. - TwoFactorRequired, -} - -/// Abstraction over the legacy (Rust sync server) and Hub auth APIs. -/// -/// CLI commands use this trait so they don't need to know which backend is -/// active — they just prompt for input and call these methods. -#[async_trait] -pub trait AuthClient: Send + Sync { - /// Log in with username + password, optionally providing a TOTP code. - async fn login( - &self, - username: &str, - password: &str, - totp_code: Option<&str>, - ) -> Result<AuthResponse>; - - /// Register a new account. - async fn register(&self, username: &str, email: &str, password: &str) -> Result<AuthResponse>; - - /// Change the account password, optionally providing a TOTP code. - async fn change_password( - &self, - current_password: &str, - new_password: &str, - totp_code: Option<&str>, - ) -> Result<MutateResponse>; - - /// Delete the account, requiring the current password and optionally a TOTP code. - async fn delete_account( - &self, - password: &str, - totp_code: Option<&str>, - ) -> Result<MutateResponse>; -} - -/// Resolve the appropriate [`AuthClient`] for the current settings. -pub async fn auth_client(settings: &Settings) -> Box<dyn AuthClient> { - Box::new(LegacyAuthClient::new( - &settings.sync_address, - settings.session_token().await.ok(), - settings.network_connect_timeout, - settings.network_timeout, - )) as Box<dyn AuthClient> -} - -// --------------------------------------------------------------------------- -// Legacy backend — talks to the Rust sync server -// --------------------------------------------------------------------------- - -pub struct LegacyAuthClient { - address: String, - session_token: Option<String>, - connect_timeout: u64, - timeout: u64, -} - -impl LegacyAuthClient { - pub fn new( - address: &str, - session_token: Option<String>, - connect_timeout: u64, - timeout: u64, - ) -> Self { - Self { - address: address.to_string(), - session_token, - connect_timeout, - timeout, - } - } - - fn authenticated_client(&self) -> Result<reqwest::Client> { - let token = self - .session_token - .as_deref() - .ok_or_else(|| eyre::eyre!("Not logged in"))?; - - ensure_crypto_provider(); - let mut headers = reqwest::header::HeaderMap::new(); - headers.insert( - reqwest::header::AUTHORIZATION, - format!("Token {token}").parse()?, - ); - headers.insert(USER_AGENT, APP_USER_AGENT.parse()?); - headers.insert(ATUIN_HEADER_VERSION, ATUIN_CARGO_VERSION.parse()?); - - Ok(reqwest::Client::builder() - .default_headers(headers) - .connect_timeout(std::time::Duration::new(self.connect_timeout, 0)) - .timeout(std::time::Duration::new(self.timeout, 0)) - .build()?) - } -} - -#[async_trait] -impl AuthClient for LegacyAuthClient { - async fn login( - &self, - username: &str, - password: &str, - _totp_code: Option<&str>, - ) -> Result<AuthResponse> { - // The legacy server has no 2FA support; totp_code is ignored. - let resp = crate::api_client::login( - &self.address, - LoginRequest { - username: username.to_string(), - password: password.to_string(), - }, - ) - .await?; - - Ok(AuthResponse::Success { - session: resp.session, - auth_type: resp.auth.or(Some("cli".into())), - }) - } - - async fn register(&self, username: &str, email: &str, password: &str) -> Result<AuthResponse> { - let resp = crate::api_client::register(&self.address, username, email, password).await?; - Ok(AuthResponse::Success { - session: resp.session, - auth_type: resp.auth.or(Some("cli".into())), - }) - } - - async fn change_password( - &self, - current_password: &str, - new_password: &str, - _totp_code: Option<&str>, - ) -> Result<MutateResponse> { - let client = self.authenticated_client()?; - let url = make_url(&self.address, "/account/password")?; - - let resp = client - .patch(&url) - .json(&ChangePasswordRequest { - current_password: current_password.to_string(), - new_password: new_password.to_string(), - }) - .send() - .await?; - - match resp.status().as_u16() { - 200 => Ok(MutateResponse::Success), - 401 => bail!("current password is incorrect"), - 403 => bail!("invalid login details"), - _ => bail!("unknown error"), - } - } - - async fn delete_account( - &self, - password: &str, - _totp_code: Option<&str>, - ) -> Result<MutateResponse> { - let client = self.authenticated_client()?; - let url = make_url(&self.address, "/account")?; - - let resp = client - .delete(&url) - .json(&serde_json::json!({ "password": password })) - .send() - .await?; - - match resp.status().as_u16() { - 200 => Ok(MutateResponse::Success), - 401 => bail!("password is incorrect"), - 403 => bail!("invalid login details"), - _ => bail!("unknown error"), - } - } -} - -// --------------------------------------------------------------------------- -// Shared helpers -// --------------------------------------------------------------------------- - -fn make_url(address: &str, path: &str) -> Result<String> { - let address = if address.ends_with('/') { - address.to_string() - } else { - format!("{address}/") - }; - - let path = path.strip_prefix('/').unwrap_or(path); - - let url = Url::parse(&address) - .context("failed to parse server address")? - .join(path) - .context("failed to join URL path")?; - - Ok(url.to_string()) -} |
