diff options
Diffstat (limited to 'crates/turtle/src/atuin_client')
| -rw-r--r-- | crates/turtle/src/atuin_client/api_client.rs | 111 | ||||
| -rw-r--r-- | crates/turtle/src/atuin_client/auth.rs | 181 | ||||
| -rw-r--r-- | crates/turtle/src/atuin_client/login.rs | 68 | ||||
| -rw-r--r-- | crates/turtle/src/atuin_client/logout.rs | 16 | ||||
| -rw-r--r-- | crates/turtle/src/atuin_client/meta.rs | 34 | ||||
| -rw-r--r-- | crates/turtle/src/atuin_client/mod.rs | 7 | ||||
| -rw-r--r-- | crates/turtle/src/atuin_client/record/sync.rs | 4 | ||||
| -rw-r--r-- | crates/turtle/src/atuin_client/register.rs | 20 | ||||
| -rw-r--r-- | crates/turtle/src/atuin_client/settings.rs | 52 |
9 files changed, 12 insertions, 481 deletions
diff --git a/crates/turtle/src/atuin_client/api_client.rs b/crates/turtle/src/atuin_client/api_client.rs index 46995c9a..b4657a47 100644 --- a/crates/turtle/src/atuin_client/api_client.rs +++ b/crates/turtle/src/atuin_client/api_client.rs @@ -1,11 +1,10 @@ -use std::collections::HashMap; use std::env; use std::time::Duration; use eyre::{Result, bail, eyre}; use reqwest::{ Response, StatusCode, Url, - header::{AUTHORIZATION, HeaderMap, USER_AGENT}, + header::{AUTHORIZATION, HeaderMap}, }; use tracing::debug; @@ -15,10 +14,7 @@ use crate::atuin_common::{ tls::ensure_crypto_provider, }; use crate::atuin_common::{ - api::{ - ChangePasswordRequest, ErrorResponse, LoginRequest, LoginResponse, MeResponse, - RegisterResponse, - }, + api::{ErrorResponse, MeResponse}, record::RecordStatus, }; @@ -63,65 +59,6 @@ fn make_url(address: &str, path: &str) -> Result<String> { Ok(url.to_string()) } -pub(crate) async fn register( - address: &str, - username: &str, - email: &str, - password: &str, -) -> Result<RegisterResponse> { - ensure_crypto_provider(); - let mut map = HashMap::new(); - map.insert("username", username); - map.insert("email", email); - map.insert("password", password); - - let url = make_url(address, &format!("/user/{username}"))?; - let resp = reqwest::get(url).await?; - - if resp.status().is_success() { - bail!("username already in use"); - } - - let url = make_url(address, "/register")?; - let client = reqwest::Client::new(); - let resp = client - .post(url) - .header(USER_AGENT, APP_USER_AGENT) - .header(ATUIN_HEADER_VERSION, ATUIN_CARGO_VERSION) - .json(&map) - .send() - .await?; - let resp = handle_resp_error(resp).await?; - - if !ensure_version(&resp)? { - bail!("could not register user due to version mismatch"); - } - - let session = resp.json::<RegisterResponse>().await?; - Ok(session) -} - -pub(crate) async fn login(address: &str, req: LoginRequest) -> Result<LoginResponse> { - ensure_crypto_provider(); - let url = make_url(address, "/login")?; - let client = reqwest::Client::new(); - - let resp = client - .post(url) - .header(USER_AGENT, APP_USER_AGENT) - .json(&req) - .send() - .await?; - let resp = handle_resp_error(resp).await?; - - if !ensure_version(&resp)? { - bail!("Could not login due to version mismatch"); - } - - let session = resp.json::<LoginResponse>().await?; - Ok(session) -} - pub(crate) fn ensure_version(response: &Response) -> Result<bool> { let version = response.headers().get(ATUIN_HEADER_VERSION); @@ -287,48 +224,4 @@ impl<'a> Client<'a> { Ok(index) } - - pub(crate) async fn delete(&self) -> Result<()> { - let url = make_url(self.sync_addr, "/account")?; - let url = Url::parse(url.as_str())?; - - let resp = self.client.delete(url).send().await?; - - if resp.status() == 403 { - bail!("invalid login details"); - } else if resp.status() == 200 { - Ok(()) - } else { - bail!("Unknown error"); - } - } - - pub(crate) async fn change_password( - &self, - current_password: String, - new_password: String, - ) -> Result<()> { - let url = make_url(self.sync_addr, "/account/password")?; - let url = Url::parse(url.as_str())?; - - let resp = self - .client - .patch(url) - .json(&ChangePasswordRequest { - current_password, - new_password, - }) - .send() - .await?; - - if resp.status() == 401 { - bail!("current password is incorrect") - } else if resp.status() == 403 { - bail!("invalid login details"); - } else if resp.status() == 200 { - Ok(()) - } else { - bail!("Unknown error"); - } - } } diff --git a/crates/turtle/src/atuin_client/auth.rs b/crates/turtle/src/atuin_client/auth.rs deleted file mode 100644 index 620e127e..00000000 --- a/crates/turtle/src/atuin_client/auth.rs +++ /dev/null @@ -1,181 +0,0 @@ -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<String>, - connect_timeout: u64, - timeout: u64, -} - -impl LegacyAuthClient { - pub(crate) 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()?) - } -} - -impl LegacyAuthClient { - /// Log in with username + password, optionally providing a TOTP code. - pub(crate) async fn login(&self, username: &str, password: &str) -> Result<AuthResponse> { - // 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<AuthResponse> { - 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<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()) -} diff --git a/crates/turtle/src/atuin_client/login.rs b/crates/turtle/src/atuin_client/login.rs deleted file mode 100644 index 91876744..00000000 --- a/crates/turtle/src/atuin_client/login.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::path::PathBuf; - -use crate::atuin_common::api::LoginRequest; -use eyre::{Context, Result, bail}; -use tokio::fs::File; -use tokio::io::AsyncWriteExt; - -use crate::atuin_client::{ - api_client, - encryption::{decode_key, load_key}, - record::{sqlite_store::SqliteStore, store::Store}, - settings::Settings, -}; - -pub(crate) async fn login( - settings: &Settings, - store: &SqliteStore, - username: String, - password: String, - key: String, -) -> Result<String> { - let key_path = settings.key_path.as_str(); - let key_path = PathBuf::from(key_path); - - if !key_path.exists() { - if decode_key(key.clone()).is_err() { - bail!("the specified key was invalid"); - } - - let mut file = File::create(&key_path).await?; - file.write_all(key.as_bytes()).await?; - } else { - // we now know that the user has logged in specifying a key, AND that the key path - // exists - - // 1. check if the saved key and the provided key match. if so, nothing to do. - // 2. if not, re-encrypt the local history and overwrite the key - let current_key: [u8; 32] = load_key(settings)?.into(); - - let encoded = key.clone(); // gonna want to save it in a bit - let new_key: [u8; 32] = decode_key(key) - .context("could not decode provided key - is not valid base64")? - .into(); - - if new_key != current_key { - println!("\nRe-encrypting local store with new key"); - - store.re_encrypt(¤t_key, &new_key).await?; - - println!("Writing new key"); - let mut file = File::create(&key_path).await?; - file.write_all(encoded.as_bytes()).await?; - } - } - - let session = api_client::login( - settings.sync_address.as_str(), - LoginRequest { username, password }, - ) - .await?; - - Settings::meta_store() - .await? - .save_session(&session.session) - .await?; - - Ok(session.session) -} diff --git a/crates/turtle/src/atuin_client/logout.rs b/crates/turtle/src/atuin_client/logout.rs deleted file mode 100644 index 2ec41e40..00000000 --- a/crates/turtle/src/atuin_client/logout.rs +++ /dev/null @@ -1,16 +0,0 @@ -use eyre::Result; - -use crate::atuin_client::settings::Settings; - -pub(crate) async fn logout() -> Result<()> { - let meta = Settings::meta_store().await?; - - if meta.logged_in().await? { - meta.delete_session().await?; - println!("You have logged out!"); - } else { - println!("You are not logged in"); - } - - Ok(()) -} diff --git a/crates/turtle/src/atuin_client/meta.rs b/crates/turtle/src/atuin_client/meta.rs index 92902c08..f3815b9e 100644 --- a/crates/turtle/src/atuin_client/meta.rs +++ b/crates/turtle/src/atuin_client/meta.rs @@ -142,22 +142,6 @@ impl MetaStore { ) .await } - - pub(crate) async fn session_token(&self) -> Result<Option<String>> { - self.get(KEY_SESSION).await - } - - pub(crate) async fn save_session(&self, token: &str) -> Result<()> { - self.set(KEY_SESSION, token).await - } - - pub(crate) async fn delete_session(&self) -> Result<()> { - self.delete(KEY_SESSION).await - } - - pub(crate) async fn logged_in(&self) -> Result<bool> { - Ok(self.session_token().await?.is_some()) - } } #[cfg(test)] @@ -205,22 +189,4 @@ mod tests { let t = store.last_sync().await.unwrap(); assert!(t > OffsetDateTime::UNIX_EPOCH); } - - #[tokio::test] - async fn test_session_crud() { - let store = new_test_store().await; - - assert!(!store.logged_in().await.unwrap()); - assert_eq!(store.session_token().await.unwrap(), None); - - store.save_session("tok123").await.unwrap(); - assert!(store.logged_in().await.unwrap()); - assert_eq!( - store.session_token().await.unwrap(), - Some("tok123".to_string()) - ); - - store.delete_session().await.unwrap(); - assert!(!store.logged_in().await.unwrap()); - } } diff --git a/crates/turtle/src/atuin_client/mod.rs b/crates/turtle/src/atuin_client/mod.rs index ff376c0c..a4323f56 100644 --- a/crates/turtle/src/atuin_client/mod.rs +++ b/crates/turtle/src/atuin_client/mod.rs @@ -1,18 +1,11 @@ #[cfg(feature = "sync")] pub(crate) mod api_client; -#[cfg(feature = "sync")] -pub(crate) mod auth; -#[cfg(feature = "sync")] -pub(crate) mod login; -#[cfg(feature = "sync")] -pub(crate) mod register; pub(crate) mod database; pub(crate) mod distro; pub(crate) mod encryption; pub(crate) mod history; pub(crate) mod import; -pub(crate) mod logout; pub(crate) mod meta; pub(crate) mod ordering; pub(crate) mod plugin; diff --git a/crates/turtle/src/atuin_client/record/sync.rs b/crates/turtle/src/atuin_client/record/sync.rs index 36eaec91..4284da87 100644 --- a/crates/turtle/src/atuin_client/record/sync.rs +++ b/crates/turtle/src/atuin_client/record/sync.rs @@ -62,8 +62,10 @@ pub(crate) async fn build_client(settings: &Settings) -> Result<Client<'_>, Sync Client::new( &settings.sync_address, settings - .sync_auth_token() + .sync_auth() .await + .map_err(|e| SyncError::RemoteRequestError { msg: e.to_string() })? + .into_auth_token() .map_err(|e| SyncError::RemoteRequestError { msg: e.to_string() })?, settings.network_connect_timeout, settings.network_timeout, diff --git a/crates/turtle/src/atuin_client/register.rs b/crates/turtle/src/atuin_client/register.rs deleted file mode 100644 index 1c78b6bc..00000000 --- a/crates/turtle/src/atuin_client/register.rs +++ /dev/null @@ -1,20 +0,0 @@ -use eyre::Result; - -use crate::atuin_client::{api_client, settings::Settings}; - -pub(crate) async fn register_classic( - settings: &Settings, - username: String, - email: String, - password: String, -) -> Result<String> { - let session = - api_client::register(settings.sync_address.as_str(), &username, &email, &password).await?; - - let meta = Settings::meta_store().await?; - meta.save_session(&session.session).await?; - - let _key = crate::atuin_client::encryption::load_key(settings)?; - - Ok(session.session) -} diff --git a/crates/turtle/src/atuin_client/settings.rs b/crates/turtle/src/atuin_client/settings.rs index 5ee7cb77..e8ff98ee 100644 --- a/crates/turtle/src/atuin_client/settings.rs +++ b/crates/turtle/src/atuin_client/settings.rs @@ -1038,7 +1038,7 @@ impl Settings { } pub(crate) async fn should_sync(&self) -> Result<bool> { - if !self.auto_sync || !Self::meta_store().await?.logged_in().await? { + if !self.auto_sync || !self.have_sync_key().await? { return Ok(false); } @@ -1055,52 +1055,14 @@ impl Settings { } } - pub(crate) async fn logged_in(&self) -> Result<bool> { - Self::meta_store().await?.logged_in().await + pub(crate) async fn have_sync_key(&self) -> Result<bool> { + let sa = self.sync_auth().await?; + Ok(matches!(sa, SyncAuth::Legacy { .. })) } - pub(crate) async fn session_token(&self) -> Result<String> { - match Self::meta_store().await?.session_token().await? { - Some(token) => Ok(token), - None => Err(eyre!("Tried to load session; not logged in")), - } - } - - /// Examines the configured sync target and available tokens to determine - /// the correct auth strategy. Also performs cleanup of mis-stored tokens - /// (e.g. a CLI token incorrectly saved in the Hub session slot). - #[cfg(feature = "sync")] - pub(crate) async fn resolve_sync_auth(&self) -> SyncAuth { - let meta = match Self::meta_store().await { - Ok(m) => m, - Err(e) => { - return SyncAuth::NotLoggedIn { - reason: format!("Failed to open meta store: {e}"), - }; - } - }; - - // Self-hosted / legacy server - match meta.session_token().await { - Ok(Some(token)) => SyncAuth::Legacy { token }, - _ => SyncAuth::NotLoggedIn { - reason: "Not logged in. Run 'atuin login' to authenticate \ - with your sync server." - .into(), - }, - } - } - - /// Returns the appropriate auth token for sync operations. - /// - /// Delegates to [`resolve_sync_auth`] and converts the result to an - /// `AuthToken`. Callers that need to distinguish between auth states - /// (e.g. to show different UI) should call `resolve_sync_auth` directly. - #[cfg(feature = "sync")] - pub(crate) async fn sync_auth_token( - &self, - ) -> Result<crate::atuin_client::api_client::AuthToken> { - self.resolve_sync_auth().await.into_auth_token() + pub(crate) async fn sync_auth(&self) -> Result<SyncAuth> { + // TODO(@bpeetz): Add this <2026-06-11> + todo!() } pub(crate) fn default_filter_mode(&self, git_root: bool) -> FilterMode { |
