aboutsummaryrefslogtreecommitdiffstats
path: root/crates/turtle/src/atuin_client
diff options
context:
space:
mode:
Diffstat (limited to 'crates/turtle/src/atuin_client')
-rw-r--r--crates/turtle/src/atuin_client/api_client.rs111
-rw-r--r--crates/turtle/src/atuin_client/auth.rs181
-rw-r--r--crates/turtle/src/atuin_client/login.rs68
-rw-r--r--crates/turtle/src/atuin_client/logout.rs16
-rw-r--r--crates/turtle/src/atuin_client/meta.rs34
-rw-r--r--crates/turtle/src/atuin_client/mod.rs7
-rw-r--r--crates/turtle/src/atuin_client/record/sync.rs4
-rw-r--r--crates/turtle/src/atuin_client/register.rs20
-rw-r--r--crates/turtle/src/atuin_client/settings.rs52
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(&current_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 {