diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/users/change_password.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/users/change_password.rs new file mode 100644 index 0000000000..0c24c08eb3 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/users/change_password.rs @@ -0,0 +1,13 @@ +use serde_derive::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct ChangePasswordRequest { + pub password: String, + pub password_confirmation: String, +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct ChangePasswordResponse { + pub success: bool, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/users/mod.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/users/mod.rs index 842e7ec03c..72c2789523 100644 --- a/crates/api_clients/artcraft/artcraft_api_defs/src/users/mod.rs +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/users/mod.rs @@ -1,3 +1,4 @@ +pub mod change_password; pub mod edit_email; pub mod edit_username; pub mod session_info; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/docs/api_doc.rs b/crates/service/web/storyteller_web/src/docs/api_doc.rs index 4a0fd106b1..abb9fb9311 100644 --- a/crates/service/web/storyteller_web/src/docs/api_doc.rs +++ b/crates/service/web/storyteller_web/src/docs/api_doc.rs @@ -102,6 +102,7 @@ use crate::http_server::endpoints::user_bookmarks::list_user_bookmarks_for_user_ use crate::http_server::endpoints::user_ratings::batch_get_user_rating_handler::*; use crate::http_server::endpoints::user_ratings::get_user_rating_handler::*; use crate::http_server::endpoints::user_ratings::set_user_rating_handler::*; +use crate::http_server::endpoints::users::change_password_handler::*; use crate::http_server::endpoints::users::create_account_handler::*; use crate::http_server::endpoints::users::edit_email_handler::*; use crate::http_server::endpoints::users::edit_username_handler::*; @@ -123,6 +124,7 @@ use crate::http_server::endpoints::weights::list::list_weights_by_user_handler:: use crate::http_server::endpoints::weights::search::search_model_weights_impl::*; use crate::http_server::endpoints::weights::update::set_model_weight_cover_image_handler::*; use crate::http_server::endpoints::weights::update::update_weight_handler::*; +use artcraft_api_defs::users::change_password::{ChangePasswordRequest, ChangePasswordResponse}; use artcraft_api_defs::users::edit_email::{EditEmailRequest, EditEmailResponse}; use artcraft_api_defs::users::edit_username::{EditUsernameRequest, EditUsernameResponse}; use artcraft_api_defs::common::responses::job_details::JobDetailsLipsyncRequest; @@ -316,6 +318,7 @@ use tokens::tokens::zs_voice_datasets::*; crate::http_server::endpoints::user_ratings::batch_get_user_rating_handler::batch_get_user_rating_handler, crate::http_server::endpoints::user_ratings::get_user_rating_handler::get_user_rating_handler, crate::http_server::endpoints::user_ratings::set_user_rating_handler::set_user_rating_handler, + crate::http_server::endpoints::users::change_password_handler::change_password_handler, crate::http_server::endpoints::users::create_account_handler::create_account_handler, crate::http_server::endpoints::users::edit_email_handler::edit_email_handler, crate::http_server::endpoints::users::edit_username_handler::edit_username_handler, @@ -512,6 +515,9 @@ use tokens::tokens::zs_voice_datasets::*; GptImage1EditImageNumImages, GptImage1EditImageImageQuality, GptImage1EditImageResponse, + ChangePasswordError, + ChangePasswordRequest, + ChangePasswordResponse, EditEmailError, EditEmailRequest, EditEmailResponse, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/users/change_password_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/users/change_password_handler.rs new file mode 100644 index 0000000000..f47b14392b --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/users/change_password_handler.rs @@ -0,0 +1,135 @@ +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +use std::fmt; +use std::sync::Arc; + +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::Json; +use actix_web::{web, HttpRequest, HttpResponse}; +use log::warn; +use utoipa::ToSchema; +use artcraft_api_defs::users::change_password::{ChangePasswordRequest, ChangePasswordResponse}; +use crate::http_server::validations::validate_passwords::validate_passwords; +use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; +use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection; +use crate::state::server_state::ServerState; +use http_server_common::request::get_request_ip::get_request_ip; +use mysql_queries::queries::users::user::update::update_password::{update_password, UpdatePasswordArgs}; +use mysql_queries::utils::transactor::Transactor; +use password::bcrypt_hash_password::bcrypt_hash_password; + +#[derive(Debug, ToSchema)] +pub enum ChangePasswordError { + BadInput(String), + NotAuthorized, + ServerError, +} + +impl ResponseError for ChangePasswordError { + fn status_code(&self) -> StatusCode { + match *self { + ChangePasswordError::BadInput(_) => StatusCode::BAD_REQUEST, + ChangePasswordError::NotAuthorized => StatusCode::UNAUTHORIZED, + ChangePasswordError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + let error_reason = match self { + ChangePasswordError::BadInput(reason) => reason.to_string(), + ChangePasswordError::NotAuthorized => "unauthorized".to_string(), + ChangePasswordError::ServerError => "server error".to_string(), + }; + + to_simple_json_error(&error_reason, self.status_code()) + } +} + +// NB: Not using derive_more::Display since Clion doesn't understand it. +impl fmt::Display for ChangePasswordError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + + +/// Change password for the current user. +#[utoipa::path( + post, + tag = "Users", + path = "/v1/user/change_password", + responses( + (status = 200, description = "Success", body = ChangePasswordResponse), + (status = 400, description = "Bad input", body = ChangePasswordError), + (status = 401, description = "Not authorized", body = ChangePasswordError), + (status = 500, description = "Server error", body = ChangePasswordError), + ), + params( + ("request" = ChangePasswordRequest, description = "Payload for Request"), + ) +)] +pub async fn change_password_handler( + http_request: HttpRequest, + request: Json, + server_state: web::Data>, +) -> Result, ChangePasswordError> +{ + let password = request.password.trim(); + let password_confirmation = request.password_confirmation.trim(); + + if let Err(reason) = validate_passwords(password, password_confirmation) { + return Err(ChangePasswordError::BadInput(reason)); + } + + let mut mysql_connection = server_state.mysql_pool + .acquire() + .await + .map_err(|err| { + warn!("MySql pool error: {:?}", err); + ChangePasswordError::ServerError + })?; + + let user_session = require_user_session_using_connection( + &http_request, + &server_state.session_checker, + &mut mysql_connection) + .await + .map_err(|_err| { + ChangePasswordError::NotAuthorized + })?; + + if user_session.role.is_banned { + return Err(ChangePasswordError::NotAuthorized); + } + + let password_hash = match bcrypt_hash_password(password.to_string()) { + Ok(hash) => hash, + Err(err) => { + warn!("Bcrypt error: {:?}", err); + return Err(ChangePasswordError::ServerError); + } + }; + + let ip_address = get_request_ip(&http_request); + + let result = update_password(UpdatePasswordArgs { + user_token: &user_session.user_token_typed, + password_hash: &password_hash, + ip_address: &ip_address, + transactor: Transactor::for_connection(&mut mysql_connection), + }).await; + + match result { + Ok(()) => {}, + Err(err) => { + warn!("Error updating password: {:?}", err); + return Err(ChangePasswordError::ServerError); + } + } + + Ok(Json(ChangePasswordResponse { success: true })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/users/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/users/mod.rs index 8c39b76a02..f3dda5f783 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/users/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/users/mod.rs @@ -1,3 +1,4 @@ +pub mod change_password_handler; pub mod create_account_handler; pub mod edit_email_handler; pub mod edit_profile_handler; diff --git a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/user_routes.rs b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/user_routes.rs index d61ad4d146..8986d5e969 100644 --- a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/user_routes.rs +++ b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/user_routes.rs @@ -8,6 +8,7 @@ use crate::http_server::deprecated_endpoints::w2l::list_user_w2l_inference_resul use crate::http_server::deprecated_endpoints::w2l::list_user_w2l_templates::list_user_w2l_templates_handler; use crate::http_server::endpoints::tts::list_user_tts_inference_results::list_user_tts_inference_results_handler; use crate::http_server::endpoints::tts::list_user_tts_models::list_user_tts_models_handler; +use crate::http_server::endpoints::users::change_password_handler::change_password_handler; use crate::http_server::endpoints::users::create_account_handler::create_account_handler; use crate::http_server::endpoints::users::edit_email_handler::edit_email_handler; use crate::http_server::endpoints::users::edit_profile_handler::edit_profile_handler; @@ -100,6 +101,11 @@ pub fn add_user_routes (app: App) -> App // NB(bt): Modern user profile routes .service(web::scope("/v1/user") + .service( + web::resource("/change_password") + .route(web::post().to(change_password_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) .service( web::resource("/edit_email") .route(web::post().to(edit_email_handler))