From 214f2491cef6c90469eee3da757bc2c91de2d355 Mon Sep 17 00:00:00 2001 From: Ty Date: Mon, 22 Jan 2024 22:17:58 -0700 Subject: [PATCH] Add change-password command & support on server --- atuin-client/src/api_client.rs | 24 +++++++- atuin-common/src/api.rs | 9 +++ atuin-server-database/src/lib.rs | 1 + atuin-server-postgres/src/lib.rs | 16 ++++++ atuin-server/src/handlers/user.rs | 27 +++++++++ atuin-server/src/router.rs | 3 +- atuin/src/command/client/account.rs | 4 ++ .../command/client/account/change_password.rs | 55 +++++++++++++++++++ 8 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 atuin/src/command/client/account/change_password.rs diff --git a/atuin-client/src/api_client.rs b/atuin-client/src/api_client.rs index affb3c98..5f154c1d 100644 --- a/atuin-client/src/api_client.rs +++ b/atuin-client/src/api_client.rs @@ -11,7 +11,7 @@ use reqwest::{ use atuin_common::{ api::{ AddHistoryRequest, CountResponse, DeleteHistoryRequest, ErrorResponse, IndexResponse, - LoginRequest, LoginResponse, RegisterResponse, StatusResponse, SyncHistoryResponse, + LoginRequest, LoginResponse, RegisterResponse, StatusResponse, SyncHistoryResponse, ChangePasswordRequest, }, record::RecordStatus, }; @@ -355,4 +355,26 @@ impl<'a> Client<'a> { bail!("Unknown error"); } } + + pub async fn change_password(&self, current_password: String, new_password: String) -> Result<()> { + let url = format!("{}/account/password", self.sync_addr); + let url = Url::parse(url.as_str())?; + + let resp = self.client.patch(url).json(&ChangePasswordRequest { + current_password, + new_password + }).send().await?; + + dbg!(&resp); + + 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/atuin-common/src/api.rs b/atuin-common/src/api.rs index b608937f..d9334ffc 100644 --- a/atuin-common/src/api.rs +++ b/atuin-common/src/api.rs @@ -33,6 +33,15 @@ pub struct RegisterResponse { #[derive(Debug, Serialize, Deserialize)] pub struct DeleteUserResponse {} +#[derive(Debug, Serialize, Deserialize)] +pub struct ChangePasswordRequest { + pub current_password: String, + pub new_password: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ChangePasswordResponse {} + #[derive(Debug, Serialize, Deserialize)] pub struct LoginRequest { pub username: String, diff --git a/atuin-server-database/src/lib.rs b/atuin-server-database/src/lib.rs index 9b154ea1..dff1204d 100644 --- a/atuin-server-database/src/lib.rs +++ b/atuin-server-database/src/lib.rs @@ -54,6 +54,7 @@ pub trait Database: Sized + Clone + Send + Sync + 'static { async fn get_user_session(&self, u: &User) -> DbResult; async fn add_user(&self, user: &NewUser) -> DbResult; async fn delete_user(&self, u: &User) -> DbResult<()>; + async fn update_user_password(&self, u: &User) -> DbResult<()>; async fn total_history(&self) -> DbResult; async fn count_history(&self, user: &User) -> DbResult; diff --git a/atuin-server-postgres/src/lib.rs b/atuin-server-postgres/src/lib.rs index c1de4d50..1f7cf47a 100644 --- a/atuin-server-postgres/src/lib.rs +++ b/atuin-server-postgres/src/lib.rs @@ -289,6 +289,22 @@ impl Database for Postgres { Ok(()) } + #[instrument(skip_all)] + 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 + .map_err(fix_error)?; + + Ok(()) + } + #[instrument(skip_all)] async fn add_user(&self, user: &NewUser) -> DbResult { let email: &str = &user.email; diff --git a/atuin-server/src/handlers/user.rs b/atuin-server/src/handlers/user.rs index 4f2ad891..e249a3e7 100644 --- a/atuin-server/src/handlers/user.rs +++ b/atuin-server/src/handlers/user.rs @@ -169,6 +169,33 @@ pub async fn delete( Ok(Json(DeleteUserResponse {})) } +#[instrument(skip_all, fields(user.id = user.id, change_password))] +pub async fn change_password( + UserAuth(mut user): UserAuth, + state: State>, + Json(change_password): Json, +) -> Result, 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 async fn login( state: State>, diff --git a/atuin-server/src/router.rs b/atuin-server/src/router.rs index 500e1a29..b2a61831 100644 --- a/atuin-server/src/router.rs +++ b/atuin-server/src/router.rs @@ -5,7 +5,7 @@ use axum::{ http::Request, middleware::Next, response::{IntoResponse, Response}, - routing::{delete, get, post}, + routing::{delete, get, post, patch}, Router, }; use eyre::Result; @@ -120,6 +120,7 @@ pub fn router(database: DB, settings: Settings) -> R .route("/history", delete(handlers::history::delete)) .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::)) diff --git a/atuin/src/command/client/account.rs b/atuin/src/command/client/account.rs index 657552fb..6d20eade 100644 --- a/atuin/src/command/client/account.rs +++ b/atuin/src/command/client/account.rs @@ -7,6 +7,7 @@ pub mod delete; pub mod login; pub mod logout; pub mod register; +pub mod change_password; #[derive(Args, Debug)] pub struct Cmd { @@ -27,6 +28,8 @@ pub enum Commands { // Delete your account, and all synced data Delete, + + ChangePassword(change_password::Cmd) } impl Cmd { @@ -36,6 +39,7 @@ impl Cmd { Commands::Register(r) => r.run(&settings).await, Commands::Logout => logout::run(&settings), Commands::Delete => delete::run(&settings).await, + Commands::ChangePassword(c) => c.run(&settings).await, } } } diff --git a/atuin/src/command/client/account/change_password.rs b/atuin/src/command/client/account/change_password.rs new file mode 100644 index 00000000..e3db454d --- /dev/null +++ b/atuin/src/command/client/account/change_password.rs @@ -0,0 +1,55 @@ +use clap::Parser; +use eyre::{bail, Result}; + +use atuin_client::{api_client, settings::Settings}; +use rpassword::prompt_password; + +#[derive(Parser, Debug)] +pub struct Cmd { + #[clap(long, short)] + pub current_password: Option, + + #[clap(long, short)] + pub new_password: Option, +} + +impl Cmd { + pub async fn run(self, settings: &Settings) -> Result<()> { + run(settings, &self.current_password, &self.new_password).await + } +} + +pub async fn run( + settings: &Settings, + current_password: &Option, + new_password: &Option, +) -> Result<()> { + let client = api_client::Client::new( + &settings.sync_address, + &settings.session_token, + settings.network_connect_timeout, + settings.network_timeout, + )?; + + let current_password = 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 = 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"); + } + + client.change_password(current_password, new_password).await?; + + println!("Account password successfully changed!"); + + Ok(()) +}