From 7b9dea72e3d2435f75825e8e66a04285332d5aa5 Mon Sep 17 00:00:00 2001 From: Yannick Ulrich Date: Tue, 16 May 2023 22:00:59 +0100 Subject: [PATCH] feat: add delete account option (attempt 2) (#980) * Added DELETE register endpoint * Added remove function to database * Added unregister to client * Updated docs * Renamed functions * Reformatting * Used execute instead of fetch in delete_user --- atuin-client/src/api_client.rs | 15 +++++++++++++++ atuin-common/src/api.rs | 3 +++ atuin-server/src/database.rs | 21 +++++++++++++++++++++ atuin-server/src/handlers/user.rs | 19 ++++++++++++++++++- atuin-server/src/router.rs | 1 + atuin/src/command/client/sync.rs | 5 +++++ atuin/src/command/client/sync/delete.rs | 23 +++++++++++++++++++++++ docs/docs/commands/sync.md | 8 ++++++++ 8 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 atuin/src/command/client/sync/delete.rs diff --git a/atuin-client/src/api_client.rs b/atuin-client/src/api_client.rs index 80051ba..2abb815 100644 --- a/atuin-client/src/api_client.rs +++ b/atuin-client/src/api_client.rs @@ -217,4 +217,19 @@ impl<'a> Client<'a> { Ok(()) } + + pub async fn delete(&self) -> Result<()> { + let url = format!("{}/register", self.sync_addr); + 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"); + } + } } diff --git a/atuin-common/src/api.rs b/atuin-common/src/api.rs index e980932..2eff464 100644 --- a/atuin-common/src/api.rs +++ b/atuin-common/src/api.rs @@ -19,6 +19,9 @@ pub struct RegisterResponse { pub session: String, } +#[derive(Debug, Serialize, Deserialize)] +pub struct DeleteUserResponse {} + #[derive(Debug, Serialize, Deserialize)] pub struct LoginRequest { pub username: String, diff --git a/atuin-server/src/database.rs b/atuin-server/src/database.rs index 7f3e5da..e7057f6 100644 --- a/atuin-server/src/database.rs +++ b/atuin-server/src/database.rs @@ -27,6 +27,7 @@ pub trait Database { async fn get_user(&self, username: &str) -> Result; async fn get_user_session(&self, u: &User) -> Result; async fn add_user(&self, user: &NewUser) -> Result; + async fn delete_user(&self, u: &User) -> Result<()>; async fn count_history(&self, user: &User) -> Result; async fn count_history_cached(&self, user: &User) -> Result; @@ -336,6 +337,26 @@ impl Database for Postgres { Ok(()) } + #[instrument(skip_all)] + async fn delete_user(&self, u: &User) -> Result<()> { + sqlx::query("delete from sessions 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?; + + sqlx::query("delete from history where user_id = $1") + .bind(u.id) + .execute(&self.pool) + .await?; + + Ok(()) + } + #[instrument(skip_all)] async fn add_user(&self, user: &NewUser) -> Result { let email: &str = &user.email; diff --git a/atuin-server/src/handlers/user.rs b/atuin-server/src/handlers/user.rs index 89aa060..ec2131e 100644 --- a/atuin-server/src/handlers/user.rs +++ b/atuin-server/src/handlers/user.rs @@ -18,7 +18,7 @@ use uuid::Uuid; use super::{ErrorResponse, ErrorResponseStatus, RespExt}; use crate::{ database::Database, - models::{NewSession, NewUser}, + models::{NewSession, NewUser, User}, router::AppState, }; @@ -138,6 +138,23 @@ pub async fn register( } } +#[instrument(skip_all, fields(user.id = user.id))] +pub async fn delete( + user: User, + state: State>, +) -> Result, 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)); + }; + Ok(Json(DeleteUserResponse {})) +} + #[instrument(skip_all, fields(user.username = login.username.as_str()))] pub async fn login( state: State>, diff --git a/atuin-server/src/router.rs b/atuin-server/src/router.rs index 58aac3b..20b11f4 100644 --- a/atuin-server/src/router.rs +++ b/atuin-server/src/router.rs @@ -72,6 +72,7 @@ pub fn router( .route("/history", post(handlers::history::add)) .route("/history", delete(handlers::history::delete)) .route("/user/:username", get(handlers::user::get)) + .route("/account", delete(handlers::user::delete)) .route("/register", post(handlers::user::register)) .route("/login", post(handlers::user::login)); diff --git a/atuin/src/command/client/sync.rs b/atuin/src/command/client/sync.rs index 3980e13..12664be 100644 --- a/atuin/src/command/client/sync.rs +++ b/atuin/src/command/client/sync.rs @@ -3,6 +3,7 @@ use eyre::{Result, WrapErr}; use atuin_client::{database::Database, settings::Settings}; +mod delete; mod login; mod logout; mod register; @@ -27,6 +28,9 @@ pub enum Cmd { /// Register with the configured server Register(register::Cmd), + /// Unregister with the configured server + Unregister, + /// Print the encryption key for transfer to another machine Key { /// Switch to base64 output of the key @@ -44,6 +48,7 @@ impl Cmd { Self::Login(l) => l.run(&settings).await, Self::Logout => logout::run(&settings), Self::Register(r) => r.run(&settings).await, + Self::Unregister => delete::run(&settings).await, Self::Status => status::run(&settings, db).await, Self::Key { base64 } => { use atuin_client::encryption::{encode_key, load_key}; diff --git a/atuin/src/command/client/sync/delete.rs b/atuin/src/command/client/sync/delete.rs new file mode 100644 index 0000000..63e5b74 --- /dev/null +++ b/atuin/src/command/client/sync/delete.rs @@ -0,0 +1,23 @@ +use atuin_client::{api_client, encryption::load_encoded_key, settings::Settings}; +use eyre::{bail, Result}; +use std::path::PathBuf; + +pub async fn run(settings: &Settings) -> Result<()> { + let session_path = settings.session_path.as_str(); + + if !PathBuf::from(session_path).exists() { + bail!("You are not logged in"); + } + + let client = api_client::Client::new( + &settings.sync_address, + &settings.session_token, + load_encoded_key(settings)?, + )?; + + client.delete().await?; + + println!("Your account is deleted"); + + Ok(()) +} diff --git a/docs/docs/commands/sync.md b/docs/docs/commands/sync.md index d77a660..8fbb0c4 100644 --- a/docs/docs/commands/sync.md +++ b/docs/docs/commands/sync.md @@ -32,6 +32,14 @@ notifications (security breaches, changes to service, etc). Upon success, you are also logged in :) Syncing should happen automatically from here! +## Delete + +You can delete your sync account with + +``` +atuin unregister +``` + ## Key As all your data is encrypted, Atuin generates a key for you. It's stored in the