Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions atuin-client/src/api_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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");
}
}
}
9 changes: 9 additions & 0 deletions atuin-common/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions atuin-server-database/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down
16 changes: 16 additions & 0 deletions atuin-server-postgres/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<i64> {
let email: &str = &user.email;
Expand Down
30 changes: 30 additions & 0 deletions atuin-server/src/handlers/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>>,
Expand Down
3 changes: 2 additions & 1 deletion atuin-server/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use axum::{
http::Request,
middleware::Next,
response::{IntoResponse, Response},
routing::{delete, get, post},
routing::{delete, get, patch, post},
Router,
};
use eyre::Result;
Expand Down Expand Up @@ -120,6 +120,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>))
Expand Down
4 changes: 4 additions & 0 deletions atuin/src/command/client/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,6 +28,8 @@ pub enum Commands {

// Delete your account, and all synced data
Delete,

ChangePassword(change_password::Cmd),
}

impl Cmd {
Expand All @@ -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,
}
}
}
57 changes: 57 additions & 0 deletions atuin/src/command/client/account/change_password.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
38 changes: 38 additions & 0 deletions atuin/tests/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,44 @@ async fn registration() {
server.await.unwrap();
}

#[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());
Expand Down