diff --git a/crates/diesel_models/src/query/user.rs b/crates/diesel_models/src/query/user.rs index b4d5976ba2..6fb5b79ddc 100644 --- a/crates/diesel_models/src/query/user.rs +++ b/crates/diesel_models/src/query/user.rs @@ -3,7 +3,7 @@ use diesel::{ associations::HasTable, debug_query, result::Error as DieselError, ExpressionMethods, JoinOnDsl, QueryDsl, }; -use error_stack::{report, IntoReport}; +use error_stack::IntoReport; use router_env::{ logger, tracing::{self, instrument}, @@ -49,19 +49,37 @@ impl User { pub async fn update_by_user_id( conn: &PgPooledConn, user_id: &str, - user: UserUpdate, + user_update: UserUpdate, ) -> StorageResult { - generics::generic_update_with_results::<::Table, _, _, _>( + generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( conn, users_dsl::user_id.eq(user_id.to_owned()), - UserUpdateInternal::from(user), + UserUpdateInternal::from(user_update), ) - .await? - .first() - .cloned() - .ok_or_else(|| { - report!(errors::DatabaseError::NotFound).attach_printable("Error while updating user") - }) + .await + } + + pub async fn update_by_user_email( + conn: &PgPooledConn, + user_email: &str, + user_update: UserUpdate, + ) -> StorageResult { + generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( + conn, + users_dsl::email.eq(user_email.to_owned()), + UserUpdateInternal::from(user_update), + ) + .await } pub async fn delete_by_user_id(conn: &PgPooledConn, user_id: &str) -> StorageResult { diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 24b6eb9d12..7050c9f002 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -1,4 +1,6 @@ use api_models::user::{self as user_api, InviteMultipleUserResponse}; +#[cfg(feature = "email")] +use diesel_models::user_role::UserRoleUpdate; use diesel_models::{enums::UserStatus, user as storage_user, user_role::UserRoleNew}; #[cfg(feature = "email")] use error_stack::IntoReport; @@ -362,18 +364,10 @@ pub async fn reset_password( let hash_password = utils::user::password::generate_password_hash(password.get_secret())?; - //TODO: Create Update by email query - let user_id = state + let user = state .store - .find_user_by_email(token.get_email()) - .await - .change_context(UserErrors::InternalServerError)? - .user_id; - - state - .store - .update_user_by_user_id( - user_id.as_str(), + .update_user_by_email( + token.get_email(), storage_user::UserUpdate::AccountUpdate { name: None, password: Some(hash_password), @@ -384,7 +378,20 @@ pub async fn reset_password( .await .change_context(UserErrors::InternalServerError)?; - //TODO: Update User role status for invited user + if let Some(inviter_merchant_id) = token.get_merchant_id() { + let update_status_result = state + .store + .update_user_role_by_user_id_merchant_id( + user.user_id.clone().as_str(), + inviter_merchant_id, + UserRoleUpdate::UpdateStatus { + status: UserStatus::Active, + modified_by: user.user_id, + }, + ) + .await; + logger::info!(?update_status_result); + } Ok(ApplicationResponse::StatusOk) } @@ -467,7 +474,7 @@ pub async fn invite_user( .store .insert_user_role(UserRoleNew { user_id: new_user.get_user_id().to_owned(), - merchant_id: user_from_token.merchant_id, + merchant_id: user_from_token.merchant_id.clone(), role_id: request.role_id, org_id: user_from_token.org_id, status: invitation_status, @@ -493,6 +500,7 @@ pub async fn invite_user( user_name: domain::UserName::new(new_user.get_name())?, settings: state.conf.clone(), subject: "You have been invited to join Hyperswitch Community!", + merchant_id: user_from_token.merchant_id, }; let send_email_result = state .email_client @@ -669,6 +677,7 @@ async fn handle_new_user_invitation( user_name: domain::UserName::new(new_user.get_name())?, settings: state.conf.clone(), subject: "You have been invited to join Hyperswitch Community!", + merchant_id: user_from_token.merchant_id.clone(), }; let send_email_result = state .email_client diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 665a920bca..0a9030bae2 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -1895,6 +1895,16 @@ impl UserInterface for KafkaStore { .await } + async fn update_user_by_email( + &self, + user_email: &str, + user: storage::UserUpdate, + ) -> CustomResult { + self.diesel_store + .update_user_by_email(user_email, user) + .await + } + async fn delete_user_by_user_id( &self, user_id: &str, diff --git a/crates/router/src/db/user.rs b/crates/router/src/db/user.rs index ecd71f7e2c..c7c005a0b5 100644 --- a/crates/router/src/db/user.rs +++ b/crates/router/src/db/user.rs @@ -33,6 +33,12 @@ pub trait UserInterface { user: storage::UserUpdate, ) -> CustomResult; + async fn update_user_by_email( + &self, + user_email: &str, + user: storage::UserUpdate, + ) -> CustomResult; + async fn delete_user_by_user_id( &self, user_id: &str, @@ -92,6 +98,18 @@ impl UserInterface for Store { .into_report() } + async fn update_user_by_email( + &self, + user_email: &str, + user: storage::UserUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::User::update_by_user_email(&conn, user_email, user) + .await + .map_err(Into::into) + .into_report() + } + async fn delete_user_by_user_id( &self, user_id: &str, @@ -229,6 +247,50 @@ impl UserInterface for MockDb { ) } + async fn update_user_by_email( + &self, + user_email: &str, + update_user: storage::UserUpdate, + ) -> CustomResult { + let mut users = self.users.lock().await; + let user_email_pii: common_utils::pii::Email = user_email + .to_string() + .try_into() + .map_err(|_| errors::StorageError::MockDbError)?; + users + .iter_mut() + .find(|user| user.email == user_email_pii) + .map(|user| { + *user = match &update_user { + storage::UserUpdate::VerifyUser => storage::User { + is_verified: true, + ..user.to_owned() + }, + storage::UserUpdate::AccountUpdate { + name, + password, + is_verified, + preferred_merchant_id, + } => storage::User { + name: name.clone().map(Secret::new).unwrap_or(user.name.clone()), + password: password.clone().unwrap_or(user.password.clone()), + is_verified: is_verified.unwrap_or(user.is_verified), + preferred_merchant_id: preferred_merchant_id + .clone() + .or(user.preferred_merchant_id.clone()), + ..user.to_owned() + }, + }; + user.to_owned() + }) + .ok_or( + errors::StorageError::ValueNotFound(format!( + "No user available for user_email = {user_email}" + )) + .into(), + ) + } + async fn delete_user_by_user_id( &self, user_id: &str, diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index d5aa992613..c68907c284 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -92,18 +92,21 @@ Email : {user_email} #[derive(serde::Serialize, serde::Deserialize)] pub struct EmailToken { email: String, + merchant_id: Option, exp: u64, } impl EmailToken { pub async fn new_token( email: domain::UserEmail, + merchant_id: Option, settings: &configs::settings::Settings, ) -> CustomResult { let expiration_duration = std::time::Duration::from_secs(consts::EMAIL_TOKEN_TIME_IN_SECS); let exp = jwt::generate_exp(expiration_duration)?.as_secs(); let token_payload = Self { email: email.get_secret().expose(), + merchant_id, exp, }; jwt::generate_jwt(&token_payload, settings).await @@ -112,6 +115,10 @@ impl EmailToken { pub fn get_email(&self) -> &str { self.email.as_str() } + + pub fn get_merchant_id(&self) -> Option<&str> { + self.merchant_id.as_deref() + } } pub fn get_link_with_token( @@ -132,7 +139,7 @@ pub struct VerifyEmail { #[async_trait::async_trait] impl EmailData for VerifyEmail { async fn get_email_data(&self) -> CustomResult { - let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) + let token = EmailToken::new_token(self.recipient_email.clone(), None, &self.settings) .await .change_context(EmailError::TokenGenerationFailure)?; @@ -161,7 +168,7 @@ pub struct ResetPassword { #[async_trait::async_trait] impl EmailData for ResetPassword { async fn get_email_data(&self) -> CustomResult { - let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) + let token = EmailToken::new_token(self.recipient_email.clone(), None, &self.settings) .await .change_context(EmailError::TokenGenerationFailure)?; @@ -191,7 +198,7 @@ pub struct MagicLink { #[async_trait::async_trait] impl EmailData for MagicLink { async fn get_email_data(&self) -> CustomResult { - let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) + let token = EmailToken::new_token(self.recipient_email.clone(), None, &self.settings) .await .change_context(EmailError::TokenGenerationFailure)?; @@ -216,14 +223,19 @@ pub struct InviteUser { pub user_name: domain::UserName, pub settings: std::sync::Arc, pub subject: &'static str, + pub merchant_id: String, } #[async_trait::async_trait] impl EmailData for InviteUser { async fn get_email_data(&self) -> CustomResult { - let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) - .await - .change_context(EmailError::TokenGenerationFailure)?; + let token = EmailToken::new_token( + self.recipient_email.clone(), + Some(self.merchant_id.clone()), + &self.settings, + ) + .await + .change_context(EmailError::TokenGenerationFailure)?; let invite_user_link = get_link_with_token(&self.settings.email.base_url, token, "set_password");