aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--atuin-client/src/api_client.rs36
-rw-r--r--atuin-common/src/api.rs9
-rw-r--r--atuin-server-database/src/lib.rs1
-rw-r--r--atuin-server-postgres/src/lib.rs16
-rw-r--r--atuin-server/src/handlers/user.rs30
-rw-r--r--atuin-server/src/router.rs3
-rw-r--r--atuin/src/command/client/account.rs4
-rw-r--r--atuin/src/command/client/account/change_password.rs57
-rw-r--r--atuin/tests/sync.rs38
9 files changed, 191 insertions, 3 deletions
diff --git a/atuin-client/src/api_client.rs b/atuin-client/src/api_client.rs
index dc835cfb..d53c9a36 100644
--- a/atuin-client/src/api_client.rs
+++ b/atuin-client/src/api_client.rs
@@ -10,8 +10,9 @@ use reqwest::{
use atuin_common::{
api::{
- AddHistoryRequest, CountResponse, DeleteHistoryRequest, ErrorResponse, LoginRequest,
- LoginResponse, RegisterResponse, StatusResponse, SyncHistoryResponse,
+ AddHistoryRequest, ChangePasswordRequest, CountResponse, DeleteHistoryRequest,
+ ErrorResponse, LoginRequest, LoginResponse, RegisterResponse, StatusResponse,
+ SyncHistoryResponse,
},
record::RecordStatus,
};
@@ -359,4 +360,35 @@ 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
@@ -34,6 +34,15 @@ pub struct RegisterResponse {
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,
pub password: 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<Session>;
async fn add_user(&self, user: &NewUser) -> DbResult<i64>;
async fn delete_user(&self, u: &User) -> DbResult<()>;
+ async fn update_user_password(&self, u: &User) -> DbResult<()>;
async fn total_history(&self) -> DbResult<i64>;
async fn count_history(&self, user: &User) -> DbResult<i64>;
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
@@ -290,6 +290,22 @@ impl Database for Postgres {
}
#[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<i64> {
let email: &str = &user.email;
let username: &str = &user.username;
diff --git a/atuin-server/src/handlers/user.rs b/atuin-server/src/handlers/user.rs
index fb281ab3..e5651fe2 100644
--- a/atuin-server/src/handlers/user.rs
+++ b/atuin-server/src/handlers/user.rs
@@ -175,6 +175,36 @@ pub async fn delete<DB: Database>(
Ok(Json(DeleteUserResponse {}))
}
+#[instrument(skip_all, fields(user.id = user.id, change_password))]
+pub async fn change_password<DB: Database>(
+ UserAuth(mut user): UserAuth,
+ state: State<AppState<DB>>,
+ 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 async fn login<DB: Database>(
state: State<AppState<DB>>,
diff --git a/atuin-server/src/router.rs b/atuin-server/src/router.rs
index 8509058f..74df229a 100644
--- a/atuin-server/src/router.rs
+++ b/atuin-server/src/router.rs
@@ -5,7 +5,7 @@ use axum::{
http::{self, request::Parts},
middleware::Next,
response::{IntoResponse, Response},
- routing::{delete, get, post},
+ routing::{delete, get, patch, post},
Router,
};
use eyre::Result;
@@ -119,6 +119,7 @@ pub fn router<DB: Database>(database: DB, settings: Settings<DB::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::<DB>))
diff --git a/atuin/src/command/client/account.rs b/atuin/src/command/client/account.rs
index 657552fb..75f8ed59 100644
--- a/atuin/src/command/client/account.rs
+++ b/atuin/src/command/client/account.rs
@@ -3,6 +3,7 @@ use eyre::Result;
use atuin_client::settings::Settings;
+pub mod change_password;
pub mod delete;
pub mod login;
pub mod logout;
@@ -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..3b5ad6f5
--- /dev/null
+++ b/atuin/src/command/client/account/change_password.rs
@@ -0,0 +1,57 @@
+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<String>,
+
+ #[clap(long, short)]
+ pub new_password: Option<String>,
+}
+
+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<String>,
+ new_password: &Option<String>,
+) -> 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(())
+}
diff --git a/atuin/tests/sync.rs b/atuin/tests/sync.rs
index 4ae56a7b..6dacbd54 100644
--- a/atuin/tests/sync.rs
+++ b/atuin/tests/sync.rs
@@ -127,6 +127,44 @@ async fn registration() {
}
#[tokio::test]
+async fn change_password() {
+ let path = format!("/{}", uuid_v7().as_simple());
+ let (address, shutdown, server) = start_server(&path).await;
+
+ // -- REGISTRATION --
+
+ let username = uuid_v7().as_simple().to_string();
+ let password = uuid_v7().as_simple().to_string();
+ let client = register_inner(&address, &username, &password).await;
+
+ // the session token works
+ let status = client.status().await.unwrap();
+ assert_eq!(status.username, username);
+
+ // -- PASSWORD CHANGE --
+
+ let current_password = password;
+ let new_password = uuid_v7().as_simple().to_string();
+ let result = client
+ .change_password(current_password, new_password.clone())
+ .await;
+
+ // the password change request succeeded
+ assert!(result.is_ok());
+
+ // -- LOGIN --
+
+ let client = login(&address, username.clone(), new_password).await;
+
+ // login with new password yields a working token
+ let status = client.status().await.unwrap();
+ assert_eq!(status.username, username);
+
+ shutdown.send(()).unwrap();
+ server.await.unwrap();
+}
+
+#[tokio::test]
async fn sync() {
let path = format!("/{}", uuid_v7().as_simple());
let (address, shutdown, server) = start_server(&path).await;