diff options
Diffstat (limited to 'crates/turtle/src/atuin_server')
| -rw-r--r-- | crates/turtle/src/atuin_server/database/db/mod.rs | 217 | ||||
| -rw-r--r-- | crates/turtle/src/atuin_server/database/db/wrappers.rs | 15 | ||||
| -rw-r--r-- | crates/turtle/src/atuin_server/database/models.rs | 51 | ||||
| -rw-r--r-- | crates/turtle/src/atuin_server/handlers/health.rs | 15 | ||||
| -rw-r--r-- | crates/turtle/src/atuin_server/handlers/mod.rs | 5 | ||||
| -rw-r--r-- | crates/turtle/src/atuin_server/handlers/record.rs | 42 | ||||
| -rw-r--r-- | crates/turtle/src/atuin_server/handlers/user.rs | 267 | ||||
| -rw-r--r-- | crates/turtle/src/atuin_server/handlers/v0/me.rs | 16 | ||||
| -rw-r--r-- | crates/turtle/src/atuin_server/handlers/v0/mod.rs | 2 | ||||
| -rw-r--r-- | crates/turtle/src/atuin_server/handlers/v0/record.rs | 12 | ||||
| -rw-r--r-- | crates/turtle/src/atuin_server/handlers/v0/store.rs | 36 | ||||
| -rw-r--r-- | crates/turtle/src/atuin_server/router.rs | 74 | ||||
| -rw-r--r-- | crates/turtle/src/atuin_server/settings.rs | 5 |
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(®ister.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")? |
