diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index a0dadfea2170..78157f62bc5d 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -51,9 +51,6 @@ pub struct AuthorizeResponse { //this field is added for audit/debug reasons #[serde(skip_serializing)] pub user_id: String, - //this field is added for audit/debug reasons - #[serde(skip_serializing)] - pub merchant_id: id_type::MerchantId, } #[derive(serde::Deserialize, Debug, serde::Serialize)] @@ -209,7 +206,6 @@ pub struct VerifyTokenResponse { #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct UpdateUserAccountDetailsRequest { pub name: Option>, - pub preferred_merchant_id: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize)] diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs index e4819b74fbc4..05087d09e808 100644 --- a/crates/api_models/src/user_role.rs +++ b/crates/api_models/src/user_role.rs @@ -147,3 +147,12 @@ pub struct ListInvitationForUserResponse { pub entity_name: Option>, pub role_id: String, } + +pub type AcceptInvitationsV2Request = Vec; +pub type AcceptInvitationsPreAuthRequest = Vec; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct Entity { + pub entity_id: String, + pub entity_type: common_enums::EntityType, +} diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index e0f2b9a0bd2e..5986ddf22594 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -1333,8 +1333,6 @@ diesel::table! { is_verified -> Bool, created_at -> Timestamp, last_modified_at -> Timestamp, - #[max_length = 64] - preferred_merchant_id -> Nullable, totp_status -> TotpStatus, totp_secret -> Nullable, totp_recovery_codes -> Nullable>>, diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 410c541441bc..c3dc45e6b62f 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -1316,8 +1316,6 @@ diesel::table! { is_verified -> Bool, created_at -> Timestamp, last_modified_at -> Timestamp, - #[max_length = 64] - preferred_merchant_id -> Nullable, totp_status -> TotpStatus, totp_secret -> Nullable, totp_recovery_codes -> Nullable>>, diff --git a/crates/diesel_models/src/user.rs b/crates/diesel_models/src/user.rs index 582041ec42e5..9f7b77dc5d68 100644 --- a/crates/diesel_models/src/user.rs +++ b/crates/diesel_models/src/user.rs @@ -18,7 +18,6 @@ pub struct User { pub is_verified: bool, pub created_at: PrimitiveDateTime, pub last_modified_at: PrimitiveDateTime, - pub preferred_merchant_id: Option, pub totp_status: TotpStatus, pub totp_secret: Option, #[diesel(deserialize_as = OptionalDieselArray>)] @@ -38,7 +37,6 @@ pub struct UserNew { pub is_verified: bool, pub created_at: Option, pub last_modified_at: Option, - pub preferred_merchant_id: Option, pub totp_status: TotpStatus, pub totp_secret: Option, pub totp_recovery_codes: Option>>, @@ -52,7 +50,6 @@ pub struct UserUpdateInternal { password: Option>, is_verified: Option, last_modified_at: PrimitiveDateTime, - preferred_merchant_id: Option, totp_status: Option, totp_secret: Option, totp_recovery_codes: Option>>, @@ -65,7 +62,6 @@ pub enum UserUpdate { AccountUpdate { name: Option, is_verified: Option, - preferred_merchant_id: Option, }, TotpUpdate { totp_status: Option, @@ -86,22 +82,16 @@ impl From for UserUpdateInternal { password: None, is_verified: Some(true), last_modified_at, - preferred_merchant_id: None, totp_status: None, totp_secret: None, totp_recovery_codes: None, last_password_modified_at: None, }, - UserUpdate::AccountUpdate { - name, - is_verified, - preferred_merchant_id, - } => Self { + UserUpdate::AccountUpdate { name, is_verified } => Self { name, password: None, is_verified, last_modified_at, - preferred_merchant_id, totp_status: None, totp_secret: None, totp_recovery_codes: None, @@ -116,7 +106,6 @@ impl From for UserUpdateInternal { password: None, is_verified: None, last_modified_at, - preferred_merchant_id: None, totp_status, totp_secret, totp_recovery_codes, @@ -127,7 +116,6 @@ impl From for UserUpdateInternal { password: Some(password), is_verified: None, last_modified_at, - preferred_merchant_id: None, last_password_modified_at: Some(last_modified_at), totp_status: None, totp_secret: None, diff --git a/crates/diesel_models/src/user_role.rs b/crates/diesel_models/src/user_role.rs index c454e8b58bc2..29be4d62aef1 100644 --- a/crates/diesel_models/src/user_role.rs +++ b/crates/diesel_models/src/user_role.rs @@ -26,52 +26,53 @@ pub struct UserRole { pub version: enums::UserRoleVersion, } -pub fn get_entity_id_and_type(user_role: &UserRole) -> (Option, Option) { - match (user_role.version, user_role.role_id.as_str()) { - (enums::UserRoleVersion::V1, consts::ROLE_ID_ORGANIZATION_ADMIN) => ( - user_role - .org_id - .clone() - .map(|org_id| org_id.get_string_repr().to_string()), - Some(EntityType::Organization), - ), - (enums::UserRoleVersion::V1, consts::ROLE_ID_INTERNAL_VIEW_ONLY_USER) - | (enums::UserRoleVersion::V1, consts::ROLE_ID_INTERNAL_ADMIN) => ( - user_role - .merchant_id - .clone() - .map(|merchant_id| merchant_id.get_string_repr().to_string()), - Some(EntityType::Internal), - ), - (enums::UserRoleVersion::V1, _) => ( - user_role - .merchant_id - .clone() - .map(|merchant_id| merchant_id.get_string_repr().to_string()), - Some(EntityType::Merchant), - ), - (enums::UserRoleVersion::V2, _) => (user_role.entity_id.clone(), user_role.entity_type), +impl UserRole { + pub fn get_entity_id_and_type(&self) -> Option<(String, EntityType)> { + match (self.version, self.role_id.as_str()) { + (enums::UserRoleVersion::V1, consts::ROLE_ID_ORGANIZATION_ADMIN) => { + let org_id = self.org_id.clone()?.get_string_repr().to_string(); + Some((org_id, EntityType::Organization)) + } + (enums::UserRoleVersion::V1, consts::ROLE_ID_INTERNAL_VIEW_ONLY_USER) + | (enums::UserRoleVersion::V1, consts::ROLE_ID_INTERNAL_ADMIN) => { + let merchant_id = self.merchant_id.clone()?.get_string_repr().to_string(); + Some((merchant_id, EntityType::Internal)) + } + (enums::UserRoleVersion::V1, _) => { + let merchant_id = self.merchant_id.clone()?.get_string_repr().to_string(); + Some((merchant_id, EntityType::Merchant)) + } + (enums::UserRoleVersion::V2, _) => self.entity_id.clone().zip(self.entity_type), + } } } impl Hash for UserRole { fn hash(&self, state: &mut H) { - let (entity_id, entity_type) = get_entity_id_and_type(self); - self.user_id.hash(state); - entity_id.hash(state); - entity_type.hash(state); + if let Some((entity_id, entity_type)) = self.get_entity_id_and_type() { + entity_id.hash(state); + entity_type.hash(state); + } } } impl PartialEq for UserRole { fn eq(&self, other: &Self) -> bool { - let (self_entity_id, self_entity_type) = get_entity_id_and_type(self); - let (other_entity_id, other_entity_type) = get_entity_id_and_type(other); - - self.user_id == other.user_id - && self_entity_id == other_entity_id - && self_entity_type == other_entity_type + match ( + self.get_entity_id_and_type(), + other.get_entity_id_and_type(), + ) { + ( + Some((self_entity_id, self_entity_type)), + Some((other_entity_id, other_entity_type)), + ) => { + self.user_id == other.user_id + && self_entity_id == other_entity_id + && self_entity_type == other_entity_type + } + _ => self.user_id == other.user_id, + } } } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 8e99a202dfe7..0c4afbe4ecd2 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -18,8 +18,6 @@ use diesel_models::{ user_authentication_method::{UserAuthenticationMethodNew, UserAuthenticationMethodUpdate}, }; use error_stack::{report, ResultExt}; -#[cfg(feature = "email")] -use external_services::email::EmailData; use masking::{ExposeInterface, PeekInterface, Secret}; #[cfg(feature = "email")] use router_env::env; @@ -64,7 +62,7 @@ pub async fn signup_with_merchant_id( .insert_user_and_merchant_in_db(state.clone()) .await?; - let user_role = new_user + let _user_role = new_user .insert_org_level_user_role_in_db( state.clone(), common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(), @@ -89,16 +87,10 @@ pub async fn signup_with_merchant_id( ) .await; - let Some(merchant_id) = user_role.merchant_id else { - return Err(report!(UserErrors::InternalServerError) - .attach_printable("merchant_id not found for user_role")); - }; - logger::info!(?send_email_result); Ok(ApplicationResponse::Json(user_api::AuthorizeResponse { is_email_sent: send_email_result.is_ok(), user_id: user_from_db.get_user_id().to_string(), - merchant_id, })) } @@ -195,7 +187,6 @@ pub async fn connect_account( if let Ok(found_user) = find_user { let user_from_db: domain::UserFromStorage = found_user.into(); - let user_role = user_from_db.get_role_from_db(state.clone()).await?; let email_contents = email_types::MagicLink { recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, @@ -212,18 +203,12 @@ pub async fn connect_account( state.conf.proxy.https_url.as_ref(), ) .await; - - let Some(merchant_id) = user_role.merchant_id else { - return Err(report!(UserErrors::InternalServerError) - .attach_printable("merchant_id not found for user_role")); - }; logger::info!(?send_email_result); return Ok(ApplicationResponse::Json( user_api::ConnectAccountResponse { is_email_sent: send_email_result.is_ok(), user_id: user_from_db.get_user_id().to_string(), - merchant_id, }, )); } else if find_user @@ -245,7 +230,7 @@ pub async fn connect_account( let user_from_db = new_user .insert_user_and_merchant_in_db(state.clone()) .await?; - let user_role = new_user + let _user_role = new_user .insert_org_level_user_role_in_db( state.clone(), common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(), @@ -269,18 +254,12 @@ pub async fn connect_account( ) .await; - let Some(merchant_id) = user_role.merchant_id else { - return Err(report!(UserErrors::InternalServerError) - .attach_printable("merchant_id not found for user_role")); - }; - logger::info!(?send_email_result); return Ok(ApplicationResponse::Json( user_api::ConnectAccountResponse { is_email_sent: send_email_result.is_ok(), user_id: user_from_db.get_user_id().to_string(), - merchant_id, }, )); } else { @@ -512,15 +491,19 @@ pub async fn invite_multiple_user( .attach_printable("Number of invite requests must not exceed 10"); } - let responses = futures::future::join_all(requests.iter().map(|request| async { - match handle_invitation(&state, &user_from_token, request, &req_state, &auth_id).await { + let responses = futures::future::join_all(requests.into_iter().map(|request| async { + match handle_invitation(&state, &user_from_token, &request, &req_state, &auth_id).await { Ok(response) => response, - Err(error) => InviteMultipleUserResponse { - email: request.email.clone(), - is_email_sent: false, - password: None, - error: Some(error.current_context().get_error_message().to_string()), - }, + Err(error) => { + logger::error!(invite_error=?error); + + InviteMultipleUserResponse { + email: request.email, + is_email_sent: false, + password: None, + error: Some(error.current_context().get_error_message().to_string()), + } + } } })) .await; @@ -570,6 +553,7 @@ async fn handle_invitation( user_from_token, request, invitee_user.into(), + role_info, auth_id, ) .await @@ -579,8 +563,15 @@ async fn handle_invitation( .err() .unwrap_or(false) { - handle_new_user_invitation(state, user_from_token, request, req_state.clone(), auth_id) - .await + handle_new_user_invitation( + state, + user_from_token, + request, + role_info, + req_state.clone(), + auth_id, + ) + .await } else { Err(UserErrors::InternalServerError.into()) } @@ -592,6 +583,7 @@ async fn handle_existing_user_invitation( user_from_token: &auth::UserFromToken, request: &user_api::InviteUserRequest, invitee_user_from_db: domain::UserFromStorage, + role_info: roles::RoleInfo, auth_id: &Option, ) -> UserResult { let now = common_utils::date_time::now(); @@ -642,24 +634,66 @@ async fn handle_existing_user_invitation( last_modified_by: user_from_token.user_id.clone(), created_at: now, last_modified: now, - entity: domain::MerchantLevel { - org_id: user_from_token.org_id.clone(), - merchant_id: user_from_token.merchant_id.clone(), - }, - } - .insert_in_v1_and_v2(state) - .await?; + entity: domain::NoLevel, + }; + + let _user_role = match role_info.get_entity_type() { + EntityType::Internal => return Err(UserErrors::InvalidRoleId.into()), + EntityType::Organization => return Err(UserErrors::InvalidRoleId.into()), + EntityType::Merchant => { + user_role + .add_entity(domain::MerchantLevel { + org_id: user_from_token.org_id.clone(), + merchant_id: user_from_token.merchant_id.clone(), + }) + .insert_in_v1_and_v2(state) + .await? + } + EntityType::Profile => { + let profile_id = user_from_token + .profile_id + .clone() + .ok_or(UserErrors::InternalServerError)?; + user_role + .add_entity(domain::ProfileLevel { + org_id: user_from_token.org_id.clone(), + merchant_id: user_from_token.merchant_id.clone(), + profile_id: profile_id.clone(), + }) + .insert_in_v2(state) + .await? + } + }; let is_email_sent; #[cfg(feature = "email")] { let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?; - let email_contents = email_types::InviteRegisteredUser { + let entity = match role_info.get_entity_type() { + EntityType::Internal => return Err(UserErrors::InvalidRoleId.into()), + EntityType::Organization => return Err(UserErrors::InvalidRoleId.into()), + EntityType::Merchant => email_types::Entity { + entity_id: user_from_token.merchant_id.get_string_repr().to_owned(), + entity_type: EntityType::Merchant, + }, + EntityType::Profile => { + let profile_id = user_from_token + .profile_id + .clone() + .ok_or(UserErrors::InternalServerError)?; + email_types::Entity { + entity_id: profile_id.get_string_repr().to_owned(), + entity_type: EntityType::Profile, + } + } + }; + + let email_contents = email_types::InviteUser { recipient_email: invitee_email, user_name: domain::UserName::new(invitee_user_from_db.get_name())?, settings: state.conf.clone(), subject: "You have been invited to join Hyperswitch Community!", - merchant_id: user_from_token.merchant_id.clone(), + entity, auth_id: auth_id.clone(), }; @@ -692,6 +726,7 @@ async fn handle_new_user_invitation( state: &SessionState, user_from_token: &auth::UserFromToken, request: &user_api::InviteUserRequest, + role_info: roles::RoleInfo, req_state: ReqState, auth_id: &Option, ) -> UserResult { @@ -718,13 +753,36 @@ async fn handle_new_user_invitation( last_modified_by: user_from_token.user_id.clone(), created_at: now, last_modified: now, - entity: domain::MerchantLevel { - merchant_id: user_from_token.merchant_id.clone(), - org_id: user_from_token.org_id.clone(), - }, - } - .insert_in_v1_and_v2(state) - .await?; + entity: domain::NoLevel, + }; + + let _user_role = match role_info.get_entity_type() { + EntityType::Internal => return Err(UserErrors::InvalidRoleId.into()), + EntityType::Organization => return Err(UserErrors::InvalidRoleId.into()), + EntityType::Merchant => { + user_role + .add_entity(domain::MerchantLevel { + org_id: user_from_token.org_id.clone(), + merchant_id: user_from_token.merchant_id.clone(), + }) + .insert_in_v1_and_v2(state) + .await? + } + EntityType::Profile => { + let profile_id = user_from_token + .profile_id + .clone() + .ok_or(UserErrors::InternalServerError)?; + user_role + .add_entity(domain::ProfileLevel { + org_id: user_from_token.org_id.clone(), + merchant_id: user_from_token.merchant_id.clone(), + profile_id: profile_id.clone(), + }) + .insert_in_v2(state) + .await? + } + }; let is_email_sent; @@ -734,18 +792,39 @@ async fn handle_new_user_invitation( // Will be adding actual usage for this variable later let _ = req_state.clone(); let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?; - let email_contents: Box = - Box::new(email_types::InviteRegisteredUser { - recipient_email: invitee_email, - 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(), - auth_id: auth_id.clone(), - }); + let entity = match role_info.get_entity_type() { + EntityType::Internal => return Err(UserErrors::InvalidRoleId.into()), + EntityType::Organization => return Err(UserErrors::InvalidRoleId.into()), + EntityType::Merchant => email_types::Entity { + entity_id: user_from_token.merchant_id.get_string_repr().to_owned(), + entity_type: EntityType::Merchant, + }, + EntityType::Profile => { + let profile_id = user_from_token + .profile_id + .clone() + .ok_or(UserErrors::InternalServerError)?; + email_types::Entity { + entity_id: profile_id.get_string_repr().to_owned(), + entity_type: EntityType::Profile, + } + } + }; + + let email_contents = email_types::InviteUser { + recipient_email: invitee_email, + user_name: domain::UserName::new(new_user.get_name())?, + settings: state.conf.clone(), + subject: "You have been invited to join Hyperswitch Community!", + entity, + auth_id: auth_id.clone(), + }; let send_email_result = state .email_client - .compose_and_send_email(email_contents, state.conf.proxy.https_url.as_ref()) + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) .await; logger::info!(?send_email_result); is_email_sent = send_email_result.is_ok(); @@ -803,40 +882,66 @@ pub async fn resend_invite( } })? .into(); - let user_role = state + + let user_role = match state .store - .find_user_role_by_user_id_merchant_id( + .find_user_role_by_user_id_and_lineage( user.get_user_id(), + &user_from_token.org_id, &user_from_token.merchant_id, - UserRoleVersion::V1, + user_from_token.profile_id.as_ref(), + UserRoleVersion::V2, ) .await - .map_err(|e| { - if e.current_context().is_db_not_found() { - e.change_context(UserErrors::InvalidRoleOperation) - .attach_printable(format!( - "User role with user_id = {} and merchant_id = {:?} is not found", - user.get_user_id(), - user_from_token.merchant_id - )) + { + Ok(user_role) => Some(user_role), + Err(err) => { + if err.current_context().is_db_not_found() { + None } else { - e.change_context(UserErrors::InternalServerError) + return Err(report!(UserErrors::InternalServerError)); } - })?; + } + }; + + let user_role = match user_role { + Some(user_role) => user_role, + None => state + .store + .find_user_role_by_user_id_and_lineage( + user.get_user_id(), + &user_from_token.org_id, + &user_from_token.merchant_id, + user_from_token.profile_id.as_ref(), + UserRoleVersion::V1, + ) + .await + .to_not_found_response(UserErrors::InvalidRoleOperationWithMessage( + "User not found in records".to_string(), + ))?, + }; if !matches!(user_role.status, UserStatus::InvitationSent) { return Err(report!(UserErrors::InvalidRoleOperation)) .attach_printable("User status is not InvitationSent".to_string()); } + let (entity_id, entity_type) = user_role + .get_entity_id_and_type() + .ok_or(UserErrors::InternalServerError)?; + let email_contents = email_types::InviteUser { recipient_email: invitee_email, user_name: domain::UserName::new(user.get_name())?, settings: state.conf.clone(), subject: "You have been invited to join Hyperswitch Community!", - merchant_id: user_from_token.merchant_id, - auth_id, + entity: email_types::Entity { + entity_id, + entity_type, + }, + auth_id: auth_id.clone(), }; + state .email_client .compose_and_send_email( @@ -878,36 +983,25 @@ pub async fn accept_invite_from_email_token_only_flow( return Err(UserErrors::LinkInvalid.into()); } - let merchant_id = email_token - .get_merchant_id() - .ok_or(UserErrors::LinkInvalid)?; - - let key_manager_state = &(&state).into(); + let entity = email_token.get_entity().ok_or(UserErrors::LinkInvalid)?; - let key_store = state - .store - .get_merchant_key_store_by_merchant_id( - key_manager_state, - merchant_id, - &state.store.get_master_key().to_vec().into(), + let (org_id, merchant_id, profile_id) = + utils::user_role::get_lineage_for_user_id_and_entity_for_accepting_invite( + &state, + &user_token.user_id, + entity.entity_id.clone(), + entity.entity_type, ) .await - .change_context(UserErrors::InternalServerError) - .attach_printable("merchant_key_store not found")?; - - let merchant_account = state - .store - .find_merchant_account_by_merchant_id(key_manager_state, merchant_id, &key_store) - .await - .change_context(UserErrors::InternalServerError) - .attach_printable("merchant_account not found")?; + .change_context(UserErrors::InternalServerError)? + .ok_or(UserErrors::InternalServerError)?; let (update_v1_result, update_v2_result) = utils::user_role::update_v1_and_v2_user_roles_in_db( &state, user_from_db.get_user_id(), - &merchant_account.organization_id, - merchant_id, - None, + &org_id, + &merchant_id, + profile_id.as_ref(), UserRoleUpdate::UpdateStatus { status: UserStatus::Active, modified_by: user_from_db.get_user_id().to_owned(), @@ -951,14 +1045,7 @@ pub async fn accept_invite_from_email_token_only_flow( )?; let next_flow = current_flow.next(user_from_db.clone(), &state).await?; - let user_role = user_from_db - .get_preferred_or_active_user_role_from_db(&state) - .await - .change_context(UserErrors::InternalServerError)?; - - let token = next_flow - .get_token_with_user_role(&state, &user_role) - .await?; + let token = next_flow.get_token(&state).await?; let response = user_api::TokenResponse { token: token.clone(), @@ -1534,28 +1621,9 @@ pub async fn update_user_details( 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, - UserRoleVersion::V1, - ) - .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()), + name: name.map(|name| name.get_secret().expose()), is_verified: None, - preferred_merchant_id: req.preferred_merchant_id, }; state @@ -2283,23 +2351,35 @@ pub async fn list_orgs_for_user( state: SessionState, user_from_token: auth::UserFromToken, ) -> UserResponse> { - let orgs = state - .store - .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { - user_id: user_from_token.user_id.as_str(), - org_id: None, - merchant_id: None, - profile_id: None, - entity_id: None, - version: None, - status: Some(UserStatus::Active), - limit: None, - }) - .await - .change_context(UserErrors::InternalServerError)? - .into_iter() - .filter_map(|user_role| user_role.org_id) - .collect::>(); + let role_info = roles::RoleInfo::from_role_id( + &state, + &user_from_token.role_id, + &user_from_token.merchant_id, + &user_from_token.org_id, + ) + .await + .change_context(UserErrors::InternalServerError)?; + + let orgs = match role_info.get_entity_type() { + EntityType::Internal => return Err(UserErrors::InvalidRoleOperation.into()), + EntityType::Organization | EntityType::Merchant | EntityType::Profile => state + .store + .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { + user_id: user_from_token.user_id.as_str(), + org_id: None, + merchant_id: None, + profile_id: None, + entity_id: None, + version: None, + status: Some(UserStatus::Active), + limit: None, + }) + .await + .change_context(UserErrors::InternalServerError)? + .into_iter() + .filter_map(|user_role| user_role.org_id) + .collect::>(), + }; let resp = futures::future::try_join_all( orgs.iter() @@ -2333,48 +2413,50 @@ pub async fn list_merchants_for_user_in_org( ) .await .change_context(UserErrors::InternalServerError)?; - let merchant_accounts = if role_info.get_entity_type() == EntityType::Organization { - state + let merchant_accounts = match role_info.get_entity_type() { + EntityType::Organization | EntityType::Internal => state .store .list_merchant_accounts_by_organization_id( &(&state).into(), user_from_token.org_id.get_string_repr(), ) .await - .change_context(UserErrors::InternalServerError)? - .into_iter() - .map( - |merchant_account| user_api::ListMerchantsForUserInOrgResponse { - merchant_name: merchant_account.merchant_name.clone(), - merchant_id: merchant_account.get_id().to_owned(), - }, - ) - .collect::>() - } else { - let merchant_ids = state - .store - .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { - user_id: user_from_token.user_id.as_str(), - org_id: Some(&user_from_token.org_id), - merchant_id: None, - profile_id: None, - entity_id: None, - version: None, - status: Some(UserStatus::Active), - limit: None, - }) - .await - .change_context(UserErrors::InternalServerError)? - .into_iter() - .filter_map(|user_role| user_role.merchant_id) - .collect::>() - .into_iter() - .collect(); - state - .store - .list_multiple_merchant_accounts(&(&state).into(), merchant_ids) - .await - .change_context(UserErrors::InternalServerError)? + .change_context(UserErrors::InternalServerError)?, + EntityType::Merchant | EntityType::Profile => { + let merchant_ids = state + .store + .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { + user_id: user_from_token.user_id.as_str(), + org_id: Some(&user_from_token.org_id), + merchant_id: None, + profile_id: None, + entity_id: None, + version: None, + status: Some(UserStatus::Active), + limit: None, + }) + .await + .change_context(UserErrors::InternalServerError)? + .into_iter() + .filter_map(|user_role| user_role.merchant_id) + .collect::>() + .into_iter() + .collect(); + + state + .store + .list_multiple_merchant_accounts(&(&state).into(), merchant_ids) + .await + .change_context(UserErrors::InternalServerError)? + } + }; + + if merchant_accounts.is_empty() { + Err(UserErrors::InternalServerError).attach_printable("No merchant found for a user")?; + } + + Ok(ApplicationResponse::Json( + merchant_accounts .into_iter() .map( |merchant_account| user_api::ListMerchantsForUserInOrgResponse { @@ -2382,14 +2464,8 @@ pub async fn list_merchants_for_user_in_org( merchant_id: merchant_account.get_id().to_owned(), }, ) - .collect::>() - }; - - if merchant_accounts.is_empty() { - Err(UserErrors::InternalServerError).attach_printable("No merchant found for a user")?; - } - - Ok(ApplicationResponse::Json(merchant_accounts)) + .collect::>(), + )) } pub async fn list_profiles_for_user_in_org_and_merchant_account( @@ -2415,27 +2491,17 @@ pub async fn list_profiles_for_user_in_org_and_merchant_account( ) .await .change_context(UserErrors::InternalServerError)?; - let user_role_level = role_info.get_entity_type(); - let profiles = - if user_role_level == EntityType::Organization || user_role_level == EntityType::Merchant { - state - .store - .list_business_profile_by_merchant_id( - key_manager_state, - &key_store, - &user_from_token.merchant_id, - ) - .await - .change_context(UserErrors::InternalServerError)? - .into_iter() - .map( - |profile| user_api::ListProfilesForUserInOrgAndMerchantAccountResponse { - profile_id: profile.get_id().to_owned(), - profile_name: profile.profile_name, - }, - ) - .collect::>() - } else { + let profiles = match role_info.get_entity_type() { + EntityType::Organization | EntityType::Merchant | EntityType::Internal => state + .store + .list_business_profile_by_merchant_id( + key_manager_state, + &key_store, + &user_from_token.merchant_id, + ) + .await + .change_context(UserErrors::InternalServerError)?, + EntityType::Profile => { let profile_ids = state .store .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { @@ -2463,6 +2529,15 @@ pub async fn list_profiles_for_user_in_org_and_merchant_account( })) .await .change_context(UserErrors::InternalServerError)? + } + }; + + if profiles.is_empty() { + Err(UserErrors::InternalServerError).attach_printable("No profile found for a user")?; + } + + Ok(ApplicationResponse::Json( + profiles .into_iter() .map( |profile| user_api::ListProfilesForUserInOrgAndMerchantAccountResponse { @@ -2470,14 +2545,8 @@ pub async fn list_profiles_for_user_in_org_and_merchant_account( profile_name: profile.profile_name, }, ) - .collect::>() - }; - - if profiles.is_empty() { - Err(UserErrors::InternalServerError).attach_printable("No profile found for a user")?; - } - - Ok(ApplicationResponse::Json(profiles)) + .collect::>(), + )) } pub async fn switch_org_for_user( diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs index 5ee71d0e509b..9d57702e3bec 100644 --- a/crates/router/src/core/user_role.rs +++ b/crates/router/src/core/user_role.rs @@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet}; use api_models::{user as user_api, user_role as user_role_api}; use diesel_models::{ enums::{UserRoleVersion, UserStatus}, - user_role::{get_entity_id_and_type, UserRoleUpdate}, + user_role::UserRoleUpdate, }; use error_stack::{report, ResultExt}; use once_cell::sync::Lazy; @@ -289,7 +289,59 @@ pub async fn accept_invitation( })) .await; - if update_result.iter().all(Result::is_err) { + if update_result.is_empty() || update_result.iter().all(Result::is_err) { + return Err(UserErrors::MerchantIdNotFound.into()); + } + + Ok(ApplicationResponse::StatusOk) +} + +pub async fn accept_invitations_v2( + state: SessionState, + user_from_token: auth::UserFromToken, + req: user_role_api::AcceptInvitationsV2Request, +) -> UserResponse<()> { + let lineages = futures::future::try_join_all(req.into_iter().map(|entity| { + utils::user_role::get_lineage_for_user_id_and_entity_for_accepting_invite( + &state, + &user_from_token.user_id, + entity.entity_id, + entity.entity_type, + ) + })) + .await? + .into_iter() + .flatten() + .collect::>(); + + let update_results = futures::future::join_all(lineages.iter().map( + |(org_id, merchant_id, profile_id)| async { + let (update_v1_result, update_v2_result) = + utils::user_role::update_v1_and_v2_user_roles_in_db( + &state, + user_from_token.user_id.as_str(), + org_id, + merchant_id, + profile_id.as_ref(), + UserRoleUpdate::UpdateStatus { + status: UserStatus::Active, + modified_by: user_from_token.user_id.clone(), + }, + ) + .await; + + if update_v1_result.is_err_and(|err| !err.current_context().is_db_not_found()) + || update_v2_result.is_err_and(|err| !err.current_context().is_db_not_found()) + { + Err(report!(UserErrors::InternalServerError)) + } else { + Ok(()) + } + }, + )) + .await; + + if update_results.is_empty() || update_results.iter().all(Result::is_err) { return Err(UserErrors::MerchantIdNotFound.into()); } @@ -333,7 +385,7 @@ pub async fn merchant_select_token_only_flow( })) .await; - if update_result.iter().all(Result::is_err) { + if update_result.is_empty() || update_result.iter().all(Result::is_err) { return Err(UserErrors::MerchantIdNotFound.into()); } @@ -344,18 +396,80 @@ pub async fn merchant_select_token_only_flow( .change_context(UserErrors::InternalServerError)? .into(); - let user_role = user_from_db - .get_preferred_or_active_user_role_from_db(&state) + let current_flow = + domain::CurrentFlow::new(user_token, domain::SPTFlow::MerchantSelect.into())?; + let next_flow = current_flow.next(user_from_db.clone(), &state).await?; + + let token = next_flow.get_token(&state).await?; + + let response = user_api::TokenResponse { + token: token.clone(), + token_type: next_flow.get_flow().into(), + }; + auth::cookies::set_cookie_response(response, token) +} + +pub async fn accept_invitations_pre_auth( + state: SessionState, + user_token: auth::UserFromSinglePurposeToken, + req: user_role_api::AcceptInvitationsPreAuthRequest, +) -> UserResponse { + let lineages = futures::future::try_join_all(req.into_iter().map(|entity| { + utils::user_role::get_lineage_for_user_id_and_entity_for_accepting_invite( + &state, + &user_token.user_id, + entity.entity_id, + entity.entity_type, + ) + })) + .await? + .into_iter() + .flatten() + .collect::>(); + + let update_results = futures::future::join_all(lineages.iter().map( + |(org_id, merchant_id, profile_id)| async { + let (update_v1_result, update_v2_result) = + utils::user_role::update_v1_and_v2_user_roles_in_db( + &state, + user_token.user_id.as_str(), + org_id, + merchant_id, + profile_id.as_ref(), + UserRoleUpdate::UpdateStatus { + status: UserStatus::Active, + modified_by: user_token.user_id.clone(), + }, + ) + .await; + + if update_v1_result.is_err_and(|err| !err.current_context().is_db_not_found()) + || update_v2_result.is_err_and(|err| !err.current_context().is_db_not_found()) + { + Err(report!(UserErrors::InternalServerError)) + } else { + Ok(()) + } + }, + )) + .await; + + if update_results.is_empty() || update_results.iter().all(Result::is_err) { + return Err(UserErrors::MerchantIdNotFound.into()); + } + + let user_from_db: domain::UserFromStorage = state + .global_store + .find_user_by_id(user_token.user_id.as_str()) .await - .change_context(UserErrors::InternalServerError)?; + .change_context(UserErrors::InternalServerError)? + .into(); let current_flow = domain::CurrentFlow::new(user_token, domain::SPTFlow::MerchantSelect.into())?; let next_flow = current_flow.next(user_from_db.clone(), &state).await?; - let token = next_flow - .get_token_with_user_role(&state, &user_role) - .await?; + let token = next_flow.get_token(&state).await?; let response = user_api::TokenResponse { token: token.clone(), @@ -711,14 +825,12 @@ pub async fn list_invitations_for_user( .collect::>() .into_iter() .filter_map(|user_role| { - let (entity_id, entity_type) = get_entity_id_and_type(&user_role); - entity_id.zip(entity_type).map(|(entity_id, entity_type)| { - user_role_api::ListInvitationForUserResponse { - entity_id, - entity_type, - entity_name: None, - role_id: user_role.role_id, - } + let (entity_id, entity_type) = user_role.get_entity_id_and_type()?; + Some(user_role_api::ListInvitationForUserResponse { + entity_id, + entity_type, + entity_name: None, + role_id: user_role.role_id, }) }) .collect(); diff --git a/crates/router/src/db/user.rs b/crates/router/src/db/user.rs index b8c5001d910e..c9244eb12b42 100644 --- a/crates/router/src/db/user.rs +++ b/crates/router/src/db/user.rs @@ -159,7 +159,6 @@ impl UserInterface for MockDb { is_verified: user_data.is_verified, created_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, totp_status: user_data.totp_status, totp_secret: user_data.totp_secret, totp_recovery_codes: user_data.totp_recovery_codes, @@ -218,16 +217,9 @@ impl UserInterface for MockDb { is_verified: true, ..user.to_owned() }, - storage::UserUpdate::AccountUpdate { - name, - is_verified, - preferred_merchant_id, - } => storage::User { + storage::UserUpdate::AccountUpdate { name, is_verified } => storage::User { name: name.clone().map(Secret::new).unwrap_or(user.name.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() }, storage::UserUpdate::TotpUpdate { @@ -273,16 +265,9 @@ impl UserInterface for MockDb { is_verified: true, ..user.to_owned() }, - storage::UserUpdate::AccountUpdate { - name, - is_verified, - preferred_merchant_id, - } => storage::User { + storage::UserUpdate::AccountUpdate { name, is_verified } => storage::User { name: name.clone().map(Secret::new).unwrap_or(user.name.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() }, storage::UserUpdate::TotpUpdate { diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index bd05740acae8..04d833ab9851 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1853,9 +1853,22 @@ impl User { web::resource("/invite_multiple").route(web::post().to(invite_multiple_user)), ) .service( - web::resource("/invite/accept") - .route(web::post().to(merchant_select)) - .route(web::put().to(accept_invitation)), + web::scope("/invite/accept") + .service( + web::resource("") + .route(web::post().to(merchant_select)) + .route(web::put().to(accept_invitation)), + ) + .service( + web::scope("/v2") + .service( + web::resource("").route(web::post().to(accept_invitations_v2)), + ) + .service( + web::resource("/pre_auth") + .route(web::post().to(accept_invitations_pre_auth)), + ), + ), ) .service(web::resource("/update_role").route(web::post().to(update_user_role))) .service(web::resource("/delete").route(web::delete().to(delete_user_role))), diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index c6160497918d..2b2a1be0878f 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -263,7 +263,9 @@ impl From for ApiIdentifier { | Flow::GetAuthorizationInfo | Flow::GetRolesInfo | Flow::AcceptInvitation + | Flow::AcceptInvitationsV2 | Flow::MerchantSelect + | Flow::AcceptInvitationsPreAuth | Flow::DeleteUserRole | Flow::CreateRole | Flow::UpdateRole diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs index 61250774c105..35c9098edd28 100644 --- a/crates/router/src/routes/user_role.rs +++ b/crates/router/src/routes/user_role.rs @@ -168,6 +168,25 @@ pub async fn accept_invitation( .await } +pub async fn accept_invitations_v2( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::AcceptInvitationsV2; + let payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload, + |state, user, req_body, _| user_role_core::accept_invitations_v2(state, user, req_body), + &auth::DashboardNoPermissionAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + pub async fn merchant_select( state: web::Data, req: HttpRequest, @@ -189,6 +208,27 @@ pub async fn merchant_select( .await } +pub async fn accept_invitations_pre_auth( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::AcceptInvitationsPreAuth; + let payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload, + |state, user, req_body, _| async move { + user_role_core::accept_invitations_pre_auth(state, user, req_body).await + }, + &auth::SinglePurposeJWTAuth(TokenPurpose::AcceptInvite), + api_locking::LockAction::NotApplicable, + )) + .await +} + pub async fn delete_user_role( state: web::Data, req: HttpRequest, diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index 3a17966fac39..6ed03e1ea061 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -1,4 +1,5 @@ use api_models::user::dashboard_metadata::ProdIntent; +use common_enums::EntityType; use common_utils::{ errors::{self, CustomResult}, pii, @@ -151,15 +152,31 @@ Email : {user_email} #[derive(serde::Serialize, serde::Deserialize)] pub struct EmailToken { email: String, - merchant_id: Option, flow: domain::Origin, exp: u64, + entity: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Clone)] +pub struct Entity { + pub entity_id: String, + pub entity_type: EntityType, +} + +impl Entity { + pub fn get_entity_type(&self) -> EntityType { + self.entity_type + } + + pub fn get_entity_id(&self) -> &str { + &self.entity_id + } } impl EmailToken { pub async fn new_token( email: domain::UserEmail, - merchant_id: Option, + entity: Option, flow: domain::Origin, settings: &configs::Settings, ) -> CustomResult { @@ -167,9 +184,9 @@ impl EmailToken { let exp = jwt::generate_exp(expiration_duration)?.as_secs(); let token_payload = Self { email: email.get_secret().expose(), - merchant_id, flow, exp, + entity, }; jwt::generate_jwt(&token_payload, settings).await } @@ -178,8 +195,8 @@ impl EmailToken { pii::Email::try_from(self.email.clone()) } - pub fn get_merchant_id(&self) -> Option<&common_utils::id_type::MerchantId> { - self.merchant_id.as_ref() + pub fn get_entity(&self) -> Option<&Entity> { + self.entity.as_ref() } pub fn get_flow(&self) -> domain::Origin { @@ -320,13 +337,12 @@ impl EmailData for MagicLink { } } -// TODO: Deprecate this and use InviteRegisteredUser for new invites pub struct InviteUser { pub recipient_email: domain::UserEmail, pub user_name: domain::UserName, pub settings: std::sync::Arc, pub subject: &'static str, - pub merchant_id: common_utils::id_type::MerchantId, + pub entity: Entity, pub auth_id: Option, } @@ -335,48 +351,7 @@ impl EmailData for InviteUser { async fn get_email_data(&self) -> CustomResult { let token = EmailToken::new_token( self.recipient_email.clone(), - Some(self.merchant_id.clone()), - domain::Origin::ResetPassword, - &self.settings, - ) - .await - .change_context(EmailError::TokenGenerationFailure)?; - - let invite_user_link = get_link_with_token( - &self.settings.user.base_url, - token, - "set_password", - &self.auth_id, - ); - - let body = html::get_html_body(EmailBody::InviteUser { - link: invite_user_link, - user_name: self.user_name.clone().get_secret().expose(), - }); - - Ok(EmailContents { - subject: self.subject.to_string(), - body: external_services::email::IntermediateString::new(body), - recipient: self.recipient_email.clone().into_inner(), - }) - } -} - -pub struct InviteRegisteredUser { - pub recipient_email: domain::UserEmail, - pub user_name: domain::UserName, - pub settings: std::sync::Arc, - pub subject: &'static str, - pub merchant_id: common_utils::id_type::MerchantId, - pub auth_id: Option, -} - -#[async_trait::async_trait] -impl EmailData for InviteRegisteredUser { - async fn get_email_data(&self) -> CustomResult { - let token = EmailToken::new_token( - self.recipient_email.clone(), - Some(self.merchant_id.clone()), + Some(self.entity.clone()), domain::Origin::AcceptInvitationFromEmail, &self.settings, ) diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index f41d7157eecd..9f73276c280d 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -706,7 +706,6 @@ impl TryFrom for storage_user::UserNew { is_verified: false, created_at: Some(now), last_modified_at: Some(now), - preferred_merchant_id: None, totp_status: TotpStatus::NotSet, totp_secret: None, totp_recovery_codes: None, @@ -934,10 +933,6 @@ impl UserFromStorage { Ok(days_left_for_password_rotate.whole_days() < 0) } - pub fn get_preferred_merchant_id(&self) -> Option { - self.0.preferred_merchant_id.clone() - } - pub async fn get_role_from_db_by_merchant_id( &self, state: &SessionState, @@ -953,29 +948,6 @@ impl UserFromStorage { .await } - pub async fn get_preferred_or_active_user_role_from_db( - &self, - state: &SessionState, - ) -> CustomResult { - if let Some(preferred_merchant_id) = self.get_preferred_merchant_id() { - self.get_role_from_db_by_merchant_id(state, &preferred_merchant_id) - .await - } else { - state - .store - .list_user_roles_by_user_id_and_version(&self.0.user_id, UserRoleVersion::V1) - .await? - .into_iter() - .find(|role| role.status == UserStatus::Active) - .ok_or( - errors::StorageError::ValueNotFound( - "No active role found for user".to_string(), - ) - .into(), - ) - } - } - pub async fn get_or_create_key_store(&self, state: &SessionState) -> UserResult { let master_key = state.store.get_master_key(); let key_manager_state = &state.into(); diff --git a/crates/router/src/types/domain/user/decision_manager.rs b/crates/router/src/types/domain/user/decision_manager.rs index 4206b16df6db..af9918af6738 100644 --- a/crates/router/src/types/domain/user/decision_manager.rs +++ b/crates/router/src/types/domain/user/decision_manager.rs @@ -1,11 +1,15 @@ use common_enums::TokenPurpose; -use diesel_models::{enums::UserStatus, user_role::UserRole}; +use diesel_models::{ + enums::{UserRoleVersion, UserStatus}, + user_role::UserRole, +}; use error_stack::{report, ResultExt}; use masking::Secret; use super::UserFromStorage; use crate::{ - core::errors::{StorageErrorExt, UserErrors, UserResult}, + core::errors::{UserErrors, UserResult}, + db::user_role::ListUserRolesByUserIdPayload, routes::SessionState, services::authentication as auth, utils, @@ -284,11 +288,22 @@ impl NextFlow { { self.user.get_verification_days_left(state)?; } - let user_role = self - .user - .get_preferred_or_active_user_role_from_db(state) + let user_role = state + .store + .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { + user_id: self.user.get_user_id(), + org_id: None, + merchant_id: None, + profile_id: None, + entity_id: None, + version: Some(UserRoleVersion::V1), + status: Some(UserStatus::Active), + limit: Some(1), + }) .await - .to_not_found_response(UserErrors::InternalServerError)?; + .change_context(UserErrors::InternalServerError)? + .pop() + .ok_or(UserErrors::InternalServerError)?; utils::user_role::set_role_permissions_in_cache_by_user_role(state, &user_role) .await; diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs index dbe3ed6d5851..aeb866d8d03e 100644 --- a/crates/router/src/utils/user_role.rs +++ b/crates/router/src/utils/user_role.rs @@ -4,7 +4,7 @@ use api_models::user_role as user_role_api; use common_enums::{EntityType, PermissionGroup}; use common_utils::id_type; use diesel_models::{ - enums::UserRoleVersion, + enums::{UserRoleVersion, UserStatus}, user_role::{UserRole, UserRoleUpdate}, }; use error_stack::{report, Report, ResultExt}; @@ -14,6 +14,7 @@ use storage_impl::errors::StorageError; use crate::{ consts, core::errors::{UserErrors, UserResult}, + db::user_role::ListUserRolesByUserIdPayload, routes::SessionState, services::authorization::{self as authz, permissions::Permission, roles}, types::domain, @@ -247,3 +248,113 @@ pub async fn get_single_merchant_id( .attach_printable("merchant_id not found"), } } + +pub async fn get_lineage_for_user_id_and_entity_for_accepting_invite( + state: &SessionState, + user_id: &str, + entity_id: String, + entity_type: EntityType, +) -> UserResult< + Option<( + id_type::OrganizationId, + id_type::MerchantId, + Option, + )>, +> { + match entity_type { + EntityType::Internal | EntityType::Organization => { + Err(UserErrors::InvalidRoleOperation.into()) + } + EntityType::Merchant => { + let Ok(merchant_id) = id_type::MerchantId::wrap(entity_id) else { + return Ok(None); + }; + + let user_roles = state + .store + .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { + user_id, + org_id: None, + merchant_id: Some(&merchant_id), + profile_id: None, + entity_id: None, + version: None, + status: Some(UserStatus::InvitationSent), + limit: None, + }) + .await + .change_context(UserErrors::InternalServerError)? + .into_iter() + .collect::>(); + + if user_roles.len() > 1 { + return Ok(None); + } + + if let Some(user_role) = user_roles.into_iter().next() { + let (_entity_id, entity_type) = user_role + .get_entity_id_and_type() + .ok_or(UserErrors::InternalServerError)?; + + if entity_type != EntityType::Merchant { + return Ok(None); + } + + return Ok(Some(( + user_role.org_id.ok_or(UserErrors::InternalServerError)?, + merchant_id, + None, + ))); + } + + Ok(None) + } + EntityType::Profile => { + let Ok(profile_id) = id_type::ProfileId::try_from(std::borrow::Cow::from(entity_id)) + else { + return Ok(None); + }; + + let user_roles = state + .store + .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { + user_id, + org_id: None, + merchant_id: None, + profile_id: Some(&profile_id), + entity_id: None, + version: None, + status: Some(UserStatus::InvitationSent), + limit: None, + }) + .await + .change_context(UserErrors::InternalServerError)? + .into_iter() + .collect::>(); + + if user_roles.len() > 1 { + return Ok(None); + } + + if let Some(user_role) = user_roles.into_iter().next() { + let (_entity_id, entity_type) = user_role + .get_entity_id_and_type() + .ok_or(UserErrors::InternalServerError)?; + + if entity_type != EntityType::Profile { + return Ok(None); + } + + return Ok(Some(( + user_role.org_id.ok_or(UserErrors::InternalServerError)?, + user_role + .merchant_id + .ok_or(UserErrors::InternalServerError)?, + Some(profile_id), + ))); + } + + Ok(None) + } + } +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 3aae72d8aa79..34e4dc720361 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -424,10 +424,14 @@ pub enum Flow { VerifyEmailRequest, /// Update user account details UpdateUserAccountDetails, - /// Accept user invitation + /// Accept user invitation using merchant_ids AcceptInvitation, + /// Accept user invitation using entities + AcceptInvitationsV2, /// Select merchant from invitations MerchantSelect, + /// Accept user invitation using entities before user login + AcceptInvitationsPreAuth, /// Initiate external authentication for a payment PaymentsExternalAuthentication, /// Authorize the payment after external 3ds authentication diff --git a/migrations/2024-09-01-094614_remove-preferred-merchant-from-users/down.sql b/migrations/2024-09-01-094614_remove-preferred-merchant-from-users/down.sql new file mode 100644 index 000000000000..ab5ef65be8bd --- /dev/null +++ b/migrations/2024-09-01-094614_remove-preferred-merchant-from-users/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE users ADD COLUMN preferred_merchant_id VARCHAR(64); diff --git a/migrations/2024-09-01-094614_remove-preferred-merchant-from-users/up.sql b/migrations/2024-09-01-094614_remove-preferred-merchant-from-users/up.sql new file mode 100644 index 000000000000..84af1f99e0c2 --- /dev/null +++ b/migrations/2024-09-01-094614_remove-preferred-merchant-from-users/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE users DROP COLUMN preferred_merchant_id;