mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 18:17:13 +08:00 
			
		
		
		
	feat(users): Add merchant_id in EmailToken and change user status in reset password (#3473)
				
					
				
			This commit is contained in:
		| @ -3,7 +3,7 @@ use diesel::{ | |||||||
|     associations::HasTable, debug_query, result::Error as DieselError, ExpressionMethods, |     associations::HasTable, debug_query, result::Error as DieselError, ExpressionMethods, | ||||||
|     JoinOnDsl, QueryDsl, |     JoinOnDsl, QueryDsl, | ||||||
| }; | }; | ||||||
| use error_stack::{report, IntoReport}; | use error_stack::IntoReport; | ||||||
| use router_env::{ | use router_env::{ | ||||||
|     logger, |     logger, | ||||||
|     tracing::{self, instrument}, |     tracing::{self, instrument}, | ||||||
| @ -49,19 +49,37 @@ impl User { | |||||||
|     pub async fn update_by_user_id( |     pub async fn update_by_user_id( | ||||||
|         conn: &PgPooledConn, |         conn: &PgPooledConn, | ||||||
|         user_id: &str, |         user_id: &str, | ||||||
|         user: UserUpdate, |         user_update: UserUpdate, | ||||||
|     ) -> StorageResult<Self> { |     ) -> StorageResult<Self> { | ||||||
|         generics::generic_update_with_results::<<Self as HasTable>::Table, _, _, _>( |         generics::generic_update_with_unique_predicate_get_result::< | ||||||
|  |             <Self as HasTable>::Table, | ||||||
|  |             _, | ||||||
|  |             _, | ||||||
|  |             _, | ||||||
|  |         >( | ||||||
|             conn, |             conn, | ||||||
|             users_dsl::user_id.eq(user_id.to_owned()), |             users_dsl::user_id.eq(user_id.to_owned()), | ||||||
|             UserUpdateInternal::from(user), |             UserUpdateInternal::from(user_update), | ||||||
|         ) |         ) | ||||||
|         .await? |         .await | ||||||
|         .first() |     } | ||||||
|         .cloned() |  | ||||||
|         .ok_or_else(|| { |     pub async fn update_by_user_email( | ||||||
|             report!(errors::DatabaseError::NotFound).attach_printable("Error while updating user") |         conn: &PgPooledConn, | ||||||
|         }) |         user_email: &str, | ||||||
|  |         user_update: UserUpdate, | ||||||
|  |     ) -> StorageResult<Self> { | ||||||
|  |         generics::generic_update_with_unique_predicate_get_result::< | ||||||
|  |             <Self as HasTable>::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<bool> { |     pub async fn delete_by_user_id(conn: &PgPooledConn, user_id: &str) -> StorageResult<bool> { | ||||||
|  | |||||||
| @ -1,4 +1,6 @@ | |||||||
| use api_models::user::{self as user_api, InviteMultipleUserResponse}; | 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}; | use diesel_models::{enums::UserStatus, user as storage_user, user_role::UserRoleNew}; | ||||||
| #[cfg(feature = "email")] | #[cfg(feature = "email")] | ||||||
| use error_stack::IntoReport; | 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())?; |     let hash_password = utils::user::password::generate_password_hash(password.get_secret())?; | ||||||
|  |  | ||||||
|     //TODO: Create Update by email query |     let user = state | ||||||
|     let user_id = state |  | ||||||
|         .store |         .store | ||||||
|         .find_user_by_email(token.get_email()) |         .update_user_by_email( | ||||||
|         .await |             token.get_email(), | ||||||
|         .change_context(UserErrors::InternalServerError)? |  | ||||||
|         .user_id; |  | ||||||
|  |  | ||||||
|     state |  | ||||||
|         .store |  | ||||||
|         .update_user_by_user_id( |  | ||||||
|             user_id.as_str(), |  | ||||||
|             storage_user::UserUpdate::AccountUpdate { |             storage_user::UserUpdate::AccountUpdate { | ||||||
|                 name: None, |                 name: None, | ||||||
|                 password: Some(hash_password), |                 password: Some(hash_password), | ||||||
| @ -384,7 +378,20 @@ pub async fn reset_password( | |||||||
|         .await |         .await | ||||||
|         .change_context(UserErrors::InternalServerError)?; |         .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) |     Ok(ApplicationResponse::StatusOk) | ||||||
| } | } | ||||||
| @ -467,7 +474,7 @@ pub async fn invite_user( | |||||||
|             .store |             .store | ||||||
|             .insert_user_role(UserRoleNew { |             .insert_user_role(UserRoleNew { | ||||||
|                 user_id: new_user.get_user_id().to_owned(), |                 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, |                 role_id: request.role_id, | ||||||
|                 org_id: user_from_token.org_id, |                 org_id: user_from_token.org_id, | ||||||
|                 status: invitation_status, |                 status: invitation_status, | ||||||
| @ -493,6 +500,7 @@ pub async fn invite_user( | |||||||
|                 user_name: domain::UserName::new(new_user.get_name())?, |                 user_name: domain::UserName::new(new_user.get_name())?, | ||||||
|                 settings: state.conf.clone(), |                 settings: state.conf.clone(), | ||||||
|                 subject: "You have been invited to join Hyperswitch Community!", |                 subject: "You have been invited to join Hyperswitch Community!", | ||||||
|  |                 merchant_id: user_from_token.merchant_id, | ||||||
|             }; |             }; | ||||||
|             let send_email_result = state |             let send_email_result = state | ||||||
|                 .email_client |                 .email_client | ||||||
| @ -669,6 +677,7 @@ async fn handle_new_user_invitation( | |||||||
|             user_name: domain::UserName::new(new_user.get_name())?, |             user_name: domain::UserName::new(new_user.get_name())?, | ||||||
|             settings: state.conf.clone(), |             settings: state.conf.clone(), | ||||||
|             subject: "You have been invited to join Hyperswitch Community!", |             subject: "You have been invited to join Hyperswitch Community!", | ||||||
|  |             merchant_id: user_from_token.merchant_id.clone(), | ||||||
|         }; |         }; | ||||||
|         let send_email_result = state |         let send_email_result = state | ||||||
|             .email_client |             .email_client | ||||||
|  | |||||||
| @ -1895,6 +1895,16 @@ impl UserInterface for KafkaStore { | |||||||
|             .await |             .await | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     async fn update_user_by_email( | ||||||
|  |         &self, | ||||||
|  |         user_email: &str, | ||||||
|  |         user: storage::UserUpdate, | ||||||
|  |     ) -> CustomResult<storage::User, errors::StorageError> { | ||||||
|  |         self.diesel_store | ||||||
|  |             .update_user_by_email(user_email, user) | ||||||
|  |             .await | ||||||
|  |     } | ||||||
|  |  | ||||||
|     async fn delete_user_by_user_id( |     async fn delete_user_by_user_id( | ||||||
|         &self, |         &self, | ||||||
|         user_id: &str, |         user_id: &str, | ||||||
|  | |||||||
| @ -33,6 +33,12 @@ pub trait UserInterface { | |||||||
|         user: storage::UserUpdate, |         user: storage::UserUpdate, | ||||||
|     ) -> CustomResult<storage::User, errors::StorageError>; |     ) -> CustomResult<storage::User, errors::StorageError>; | ||||||
|  |  | ||||||
|  |     async fn update_user_by_email( | ||||||
|  |         &self, | ||||||
|  |         user_email: &str, | ||||||
|  |         user: storage::UserUpdate, | ||||||
|  |     ) -> CustomResult<storage::User, errors::StorageError>; | ||||||
|  |  | ||||||
|     async fn delete_user_by_user_id( |     async fn delete_user_by_user_id( | ||||||
|         &self, |         &self, | ||||||
|         user_id: &str, |         user_id: &str, | ||||||
| @ -92,6 +98,18 @@ impl UserInterface for Store { | |||||||
|             .into_report() |             .into_report() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     async fn update_user_by_email( | ||||||
|  |         &self, | ||||||
|  |         user_email: &str, | ||||||
|  |         user: storage::UserUpdate, | ||||||
|  |     ) -> CustomResult<storage::User, errors::StorageError> { | ||||||
|  |         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( |     async fn delete_user_by_user_id( | ||||||
|         &self, |         &self, | ||||||
|         user_id: &str, |         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<storage::User, errors::StorageError> { | ||||||
|  |         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( |     async fn delete_user_by_user_id( | ||||||
|         &self, |         &self, | ||||||
|         user_id: &str, |         user_id: &str, | ||||||
|  | |||||||
| @ -92,18 +92,21 @@ Email         : {user_email} | |||||||
| #[derive(serde::Serialize, serde::Deserialize)] | #[derive(serde::Serialize, serde::Deserialize)] | ||||||
| pub struct EmailToken { | pub struct EmailToken { | ||||||
|     email: String, |     email: String, | ||||||
|  |     merchant_id: Option<String>, | ||||||
|     exp: u64, |     exp: u64, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl EmailToken { | impl EmailToken { | ||||||
|     pub async fn new_token( |     pub async fn new_token( | ||||||
|         email: domain::UserEmail, |         email: domain::UserEmail, | ||||||
|  |         merchant_id: Option<String>, | ||||||
|         settings: &configs::settings::Settings, |         settings: &configs::settings::Settings, | ||||||
|     ) -> CustomResult<String, UserErrors> { |     ) -> CustomResult<String, UserErrors> { | ||||||
|         let expiration_duration = std::time::Duration::from_secs(consts::EMAIL_TOKEN_TIME_IN_SECS); |         let expiration_duration = std::time::Duration::from_secs(consts::EMAIL_TOKEN_TIME_IN_SECS); | ||||||
|         let exp = jwt::generate_exp(expiration_duration)?.as_secs(); |         let exp = jwt::generate_exp(expiration_duration)?.as_secs(); | ||||||
|         let token_payload = Self { |         let token_payload = Self { | ||||||
|             email: email.get_secret().expose(), |             email: email.get_secret().expose(), | ||||||
|  |             merchant_id, | ||||||
|             exp, |             exp, | ||||||
|         }; |         }; | ||||||
|         jwt::generate_jwt(&token_payload, settings).await |         jwt::generate_jwt(&token_payload, settings).await | ||||||
| @ -112,6 +115,10 @@ impl EmailToken { | |||||||
|     pub fn get_email(&self) -> &str { |     pub fn get_email(&self) -> &str { | ||||||
|         self.email.as_str() |         self.email.as_str() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     pub fn get_merchant_id(&self) -> Option<&str> { | ||||||
|  |         self.merchant_id.as_deref() | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| pub fn get_link_with_token( | pub fn get_link_with_token( | ||||||
| @ -132,7 +139,7 @@ pub struct VerifyEmail { | |||||||
| #[async_trait::async_trait] | #[async_trait::async_trait] | ||||||
| impl EmailData for VerifyEmail { | impl EmailData for VerifyEmail { | ||||||
|     async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> { |     async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> { | ||||||
|         let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) |         let token = EmailToken::new_token(self.recipient_email.clone(), None, &self.settings) | ||||||
|             .await |             .await | ||||||
|             .change_context(EmailError::TokenGenerationFailure)?; |             .change_context(EmailError::TokenGenerationFailure)?; | ||||||
|  |  | ||||||
| @ -161,7 +168,7 @@ pub struct ResetPassword { | |||||||
| #[async_trait::async_trait] | #[async_trait::async_trait] | ||||||
| impl EmailData for ResetPassword { | impl EmailData for ResetPassword { | ||||||
|     async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> { |     async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> { | ||||||
|         let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) |         let token = EmailToken::new_token(self.recipient_email.clone(), None, &self.settings) | ||||||
|             .await |             .await | ||||||
|             .change_context(EmailError::TokenGenerationFailure)?; |             .change_context(EmailError::TokenGenerationFailure)?; | ||||||
|  |  | ||||||
| @ -191,7 +198,7 @@ pub struct MagicLink { | |||||||
| #[async_trait::async_trait] | #[async_trait::async_trait] | ||||||
| impl EmailData for MagicLink { | impl EmailData for MagicLink { | ||||||
|     async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> { |     async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> { | ||||||
|         let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) |         let token = EmailToken::new_token(self.recipient_email.clone(), None, &self.settings) | ||||||
|             .await |             .await | ||||||
|             .change_context(EmailError::TokenGenerationFailure)?; |             .change_context(EmailError::TokenGenerationFailure)?; | ||||||
|  |  | ||||||
| @ -216,14 +223,19 @@ pub struct InviteUser { | |||||||
|     pub user_name: domain::UserName, |     pub user_name: domain::UserName, | ||||||
|     pub settings: std::sync::Arc<configs::settings::Settings>, |     pub settings: std::sync::Arc<configs::settings::Settings>, | ||||||
|     pub subject: &'static str, |     pub subject: &'static str, | ||||||
|  |     pub merchant_id: String, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[async_trait::async_trait] | #[async_trait::async_trait] | ||||||
| impl EmailData for InviteUser { | impl EmailData for InviteUser { | ||||||
|     async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> { |     async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> { | ||||||
|         let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) |         let token = EmailToken::new_token( | ||||||
|             .await |             self.recipient_email.clone(), | ||||||
|             .change_context(EmailError::TokenGenerationFailure)?; |             Some(self.merchant_id.clone()), | ||||||
|  |             &self.settings, | ||||||
|  |         ) | ||||||
|  |         .await | ||||||
|  |         .change_context(EmailError::TokenGenerationFailure)?; | ||||||
|  |  | ||||||
|         let invite_user_link = |         let invite_user_link = | ||||||
|             get_link_with_token(&self.settings.email.base_url, token, "set_password"); |             get_link_with_token(&self.settings.email.base_url, token, "set_password"); | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Mani Chandra
					Mani Chandra