mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-11-01 02:57:02 +08:00 
			
		
		
		
	feat(users): Add preferred_merchant_id column and update user details API (#3373)
				
					
				
			Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
		| @ -13,7 +13,8 @@ use crate::user::{ | |||||||
|     AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest, |     AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest, | ||||||
|     DashboardEntryResponse, ForgotPasswordRequest, GetUsersResponse, InviteUserRequest, |     DashboardEntryResponse, ForgotPasswordRequest, GetUsersResponse, InviteUserRequest, | ||||||
|     InviteUserResponse, ResetPasswordRequest, SendVerifyEmailRequest, SignUpRequest, |     InviteUserResponse, ResetPasswordRequest, SendVerifyEmailRequest, SignUpRequest, | ||||||
|     SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, UserMerchantCreate, VerifyEmailRequest, |     SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, UpdateUserAccountDetailsRequest, | ||||||
|  |     UserMerchantCreate, VerifyEmailRequest, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| impl ApiEventMetric for DashboardEntryResponse { | impl ApiEventMetric for DashboardEntryResponse { | ||||||
| @ -54,7 +55,8 @@ common_utils::impl_misc_api_event_type!( | |||||||
|     InviteUserRequest, |     InviteUserRequest, | ||||||
|     InviteUserResponse, |     InviteUserResponse, | ||||||
|     VerifyEmailRequest, |     VerifyEmailRequest, | ||||||
|     SendVerifyEmailRequest |     SendVerifyEmailRequest, | ||||||
|  |     UpdateUserAccountDetailsRequest | ||||||
| ); | ); | ||||||
|  |  | ||||||
| #[cfg(feature = "dummy_connector")] | #[cfg(feature = "dummy_connector")] | ||||||
|  | |||||||
| @ -147,3 +147,9 @@ pub struct VerifyTokenResponse { | |||||||
|     pub merchant_id: String, |     pub merchant_id: String, | ||||||
|     pub user_email: pii::Email, |     pub user_email: pii::Email, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, serde::Deserialize, serde::Serialize)] | ||||||
|  | pub struct UpdateUserAccountDetailsRequest { | ||||||
|  |     pub name: Option<Secret<String>>, | ||||||
|  |     pub preferred_merchant_id: Option<String>, | ||||||
|  | } | ||||||
|  | |||||||
| @ -19,6 +19,20 @@ impl UserRole { | |||||||
|         .await |         .await | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     pub async fn find_by_user_id_merchant_id( | ||||||
|  |         conn: &PgPooledConn, | ||||||
|  |         user_id: String, | ||||||
|  |         merchant_id: String, | ||||||
|  |     ) -> StorageResult<Self> { | ||||||
|  |         generics::generic_find_one::<<Self as HasTable>::Table, _, _>( | ||||||
|  |             conn, | ||||||
|  |             dsl::user_id | ||||||
|  |                 .eq(user_id) | ||||||
|  |                 .and(dsl::merchant_id.eq(merchant_id)), | ||||||
|  |         ) | ||||||
|  |         .await | ||||||
|  |     } | ||||||
|  |  | ||||||
|     pub async fn update_by_user_id_merchant_id( |     pub async fn update_by_user_id_merchant_id( | ||||||
|         conn: &PgPooledConn, |         conn: &PgPooledConn, | ||||||
|         user_id: String, |         user_id: String, | ||||||
|  | |||||||
| @ -1056,6 +1056,8 @@ diesel::table! { | |||||||
|         is_verified -> Bool, |         is_verified -> Bool, | ||||||
|         created_at -> Timestamp, |         created_at -> Timestamp, | ||||||
|         last_modified_at -> Timestamp, |         last_modified_at -> Timestamp, | ||||||
|  |         #[max_length = 64] | ||||||
|  |         preferred_merchant_id -> Nullable<Varchar>, | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
| @ -19,6 +19,7 @@ pub struct User { | |||||||
|     pub is_verified: bool, |     pub is_verified: bool, | ||||||
|     pub created_at: PrimitiveDateTime, |     pub created_at: PrimitiveDateTime, | ||||||
|     pub last_modified_at: PrimitiveDateTime, |     pub last_modified_at: PrimitiveDateTime, | ||||||
|  |     pub preferred_merchant_id: Option<String>, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive( | #[derive( | ||||||
| @ -33,6 +34,7 @@ pub struct UserNew { | |||||||
|     pub is_verified: bool, |     pub is_verified: bool, | ||||||
|     pub created_at: Option<PrimitiveDateTime>, |     pub created_at: Option<PrimitiveDateTime>, | ||||||
|     pub last_modified_at: Option<PrimitiveDateTime>, |     pub last_modified_at: Option<PrimitiveDateTime>, | ||||||
|  |     pub preferred_merchant_id: Option<String>, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] | #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] | ||||||
| @ -42,6 +44,7 @@ pub struct UserUpdateInternal { | |||||||
|     password: Option<Secret<String>>, |     password: Option<Secret<String>>, | ||||||
|     is_verified: Option<bool>, |     is_verified: Option<bool>, | ||||||
|     last_modified_at: PrimitiveDateTime, |     last_modified_at: PrimitiveDateTime, | ||||||
|  |     preferred_merchant_id: Option<String>, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug)] | #[derive(Debug)] | ||||||
| @ -51,6 +54,7 @@ pub enum UserUpdate { | |||||||
|         name: Option<String>, |         name: Option<String>, | ||||||
|         password: Option<Secret<String>>, |         password: Option<Secret<String>>, | ||||||
|         is_verified: Option<bool>, |         is_verified: Option<bool>, | ||||||
|  |         preferred_merchant_id: Option<String>, | ||||||
|     }, |     }, | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -63,16 +67,19 @@ impl From<UserUpdate> for UserUpdateInternal { | |||||||
|                 password: None, |                 password: None, | ||||||
|                 is_verified: Some(true), |                 is_verified: Some(true), | ||||||
|                 last_modified_at, |                 last_modified_at, | ||||||
|  |                 preferred_merchant_id: None, | ||||||
|             }, |             }, | ||||||
|             UserUpdate::AccountUpdate { |             UserUpdate::AccountUpdate { | ||||||
|                 name, |                 name, | ||||||
|                 password, |                 password, | ||||||
|                 is_verified, |                 is_verified, | ||||||
|  |                 preferred_merchant_id, | ||||||
|             } => Self { |             } => Self { | ||||||
|                 name, |                 name, | ||||||
|                 password, |                 password, | ||||||
|                 is_verified, |                 is_verified, | ||||||
|                 last_modified_at, |                 last_modified_at, | ||||||
|  |                 preferred_merchant_id, | ||||||
|             }, |             }, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -253,6 +253,7 @@ pub async fn change_password( | |||||||
|                 name: None, |                 name: None, | ||||||
|                 password: Some(new_password_hash), |                 password: Some(new_password_hash), | ||||||
|                 is_verified: None, |                 is_verified: None, | ||||||
|  |                 preferred_merchant_id: None, | ||||||
|             }, |             }, | ||||||
|         ) |         ) | ||||||
|         .await |         .await | ||||||
| @ -330,6 +331,7 @@ pub async fn reset_password( | |||||||
|                 name: None, |                 name: None, | ||||||
|                 password: Some(hash_password), |                 password: Some(hash_password), | ||||||
|                 is_verified: Some(true), |                 is_verified: Some(true), | ||||||
|  |                 preferred_merchant_id: None, | ||||||
|             }, |             }, | ||||||
|         ) |         ) | ||||||
|         .await |         .await | ||||||
| @ -786,3 +788,47 @@ pub async fn verify_token( | |||||||
|         user_email: user.email, |         user_email: user.email, | ||||||
|     })) |     })) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | pub async fn update_user_details( | ||||||
|  |     state: AppState, | ||||||
|  |     user_token: auth::UserFromToken, | ||||||
|  |     req: user_api::UpdateUserAccountDetailsRequest, | ||||||
|  | ) -> UserResponse<()> { | ||||||
|  |     let user: domain::UserFromStorage = state | ||||||
|  |         .store | ||||||
|  |         .find_user_by_id(&user_token.user_id) | ||||||
|  |         .await | ||||||
|  |         .change_context(UserErrors::InternalServerError)? | ||||||
|  |         .into(); | ||||||
|  |  | ||||||
|  |     let name = req.name.map(domain::UserName::new).transpose()?; | ||||||
|  |  | ||||||
|  |     if let Some(ref preferred_merchant_id) = req.preferred_merchant_id { | ||||||
|  |         let _ = state | ||||||
|  |             .store | ||||||
|  |             .find_user_role_by_user_id_merchant_id(user.get_user_id(), preferred_merchant_id) | ||||||
|  |             .await | ||||||
|  |             .map_err(|e| { | ||||||
|  |                 if e.current_context().is_db_not_found() { | ||||||
|  |                     e.change_context(UserErrors::MerchantIdNotFound) | ||||||
|  |                 } else { | ||||||
|  |                     e.change_context(UserErrors::InternalServerError) | ||||||
|  |                 } | ||||||
|  |             })?; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let user_update = storage_user::UserUpdate::AccountUpdate { | ||||||
|  |         name: name.map(|x| x.get_secret().expose()), | ||||||
|  |         password: None, | ||||||
|  |         is_verified: None, | ||||||
|  |         preferred_merchant_id: req.preferred_merchant_id, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     state | ||||||
|  |         .store | ||||||
|  |         .update_user_by_user_id(user.get_user_id(), user_update) | ||||||
|  |         .await | ||||||
|  |         .change_context(UserErrors::InternalServerError)?; | ||||||
|  |  | ||||||
|  |     Ok(ApplicationResponse::StatusOk) | ||||||
|  | } | ||||||
|  | |||||||
| @ -1927,12 +1927,24 @@ impl UserRoleInterface for KafkaStore { | |||||||
|     ) -> CustomResult<user_storage::UserRole, errors::StorageError> { |     ) -> CustomResult<user_storage::UserRole, errors::StorageError> { | ||||||
|         self.diesel_store.insert_user_role(user_role).await |         self.diesel_store.insert_user_role(user_role).await | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async fn find_user_role_by_user_id( |     async fn find_user_role_by_user_id( | ||||||
|         &self, |         &self, | ||||||
|         user_id: &str, |         user_id: &str, | ||||||
|     ) -> CustomResult<user_storage::UserRole, errors::StorageError> { |     ) -> CustomResult<user_storage::UserRole, errors::StorageError> { | ||||||
|         self.diesel_store.find_user_role_by_user_id(user_id).await |         self.diesel_store.find_user_role_by_user_id(user_id).await | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     async fn find_user_role_by_user_id_merchant_id( | ||||||
|  |         &self, | ||||||
|  |         user_id: &str, | ||||||
|  |         merchant_id: &str, | ||||||
|  |     ) -> CustomResult<user_storage::UserRole, errors::StorageError> { | ||||||
|  |         self.diesel_store | ||||||
|  |             .find_user_role_by_user_id_merchant_id(user_id, merchant_id) | ||||||
|  |             .await | ||||||
|  |     } | ||||||
|  |  | ||||||
|     async fn update_user_role_by_user_id_merchant_id( |     async fn update_user_role_by_user_id_merchant_id( | ||||||
|         &self, |         &self, | ||||||
|         user_id: &str, |         user_id: &str, | ||||||
| @ -1943,9 +1955,11 @@ impl UserRoleInterface for KafkaStore { | |||||||
|             .update_user_role_by_user_id_merchant_id(user_id, merchant_id, update) |             .update_user_role_by_user_id_merchant_id(user_id, merchant_id, update) | ||||||
|             .await |             .await | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async fn delete_user_role(&self, user_id: &str) -> CustomResult<bool, errors::StorageError> { |     async fn delete_user_role(&self, user_id: &str) -> CustomResult<bool, errors::StorageError> { | ||||||
|         self.diesel_store.delete_user_role(user_id).await |         self.diesel_store.delete_user_role(user_id).await | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async fn list_user_roles_by_user_id( |     async fn list_user_roles_by_user_id( | ||||||
|         &self, |         &self, | ||||||
|         user_id: &str, |         user_id: &str, | ||||||
|  | |||||||
| @ -145,6 +145,7 @@ impl UserInterface for MockDb { | |||||||
|             is_verified: user_data.is_verified, |             is_verified: user_data.is_verified, | ||||||
|             created_at: user_data.created_at.unwrap_or(time_now), |             created_at: user_data.created_at.unwrap_or(time_now), | ||||||
|             last_modified_at: user_data.created_at.unwrap_or(time_now), |             last_modified_at: user_data.created_at.unwrap_or(time_now), | ||||||
|  |             preferred_merchant_id: user_data.preferred_merchant_id, | ||||||
|         }; |         }; | ||||||
|         users.push(user.clone()); |         users.push(user.clone()); | ||||||
|         Ok(user) |         Ok(user) | ||||||
| @ -207,10 +208,14 @@ impl UserInterface for MockDb { | |||||||
|                         name, |                         name, | ||||||
|                         password, |                         password, | ||||||
|                         is_verified, |                         is_verified, | ||||||
|  |                         preferred_merchant_id, | ||||||
|                     } => storage::User { |                     } => storage::User { | ||||||
|                         name: name.clone().map(Secret::new).unwrap_or(user.name.clone()), |                         name: name.clone().map(Secret::new).unwrap_or(user.name.clone()), | ||||||
|                         password: password.clone().unwrap_or(user.password.clone()), |                         password: password.clone().unwrap_or(user.password.clone()), | ||||||
|                         is_verified: is_verified.unwrap_or(user.is_verified), |                         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() | ||||||
|                     }, |                     }, | ||||||
|                 }; |                 }; | ||||||
|  | |||||||
| @ -14,16 +14,25 @@ pub trait UserRoleInterface { | |||||||
|         &self, |         &self, | ||||||
|         user_role: storage::UserRoleNew, |         user_role: storage::UserRoleNew, | ||||||
|     ) -> CustomResult<storage::UserRole, errors::StorageError>; |     ) -> CustomResult<storage::UserRole, errors::StorageError>; | ||||||
|  |  | ||||||
|     async fn find_user_role_by_user_id( |     async fn find_user_role_by_user_id( | ||||||
|         &self, |         &self, | ||||||
|         user_id: &str, |         user_id: &str, | ||||||
|     ) -> CustomResult<storage::UserRole, errors::StorageError>; |     ) -> CustomResult<storage::UserRole, errors::StorageError>; | ||||||
|  |  | ||||||
|  |     async fn find_user_role_by_user_id_merchant_id( | ||||||
|  |         &self, | ||||||
|  |         user_id: &str, | ||||||
|  |         merchant_id: &str, | ||||||
|  |     ) -> CustomResult<storage::UserRole, errors::StorageError>; | ||||||
|  |  | ||||||
|     async fn update_user_role_by_user_id_merchant_id( |     async fn update_user_role_by_user_id_merchant_id( | ||||||
|         &self, |         &self, | ||||||
|         user_id: &str, |         user_id: &str, | ||||||
|         merchant_id: &str, |         merchant_id: &str, | ||||||
|         update: storage::UserRoleUpdate, |         update: storage::UserRoleUpdate, | ||||||
|     ) -> CustomResult<storage::UserRole, errors::StorageError>; |     ) -> CustomResult<storage::UserRole, errors::StorageError>; | ||||||
|  |  | ||||||
|     async fn delete_user_role(&self, user_id: &str) -> CustomResult<bool, errors::StorageError>; |     async fn delete_user_role(&self, user_id: &str) -> CustomResult<bool, errors::StorageError>; | ||||||
|  |  | ||||||
|     async fn list_user_roles_by_user_id( |     async fn list_user_roles_by_user_id( | ||||||
| @ -57,6 +66,22 @@ impl UserRoleInterface for Store { | |||||||
|             .into_report() |             .into_report() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     async fn find_user_role_by_user_id_merchant_id( | ||||||
|  |         &self, | ||||||
|  |         user_id: &str, | ||||||
|  |         merchant_id: &str, | ||||||
|  |     ) -> CustomResult<storage::UserRole, errors::StorageError> { | ||||||
|  |         let conn = connection::pg_connection_write(self).await?; | ||||||
|  |         storage::UserRole::find_by_user_id_merchant_id( | ||||||
|  |             &conn, | ||||||
|  |             user_id.to_owned(), | ||||||
|  |             merchant_id.to_owned(), | ||||||
|  |         ) | ||||||
|  |         .await | ||||||
|  |         .map_err(Into::into) | ||||||
|  |         .into_report() | ||||||
|  |     } | ||||||
|  |  | ||||||
|     async fn update_user_role_by_user_id_merchant_id( |     async fn update_user_role_by_user_id_merchant_id( | ||||||
|         &self, |         &self, | ||||||
|         user_id: &str, |         user_id: &str, | ||||||
| @ -148,6 +173,24 @@ impl UserRoleInterface for MockDb { | |||||||
|             ) |             ) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     async fn find_user_role_by_user_id_merchant_id( | ||||||
|  |         &self, | ||||||
|  |         user_id: &str, | ||||||
|  |         merchant_id: &str, | ||||||
|  |     ) -> CustomResult<storage::UserRole, errors::StorageError> { | ||||||
|  |         let user_roles = self.user_roles.lock().await; | ||||||
|  |         user_roles | ||||||
|  |             .iter() | ||||||
|  |             .find(|user_role| user_role.user_id == user_id && user_role.merchant_id == merchant_id) | ||||||
|  |             .cloned() | ||||||
|  |             .ok_or( | ||||||
|  |                 errors::StorageError::ValueNotFound(format!( | ||||||
|  |                     "No user role available for user_id = {user_id} and merchant_id = {merchant_id}" | ||||||
|  |                 )) | ||||||
|  |                 .into(), | ||||||
|  |             ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|     async fn update_user_role_by_user_id_merchant_id( |     async fn update_user_role_by_user_id_merchant_id( | ||||||
|         &self, |         &self, | ||||||
|         user_id: &str, |         user_id: &str, | ||||||
|  | |||||||
| @ -921,6 +921,7 @@ impl User { | |||||||
|             .service(web::resource("/role/list").route(web::get().to(list_roles))) |             .service(web::resource("/role/list").route(web::get().to(list_roles))) | ||||||
|             .service(web::resource("/role/{role_id}").route(web::get().to(get_role))) |             .service(web::resource("/role/{role_id}").route(web::get().to(get_role))) | ||||||
|             .service(web::resource("/user/invite").route(web::post().to(invite_user))) |             .service(web::resource("/user/invite").route(web::post().to(invite_user))) | ||||||
|  |             .service(web::resource("/update").route(web::post().to(update_user_account_details))) | ||||||
|             .service( |             .service( | ||||||
|                 web::resource("/data") |                 web::resource("/data") | ||||||
|                     .route(web::get().to(get_multiple_dashboard_metadata)) |                     .route(web::get().to(get_multiple_dashboard_metadata)) | ||||||
|  | |||||||
| @ -178,7 +178,8 @@ impl From<Flow> for ApiIdentifier { | |||||||
|             | Flow::InviteUser |             | Flow::InviteUser | ||||||
|             | Flow::UserSignUpWithMerchantId |             | Flow::UserSignUpWithMerchantId | ||||||
|             | Flow::VerifyEmail |             | Flow::VerifyEmail | ||||||
|             | Flow::VerifyEmailRequest => Self::User, |             | Flow::VerifyEmailRequest | ||||||
|  |             | Flow::UpdateUserAccountDetails => Self::User, | ||||||
|  |  | ||||||
|             Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => { |             Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => { | ||||||
|                 Self::UserRole |                 Self::UserRole | ||||||
|  | |||||||
| @ -403,3 +403,21 @@ pub async fn verify_recon_token(state: web::Data<AppState>, http_req: HttpReques | |||||||
|     )) |     )) | ||||||
|     .await |     .await | ||||||
| } | } | ||||||
|  |  | ||||||
|  | pub async fn update_user_account_details( | ||||||
|  |     state: web::Data<AppState>, | ||||||
|  |     req: HttpRequest, | ||||||
|  |     json_payload: web::Json<user_api::UpdateUserAccountDetailsRequest>, | ||||||
|  | ) -> HttpResponse { | ||||||
|  |     let flow = Flow::UpdateUserAccountDetails; | ||||||
|  |     Box::pin(api::server_wrap( | ||||||
|  |         flow, | ||||||
|  |         state.clone(), | ||||||
|  |         &req, | ||||||
|  |         json_payload.into_inner(), | ||||||
|  |         user_core::update_user_details, | ||||||
|  |         &auth::DashboardNoPermissionAuth, | ||||||
|  |         api_locking::LockAction::NotApplicable, | ||||||
|  |     )) | ||||||
|  |     .await | ||||||
|  | } | ||||||
|  | |||||||
| @ -331,6 +331,8 @@ pub enum Flow { | |||||||
|     VerifyEmail, |     VerifyEmail, | ||||||
|     /// Send verify email |     /// Send verify email | ||||||
|     VerifyEmailRequest, |     VerifyEmailRequest, | ||||||
|  |     /// Update user account details | ||||||
|  |     UpdateUserAccountDetails, | ||||||
| } | } | ||||||
|  |  | ||||||
| /// | /// | ||||||
|  | |||||||
| @ -0,0 +1,2 @@ | |||||||
|  | -- This file should undo anything in `up.sql` | ||||||
|  | ALTER TABLE users DROP COLUMN preferred_merchant_id; | ||||||
| @ -0,0 +1,2 @@ | |||||||
|  | -- Your SQL goes here | ||||||
|  | ALTER TABLE users ADD COLUMN preferred_merchant_id VARCHAR(64); | ||||||
		Reference in New Issue
	
	Block a user
	 Mani Chandra
					Mani Chandra