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
This commit is contained in:
Yannick Ulrich 2023-05-16 22:00:59 +01:00 committed by GitHub
parent dc523416f6
commit 7b9dea72e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 94 additions and 1 deletions

View file

@ -217,4 +217,19 @@ impl<'a> Client<'a> {
Ok(()) 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");
}
}
} }

View file

@ -19,6 +19,9 @@ pub struct RegisterResponse {
pub session: String, pub session: String,
} }
#[derive(Debug, Serialize, Deserialize)]
pub struct DeleteUserResponse {}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct LoginRequest { pub struct LoginRequest {
pub username: String, pub username: String,

View file

@ -27,6 +27,7 @@ pub trait Database {
async fn get_user(&self, username: &str) -> Result<User>; async fn get_user(&self, username: &str) -> Result<User>;
async fn get_user_session(&self, u: &User) -> Result<Session>; async fn get_user_session(&self, u: &User) -> Result<Session>;
async fn add_user(&self, user: &NewUser) -> Result<i64>; async fn add_user(&self, user: &NewUser) -> Result<i64>;
async fn delete_user(&self, u: &User) -> Result<()>;
async fn count_history(&self, user: &User) -> Result<i64>; async fn count_history(&self, user: &User) -> Result<i64>;
async fn count_history_cached(&self, user: &User) -> Result<i64>; async fn count_history_cached(&self, user: &User) -> Result<i64>;
@ -336,6 +337,26 @@ impl Database for Postgres {
Ok(()) 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)] #[instrument(skip_all)]
async fn add_user(&self, user: &NewUser) -> Result<i64> { async fn add_user(&self, user: &NewUser) -> Result<i64> {
let email: &str = &user.email; let email: &str = &user.email;

View file

@ -18,7 +18,7 @@ use uuid::Uuid;
use super::{ErrorResponse, ErrorResponseStatus, RespExt}; use super::{ErrorResponse, ErrorResponseStatus, RespExt};
use crate::{ use crate::{
database::Database, database::Database,
models::{NewSession, NewUser}, models::{NewSession, NewUser, User},
router::AppState, router::AppState,
}; };
@ -138,6 +138,23 @@ pub async fn register<DB: Database>(
} }
} }
#[instrument(skip_all, fields(user.id = user.id))]
pub async fn delete<DB: Database>(
user: User,
state: State<AppState<DB>>,
) -> 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));
};
Ok(Json(DeleteUserResponse {}))
}
#[instrument(skip_all, fields(user.username = login.username.as_str()))] #[instrument(skip_all, fields(user.username = login.username.as_str()))]
pub async fn login<DB: Database>( pub async fn login<DB: Database>(
state: State<AppState<DB>>, state: State<AppState<DB>>,

View file

@ -72,6 +72,7 @@ pub fn router<DB: Database + Clone + Send + Sync + 'static>(
.route("/history", post(handlers::history::add)) .route("/history", post(handlers::history::add))
.route("/history", delete(handlers::history::delete)) .route("/history", delete(handlers::history::delete))
.route("/user/:username", get(handlers::user::get)) .route("/user/:username", get(handlers::user::get))
.route("/account", delete(handlers::user::delete))
.route("/register", post(handlers::user::register)) .route("/register", post(handlers::user::register))
.route("/login", post(handlers::user::login)); .route("/login", post(handlers::user::login));

View file

@ -3,6 +3,7 @@ use eyre::{Result, WrapErr};
use atuin_client::{database::Database, settings::Settings}; use atuin_client::{database::Database, settings::Settings};
mod delete;
mod login; mod login;
mod logout; mod logout;
mod register; mod register;
@ -27,6 +28,9 @@ pub enum Cmd {
/// Register with the configured server /// Register with the configured server
Register(register::Cmd), Register(register::Cmd),
/// Unregister with the configured server
Unregister,
/// Print the encryption key for transfer to another machine /// Print the encryption key for transfer to another machine
Key { Key {
/// Switch to base64 output of the key /// Switch to base64 output of the key
@ -44,6 +48,7 @@ impl Cmd {
Self::Login(l) => l.run(&settings).await, Self::Login(l) => l.run(&settings).await,
Self::Logout => logout::run(&settings), Self::Logout => logout::run(&settings),
Self::Register(r) => r.run(&settings).await, Self::Register(r) => r.run(&settings).await,
Self::Unregister => delete::run(&settings).await,
Self::Status => status::run(&settings, db).await, Self::Status => status::run(&settings, db).await,
Self::Key { base64 } => { Self::Key { base64 } => {
use atuin_client::encryption::{encode_key, load_key}; use atuin_client::encryption::{encode_key, load_key};

View file

@ -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(())
}

View file

@ -32,6 +32,14 @@ notifications (security breaches, changes to service, etc).
Upon success, you are also logged in :) Syncing should happen automatically from Upon success, you are also logged in :) Syncing should happen automatically from
here! here!
## Delete
You can delete your sync account with
```
atuin unregister
```
## Key ## Key
As all your data is encrypted, Atuin generates a key for you. It's stored in the As all your data is encrypted, Atuin generates a key for you. It's stored in the