aboutsummaryrefslogtreecommitdiffstats
path: root/crates/atuin-client/src/auth.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/atuin-client/src/auth.rs')
-rw-r--r--crates/atuin-client/src/auth.rs230
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())
-}