feat(users): Add profile level invites (#5793)

This commit is contained in:
Mani Chandra
2024-09-04 20:26:50 +05:30
committed by GitHub
parent eea5c4e7ee
commit 28e7a7fc5e
19 changed files with 686 additions and 394 deletions

View File

@ -51,9 +51,6 @@ pub struct AuthorizeResponse {
//this field is added for audit/debug reasons //this field is added for audit/debug reasons
#[serde(skip_serializing)] #[serde(skip_serializing)]
pub user_id: String, 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)] #[derive(serde::Deserialize, Debug, serde::Serialize)]
@ -209,7 +206,6 @@ pub struct VerifyTokenResponse {
#[derive(Debug, serde::Deserialize, serde::Serialize)] #[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct UpdateUserAccountDetailsRequest { pub struct UpdateUserAccountDetailsRequest {
pub name: Option<Secret<String>>, pub name: Option<Secret<String>>,
pub preferred_merchant_id: Option<id_type::MerchantId>,
} }
#[derive(Debug, serde::Deserialize, serde::Serialize)] #[derive(Debug, serde::Deserialize, serde::Serialize)]

View File

@ -147,3 +147,12 @@ pub struct ListInvitationForUserResponse {
pub entity_name: Option<Secret<String>>, pub entity_name: Option<Secret<String>>,
pub role_id: String, pub role_id: String,
} }
pub type AcceptInvitationsV2Request = Vec<Entity>;
pub type AcceptInvitationsPreAuthRequest = Vec<Entity>;
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Entity {
pub entity_id: String,
pub entity_type: common_enums::EntityType,
}

View File

@ -1334,8 +1334,6 @@ 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>,
totp_status -> TotpStatus, totp_status -> TotpStatus,
totp_secret -> Nullable<Bytea>, totp_secret -> Nullable<Bytea>,
totp_recovery_codes -> Nullable<Array<Nullable<Text>>>, totp_recovery_codes -> Nullable<Array<Nullable<Text>>>,

View File

@ -1298,8 +1298,6 @@ 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>,
totp_status -> TotpStatus, totp_status -> TotpStatus,
totp_secret -> Nullable<Bytea>, totp_secret -> Nullable<Bytea>,
totp_recovery_codes -> Nullable<Array<Nullable<Text>>>, totp_recovery_codes -> Nullable<Array<Nullable<Text>>>,

View File

