Add change-password command & support on server

This commit is contained in:
Ty 2024-01-22 22:17:58 -07:00
parent d84f5b2d33
commit 214f2491ce
No known key found for this signature in database
GPG Key ID: 2813440C772555A4
8 changed files with 137 additions and 2 deletions

View File

@ -11,7 +11,7 @@ use reqwest::{
use atuin_common::{ use atuin_common::{
api::{ api::{
AddHistoryRequest, CountResponse, DeleteHistoryRequest, ErrorResponse, IndexResponse, AddHistoryRequest, CountResponse, DeleteHistoryRequest, ErrorResponse, IndexResponse,
LoginRequest, LoginResponse, RegisterResponse, StatusResponse, SyncHistoryResponse, LoginRequest, LoginResponse, RegisterResponse, StatusResponse, SyncHistoryResponse, ChangePasswordRequest,
}, },
record::RecordStatus, record::RecordStatus,
}; };
@ -355,4 +355,26 @@ impl<'a> Client<'a> {
bail!("Unknown error"); 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");
}
}
} }

View File

@ -33,6 +33,15 @@ pub struct RegisterResponse {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct DeleteUserResponse {} 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)] #[derive(Debug, Serialize, Deserialize)]
pub struct LoginRequest { pub struct LoginRequest {
pub username: String, pub username: String,

View File

@ -54,6 +54,7 @@ pub trait Database: Sized + Clone + Send + Sync + 'static {
async fn get_user_session(&self, u: &User) -> DbResult<Session>; async fn get_user_session(&self, u: &User) -> DbResult<Session>;
async fn add_user(&self, user: &NewUser) -> DbResult<i64>; async fn add_user(&self, user: &NewUser) -> DbResult<i64>;
async fn delete_user(&self, u: &User) -> DbResult<()>; 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 total_history(&self) -> DbResult<i64>;
async fn count_history(&self, user: &User) -> DbResult<i64>; async fn count_history(&self, user: &User) -> DbResult<i64>;

View File

@ -289,6 +289,22 @@ impl Database for Postgres {
Ok(()) 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)] #[instrument(skip_all)]
async fn add_user(&self, user: &NewUser) -> DbResult<i64> { async fn add_user(&self, user: &NewUser) -> DbResult<i64> {
let email: &str = &user.email; let email: &str = &user.email;

View File

@ -169,6 +169,33 @@ pub async fn delete<DB: Database>(
Ok(Json(DeleteUserResponse {})) 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()))] #[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

@ -5,7 +5,7 @@ use axum::{
http::Request, http::Request,
middleware::Next, middleware::Next,
response::{IntoResponse, Response}, response::{IntoResponse, Response},
routing::{delete, get, post}, routing::{delete, get, post, patch},
Router, Router,
}; };
use eyre::Result; use eyre::Result;
@ -120,6 +120,7 @@ pub fn router<DB: Database>(database: DB, settings: Settings<DB::Settings>) -> R
.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("/account", delete(handlers::user::delete))
.route("/account/password", patch(handlers::user::change_password))
.route("/register", post(handlers::user::register)) .route("/register", post(handlers::user::register))
.route("/login", post(handlers::user::login)) .route("/login", post(handlers::user::login))
.route("/record", post(handlers::record::post::<DB>)) .route("/record", post(handlers::record::post::<DB>))

View File

@ -7,6 +7,7 @@ pub mod delete;
pub mod login; pub mod login;
pub mod logout; pub mod logout;
pub mod register; pub mod register;
pub mod change_password;
#[derive(Args, Debug)] #[derive(Args, Debug)]
pub struct Cmd { pub struct Cmd {
@ -27,6 +28,8 @@ pub enum Commands {
// Delete your account, and all synced data // Delete your account, and all synced data
Delete, Delete,
ChangePassword(change_password::Cmd)
} }
impl Cmd { impl Cmd {
@ -36,6 +39,7 @@ impl Cmd {
Commands::Register(r) => r.run(&settings).await, Commands::Register(r) => r.run(&settings).await,
Commands::Logout => logout::run(&settings), Commands::Logout => logout::run(&settings),
Commands::Delete => delete::run(&settings).await, Commands::Delete => delete::run(&settings).await,
Commands::ChangePassword(c) => c.run(&settings).await,
} }
} }
} }

View File

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