aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-06-11 18:02:55 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-06-11 18:02:55 +0200
commit0b6ca5cb8ca4c46265e08e13053260d9b5cff568 (patch)
tree9dc656095f806e6dd1177e40b9a87cf6d6f10f1b
parentchore(server): Remove the last remnants of the "hub" sync-server thingy (diff)
downloadatuin-0b6ca5cb8ca4c46265e08e13053260d9b5cff568.zip
feat(server): Make user stuff stateless
-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
-rw-r--r--crates/turtle/src/atuin_common/api.rs18
-rw-r--r--crates/turtle/src/atuin_daemon/components/sync.rs2
-rw-r--r--crates/turtle/src/atuin_server/database/db/mod.rs217
-rw-r--r--crates/turtle/src/atuin_server/database/db/wrappers.rs15
-rw-r--r--crates/turtle/src/atuin_server/database/models.rs51
-rw-r--r--crates/turtle/src/atuin_server/handlers/health.rs15
-rw-r--r--crates/turtle/src/atuin_server/handlers/mod.rs5
-rw-r--r--crates/turtle/src/atuin_server/handlers/record.rs42
-rw-r--r--crates/turtle/src/atuin_server/handlers/user.rs267
-rw-r--r--crates/turtle/src/atuin_server/handlers/v0/me.rs16
-rw-r--r--crates/turtle/src/atuin_server/handlers/v0/mod.rs2
-rw-r--r--crates/turtle/src/atuin_server/handlers/v0/record.rs12
-rw-r--r--crates/turtle/src/atuin_server/handlers/v0/store.rs36
-rw-r--r--crates/turtle/src/atuin_server/router.rs74
-rw-r--r--crates/turtle/src/atuin_server/settings.rs5
-rw-r--r--crates/turtle/src/command/client.rs16
-rw-r--r--crates/turtle/src/command/client/account.rs47
-rw-r--r--crates/turtle/src/command/client/account/change_password.rs55
-rw-r--r--crates/turtle/src/command/client/account/delete.rs45
-rw-r--r--crates/turtle/src/command/client/account/login.rs201
-rw-r--r--crates/turtle/src/command/client/account/logout.rs5
-rw-r--r--crates/turtle/src/command/client/account/register.rs67
-rw-r--r--crates/turtle/src/command/client/doctor.rs35
-rw-r--r--crates/turtle/src/command/client/setup.rs81
-rw-r--r--crates/turtle/src/command/client/store/push.rs2
-rw-r--r--crates/turtle/src/command/client/sync.rs14
-rw-r--r--crates/turtle/src/command/client/sync/status.rs6
36 files changed, 104 insertions, 1740 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 {
diff --git a/crates/turtle/src/atuin_common/api.rs b/crates/turtle/src/atuin_common/api.rs
index 4566c6e9..c18db04f 100644
--- a/crates/turtle/src/atuin_common/api.rs
+++ b/crates/turtle/src/atuin_common/api.rs
@@ -11,35 +11,17 @@ pub(crate) static ATUIN_VERSION: LazyLock<Version> =
LazyLock::new(|| Version::parse(ATUIN_CARGO_VERSION).expect("failed to parse self semver"));
#[derive(Debug, Serialize, Deserialize)]
-pub(crate) struct UserResponse {
- pub(crate) username: String,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub(crate) struct RegisterRequest {
- pub(crate) email: String,
- pub(crate) username: String,
- pub(crate) password: String,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct RegisterResponse {
pub(crate) session: String,
}
#[derive(Debug, Serialize, Deserialize)]
-pub(crate) struct DeleteUserResponse {}
-
-#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct ChangePasswordRequest {
pub(crate) current_password: String,
pub(crate) new_password: String,
}
#[derive(Debug, Serialize, Deserialize)]
-pub(crate) struct ChangePasswordResponse {}
-
-#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct LoginRequest {
pub(crate) username: String,
pub(crate) password: String,
diff --git a/crates/turtle/src/atuin_daemon/components/sync.rs b/crates/turtle/src/atuin_daemon/components/sync.rs
index 93d1024a..fdd00b5f 100644
--- a/crates/turtle/src/atuin_daemon/components/sync.rs
+++ b/crates/turtle/src/atuin_daemon/components/sync.rs
@@ -190,7 +190,7 @@ async fn do_sync_tick(
tracing::info!("sync tick");
// Check if logged in
- let logged_in = match settings.logged_in().await {
+ let logged_in = match settings.have_sync_key().await {
Ok(v) => v,
Err(e) => {
tracing::warn!("failed to check login status, skipping sync tick: {e}");
diff --git a/crates/turtle/src/atuin_server/database/db/mod.rs b/crates/turtle/src/atuin_server/database/db/mod.rs
index e0c6b736..4ec51bf1 100644
--- a/crates/turtle/src/atuin_server/database/db/mod.rs
+++ b/crates/turtle/src/atuin_server/database/db/mod.rs
@@ -4,16 +4,13 @@ use rand::Rng;
use crate::{
atuin_common::record::{EncryptedData, HostId, Record, RecordIdx, RecordStatus},
- atuin_server::database::{
- DbError, DbResult, DbSettings,
- models::{NewSession, NewUser, Session, User},
- },
+ atuin_server::database::{DbError, DbResult, DbSettings, models::User},
};
use sqlx::postgres::PgPoolOptions;
use tracing::instrument;
use uuid::Uuid;
-use wrappers::{DbRecord, DbSession, DbUser};
+use wrappers::DbRecord;
mod wrappers;
@@ -96,148 +93,6 @@ impl Database {
}
#[instrument(skip_all)]
- pub(crate) async fn get_user(&self, username: &str) -> DbResult<User> {
- sqlx::query_as("select id, username, email, password from users where username = $1")
- .bind(username)
- .fetch_one(self.read_pool())
- .await
- .map_err(Into::into)
- .map(|DbUser(user)| user)
- }
-
- #[instrument(skip_all)]
- pub(crate) async fn get_session_user(&self, token: &str) -> DbResult<User> {
- sqlx::query_as(
- "select users.id, users.username, users.email, users.password from users
- inner join sessions
- on users.id = sessions.user_id
- and sessions.token = $1",
- )
- .bind(token)
- .fetch_one(self.read_pool())
- .await
- .map_err(Into::into)
- .map(|DbUser(user)| user)
- }
-
- pub(crate) async fn delete_store(&self, user: &User) -> DbResult<()> {
- let mut tx = self.pool.begin().await?;
-
- sqlx::query(
- "delete from store
- where user_id = $1",
- )
- .bind(user.id)
- .execute(&mut *tx)
- .await?;
-
- sqlx::query(
- "delete from store_idx_cache
- where user_id = $1",
- )
- .bind(user.id)
- .execute(&mut *tx)
- .await?;
-
- tx.commit().await?;
-
- Ok(())
- }
-
- #[instrument(skip_all)]
- pub(crate) async fn delete_user(&self, u: &User) -> DbResult<()> {
- sqlx::query("delete from sessions where user_id = $1")
- .bind(u.id)
- .execute(&self.pool)
- .await?;
-
- sqlx::query("delete from history where user_id = $1")
- .bind(u.id)
- .execute(&self.pool)
- .await?;
-
- sqlx::query("delete from store where user_id = $1")
- .bind(u.id)
- .execute(&self.pool)
- .await?;
-
- sqlx::query("delete from total_history_count_user where user_id = $1")
- .bind(u.id)
- .execute(&self.pool)
- .await?;
-
- sqlx::query("delete from users where id = $1")
- .bind(u.id)
- .execute(&self.pool)
- .await?;
-
- Ok(())
- }
-
- #[instrument(skip_all)]
- pub(crate) async fn update_user_password(&self, user: &User) -> DbResult<()> {
- sqlx::query(
- "update users
- set password = $1
- where id = $2",
- )
- .bind(&user.password)
- .bind(user.id)
- .execute(&self.pool)
- .await?;
-
- Ok(())
- }
-
- #[instrument(skip_all)]
- pub(crate) async fn add_user(&self, user: &NewUser) -> DbResult<i64> {
- let email: &str = &user.email;
- let username: &str = &user.username;
- let password: &str = &user.password;
-
- let res: (i64,) = sqlx::query_as(
- "insert into users
- (username, email, password)
- values($1, $2, $3)
- returning id",
- )
- .bind(username)
- .bind(email)
- .bind(password)
- .fetch_one(&self.pool)
- .await?;
-
- Ok(res.0)
- }
-
- #[instrument(skip_all)]
- pub(crate) async fn add_session(&self, session: &NewSession) -> DbResult<()> {
- let token: &str = &session.token;
-
- sqlx::query(
- "insert into sessions
- (user_id, token)
- values($1, $2)",
- )
- .bind(session.user_id)
- .bind(token)
- .execute(&self.pool)
- .await?;
-
- Ok(())
- }
-
- #[instrument(skip_all)]
- pub(crate) async fn get_user_session(&self, u: &User) -> DbResult<Session> {
- sqlx::query_as("select id, user_id, token from sessions where user_id = $1")
- .bind(u.id)
- .fetch_one(self.read_pool())
- .await
- .map_err(Into::into)
- .map(|DbSession(session)| session)
- }
-
- #[instrument(skip_all)]
pub(crate) async fn add_records(
&self,
user: &User,
@@ -258,10 +113,10 @@ impl Database {
let id = crate::atuin_common::utils::uuid_v7();
let result = sqlx::query(
- "insert into store
- (id, client_id, host, idx, timestamp, version, tag, data, cek, user_id)
- values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
- on conflict do nothing
+ "
+ INSERT INTO store (id, client_id, host, idx, timestamp, version, tag, data, cek, user_id)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
+ ON conflict DO nothing
",
)
.bind(id)
@@ -293,10 +148,11 @@ impl Database {
// we've built the map of heads for this push, so commit it to the database
for ((host, tag), idx) in heads {
sqlx::query(
- "insert into store_idx_cache
- (user_id, host, tag, idx)
- values ($1, $2, $3, $4)
- on conflict(user_id, host, tag) do update set idx = greatest(store_idx_cache.idx, $4)
+ "
+ INSERT INTO store_idx_cache (user_id, host, tag, idx)
+ VALUES ($1, $2, $3, $4)
+ ON conflict(user_id, host, tag) DO update
+ SET idx = greatest(store_idx_cache.idx, $4)
",
)
.bind(user.id)
@@ -304,8 +160,7 @@ impl Database {
.bind(tag)
.bind(idx as i64)
.execute(&mut *tx)
- .await
- ?;
+ .await?;
}
tx.commit().await?;
@@ -326,13 +181,15 @@ impl Database {
let start = start.unwrap_or(0);
let records: Result<Vec<DbRecord>, DbError> = sqlx::query_as(
- "select client_id, host, idx, timestamp, version, tag, data, cek from store
- where user_id = $1
- and tag = $2
- and host = $3
- and idx >= $4
- order by idx asc
- limit $5",
+ "
+ SELECT client_id, host, idx, timestamp, version, tag, data, cek FROM store
+ WHERE user_id = $1
+ AND tag = $2
+ AND host = $3
+ AND idx >= $4
+ ORDER BY idx asc
+ LIMIT $5
+ ",
)
.bind(user.id)
.bind(tag.clone())
@@ -366,9 +223,6 @@ impl Database {
}
pub(crate) async fn status(&self, user: &User) -> DbResult<RecordStatus> {
- const STATUS_SQL: &str =
- "select host, tag, max(idx) from store where user_id = $1 group by host, tag";
-
// If IDX_CACHE_ROLLOUT is set, then we
// 1. Read the value of the var, use it as a % chance of using the cache
// 2. If we use the cache, just read from the cache table
@@ -381,16 +235,29 @@ impl Database {
let mut res: Vec<(Uuid, String, i64)> = if use_idx_cache {
tracing::debug!("using idx cache for user {}", user.id);
- sqlx::query_as("select host, tag, idx from store_idx_cache where user_id = $1")
- .bind(user.id)
- .fetch_all(self.read_pool())
- .await?
+ sqlx::query_as(
+ "
+ SELECT host, tag, idx
+ FROM store_idx_cache
+ WHERE user_id = $1
+ ",
+ )
+ .bind(user.id)
+ .fetch_all(self.read_pool())
+ .await?
} else {
tracing::debug!("using aggregate query for user {}", user.id);
- sqlx::query_as(STATUS_SQL)
- .bind(user.id)
- .fetch_all(self.read_pool())
- .await?
+ sqlx::query_as(
+ "
+ SELECT host, tag, max(idx)
+ FROM store
+ WHERE user_id = $1
+ GROUP BY host, tag
+ ",
+ )
+ .bind(user.id)
+ .fetch_all(self.read_pool())
+ .await?
};
res.sort();
diff --git a/crates/turtle/src/atuin_server/database/db/wrappers.rs b/crates/turtle/src/atuin_server/database/db/wrappers.rs
index c0633202..40fd5b4a 100644
--- a/crates/turtle/src/atuin_server/database/db/wrappers.rs
+++ b/crates/turtle/src/atuin_server/database/db/wrappers.rs
@@ -1,25 +1,12 @@
use crate::{
atuin_common::record::{EncryptedData, Host, Record},
- atuin_server::database::models::{Session, User},
+ atuin_server::database::models::Session,
};
-use ::sqlx::{FromRow, Result};
use sqlx::{Row, postgres::PgRow};
-pub struct DbUser(pub User);
pub struct DbSession(pub Session);
pub struct DbRecord(pub Record<EncryptedData>);
-impl<'a> FromRow<'a, PgRow> for DbUser {
- fn from_row(row: &'a PgRow) -> Result<Self> {
- Ok(Self(User {
- id: row.try_get("id")?,
- username: row.try_get("username")?,
- email: row.try_get("email")?,
- password: row.try_get("password")?,
- }))
- }
-}
-
impl<'a> ::sqlx::FromRow<'a, PgRow> for DbSession {
fn from_row(row: &'a PgRow) -> ::sqlx::Result<Self> {
Ok(Self(Session {
diff --git a/crates/turtle/src/atuin_server/database/models.rs b/crates/turtle/src/atuin_server/database/models.rs
index e47d614d..3fa6f471 100644
--- a/crates/turtle/src/atuin_server/database/models.rs
+++ b/crates/turtle/src/atuin_server/database/models.rs
@@ -1,52 +1,5 @@
-use time::OffsetDateTime;
-
-pub(crate) struct History {
- pub(crate) id: i64,
- pub(crate) client_id: String, // a client generated ID
- pub(crate) user_id: i64,
- pub(crate) hostname: String,
- pub(crate) timestamp: OffsetDateTime,
-
- /// All the data we have about this command, encrypted.
- ///
- /// Currently this is an encrypted msgpack object, but this may change in the future.
- pub(crate) data: String,
-
- pub(crate) created_at: OffsetDateTime,
-}
-
-pub(crate) struct NewHistory {
- pub(crate) client_id: String,
- pub(crate) user_id: i64,
- pub(crate) hostname: String,
- pub(crate) timestamp: OffsetDateTime,
-
- /// All the data we have about this command, encrypted.
- ///
- /// Currently this is an encrypted msgpack object, but this may change in the future.
- pub(crate) data: String,
-}
+use uuid::Uuid;
pub(crate) struct User {
- pub(crate) id: i64,
- pub(crate) username: String,
- pub(crate) email: String,
- pub(crate) password: String,
-}
-
-pub(crate) struct Session {
- pub(crate) id: i64,
- pub(crate) user_id: i64,
- pub(crate) token: String,
-}
-
-pub(crate) struct NewUser {
- pub(crate) username: String,
- pub(crate) email: String,
- pub(crate) password: String,
-}
-
-pub(crate) struct NewSession {
- pub(crate) user_id: i64,
- pub(crate) token: String,
+ pub(crate) id: Uuid,
}
diff --git a/crates/turtle/src/atuin_server/handlers/health.rs b/crates/turtle/src/atuin_server/handlers/health.rs
deleted file mode 100644
index d39f7aa5..00000000
--- a/crates/turtle/src/atuin_server/handlers/health.rs
+++ /dev/null
@@ -1,15 +0,0 @@
-use axum::{Json, http, response::IntoResponse};
-
-use serde::Serialize;
-
-#[derive(Serialize)]
-pub(crate) struct HealthResponse {
- pub(crate) status: &'static str,
-}
-
-pub(crate) async fn health_check() -> impl IntoResponse {
- (
- http::StatusCode::OK,
- Json(HealthResponse { status: "healthy" }),
- )
-}
diff --git a/crates/turtle/src/atuin_server/handlers/mod.rs b/crates/turtle/src/atuin_server/handlers/mod.rs
index 3b935834..7aded3de 100644
--- a/crates/turtle/src/atuin_server/handlers/mod.rs
+++ b/crates/turtle/src/atuin_server/handlers/mod.rs
@@ -3,9 +3,6 @@ use axum::{Json, extract::State, http, response::IntoResponse};
use crate::atuin_server::router::AppState;
-pub(crate) mod health;
-pub(crate) mod record;
-pub(crate) mod user;
pub(crate) mod v0;
const VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -17,7 +14,7 @@ pub(crate) async fn index(state: State<AppState>) -> Json<IndexResponse> {
.settings
.fake_version
.clone()
- .unwrap_or(VERSION.to_string());
+ .unwrap_or_else(|| VERSION.to_string());
Json(IndexResponse {
homage: homage.to_string(),
diff --git a/crates/turtle/src/atuin_server/handlers/record.rs b/crates/turtle/src/atuin_server/handlers/record.rs
deleted file mode 100644
index 39060423..00000000
--- a/crates/turtle/src/atuin_server/handlers/record.rs
+++ /dev/null
@@ -1,42 +0,0 @@
-use axum::{Json, http::StatusCode, response::IntoResponse};
-use serde_json::json;
-use tracing::instrument;
-
-use super::{ErrorResponse, ErrorResponseStatus, RespExt};
-use crate::atuin_server::router::UserAuth;
-
-use crate::atuin_common::record::{EncryptedData, Record};
-
-#[instrument(skip_all, fields(user.id = user.id))]
-pub(crate) async fn post(UserAuth(user): UserAuth) -> Result<(), ErrorResponseStatus<'static>> {
- // anyone who has actually used the old record store (a very small number) will see this error
- // upon trying to sync.
- // 1. The status endpoint will say that the server has nothing
- // 2. The client will try to upload local records
- // 3. Sync will fail with this error
-
- // If the client has no local records, they will see the empty index and do nothing. For the
- // vast majority of users, this is the case.
- return Err(
- ErrorResponse::reply("record store deprecated; please upgrade")
- .with_status(StatusCode::BAD_REQUEST),
- );
-}
-
-#[instrument(skip_all, fields(user.id = user.id))]
-pub(crate) async fn index(UserAuth(user): UserAuth) -> axum::response::Response {
- let ret = json!({
- "hosts": {}
- });
-
- ret.to_string().into_response()
-}
-
-#[instrument(skip_all, fields(user.id = user.id))]
-pub(crate) async fn next(
- UserAuth(user): UserAuth,
-) -> Result<Json<Vec<Record<EncryptedData>>>, ErrorResponseStatus<'static>> {
- let records = Vec::new();
-
- Ok(Json(records))
-}
diff --git a/crates/turtle/src/atuin_server/handlers/user.rs b/crates/turtle/src/atuin_server/handlers/user.rs
deleted file mode 100644
index e777acc3..00000000
--- a/crates/turtle/src/atuin_server/handlers/user.rs
+++ /dev/null
@@ -1,267 +0,0 @@
-use std::borrow::Borrow;
-use std::collections::HashMap;
-use std::time::Duration;
-
-use argon2::{
- Algorithm, Argon2, Params, PasswordHash, PasswordHasher, PasswordVerifier, Version,
- password_hash::SaltString,
-};
-use axum::{
- Json,
- extract::{Path, State},
- http::StatusCode,
-};
-use metrics::counter;
-
-use rand::rngs::OsRng;
-use tracing::{debug, error, info, instrument};
-
-use crate::{
- atuin_common::tls::ensure_crypto_provider,
- atuin_server::database::{
- DbError,
- models::{NewSession, NewUser},
- },
-};
-
-use super::{ErrorResponse, ErrorResponseStatus, RespExt};
-use crate::atuin_server::router::{AppState, UserAuth};
-
-use reqwest::header::CONTENT_TYPE;
-
-use crate::atuin_common::{api::*, utils::crypto_random_string};
-
-pub(crate) fn verify_str(hash: &str, password: &str) -> bool {
- let arg2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::default());
- let Ok(hash) = PasswordHash::new(hash) else {
- return false;
- };
- arg2.verify_password(password.as_bytes(), &hash).is_ok()
-}
-
-// Try to send a Discord webhook once - if it fails, we don't retry. "At most once", and best effort.
-// Don't return the status because if this fails, we don't really care.
-async fn send_register_hook(url: &str, username: String, registered: String) {
- ensure_crypto_provider();
- let hook = HashMap::from([
- ("username", username),
- ("content", format!("{registered} has just signed up!")),
- ]);
-
- let client = reqwest::Client::new();
-
- let resp = client
- .post(url)
- .timeout(Duration::new(5, 0))
- .header(CONTENT_TYPE, "application/json")
- .json(&hook)
- .send()
- .await;
-
- match resp {
- Ok(_) => info!("register webhook sent ok!"),
- Err(e) => error!("failed to send register webhook: {}", e),
- }
-}
-
-#[instrument(skip_all, fields(user.username = username.as_str()))]
-pub(crate) async fn get(
- Path(username): Path<String>,
- state: State<AppState>,
-) -> Result<Json<UserResponse>, ErrorResponseStatus<'static>> {
- let db = &state.0.database;
- let user = match db.get_user(username.as_ref()).await {
- Ok(user) => user,
- Err(DbError::NotFound) => {
- debug!("user not found: {}", username);
- return Err(ErrorResponse::reply("user not found").with_status(StatusCode::NOT_FOUND));
- }
- Err(DbError::Other(err)) => {
- error!("database error: {}", err);
- return Err(ErrorResponse::reply("database error")
- .with_status(StatusCode::INTERNAL_SERVER_ERROR));
- }
- };
-
- Ok(Json(UserResponse {
- username: user.username,
- }))
-}
-
-#[instrument(skip_all)]
-pub(crate) async fn register(
- state: State<AppState>,
- Json(register): Json<RegisterRequest>,
-) -> Result<Json<RegisterResponse>, ErrorResponseStatus<'static>> {
- if !state.settings.open_registration {
- return Err(
- ErrorResponse::reply("this server is not open for registrations")
- .with_status(StatusCode::BAD_REQUEST),
- );
- }
-
- for c in register.username.chars() {
- match c {
- 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' => {}
- _ => {
- return Err(ErrorResponse::reply(
- "Only alphanumeric and hyphens (-) are allowed in usernames",
- )
- .with_status(StatusCode::BAD_REQUEST));
- }
- }
- }
-
- let hashed = hash_secret(&register.password);
-
- let new_user = NewUser {
- email: register.email.clone(),
- username: register.username.clone(),
- password: hashed,
- };
-
- let db = &state.0.database;
- let user_id = match db.add_user(&new_user).await {
- Ok(id) => id,
- Err(e) => {
- error!("failed to add user: {}", e);
- return Err(
- ErrorResponse::reply("failed to add user").with_status(StatusCode::BAD_REQUEST)
- );
- }
- };
-
- // 24 bytes encoded as base64
- let token = crypto_random_string::<24>();
-
- let new_session = NewSession {
- user_id,
- token: (&token).into(),
- };
-
- if let Some(url) = &state.settings.register_webhook_url {
- // Could probs be run on another thread, but it's ok atm
- send_register_hook(
- url,
- state.settings.register_webhook_username.clone(),
- register.username,
- )
- .await;
- }
-
- counter!("atuin_users_registered").increment(1);
-
- match db.add_session(&new_session).await {
- Ok(_) => Ok(Json(RegisterResponse { session: token })),
- Err(e) => {
- error!("failed to add session: {}", e);
- Err(ErrorResponse::reply("failed to register user")
- .with_status(StatusCode::BAD_REQUEST))
- }
- }
-}
-
-#[instrument(skip_all, fields(user.id = user.id))]
-pub(crate) async fn delete(
- UserAuth(user): UserAuth,
- state: State<AppState>,
-) -> Result<Json<DeleteUserResponse>, ErrorResponseStatus<'static>> {
- debug!("request to delete user {}", user.id);
-
- let db = &state.0.database;
- if let Err(e) = db.delete_user(&user).await {
- error!("failed to delete user: {}", e);
-
- return Err(ErrorResponse::reply("failed to delete user")
- .with_status(StatusCode::INTERNAL_SERVER_ERROR));
- };
-
- counter!("atuin_users_deleted").increment(1);
-
- Ok(Json(DeleteUserResponse {}))
-}
-
-#[instrument(skip_all, fields(user.id = user.id, change_password))]
-pub(crate) async fn change_password(
- UserAuth(mut user): UserAuth,
- state: State<AppState>,
- Json(change_password): Json<ChangePasswordRequest>,
-) -> Result<Json<ChangePasswordResponse>, ErrorResponseStatus<'static>> {
- let db = &state.0.database;
-
- let verified = verify_str(
- user.password.as_str(),
- change_password.current_password.borrow(),
- );
- if !verified {
- return Err(
- ErrorResponse::reply("password is not correct").with_status(StatusCode::UNAUTHORIZED)
- );
- }
-
- let hashed = hash_secret(&change_password.new_password);
- user.password = hashed;
-
- if let Err(e) = db.update_user_password(&user).await {
- error!("failed to change user password: {}", e);
-
- return Err(ErrorResponse::reply("failed to change user password")
- .with_status(StatusCode::INTERNAL_SERVER_ERROR));
- }
- Ok(Json(ChangePasswordResponse {}))
-}
-
-#[instrument(skip_all, fields(user.username = login.username.as_str()))]
-pub(crate) async fn login(
- state: State<AppState>,
- login: Json<LoginRequest>,
-) -> Result<Json<LoginResponse>, ErrorResponseStatus<'static>> {
- let db = &state.0.database;
- let user = match db.get_user(login.username.borrow()).await {
- Ok(u) => u,
- Err(DbError::NotFound) => {
- return Err(ErrorResponse::reply("user not found").with_status(StatusCode::NOT_FOUND));
- }
- Err(DbError::Other(e)) => {
- error!("failed to get user {}: {}", login.username.clone(), e);
-
- return Err(ErrorResponse::reply("database error")
- .with_status(StatusCode::INTERNAL_SERVER_ERROR));
- }
- };
-
- let session = match db.get_user_session(&user).await {
- Ok(u) => u,
- Err(DbError::NotFound) => {
- debug!("user session not found for user id={}", user.id);
- return Err(ErrorResponse::reply("user not found").with_status(StatusCode::NOT_FOUND));
- }
- Err(DbError::Other(err)) => {
- error!("database error for user {}: {}", login.username, err);
- return Err(ErrorResponse::reply("database error")
- .with_status(StatusCode::INTERNAL_SERVER_ERROR));
- }
- };
-
- let verified = verify_str(user.password.as_str(), login.password.borrow());
-
- if !verified {
- debug!(user = user.username, "login failed");
- return Err(
- ErrorResponse::reply("password is not correct").with_status(StatusCode::UNAUTHORIZED)
- );
- }
-
- debug!(user = user.username, "login success");
-
- Ok(Json(LoginResponse {
- session: session.token,
- }))
-}
-
-fn hash_secret(password: &str) -> String {
- let arg2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::default());
- let salt = SaltString::generate(&mut OsRng);
- let hash = arg2.hash_password(password.as_bytes(), &salt).unwrap();
- hash.to_string()
-}
diff --git a/crates/turtle/src/atuin_server/handlers/v0/me.rs b/crates/turtle/src/atuin_server/handlers/v0/me.rs
deleted file mode 100644
index 1f5f5016..00000000
--- a/crates/turtle/src/atuin_server/handlers/v0/me.rs
+++ /dev/null
@@ -1,16 +0,0 @@
-use axum::Json;
-use tracing::instrument;
-
-use crate::atuin_server::handlers::ErrorResponseStatus;
-use crate::atuin_server::router::UserAuth;
-
-use crate::atuin_common::api::*;
-
-#[instrument(skip_all, fields(user.id = user.id))]
-pub(crate) async fn get(
- UserAuth(user): UserAuth,
-) -> Result<Json<MeResponse>, ErrorResponseStatus<'static>> {
- Ok(Json(MeResponse {
- username: user.username,
- }))
-}
diff --git a/crates/turtle/src/atuin_server/handlers/v0/mod.rs b/crates/turtle/src/atuin_server/handlers/v0/mod.rs
index d6f880f2..78fb47b8 100644
--- a/crates/turtle/src/atuin_server/handlers/v0/mod.rs
+++ b/crates/turtle/src/atuin_server/handlers/v0/mod.rs
@@ -1,3 +1 @@
-pub(crate) mod me;
pub(crate) mod record;
-pub(crate) mod store;
diff --git a/crates/turtle/src/atuin_server/handlers/v0/record.rs b/crates/turtle/src/atuin_server/handlers/v0/record.rs
index 88027547..9350e1c8 100644
--- a/crates/turtle/src/atuin_server/handlers/v0/record.rs
+++ b/crates/turtle/src/atuin_server/handlers/v0/record.rs
@@ -10,7 +10,7 @@ use crate::atuin_server::{
use crate::atuin_common::record::{EncryptedData, HostId, Record, RecordIdx, RecordStatus};
-#[instrument(skip_all, fields(user.id = user.id))]
+#[instrument(skip_all, fields(user.id = user.id.to_string()))]
pub(crate) async fn post(
UserAuth(user): UserAuth,
state: State<AppState>,
@@ -20,7 +20,7 @@ pub(crate) async fn post(
tracing::debug!(
count = records.len(),
- user = user.username,
+ user = user.id.to_string(),
"request to add records"
);
@@ -44,12 +44,12 @@ pub(crate) async fn post(
return Err(ErrorResponse::reply("failed to add record")
.with_status(StatusCode::INTERNAL_SERVER_ERROR));
- };
+ }
Ok(())
}
-#[instrument(skip_all, fields(user.id = user.id))]
+#[instrument(skip_all, fields(user.id = user.id.to_string()))]
pub(crate) async fn index(
UserAuth(user): UserAuth,
state: State<AppState>,
@@ -69,7 +69,7 @@ pub(crate) async fn index(
}
};
- tracing::debug!(user = user.username, "record index request");
+ tracing::debug!(user = user.id.to_string(), "record index request");
Ok(Json(record_index))
}
@@ -82,7 +82,7 @@ pub(crate) struct NextParams {
count: u64,
}
-#[instrument(skip_all, fields(user.id = user.id))]
+#[instrument(skip_all, fields(user.id = user.id.to_string()))]
pub(crate) async fn next(
params: Query<NextParams>,
UserAuth(user): UserAuth,
diff --git a/crates/turtle/src/atuin_server/handlers/v0/store.rs b/crates/turtle/src/atuin_server/handlers/v0/store.rs
deleted file mode 100644
index f0aa1b36..00000000
--- a/crates/turtle/src/atuin_server/handlers/v0/store.rs
+++ /dev/null
@@ -1,36 +0,0 @@
-use axum::{extract::Query, extract::State, http::StatusCode};
-use metrics::counter;
-use serde::Deserialize;
-use tracing::{error, instrument};
-
-use crate::atuin_server::{
- handlers::{ErrorResponse, ErrorResponseStatus, RespExt},
- router::{AppState, UserAuth},
-};
-
-#[derive(Deserialize)]
-pub(crate) struct DeleteParams {}
-
-#[instrument(skip_all, fields(user.id = user.id))]
-pub(crate) async fn delete(
- _params: Query<DeleteParams>,
- UserAuth(user): UserAuth,
- state: State<AppState>,
-) -> Result<(), ErrorResponseStatus<'static>> {
- let State(AppState {
- database,
- settings: _,
- }) = state;
-
- if let Err(e) = database.delete_store(&user).await {
- counter!("atuin_store_delete_failed").increment(1);
- error!("failed to delete store {e:?}");
-
- return Err(ErrorResponse::reply("failed to delete store")
- .with_status(StatusCode::INTERNAL_SERVER_ERROR));
- }
-
- counter!("atuin_store_deleted").increment(1);
-
- Ok(())
-}
diff --git a/crates/turtle/src/atuin_server/router.rs b/crates/turtle/src/atuin_server/router.rs
index 778e699a..dfc2cac4 100644
--- a/crates/turtle/src/atuin_server/router.rs
+++ b/crates/turtle/src/atuin_server/router.rs
@@ -1,18 +1,19 @@
use crate::{
atuin_common::api::{ATUIN_CARGO_VERSION, ATUIN_HEADER_VERSION, ErrorResponse},
- atuin_server::database::{DbError, db::Database, models::User},
+ atuin_server::database::{db::Database, models::User},
};
use axum::{
Router,
- extract::{FromRequestParts, Request},
+ extract::{FromRequestParts, Path, Request},
http::{self, request::Parts},
middleware::Next,
response::{IntoResponse, Response},
- routing::{delete, get, patch, post},
+ routing::{get, post},
};
use eyre::Result;
use tower::ServiceBuilder;
use tower_http::trace::TraceLayer;
+use uuid::Uuid;
use super::handlers;
use crate::atuin_server::{
@@ -30,42 +31,19 @@ impl FromRequestParts<AppState> for UserAuth {
req: &mut Parts,
state: &AppState,
) -> Result<Self, Self::Rejection> {
- let auth_header = req
- .headers
- .get(http::header::AUTHORIZATION)
- .ok_or_else(|| {
- ErrorResponse::reply("missing authorization header")
- .with_status(http::StatusCode::BAD_REQUEST)
- })?;
- let auth_header = auth_header.to_str().map_err(|_| {
- ErrorResponse::reply("invalid authorization header encoding")
- .with_status(http::StatusCode::BAD_REQUEST)
- })?;
- let (typ, token) = auth_header.split_once(' ').ok_or_else(|| {
- ErrorResponse::reply("invalid authorization header encoding")
- .with_status(http::StatusCode::BAD_REQUEST)
- })?;
+ let user_id = {
+ let Path(user_id) =
+ <Path<Uuid> as FromRequestParts<AppState>>::from_request_parts(req, state)
+ .await
+ .map_err(|_| {
+ ErrorResponse::reply("invalid user_id path param")
+ .with_status(http::StatusCode::BAD_REQUEST)
+ })?;
- if typ != "Token" {
- return Err(
- ErrorResponse::reply("invalid authorization header encoding")
- .with_status(http::StatusCode::BAD_REQUEST),
- );
- }
+ user_id
+ };
- let user = state
- .database
- .get_session_user(token)
- .await
- .map_err(|e| match e {
- DbError::NotFound => ErrorResponse::reply("session not found")
- .with_status(http::StatusCode::FORBIDDEN),
- DbError::Other(e) => {
- tracing::error!(error = ?e, "could not query user session");
- ErrorResponse::reply("could not query user session")
- .with_status(http::StatusCode::INTERNAL_SERVER_ERROR)
- }
- })?;
+ let user = User { id: user_id };
Ok(UserAuth(user))
}
@@ -96,22 +74,12 @@ pub(crate) struct AppState {
pub(crate) fn router(database: Database, settings: Settings) -> Router {
let routes = Router::new()
.route("/", get(handlers::index))
- .route("/healthz", get(handlers::health::health_check));
-
- let routes = routes
- .route("/user/{username}", get(handlers::user::get))
- .route("/account", delete(handlers::user::delete))
- .route("/account/password", patch(handlers::user::change_password))
- .route("/register", post(handlers::user::register))
- .route("/login", post(handlers::user::login))
- .route("/record", post(handlers::record::post))
- .route("/record", get(handlers::record::index))
- .route("/record/next", get(handlers::record::next))
- .route("/api/v0/me", get(handlers::v0::me::get))
- .route("/api/v0/record", post(handlers::v0::record::post))
- .route("/api/v0/record", get(handlers::v0::record::index))
- .route("/api/v0/record/next", get(handlers::v0::record::next))
- .route("/api/v0/store", delete(handlers::v0::store::delete));
+ .route("/api/v0/{user_id}/record", post(handlers::v0::record::post))
+ .route("/api/v0/{user_id}/record", get(handlers::v0::record::index))
+ .route(
+ "/api/v0/{user_id}/record/next",
+ get(handlers::v0::record::next),
+ );
let path = settings.path.as_str();
if path.is_empty() {
diff --git a/crates/turtle/src/atuin_server/settings.rs b/crates/turtle/src/atuin_server/settings.rs
index b62f24e1..9424715d 100644
--- a/crates/turtle/src/atuin_server/settings.rs
+++ b/crates/turtle/src/atuin_server/settings.rs
@@ -30,12 +30,9 @@ pub(crate) struct Settings {
pub(crate) host: String,
pub(crate) port: u16,
pub(crate) path: String,
- pub(crate) open_registration: bool,
pub(crate) max_history_length: usize,
pub(crate) max_record_size: usize,
pub(crate) page_size: i64,
- pub(crate) register_webhook_url: Option<String>,
- pub(crate) register_webhook_username: String,
pub(crate) metrics: Metrics,
/// Advertise a version that is not what we are _actually_ running
@@ -66,11 +63,9 @@ impl Settings {
let config_builder = Config::builder()
.set_default("host", "127.0.0.1")?
.set_default("port", 8888)?
- .set_default("open_registration", false)?
.set_default("max_history_length", 8192)?
.set_default("max_record_size", 1024 * 1024 * 1024)? // pretty chonky
.set_default("path", "")?
- .set_default("register_webhook_username", "")?
.set_default("page_size", 1100)?
.set_default("metrics.enable", false)?
.set_default("metrics.host", "127.0.0.1")?
diff --git a/crates/turtle/src/command/client.rs b/crates/turtle/src/command/client.rs
index 15df60f8..9d5b4605 100644
--- a/crates/turtle/src/command/client.rs
+++ b/crates/turtle/src/command/client.rs
@@ -43,9 +43,6 @@ fn cleanup_old_logs(log_dir: &Path, prefix: &str, retention_days: u64) {
#[cfg(feature = "sync")]
mod sync;
-#[cfg(feature = "sync")]
-mod account;
-
#[cfg(feature = "daemon")]
mod daemon;
@@ -58,7 +55,6 @@ mod info;
mod init;
mod search;
mod server;
-mod setup;
mod stats;
mod store;
mod wrapped;
@@ -66,10 +62,6 @@ mod wrapped;
#[derive(Subcommand, Debug)]
#[command(infer_subcommands = true)]
pub(crate) enum Cmd {
- /// Setup Atuin features
- #[command()]
- Setup,
-
/// Manipulate shell history
#[command(subcommand)]
History(history::Cmd),
@@ -92,10 +84,6 @@ pub(crate) enum Cmd {
#[command(subcommand)]
Server(server::Cmd),
- /// Manage your sync account
- #[cfg(feature = "sync")]
- Account(account::Cmd),
-
/// Manage the atuin data store
#[command(subcommand)]
Store(store::Cmd),
@@ -333,7 +321,6 @@ impl Cmd {
let theme = theme_manager.load_theme(theme_name.as_str(), settings.theme.max_depth);
match self {
- Self::Setup => setup::run(&settings).await,
Self::Import(import) => import.run(&db).await,
Self::Stats(stats) => stats.run(&db, &settings, theme).await,
Self::Search(search) => search.run(db, &mut settings, sqlite_store, theme).await,
@@ -341,9 +328,6 @@ impl Cmd {
#[cfg(feature = "sync")]
Self::Sync(sync) => sync.run(settings, &db, sqlite_store).await,
- #[cfg(feature = "sync")]
- Self::Account(account) => account.run(settings, sqlite_store).await,
-
Self::Store(store) => store.run(&settings, &db, sqlite_store).await,
Self::Server(server) => server.run().await,
diff --git a/crates/turtle/src/command/client/account.rs b/crates/turtle/src/command/client/account.rs
deleted file mode 100644
index f2ceb10b..00000000
--- a/crates/turtle/src/command/client/account.rs
+++ /dev/null
@@ -1,47 +0,0 @@
-use clap::{Args, Subcommand};
-use eyre::Result;
-
-use crate::atuin_client::record::sqlite_store::SqliteStore;
-use crate::atuin_client::settings::Settings;
-
-pub(crate) mod change_password;
-pub(crate) mod delete;
-pub(crate) mod login;
-pub(crate) mod logout;
-pub(crate) mod register;
-
-#[derive(Args, Debug)]
-pub(crate) struct Cmd {
- #[command(subcommand)]
- command: Commands,
-}
-
-#[derive(Subcommand, Debug)]
-pub(crate) enum Commands {
- /// Login to the configured server
- Login(login::Cmd),
-
- /// Register a new account
- Register(register::Cmd),
-
- /// Log out
- Logout,
-
- /// Delete your account, and all synced data
- Delete(delete::Cmd),
-
- /// Change your password
- ChangePassword(change_password::Cmd),
-}
-
-impl Cmd {
- pub(crate) async fn run(self, settings: Settings, store: SqliteStore) -> Result<()> {
- match self.command {
- Commands::Login(l) => l.run(&settings, &store).await,
- Commands::Register(r) => r.run(&settings).await,
- Commands::Logout => logout::run().await,
- Commands::Delete(d) => d.run(&settings).await,
- Commands::ChangePassword(c) => c.run(&settings).await,
- }
- }
-}
diff --git a/crates/turtle/src/command/client/account/change_password.rs b/crates/turtle/src/command/client/account/change_password.rs
deleted file mode 100644
index b23f518d..00000000
--- a/crates/turtle/src/command/client/account/change_password.rs
+++ /dev/null
@@ -1,55 +0,0 @@
-use clap::Parser;
-use eyre::{Result, bail};
-
-use crate::atuin_client::{auth, settings::Settings};
-use rpassword::prompt_password;
-
-#[derive(Parser, Debug)]
-pub(crate) struct Cmd {
- #[clap(long, short)]
- pub(crate) current_password: Option<String>,
-
- #[clap(long, short)]
- pub(crate) new_password: Option<String>,
-
- /// The two-factor authentication code for your account, if any
- #[clap(long, short)]
- pub(crate) totp_code: Option<String>,
-}
-
-impl Cmd {
- pub(crate) async fn run(&self, settings: &Settings) -> Result<()> {
- if !settings.logged_in().await? {
- bail!("You are not logged in");
- }
-
- let client = auth::auth_client(settings).await;
-
- let current_password = self.current_password.clone().unwrap_or_else(|| {
- prompt_password("Please enter the current password: ")
- .expect("Failed to read from input")
- });
-
- if current_password.is_empty() {
- bail!("please provide the current password");
- }
-
- let new_password = self.new_password.clone().unwrap_or_else(|| {
- prompt_password("Please enter the new password: ").expect("Failed to read from input")
- });
-
- if new_password.is_empty() {
- bail!("please provide a new password");
- }
-
- let totp_code = self.totp_code.clone();
-
- client
- .change_password(&current_password, &new_password, totp_code.as_deref())
- .await?;
-
- println!("Account password successfully changed!");
-
- Ok(())
- }
-}
diff --git a/crates/turtle/src/command/client/account/delete.rs b/crates/turtle/src/command/client/account/delete.rs
deleted file mode 100644
index 722c39ec..00000000
--- a/crates/turtle/src/command/client/account/delete.rs
+++ /dev/null
@@ -1,45 +0,0 @@
-use crate::atuin_client::{auth, settings::Settings};
-use clap::Parser;
-use eyre::{Result, bail};
-
-use super::login::read_user_password;
-
-#[derive(Parser, Debug)]
-pub(crate) struct Cmd {
- #[clap(long, short)]
- pub(crate) password: Option<String>,
-
- /// The two-factor authentication code for your account, if any
- #[clap(long, short)]
- pub(crate) totp_code: Option<String>,
-}
-
-impl Cmd {
- pub(crate) async fn run(&self, settings: &Settings) -> Result<()> {
- if !settings.logged_in().await? {
- bail!("You are not logged in");
- }
-
- let client = auth::auth_client(settings).await;
-
- let password = self.password.clone().unwrap_or_else(read_user_password);
-
- if password.is_empty() {
- bail!("please provide your password");
- }
-
- let mut totp_code = self.totp_code.clone();
-
- client
- .delete_account(&password, totp_code.as_deref())
- .await?;
-
- // Clean up sessions from meta store
- let meta = Settings::meta_store().await?;
- meta.delete_session().await?;
-
- println!("Your account is deleted");
-
- Ok(())
- }
-}
diff --git a/crates/turtle/src/command/client/account/login.rs b/crates/turtle/src/command/client/account/login.rs
deleted file mode 100644
index e9513879..00000000
--- a/crates/turtle/src/command/client/account/login.rs
+++ /dev/null
@@ -1,201 +0,0 @@
-use std::{io, path::PathBuf};
-
-use clap::Parser;
-use eyre::{Context, Result, bail};
-use tokio::{fs::File, io::AsyncWriteExt};
-
-use crate::atuin_client::{
- auth,
- encryption::{decode_key, load_key},
- record::sqlite_store::SqliteStore,
- record::store::Store,
- record::sync::{self, SyncError},
- settings::{Settings, SyncAuth},
-};
-use rpassword::prompt_password;
-
-#[derive(Parser, Debug)]
-pub(crate) struct Cmd {
- #[clap(long, short)]
- pub(crate) username: Option<String>,
-
- #[clap(long, short)]
- pub(crate) password: Option<String>,
-
- /// The encryption key for your account
- #[clap(long, short)]
- pub(crate) key: Option<String>,
-
- /// The two-factor authentication code for your account, if any
- #[clap(long, short)]
- pub(crate) totp_code: Option<String>,
-
- #[clap(long, hide = true)]
- pub(crate) from_registration: bool,
-}
-
-fn get_input() -> Result<String> {
- let mut input = String::new();
- io::stdin().read_line(&mut input)?;
- Ok(input.trim_end_matches(&['\r', '\n'][..]).to_string())
-}
-
-impl Cmd {
- pub(crate) async fn run(&self, settings: &Settings, store: &SqliteStore) -> Result<()> {
- match settings.resolve_sync_auth().await {
- SyncAuth::Legacy { .. } => {
- println!("You are logged in to your sync server.");
- println!("Run 'atuin logout' to log out.");
- return Ok(());
- }
- SyncAuth::NotLoggedIn { .. } => {}
- }
-
- self.run_legacy_login(settings, store).await?;
-
- verify_key_against_remote(settings).await
- }
-
- /// Legacy login: always prompt for username/password interactively
- /// (or accept them via flags).
- async fn run_legacy_login(&self, settings: &Settings, store: &SqliteStore) -> Result<()> {
- let username = or_user_input(self.username.clone(), "username");
- let password = self.password.clone().unwrap_or_else(read_user_password);
-
- self.prompt_and_store_key(settings, store).await?;
-
- let client = auth::auth_client(settings).await;
- let response = client.login(&username, &password).await?;
-
- Settings::meta_store()
- .await?
- .save_session(&response.session)
- .await?;
-
- println!("Logged in!");
- Ok(())
- }
-
- async fn prompt_and_store_key(&self, settings: &Settings, store: &SqliteStore) -> Result<()> {
- let key_path = settings.key_path.as_str();
- let key_path = PathBuf::from(key_path);
-
- println!("IMPORTANT");
- println!(
- "If you are already logged in on another machine, you must ensure that the key you use here is the same as the key you used there."
- );
- println!("You can find your key by running 'atuin key' on the other machine.");
- println!("Do not share this key with anyone.");
- println!("\nRead more here: https://docs.atuin.sh/guide/sync/#login \n");
-
- let key = or_user_input(
- self.key.clone(),
- "encryption key [blank to use existing key file]",
- );
-
- if key.is_empty() {
- if key_path.exists() {
- let bytes = fs_err::read_to_string(&key_path).context(format!(
- "Existing key file at '{}' could not be read",
- key_path.to_string_lossy()
- ))?;
- if decode_key(bytes).is_err() {
- bail!(format!(
- "The key in existing key file at '{}' is invalid",
- key_path.to_string_lossy()
- ));
- }
- } else {
- panic!(
- "No key provided and no existing key file found. Please use 'atuin key' on your other machine, or recover your key from a backup"
- )
- }
- } else if !key_path.exists() {
- if decode_key(key.clone()).is_err() {
- bail!("The specified key is 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-encoded key")?
- .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?;
- }
- }
-
- Ok(())
- }
-}
-
-async fn verify_key_against_remote(settings: &Settings) -> Result<()> {
- let key: [u8; 32] = load_key(settings)
- .context("could not load encryption key for verification")?
- .into();
-
- let client = sync::build_client(settings).await?;
- let remote_index = match client.record_status().await {
- Ok(idx) => idx,
- Err(e) => {
- tracing::warn!("could not fetch remote status to verify key: {e}");
- return Ok(());
- }
- };
-
- match sync::check_encryption_key(&client, &remote_index, &key).await {
- Ok(()) => Ok(()),
- Err(SyncError::WrongKey) => {
- // Roll back the saved session so the user is not left in a
- // half-authenticated state with a key that can't read the data.
- if let Ok(meta) = Settings::meta_store().await {
- let _ = meta.delete_session().await;
- }
- crate::print_error::print_error(
- "Wrong encryption key",
- "The encryption key on this machine does not match the data on the server. \
- You have been logged out.\n\n\
- To fix this, find your existing key by running `atuin key` on a machine that \
- already syncs successfully, then run `atuin login` again here with that key.",
- );
- std::process::exit(1);
- }
- Err(e) => {
- // Non-key error (e.g. transient network issue). Don't fail the
- // login — the user is authenticated and can sync later when the
- // network recovers.
- tracing::warn!("could not verify encryption key against remote: {e}");
- Ok(())
- }
- }
-}
-
-pub(super) fn or_user_input(value: Option<String>, name: &'static str) -> String {
- value.unwrap_or_else(|| read_user_input(name))
-}
-
-pub(super) fn read_user_password() -> String {
- let password = prompt_password("Please enter password: ");
- password.expect("Failed to read from input")
-}
-
-fn read_user_input(name: &'static str) -> String {
- eprint!("Please enter {name}: ");
- get_input().expect("Failed to read from input")
-}
diff --git a/crates/turtle/src/command/client/account/logout.rs b/crates/turtle/src/command/client/account/logout.rs
deleted file mode 100644
index 5708e34c..00000000
--- a/crates/turtle/src/command/client/account/logout.rs
+++ /dev/null
@@ -1,5 +0,0 @@
-use eyre::Result;
-
-pub(crate) async fn run() -> Result<()> {
- crate::atuin_client::logout::logout().await
-}
diff --git a/crates/turtle/src/command/client/account/register.rs b/crates/turtle/src/command/client/account/register.rs
deleted file mode 100644
index 64fb9f8d..00000000
--- a/crates/turtle/src/command/client/account/register.rs
+++ /dev/null
@@ -1,67 +0,0 @@
-use clap::Parser;
-use eyre::{Result, bail};
-
-use super::login::or_user_input;
-use crate::atuin_client::settings::{Settings, SyncAuth};
-
-#[derive(Parser, Debug)]
-pub(crate) struct Cmd {
- #[clap(long, short)]
- pub(crate) username: Option<String>,
-
- #[clap(long, short)]
- pub(crate) password: Option<String>,
-
- #[clap(long, short)]
- pub(crate) email: Option<String>,
-}
-
-impl Cmd {
- pub(crate) async fn run(&self, settings: &Settings) -> Result<()> {
- match settings.resolve_sync_auth().await {
- SyncAuth::Legacy { .. } => {
- println!("You are already logged in.");
- println!("Run 'atuin logout' to log out.");
- return Ok(());
- }
-
- SyncAuth::NotLoggedIn { .. } => {}
- }
-
- // Legacy registration flow
- println!("Registering for an Atuin Sync account");
-
- let username = or_user_input(self.username.clone(), "username");
- let email = or_user_input(self.email.clone(), "email");
- let password = self
- .password
- .clone()
- .unwrap_or_else(super::login::read_user_password);
-
- if password.is_empty() {
- bail!("please provide a password");
- }
-
- let session = crate::atuin_client::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)?;
-
- println!(
- "Registration successful! Please make a note of your key (run 'atuin key') and keep it safe."
- );
- println!(
- "You will need it to log in on other devices, and we cannot help recover it if you lose it."
- );
-
- Ok(())
- }
-}
diff --git a/crates/turtle/src/command/client/doctor.rs b/crates/turtle/src/command/client/doctor.rs
index 1ed90c47..eec690a5 100644
--- a/crates/turtle/src/command/client/doctor.rs
+++ b/crates/turtle/src/command/client/doctor.rs
@@ -243,15 +243,8 @@ struct SyncInfo {
}
impl SyncInfo {
- pub(crate) async fn new(settings: &Settings) -> Self {
- // Build auth state description from raw token state without calling
- // resolve_sync_auth(), which has side effects (token migration cleanup)
- // that a diagnostic command should not trigger.
- let meta = Settings::meta_store().await.ok();
- let has_cli_token = match &meta {
- Some(m) => m.session_token().await.ok().flatten().is_some(),
- None => false,
- };
+ pub(crate) async fn new(settings: &Settings) -> Result<Self> {
+ let has_cli_token = settings.have_sync_key().await?;
let auth_state = if has_cli_token {
"Self-hosted (authenticated)".into()
@@ -259,13 +252,13 @@ impl SyncInfo {
"Not authenticated".into()
};
- Self {
+ Ok(Self {
auth_state,
auto_sync: settings.auto_sync,
last_sync: Settings::last_sync()
.await
.map_or_else(|_| "no last sync".to_string(), |v| v.to_string()),
- }
+ })
}
}
@@ -318,11 +311,11 @@ struct AtuinInfo {
}
impl AtuinInfo {
- pub(crate) async fn new(settings: &Settings) -> Self {
- let logged_in = settings.logged_in().await.unwrap_or(false);
+ pub(crate) async fn new(settings: &Settings) -> Result<Self> {
+ let logged_in = settings.have_sync_key().await?;
let sync = if logged_in {
- Some(SyncInfo::new(settings).await)
+ Some(SyncInfo::new(settings).await?)
} else {
None
};
@@ -335,13 +328,13 @@ impl AtuinInfo {
Err(_) => "error".to_string(),
};
- Self {
+ Ok(Self {
version: crate::VERSION.to_string(),
commit: crate::SHA.to_string(),
sync,
sqlite_version,
setting_paths: SettingPaths::new(settings),
- }
+ })
}
}
@@ -353,12 +346,12 @@ struct DoctorDump {
}
impl DoctorDump {
- pub(crate) async fn new(settings: &Settings) -> Self {
- Self {
- atuin: AtuinInfo::new(settings).await,
+ pub(crate) async fn new(settings: &Settings) -> Result<Self> {
+ Ok(Self {
+ atuin: AtuinInfo::new(settings).await?,
shell: ShellInfo::new(),
system: SystemInfo::new(),
- }
+ })
}
}
@@ -399,7 +392,7 @@ fn checks(info: &DoctorDump) {
pub(crate) async fn run(settings: &Settings) -> Result<()> {
println!("{}", "Atuin Doctor".bold());
println!("Checking for diagnostics");
- let dump = DoctorDump::new(settings).await;
+ let dump = DoctorDump::new(settings).await?;
checks(&dump);
diff --git a/crates/turtle/src/command/client/setup.rs b/crates/turtle/src/command/client/setup.rs
deleted file mode 100644
index 3231b6ec..00000000
--- a/crates/turtle/src/command/client/setup.rs
+++ /dev/null
@@ -1,81 +0,0 @@
-use crate::atuin_client::settings::Settings;
-
-use colored::Colorize;
-use eyre::Result;
-use std::io::{self, Write};
-use toml_edit::{DocumentMut, value};
-
-pub(crate) async fn run(_settings: &Settings) -> Result<()> {
- let enable_ai = prompt(
- "Atuin AI",
- "This will enable command generation and other AI features via the question mark key",
- Some(
- "By default, Atuin AI only has access to the name and version of your operating system and shell - your shell history is not sent to the AI.",
- ),
- )?;
-
- let enable_daemon = prompt(
- "Atuin Daemon",
- "This will enable improved search and history sync using a persistent background process",
- None,
- )?;
-
- let config_file = Settings::get_config_path()?;
- let config_str = tokio::fs::read_to_string(&config_file).await?;
- let mut doc = config_str.parse::<DocumentMut>()?;
-
- let mut changed = false;
- if enable_ai {
- changed = true;
- if !doc.contains_key("ai") {
- doc["ai"] = toml_edit::table();
- }
- doc["ai"]["enabled"] = value(true);
- }
-
- if enable_daemon {
- changed = true;
- if !doc.contains_key("daemon") {
- doc["daemon"] = toml_edit::table();
- }
- doc["daemon"]["enabled"] = value(true);
- doc["daemon"]["autostart"] = value(true);
- doc["search_mode"] = value("daemon-fuzzy");
- }
-
- if changed {
- tokio::fs::write(config_file, doc.to_string()).await?;
-
- println!(
- "{check} Settings updated successfully",
- check = "✓".bold().bright_green()
- );
- } else {
- println!(
- "{check} No settings changed",
- check = "✓".bold().bright_green()
- );
- }
-
- Ok(())
-}
-
-pub(crate) fn prompt(feature: &str, description: &str, note: Option<&str>) -> Result<bool> {
- println!(
- "> Enable {feature}?",
- feature = feature.bold().bright_blue()
- );
- if let Some(note) = note {
- println!(" {description}");
- print!(" {note} {q} ", q = "[Y/n]".bold());
- } else {
- print!(" {description} {q} ", q = "[Y/n]".bold());
- }
-
- io::stdout().flush().ok();
-
- let mut input = String::new();
- io::stdin().read_line(&mut input)?;
- let answer = input.trim().to_lowercase();
- Ok(answer.is_empty() || answer == "y" || answer == "yes")
-}
diff --git a/crates/turtle/src/command/client/store/push.rs b/crates/turtle/src/command/client/store/push.rs
index 042ad201..30177dbd 100644
--- a/crates/turtle/src/command/client/store/push.rs
+++ b/crates/turtle/src/command/client/store/push.rs
@@ -43,7 +43,7 @@ impl Push {
let client = Client::new(
&settings.sync_address,
- settings.sync_auth_token().await?,
+ settings.sync_auth().await?.into_auth_token()?,
settings.network_connect_timeout,
settings.network_timeout * 10, // we may be deleting a lot of data... so up the
// timeout
diff --git a/crates/turtle/src/command/client/sync.rs b/crates/turtle/src/command/client/sync.rs
index 8d7cb50a..7adf90ed 100644
--- a/crates/turtle/src/command/client/sync.rs
+++ b/crates/turtle/src/command/client/sync.rs
@@ -11,8 +11,6 @@ use crate::atuin_client::{
mod status;
-use crate::command::client::account;
-
#[derive(Subcommand, Debug)]
#[command(infer_subcommands = true)]
pub(crate) enum Cmd {
@@ -23,15 +21,6 @@ pub(crate) enum Cmd {
force: bool,
},
- /// Login to the configured server
- Login(account::login::Cmd),
-
- /// Log out
- Logout,
-
- /// Register with the configured server
- Register(account::register::Cmd),
-
/// Print the encryption key for transfer to another machine
Key {},
@@ -48,9 +37,6 @@ impl Cmd {
) -> Result<()> {
match self {
Self::Sync { force } => run(&settings, force, db, store).await,
- Self::Login(l) => l.run(&settings, &store).await,
- Self::Logout => account::logout::run().await,
- Self::Register(r) => r.run(&settings).await,
Self::Status => status::run(&settings).await,
Self::Key {} => {
use crate::atuin_client::encryption::{encode_key, load_key};
diff --git a/crates/turtle/src/command/client/sync/status.rs b/crates/turtle/src/command/client/sync/status.rs
index cb0d86e4..27b10dbd 100644
--- a/crates/turtle/src/command/client/sync/status.rs
+++ b/crates/turtle/src/command/client/sync/status.rs
@@ -1,16 +1,16 @@
-use crate::{SHA, VERSION};
use crate::atuin_client::{api_client, settings::Settings};
+use crate::{SHA, VERSION};
use colored::Colorize;
use eyre::{Result, bail};
pub(crate) async fn run(settings: &Settings) -> Result<()> {
- if !settings.logged_in().await? {
+ if !settings.have_sync_key().await? {
bail!("You are not logged in to a sync server - cannot show sync status");
}
let client = api_client::Client::new(
&settings.sync_address,
- settings.sync_auth_token().await?,
+ settings.sync_auth().await?.into_auth_token()?,
settings.network_connect_timeout,
settings.network_timeout,
)?;