@ -18,7 +18,6 @@ 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<common_utils::id_type::MerchantId>,
pub totp_status: TotpStatus, pub totp_status: TotpStatus,
pub totp_secret: Option<Encryption>, pub totp_secret: Option<Encryption>,
#[diesel(deserialize_as = OptionalDieselArray<Secret<String>>)] #[diesel(deserialize_as = OptionalDieselArray<Secret<String>>)]
@ -38,7 +37,6 @@ 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<common_utils::id_type::MerchantId>,
pub totp_status: TotpStatus, pub totp_status: TotpStatus,
pub totp_secret: Option<Encryption>, pub totp_secret: Option<Encryption>,
pub totp_recovery_codes: Option<Vec<Secret<String>>>, pub totp_recovery_codes: Option<Vec<Secret<String>>>,
@ -52,7 +50,6 @@ 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<common_utils::id_type::MerchantId>,
totp_status: Option<TotpStatus>, totp_status: Option<TotpStatus>,
totp_secret: Option<Encryption>, totp_secret: Option<Encryption>,
totp_recovery_codes: Option<Vec<Secret<String>>>, totp_recovery_codes: Option<Vec<Secret<String>>>,
@ -65,7 +62,6 @@ pub enum UserUpdate {
AccountUpdate { AccountUpdate {
name: Option<String>, name: Option<String>,
is_verified: Option<bool>, is_verified: Option<bool>,
preferred_merchant_id: Option<common_utils::id_type::MerchantId>,
}, },
TotpUpdate { TotpUpdate {
totp_status: Option<TotpStatus>, totp_status: Option<TotpStatus>,
@ -86,22 +82,16 @@ 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,
totp_status: None, totp_status: None,
totp_secret: None, totp_secret: None,
totp_recovery_codes: None, totp_recovery_codes: None,
last_password_modified_at: None, last_password_modified_at: None,
}, },
UserUpdate::AccountUpdate { UserUpdate::AccountUpdate { name, is_verified } => Self {
name,
is_verified,
preferred_merchant_id,
} => Self {
name, name,
password: None, password: None,
is_verified, is_verified,
last_modified_at, last_modified_at,
preferred_merchant_id,
totp_status: None, totp_status: None,
totp_secret: None, totp_secret: None,
totp_recovery_codes: None, totp_recovery_codes: None,
@ -116,7 +106,6 @@ impl From<UserUpdate> for UserUpdateInternal {
password: None, password: None,
is_verified: None, is_verified: None,
last_modified_at, last_modified_at,
preferred_merchant_id: None,
totp_status, totp_status,
totp_secret, totp_secret,
totp_recovery_codes, totp_recovery_codes,
@ -127,7 +116,6 @@ impl From<UserUpdate> for UserUpdateInternal {
password: Some(password), password: Some(password),
is_verified: None, is_verified: None,
last_modified_at, last_modified_at,
preferred_merchant_id: None,
last_password_modified_at: Some(last_modified_at), last_password_modified_at: Some(last_modified_at),
totp_status: None, totp_status: None,
totp_secret: None, totp_secret: None,

View File

@ -26,52 +26,53 @@ pub struct UserRole {
pub version: enums::UserRoleVersion, pub version: enums::UserRoleVersion,
} }
pub fn get_entity_id_and_type(user_role: &UserRole) -> (Option<String>, Option<EntityType>) { impl UserRole {
match (user_role.version, user_role.role_id.as_str()) { pub fn get_entity_id_and_type(&self) -> Option<(String, EntityType)> {
(enums::UserRoleVersion::V1, consts::ROLE_ID_ORGANIZATION_ADMIN) => ( match (self.version, self.role_id.as_str()) {
user_role (enums::UserRoleVersion::V1, consts::ROLE_ID_ORGANIZATION_ADMIN) => {
.org_id let org_id = self.org_id.clone()?.get_string_repr().to_string();
.clone() Some((org_id, EntityType::Organization))
.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) => {
(enums::UserRoleVersion::V1, consts::ROLE_ID_INTERNAL_VIEW_ONLY_USER) let merchant_id = self.merchant_id.clone()?.get_string_repr().to_string();
| (enums::UserRoleVersion::V1, consts::ROLE_ID_INTERNAL_ADMIN) => ( Some((merchant_id, EntityType::Internal))
user_role }
.merchant_id (enums::UserRoleVersion::V1, _) => {
.clone() let merchant_id = self.merchant_id.clone()?.get_string_repr().to_string();
.map(|merchant_id| merchant_id.get_string_repr().to_string()), Some((merchant_id, EntityType::Merchant))
Some(EntityType::Internal), }
), (enums::UserRoleVersion::V2, _) => self.entity_id.clone().zip(self.entity_type),
(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 Hash for UserRole { impl Hash for UserRole {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
let (entity_id, entity_type) = get_entity_id_and_type(self);
self.user_id.hash(state); self.user_id.hash(state);
entity_id.hash(state); if let Some((entity_id, entity_type)) = self.get_entity_id_and_type() {
entity_type.hash(state); entity_id.hash(state);
entity_type.hash(state);
}
} }
} }
impl PartialEq for UserRole { impl PartialEq for UserRole {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
let (self_entity_id, self_entity_type) = get_entity_id_and_type(self); match (
let (other_entity_id, other_entity_type) = get_entity_id_and_type(other); self.get_entity_id_and_type(),
other.get_entity_id_and_type(),
self.user_id == other.user_id ) {
&& self_entity_id == other_entity_id (
&& self_entity_type == other_entity_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,
}
} }
} }

View File

@ -18,8 +18,6 @@ use diesel_models::{
user_authentication_method::{UserAuthenticationMethodNew, UserAuthenticationMethodUpdate}, user_authentication_method::{UserAuthenticationMethodNew, UserAuthenticationMethodUpdate},
}; };
use error_stack::{report, ResultExt}; use error_stack::{report, ResultExt};
#[cfg(feature = "email")]
use external_services::email::EmailData;
use masking::{ExposeInterface, PeekInterface, Secret}; use masking::{ExposeInterface, PeekInterface, Secret};
#[cfg(feature = "email")] #[cfg(feature = "email")]
use router_env::env; use router_env::env;
@ -64,7 +62,7 @@ pub async fn signup_with_merchant_id(
.insert_user_and_merchant_in_db(state.clone()) .insert_user_and_merchant_in_db(state.clone())
.await?; .await?;
let user_role = new_user let _user_role = new_user
.insert_org_level_user_role_in_db( .insert_org_level_user_role_in_db(
state.clone(), state.clone(),
common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(), common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
@ -89,16 +87,10 @@ pub async fn signup_with_merchant_id(
) )
.await; .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); logger::info!(?send_email_result);
Ok(ApplicationResponse::Json(user_api::AuthorizeResponse { Ok(ApplicationResponse::Json(user_api::AuthorizeResponse {
is_email_sent: send_email_result.is_ok(), is_email_sent: send_email_result.is_ok(),
user_id: user_from_db.get_user_id().to_string(), 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 { if let Ok(found_user) = find_user {
let user_from_db: domain::UserFromStorage = found_user.into(); 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 { let email_contents = email_types::MagicLink {
recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, 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(), state.conf.proxy.https_url.as_ref(),
) )
.await; .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); logger::info!(?send_email_result);
return Ok(ApplicationResponse::Json( return Ok(ApplicationResponse::Json(
user_api::ConnectAccountResponse { user_api::ConnectAccountResponse {
is_email_sent: send_email_result.is_ok(), is_email_sent: send_email_result.is_ok(),
user_id: user_from_db.get_user_id().to_string(), user_id: user_from_db.get_user_id().to_string(),
merchant_id,
}, },
)); ));
} else if find_user } else if find_user
@ -245,7 +230,7 @@ pub async fn connect_account(
let user_from_db = new_user let user_from_db = new_user
.insert_user_and_merchant_in_db(state.clone()) .insert_user_and_merchant_in_db(state.clone())
.await?; .await?;
let user_role = new_user let _user_role = new_user
.insert_org_level_user_role_in_db( .insert_org_level_user_role_in_db(
state.clone(), state.clone(),
common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(), common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
@ -269,18 +254,12 @@ pub async fn connect_account(
) )
.await; .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); logger::info!(?send_email_result);
return Ok(ApplicationResponse::Json( return Ok(ApplicationResponse::Json(
user_api::ConnectAccountResponse { user_api::ConnectAccountResponse {
is_email_sent: send_email_result.is_ok(), is_email_sent: send_email_result.is_ok(),
user_id: user_from_db.get_user_id().to_string(), user_id: user_from_db.get_user_id().to_string(),
merchant_id,
}, },
)); ));
} else { } else {
@ -512,15 +491,19 @@ pub async fn invite_multiple_user(
.attach_printable("Number of invite requests must not exceed 10"); .attach_printable("Number of invite requests must not exceed 10");
} }
let responses = futures::future::join_all(requests.iter().map(|request| async { 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 { match handle_invitation(&state, &user_from_token, &request, &req_state, &auth_id).await {
Ok(response) => response, Ok(response) => response,
Err(error) => InviteMultipleUserResponse { Err(error) => {
email: request.email.clone(), logger::error!(invite_error=?error);
is_email_sent: false,
password: None, InviteMultipleUserResponse {
error: Some(error.current_context().get_error_message().to_string()), email: request.email,
}, is_email_sent: false,
password: None,
error: Some(error.current_context().get_error_message().to_string()),
}
}
} }
})) }))
.await; .await;
@ -570,6 +553,7 @@ async fn handle_invitation(
user_from_token, user_from_token,
request, request,
invitee_user.into(), invitee_user.into(),
role_info,
auth_id, auth_id,
) )
.await .await
@ -579,8 +563,15 @@ async fn handle_invitation(
.err() .err()
.unwrap_or(false) .unwrap_or(false)
{ {
handle_new_user_invitation(state, user_from_token, request, req_state.clone(), auth_id) handle_new_user_invitation(
.await state,
user_from_token,
request,
role_info,
req_state.clone(),
auth_id,
)
.await
} else { } else {
Err(UserErrors::InternalServerError.into()) Err(UserErrors::InternalServerError.into())
} }
@ -592,6 +583,7 @@ async fn handle_existing_user_invitation(
user_from_token: &auth::UserFromToken, user_from_token: &auth::UserFromToken,
request: &user_api::InviteUserRequest, request: &user_api::InviteUserRequest,
invitee_user_from_db: domain::UserFromStorage, invitee_user_from_db: domain::UserFromStorage,
role_info: roles::RoleInfo,
auth_id: &Option<String>, auth_id: &Option<String>,
) -> UserResult<InviteMultipleUserResponse> { ) -> UserResult<InviteMultipleUserResponse> {
let now = common_utils::date_time::now(); 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(), last_modified_by: user_from_token.user_id.clone(),
created_at: now, created_at: now,
last_modified: now, last_modified: now,
entity: domain::MerchantLevel { entity: domain::NoLevel,
org_id: user_from_token.org_id.clone(), };
merchant_id: user_from_token.merchant_id.clone(),
}, let _user_role = match role_info.get_entity_type() {
} EntityType::Internal => return Err(UserErrors::InvalidRoleId.into()),
.insert_in_v1_and_v2(state) EntityType::Organization => return Err(UserErrors::InvalidRoleId.into()),
.await?; 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; let is_email_sent;
#[cfg(feature = "email")] #[cfg(feature = "email")]
{ {
let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?; 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, recipient_email: invitee_email,
user_name: domain::UserName::new(invitee_user_from_db.get_name())?, user_name: domain::UserName::new(invitee_user_from_db.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(), entity,
auth_id: auth_id.clone(), auth_id: auth_id.clone(),
}; };
@ -692,6 +726,7 @@ async fn handle_new_user_invitation(
state: &SessionState, state: &SessionState,
user_from_token: &auth::UserFromToken, user_from_token: &auth::UserFromToken,
request: &user_api::InviteUserRequest, request: &user_api::InviteUserRequest,
role_info: roles::RoleInfo,
req_state: ReqState, req_state: ReqState,
auth_id: &Option<String>, auth_id: &Option<String>,
) -> UserResult<InviteMultipleUserResponse> { ) -> UserResult<InviteMultipleUserResponse> {
@ -718,13 +753,36 @@ async fn handle_new_user_invitation(
last_modified_by: user_from_token.user_id.clone(), last_modified_by: user_from_token.user_id.clone(),
created_at: now, created_at: now,
last_modified: now, last_modified: now,
entity: domain::MerchantLevel { entity: domain::NoLevel,
merchant_id: user_from_token.merchant_id.clone(), };
org_id: user_from_token.org_id.clone(),
}, let _user_role = match role_info.get_entity_type() {
} EntityType::Internal => return Err(UserErrors::InvalidRoleId.into()),
.insert_in_v1_and_v2(state) EntityType::Organization => return Err(UserErrors::InvalidRoleId.into()),
.await?; 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; let is_email_sent;
@ -734,18 +792,39 @@ async fn handle_new_user_invitation(
// Will be adding actual usage for this variable later // Will be adding actual usage for this variable later
let _ = req_state.clone(); let _ = req_state.clone();
let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?; let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?;
let email_contents: Box<dyn EmailData + Send + 'static> = let entity = match role_info.get_entity_type() {
Box::new(email_types::InviteRegisteredUser { EntityType::Internal => return Err(UserErrors::InvalidRoleId.into()),
recipient_email: invitee_email, EntityType::Organization => return Err(UserErrors::InvalidRoleId.into()),
user_name: domain::UserName::new(new_user.get_name())?, EntityType::Merchant => email_types::Entity {
settings: state.conf.clone(), entity_id: user_from_token.merchant_id.get_string_repr().to_owned(),
subject: "You have been invited to join Hyperswitch Community!", entity_type: EntityType::Merchant,
merchant_id: user_from_token.merchant_id.clone(), },
auth_id: auth_id.clone(), 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 let send_email_result = state
.email_client .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; .await;
logger::info!(?send_email_result); logger::info!(?send_email_result);
is_email_sent = send_email_result.is_ok(); is_email_sent = send_email_result.is_ok();
@ -803,40 +882,66 @@ pub async fn resend_invite(
} }
})? })?
.into(); .into();
let user_role = state
let user_role = match state
.store .store
.find_user_role_by_user_id_merchant_id( .find_user_role_by_user_id_and_lineage(
user.get_user_id(), user.get_user_id(),
&user_from_token.org_id,
&user_from_token.merchant_id, &user_from_token.merchant_id,
UserRoleVersion::V1, user_from_token.profile_id.as_ref(),
UserRoleVersion::V2,
) )
.await .await
.map_err(|e| { {
if e.current_context().is_db_not_found() { Ok(user_role) => Some(user_role),
e.change_context(UserErrors::InvalidRoleOperation) Err(err) => {
.attach_printable(format!( if err.current_context().is_db_not_found() {
"User role with user_id = {} and merchant_id = {:?} is not found", None
user.get_user_id(),
user_from_token.merchant_id
))
} else { } 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) { if !matches!(user_role.status, UserStatus::InvitationSent) {
return Err(report!(UserErrors::InvalidRoleOperation)) return Err(report!(UserErrors::InvalidRoleOperation))
.attach_printable("User status is not InvitationSent".to_string()); .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 { let email_contents = email_types::InviteUser {
recipient_email: invitee_email, recipient_email: invitee_email,
user_name: domain::UserName::new(user.get_name())?, user_name: domain::UserName::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, entity: email_types::Entity {
auth_id, entity_id,
entity_type,
},
auth_id: auth_id.clone(),
}; };
state state
.email_client .email_client
.compose_and_send_email( .compose_and_send_email(
@ -878,36 +983,25 @@ pub async fn accept_invite_from_email_token_only_flow(
return Err(UserErrors::LinkInvalid.into()); return Err(UserErrors::LinkInvalid.into());
} }
let merchant_id = email_token let entity = email_token.get_entity().ok_or(UserErrors::LinkInvalid)?;
.get_merchant_id()
.ok_or(UserErrors::LinkInvalid)?;
let key_manager_state = &(&state).into(); let (org_id, merchant_id, profile_id) =
utils::user_role::get_lineage_for_user_id_and_entity_for_accepting_invite(
let key_store = state &state,
.store &user_token.user_id,
.get_merchant_key_store_by_merchant_id( entity.entity_id.clone(),
key_manager_state, entity.entity_type,
merchant_id,
&state.store.get_master_key().to_vec().into(),
) )
.await .await
.change_context(UserErrors::InternalServerError) .change_context(UserErrors::InternalServerError)?
.attach_printable("merchant_key_store not found")?; .ok_or(UserErrors::InternalServerError)?;
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")?;
let (update_v1_result, update_v2_result) = utils::user_role::update_v1_and_v2_user_roles_in_db( let (update_v1_result, update_v2_result) = utils::user_role::update_v1_and_v2_user_roles_in_db(
&state, &state,
user_from_db.get_user_id(), user_from_db.get_user_id(),
&merchant_account.organization_id, &org_id,
merchant_id, &merchant_id,
None, profile_id.as_ref(),
UserRoleUpdate::UpdateStatus { UserRoleUpdate::UpdateStatus {
status: UserStatus::Active, status: UserStatus::Active,
modified_by: user_from_db.get_user_id().to_owned(), 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 next_flow = current_flow.next(user_from_db.clone(), &state).await?;
let user_role = user_from_db let token = next_flow.get_token(&state).await?;
.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 response = user_api::TokenResponse { let response = user_api::TokenResponse {
token: token.clone(), token: token.clone(),
@ -1534,28 +1621,9 @@ pub async fn update_user_details(
let name = req.name.map(domain::UserName::new).transpose()?; 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 { 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, is_verified: None,
preferred_merchant_id: req.preferred_merchant_id,
}; };
state state
@ -2283,23 +2351,35 @@ pub async fn list_orgs_for_user(
state: SessionState, state: SessionState,
user_from_token: auth::UserFromToken, user_from_token: auth::UserFromToken,
) -> UserResponse<Vec<user_api::ListOrgsForUserResponse>> { ) -> UserResponse<Vec<user_api::ListOrgsForUserResponse>> {
let orgs = state let role_info = roles::RoleInfo::from_role_id(
.store &state,
.list_user_roles_by_user_id(ListUserRolesByUserIdPayload { &user_from_token.role_id,
user_id: user_from_token.user_id.as_str(), &user_from_token.merchant_id,
org_id: None, &user_from_token.org_id,
merchant_id: None, )
profile_id: None, .await
entity_id: None, .change_context(UserErrors::InternalServerError)?;
version: None,
status: Some(UserStatus::Active), let orgs = match role_info.get_entity_type() {
limit: None, EntityType::Internal => return Err(UserErrors::InvalidRoleOperation.into()),
}) EntityType::Organization | EntityType::Merchant | EntityType::Profile => state
.await .store
.change_context(UserErrors::InternalServerError)? .list_user_roles_by_user_id(ListUserRolesByUserIdPayload {
.into_iter() user_id: user_from_token.user_id.as_str(),
.filter_map(|user_role| user_role.org_id) org_id: None,
.collect::<HashSet<_>>(); 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::<HashSet<_>>(),
};
let resp = futures::future::try_join_all( let resp = futures::future::try_join_all(
orgs.iter() orgs.iter()
@ -2333,63 +2413,59 @@ pub async fn list_merchants_for_user_in_org(
) )
.await .await
.change_context(UserErrors::InternalServerError)?; .change_context(UserErrors::InternalServerError)?;
let merchant_accounts = if role_info.get_entity_type() == EntityType::Organization { let merchant_accounts = match role_info.get_entity_type() {
state EntityType::Organization | EntityType::Internal => state
.store .store
.list_merchant_accounts_by_organization_id( .list_merchant_accounts_by_organization_id(
&(&state).into(), &(&state).into(),
user_from_token.org_id.get_string_repr(), user_from_token.org_id.get_string_repr(),
) )
.await .await
.change_context(UserErrors::InternalServerError)? .change_context(UserErrors::InternalServerError)?,
.into_iter() EntityType::Merchant | EntityType::Profile => {
.map( let merchant_ids = state
|merchant_account| user_api::ListMerchantsForUserInOrgResponse { .store
merchant_name: merchant_account.merchant_name.clone(), .list_user_roles_by_user_id(ListUserRolesByUserIdPayload {
merchant_id: merchant_account.get_id().to_owned(), user_id: user_from_token.user_id.as_str(),
}, org_id: Some(&user_from_token.org_id),
) merchant_id: None,
.collect::<Vec<_>>() profile_id: None,
} else { entity_id: None,
let merchant_ids = state version: None,
.store status: Some(UserStatus::Active),
.list_user_roles_by_user_id(ListUserRolesByUserIdPayload { limit: None,
user_id: user_from_token.user_id.as_str(), })
org_id: Some(&user_from_token.org_id), .await
merchant_id: None, .change_context(UserErrors::InternalServerError)?
profile_id: None, .into_iter()
entity_id: None, .filter_map(|user_role| user_role.merchant_id)
version: None, .collect::<HashSet<_>>()
status: Some(UserStatus::Active), .into_iter()
limit: None, .collect();
})
.await state
.change_context(UserErrors::InternalServerError)? .store
.into_iter() .list_multiple_merchant_accounts(&(&state).into(), merchant_ids)
.filter_map(|user_role| user_role.merchant_id) .await
.collect::<HashSet<_>>() .change_context(UserErrors::InternalServerError)?
.into_iter() }
.collect();
state
.store
.list_multiple_merchant_accounts(&(&state).into(), merchant_ids)
.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::<Vec<_>>()
}; };
if merchant_accounts.is_empty() { if merchant_accounts.is_empty() {
Err(UserErrors::InternalServerError).attach_printable("No merchant found for a user")?; Err(UserErrors::InternalServerError).attach_printable("No merchant found for a user")?;
} }
Ok(ApplicationResponse::Json(merchant_accounts)) Ok(ApplicationResponse::Json(
merchant_accounts
.into_iter()
.map(
|merchant_account| user_api::ListMerchantsForUserInOrgResponse {
merchant_name: merchant_account.merchant_name.clone(),
merchant_id: merchant_account.get_id().to_owned(),
},
)
.collect::<Vec<_>>(),
))
} }
pub async fn list_profiles_for_user_in_org_and_merchant_account( 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 .await
.change_context(UserErrors::InternalServerError)?; .change_context(UserErrors::InternalServerError)?;
let user_role_level = role_info.get_entity_type(); let profiles = match role_info.get_entity_type() {
let profiles = EntityType::Organization | EntityType::Merchant | EntityType::Internal => state
if user_role_level == EntityType::Organization || user_role_level == EntityType::Merchant { .store
state .list_business_profile_by_merchant_id(
.store key_manager_state,
.list_business_profile_by_merchant_id( &key_store,
key_manager_state, &user_from_token.merchant_id,
&key_store, )
&user_from_token.merchant_id, .await
) .change_context(UserErrors::InternalServerError)?,
.await EntityType::Profile => {
.change_context(UserErrors::InternalServerError)?
.into_iter()
.map(
|profile| user_api::ListProfilesForUserInOrgAndMerchantAccountResponse {
profile_id: profile.get_id().to_owned(),
profile_name: profile.profile_name,
},
)
.collect::<Vec<_>>()
} else {
let profile_ids = state let profile_ids = state
.store .store
.list_user_roles_by_user_id(ListUserRolesByUserIdPayload { .list_user_roles_by_user_id(ListUserRolesByUserIdPayload {
@ -2463,6 +2529,15 @@ pub async fn list_profiles_for_user_in_org_and_merchant_account(
})) }))
.await .await
.change_context(UserErrors::InternalServerError)? .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() .into_iter()
.map( .map(
|profile| user_api::ListProfilesForUserInOrgAndMerchantAccountResponse { |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, profile_name: profile.profile_name,
}, },
) )
.collect::<Vec<_>>() .collect::<Vec<_>>(),
}; ))
if profiles.is_empty() {
Err(UserErrors::InternalServerError).attach_printable("No profile found for a user")?;
}
Ok(ApplicationResponse::Json(profiles))
} }
pub async fn switch_org_for_user( pub async fn switch_org_for_user(

View File

@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet};
use api_models::{user as user_api, user_role as user_role_api}; use api_models::{user as user_api, user_role as user_role_api};
use diesel_models::{ use diesel_models::{
enums::{UserRoleVersion, UserStatus}, enums::{UserRoleVersion, UserStatus},
user_role::{get_entity_id_and_type, UserRoleUpdate}, user_role::UserRoleUpdate,
}; };
use error_stack::{report, ResultExt}; use error_stack::{report, ResultExt};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
@ -287,7 +287,59 @@ pub async fn accept_invitation(
})) }))
.await; .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::<Vec<_>>();
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()); return Err(UserErrors::MerchantIdNotFound.into());
} }
@ -331,7 +383,7 @@ pub async fn merchant_select_token_only_flow(
})) }))
.await; .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()); return Err(UserErrors::MerchantIdNotFound.into());
} }
@ -342,18 +394,80 @@ pub async fn merchant_select_token_only_flow(
.change_context(UserErrors::InternalServerError)? .change_context(UserErrors::InternalServerError)?
.into(); .into();
let user_role = user_from_db let current_flow =
.get_preferred_or_active_user_role_from_db(&state) 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<user_api::TokenResponse> {
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::<Vec<_>>();
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 .await
.change_context(UserErrors::InternalServerError)?; .change_context(UserErrors::InternalServerError)?
.into();
let current_flow = let current_flow =
domain::CurrentFlow::new(user_token, domain::SPTFlow::MerchantSelect.into())?; domain::CurrentFlow::new(user_token, domain::SPTFlow::MerchantSelect.into())?;
let next_flow = current_flow.next(user_from_db.clone(), &state).await?; let next_flow = current_flow.next(user_from_db.clone(), &state).await?;
let token = next_flow let token = next_flow.get_token(&state).await?;
.get_token_with_user_role(&state, &user_role)
.await?;
let response = user_api::TokenResponse { let response = user_api::TokenResponse {
token: token.clone(), token: token.clone(),
@ -709,14 +823,12 @@ pub async fn list_invitations_for_user(
.collect::<HashSet<_>>() .collect::<HashSet<_>>()
.into_iter() .into_iter()
.filter_map(|user_role| { .filter_map(|user_role| {
let (entity_id, entity_type) = get_entity_id_and_type(&user_role); let (entity_id, entity_type) = user_role.get_entity_id_and_type()?;
entity_id.zip(entity_type).map(|(entity_id, entity_type)| { Some(user_role_api::ListInvitationForUserResponse {
user_role_api::ListInvitationForUserResponse { entity_id,
entity_id, entity_type,
entity_type, entity_name: None,
entity_name: None, role_id: user_role.role_id,
role_id: user_role.role_id,
}
}) })
}) })
.collect(); .collect();

View File

@ -159,7 +159,6 @@ 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,
totp_status: user_data.totp_status, totp_status: user_data.totp_status,
totp_secret: user_data.totp_secret, totp_secret: user_data.totp_secret,
totp_recovery_codes: user_data.totp_recovery_codes, totp_recovery_codes: user_data.totp_recovery_codes,
@ -218,16 +217,9 @@ impl UserInterface for MockDb {
is_verified: true, is_verified: true,
..user.to_owned() ..user.to_owned()
}, },
storage::UserUpdate::AccountUpdate { storage::UserUpdate::AccountUpdate { name, is_verified } => storage::User {
name,
is_verified,
preferred_merchant_id,
} => storage::User {
name: name.clone().map(Secret::new).unwrap_or(user.name.clone()), name: name.clone().map(Secret::new).unwrap_or(user.name.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()
}, },
storage::UserUpdate::TotpUpdate { storage::UserUpdate::TotpUpdate {
@ -273,16 +265,9 @@ impl UserInterface for MockDb {
is_verified: true, is_verified: true,
..user.to_owned() ..user.to_owned()
}, },
storage::UserUpdate::AccountUpdate { storage::UserUpdate::AccountUpdate { name, is_verified } => storage::User {
name,
is_verified,
preferred_merchant_id,
} => storage::User {
name: name.clone().map(Secret::new).unwrap_or(user.name.clone()), name: name.clone().map(Secret::new).unwrap_or(user.name.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()
}, },
storage::UserUpdate::TotpUpdate { storage::UserUpdate::TotpUpdate {

View File

@ -1825,9 +1825,22 @@ impl User {
web::resource("/invite_multiple").route(web::post().to(invite_multiple_user)), web::resource("/invite_multiple").route(web::post().to(invite_multiple_user)),
) )
.service( .service(
web::resource("/invite/accept") web::scope("/invite/accept")
.route(web::post().to(merchant_select)) .service(
.route(web::put().to(accept_invitation)), 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("/update_role").route(web::post().to(update_user_role)))
.service(web::resource("/delete").route(web::delete().to(delete_user_role))), .service(web::resource("/delete").route(web::delete().to(delete_user_role))),

View File

@ -263,7 +263,9 @@ impl From<Flow> for ApiIdentifier {
| Flow::GetAuthorizationInfo | Flow::GetAuthorizationInfo
| Flow::GetRolesInfo | Flow::GetRolesInfo
| Flow::AcceptInvitation | Flow::AcceptInvitation
| Flow::AcceptInvitationsV2
| Flow::MerchantSelect | Flow::MerchantSelect
| Flow::AcceptInvitationsPreAuth
| Flow::DeleteUserRole | Flow::DeleteUserRole
| Flow::CreateRole | Flow::CreateRole
| Flow::UpdateRole | Flow::UpdateRole

View File

@ -168,6 +168,25 @@ pub async fn accept_invitation(
.await .await
} }
pub async fn accept_invitations_v2(
state: web::Data<AppState>,
req: HttpRequest,
json_payload: web::Json<user_role_api::AcceptInvitationsV2Request>,
) -> 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( pub async fn merchant_select(
state: web::Data<AppState>, state: web::Data<AppState>,
req: HttpRequest, req: HttpRequest,
@ -189,6 +208,27 @@ pub async fn merchant_select(
.await .await
} }
pub async fn accept_invitations_pre_auth(
state: web::Data<AppState>,
req: HttpRequest,
json_payload: web::Json<user_role_api::AcceptInvitationsPreAuthRequest>,
) -> 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( pub async fn delete_user_role(
state: web::Data<AppState>, state: web::Data<AppState>,
req: HttpRequest, req: HttpRequest,

View File

@ -1,4 +1,5 @@
use api_models::user::dashboard_metadata::ProdIntent; use api_models::user::dashboard_metadata::ProdIntent;
use common_enums::EntityType;
use common_utils::{ use common_utils::{
errors::{self, CustomResult}, errors::{self, CustomResult},
pii, pii,
@ -151,15 +152,31 @@ 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<common_utils::id_type::MerchantId>,
flow: domain::Origin, flow: domain::Origin,
exp: u64, exp: u64,
entity: Option<Entity>,
}
#[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 { impl EmailToken {
pub async fn new_token( pub async fn new_token(
email: domain::UserEmail, email: domain::UserEmail,
merchant_id: Option<common_utils::id_type::MerchantId>, entity: Option<Entity>,
flow: domain::Origin, flow: domain::Origin,
settings: &configs::Settings, settings: &configs::Settings,
) -> CustomResult<String, UserErrors> { ) -> CustomResult<String, UserErrors> {
@ -167,9 +184,9 @@ impl EmailToken {
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,
flow, flow,
exp, exp,
entity,
}; };
jwt::generate_jwt(&token_payload, settings).await jwt::generate_jwt(&token_payload, settings).await
} }
@ -178,8 +195,8 @@ impl EmailToken {
pii::Email::try_from(self.email.clone()) pii::Email::try_from(self.email.clone())
} }
pub fn get_merchant_id(&self) -> Option<&common_utils::id_type::MerchantId> { pub fn get_entity(&self) -> Option<&Entity> {
self.merchant_id.as_ref() self.entity.as_ref()
} }
pub fn get_flow(&self) -> domain::Origin { 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 struct InviteUser {
pub recipient_email: domain::UserEmail, pub recipient_email: domain::UserEmail,
pub user_name: domain::UserName, pub user_name: domain::UserName,
pub settings: std::sync::Arc<configs::Settings>, pub settings: std::sync::Arc<configs::Settings>,
pub subject: &'static str, pub subject: &'static str,
pub merchant_id: common_utils::id_type::MerchantId, pub entity: Entity,
pub auth_id: Option<String>, pub auth_id: Option<String>,
} }
@ -335,48 +351,7 @@ 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( let token = EmailToken::new_token(
self.recipient_email.clone(), self.recipient_email.clone(),
Some(self.merchant_id.clone()), Some(self.entity.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<configs::Settings>,
pub subject: &'static str,
pub merchant_id: common_utils::id_type::MerchantId,
pub auth_id: Option<String>,
}
#[async_trait::async_trait]
impl EmailData for InviteRegisteredUser {
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
let token = EmailToken::new_token(
self.recipient_email.clone(),
Some(self.merchant_id.clone()),
domain::Origin::AcceptInvitationFromEmail, domain::Origin::AcceptInvitationFromEmail,
&self.settings, &self.settings,
) )

View File

@ -703,7 +703,6 @@ impl TryFrom<NewUser> for storage_user::UserNew {
is_verified: false, is_verified: false,
created_at: Some(now), created_at: Some(now),
last_modified_at: Some(now), last_modified_at: Some(now),
preferred_merchant_id: None,
totp_status: TotpStatus::NotSet, totp_status: TotpStatus::NotSet,
totp_secret: None, totp_secret: None,
totp_recovery_codes: None, totp_recovery_codes: None,
@ -931,10 +930,6 @@ impl UserFromStorage {
Ok(days_left_for_password_rotate.whole_days() < 0) Ok(days_left_for_password_rotate.whole_days() < 0)
} }
pub fn get_preferred_merchant_id(&self) -> Option<id_type::MerchantId> {
self.0.preferred_merchant_id.clone()
}
pub async fn get_role_from_db_by_merchant_id( pub async fn get_role_from_db_by_merchant_id(
&self, &self,
state: &SessionState, state: &SessionState,
@ -950,29 +945,6 @@ impl UserFromStorage {
.await .await
} }
pub async fn get_preferred_or_active_user_role_from_db(
&self,
state: &SessionState,
) -> CustomResult<UserRole, errors::StorageError> {
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<UserKeyStore> { pub async fn get_or_create_key_store(&self, state: &SessionState) -> UserResult<UserKeyStore> {
let master_key = state.store.get_master_key(); let master_key = state.store.get_master_key();
let key_manager_state = &state.into(); let key_manager_state = &state.into();

View File

@ -1,11 +1,15 @@
use common_enums::TokenPurpose; 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 error_stack::{report, ResultExt};
use masking::Secret; use masking::Secret;
use super::UserFromStorage; use super::UserFromStorage;
use crate::{ use crate::{
core::errors::{StorageErrorExt, UserErrors, UserResult}, core::errors::{UserErrors, UserResult},
db::user_role::ListUserRolesByUserIdPayload,
routes::SessionState, routes::SessionState,
services::authentication as auth, services::authentication as auth,
utils, utils,
@ -284,11 +288,22 @@ impl NextFlow {
{ {
self.user.get_verification_days_left(state)?; self.user.get_verification_days_left(state)?;
} }
let user_role = self let user_role = state
.user .store
.get_preferred_or_active_user_role_from_db(state) .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 .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) utils::user_role::set_role_permissions_in_cache_by_user_role(state, &user_role)
.await; .await;

View File

@ -4,7 +4,7 @@ use api_models::user_role as user_role_api;
use common_enums::{EntityType, PermissionGroup}; use common_enums::{EntityType, PermissionGroup};
use common_utils::id_type; use common_utils::id_type;
use diesel_models::{ use diesel_models::{
enums::UserRoleVersion, enums::{UserRoleVersion, UserStatus},
user_role::{UserRole, UserRoleUpdate}, user_role::{UserRole, UserRoleUpdate},
}; };
use error_stack::{report, Report, ResultExt}; use error_stack::{report, Report, ResultExt};
@ -14,6 +14,7 @@ use storage_impl::errors::StorageError;
use crate::{ use crate::{
consts, consts,
core::errors::{UserErrors, UserResult}, core::errors::{UserErrors, UserResult},
db::user_role::ListUserRolesByUserIdPayload,
routes::SessionState, routes::SessionState,
services::authorization::{self as authz, permissions::Permission, roles}, services::authorization::{self as authz, permissions::Permission, roles},
types::domain, types::domain,
@ -247,3 +248,113 @@ pub async fn get_single_merchant_id(
.attach_printable("merchant_id not found"), .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<id_type::ProfileId>,
)>,
> {
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::<HashSet<_>>();
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::<HashSet<_>>();
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)
}
}
}

View File

@ -424,10 +424,14 @@ pub enum Flow {
VerifyEmailRequest, VerifyEmailRequest,
/// Update user account details /// Update user account details
UpdateUserAccountDetails, UpdateUserAccountDetails,
/// Accept user invitation /// Accept user invitation using merchant_ids
AcceptInvitation, AcceptInvitation,
/// Accept user invitation using entities
AcceptInvitationsV2,
/// Select merchant from invitations /// Select merchant from invitations
MerchantSelect, MerchantSelect,
/// Accept user invitation using entities before user login
AcceptInvitationsPreAuth,
/// Initiate external authentication for a payment /// Initiate external authentication for a payment
PaymentsExternalAuthentication, PaymentsExternalAuthentication,
/// Authorize the payment after external 3ds authentication /// Authorize the payment after external 3ds authentication

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
ALTER TABLE users ADD COLUMN preferred_merchant_id VARCHAR(64);

View File

@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE users DROP COLUMN preferred_merchant_id;