aboutsummaryrefslogtreecommitdiffstats
path: root/crates/turtle/src/atuin_server
diff options
context:
space:
mode:
Diffstat (limited to 'crates/turtle/src/atuin_server')
-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
13 files changed, 73 insertions, 684 deletions
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")?