mirror of
https://github.com/atuinsh/atuin.git
synced 2024-11-25 09:44:03 +01:00
feat: Add change-password command & support on server (#1615)
* Add change-password command & support on server * Add a test for password change * review: run format --------- Co-authored-by: Ellie Huxtable <ellie@elliehuxtable.com>
This commit is contained in:
parent
e1c2b9c783
commit
0faf414cd9
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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>;
|
||||
|
@ -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;
|
||||
|
@ -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>>,
|
||||
|
@ -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>))
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
57
atuin/src/command/client/account/change_password.rs
Normal file
57
atuin/src/command/client/account/change_password.rs
Normal file
@ -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(())
|
||||
}
|
@ -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());
|
||||
|
Loading…
Reference in New Issue
Block a user