use eyre::{Context, Result, bail}; use reqwest::{Url, header::USER_AGENT}; use crate::{ atuin_client::api_client, atuin_common::{ api::{ATUIN_CARGO_VERSION, ATUIN_HEADER_VERSION, ChangePasswordRequest, LoginRequest}, tls::ensure_crypto_provider, }, }; use crate::atuin_client::settings::Settings; static APP_USER_AGENT: &str = concat!("atuin/", env!("CARGO_PKG_VERSION")); /// Result of an auth operation pub(crate) struct AuthResponse { pub(crate) session: String, } /// Resolve the appropriate [`AuthClient`] for the current settings. pub(crate) async fn auth_client(settings: &Settings) -> LegacyAuthClient { LegacyAuthClient::new( &settings.sync_address, settings.session_token().await.ok(), settings.network_connect_timeout, settings.network_timeout, ) } // --------------------------------------------------------------------------- // Legacy backend — talks to the Rust sync server // --------------------------------------------------------------------------- pub(crate) struct LegacyAuthClient { address: String, session_token: Option, connect_timeout: u64, timeout: u64, } impl LegacyAuthClient { pub(crate) fn new( address: &str, session_token: Option, connect_timeout: u64, timeout: u64, ) -> Self { Self { address: address.to_string(), session_token, connect_timeout, timeout, } } fn authenticated_client(&self) -> Result { 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()?) } } impl LegacyAuthClient { /// Log in with username + password, optionally providing a TOTP code. pub(crate) async fn login(&self, username: &str, password: &str) -> Result { // The legacy server has no 2FA support; totp_code is ignored. let resp = api_client::login( &self.address, LoginRequest { username: username.to_string(), password: password.to_string(), }, ) .await?; Ok(AuthResponse { session: resp.session, }) } /// Register a new account. pub(crate) async fn register( &self, username: &str, email: &str, password: &str, ) -> Result { let resp = api_client::register(&self.address, username, email, password).await?; Ok(AuthResponse { session: resp.session, }) } /// Change the account password, optionally providing a TOTP code. pub(crate) async fn change_password( &self, current_password: &str, new_password: &str, _totp_code: Option<&str>, ) -> Result<()> { 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(()), 401 => bail!("current password is incorrect"), 403 => bail!("invalid login details"), _ => bail!("unknown error"), } } /// Delete the account, requiring the current password and optionally a TOTP code. pub(crate) async fn delete_account( &self, password: &str, _totp_code: Option<&str>, ) -> Result<()> { 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(()), 401 => bail!("password is incorrect"), 403 => bail!("invalid login details"), _ => bail!("unknown error"), } } } // --------------------------------------------------------------------------- // Shared helpers // --------------------------------------------------------------------------- fn make_url(address: &str, path: &str) -> Result { 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()) }