mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 20:23:43 +08:00
feat(users): Add profile level invites (#5793)
This commit is contained in:
@ -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<Secret<String>>,
|
||||
pub preferred_merchant_id: Option<id_type::MerchantId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
|
||||
@ -147,3 +147,12 @@ pub struct ListInvitationForUserResponse {
|
||||
pub entity_name: Option<Secret<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,
|
||||
}
|
||||
|
||||
@ -1334,8 +1334,6 @@ diesel::table! {
|
||||
is_verified -> Bool,
|
||||
created_at -> Timestamp,
|
||||
last_modified_at -> Timestamp,
|
||||
#[max_length = 64]
|
||||
preferred_merchant_id -> Nullable<Varchar>,
|
||||
totp_status -> TotpStatus,
|
||||
totp_secret -> Nullable<Bytea>,
|
||||
totp_recovery_codes -> Nullable<Array<Nullable<Text>>>,
|
||||
|
||||
@ -1298,8 +1298,6 @@ diesel::table! {
|
||||
is_verified -> Bool,
|
||||
created_at -> Timestamp,
|
||||
last_modified_at -> Timestamp,
|
||||
#[max_length = 64]
|
||||
preferred_merchant_id -> Nullable<Varchar>,
|
||||
totp_status -> TotpStatus,
|
||||
totp_secret -> Nullable<Bytea>,
|
||||
totp_recovery_codes -> Nullable<Array<Nullable<Text>>>,
|
||||
|
||||
@ -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<common_utils::id_type::MerchantId>,
|
||||
pub totp_status: TotpStatus,
|
||||
pub totp_secret: Option<Encryption>,
|
||||
#[diesel(deserialize_as = OptionalDieselArray<Secret<String>>)]
|
||||
@ -38,7 +37,6 @@ pub struct UserNew {
|
||||
pub is_verified: bool,
|
||||
pub created_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_secret: Option<Encryption>,
|
||||
pub totp_recovery_codes: Option<Vec<Secret<String>>>,
|
||||
@ -52,7 +50,6 @@ pub struct UserUpdateInternal {
|
||||
password: Option<Secret<String>>,
|
||||
is_verified: Option<bool>,
|
||||
last_modified_at: PrimitiveDateTime,
|
||||
preferred_merchant_id: Option<common_utils::id_type::MerchantId>,
|
||||
totp_status: Option<TotpStatus>,
|
||||
totp_secret: Option<Encryption>,
|
||||
totp_recovery_codes: Option<Vec<Secret<String>>>,
|
||||
@ -65,7 +62,6 @@ pub enum UserUpdate {
|
||||
AccountUpdate {
|
||||
name: Option<String>,
|
||||
is_verified: Option<bool>,
|
||||
preferred_merchant_id: Option<common_utils::id_type::MerchantId>,
|
||||
},
|
||||
TotpUpdate {
|
||||
totp_status: Option<TotpStatus>,
|
||||
@ -86,22 +82,16 @@ impl From<UserUpdate> 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<UserUpdate> 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<UserUpdate> 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,
|
||||
|
||||
@ -26,53 +26,54 @@ pub struct UserRole {
|
||||
pub version: enums::UserRoleVersion,
|
||||
}
|
||||
|
||||
pub fn get_entity_id_and_type(user_role: &UserRole) -> (Option<String>, Option<EntityType>) {
|
||||
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),
|
||||
),
|
||||
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) => (
|
||||
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),
|
||||
| (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<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
let (entity_id, entity_type) = get_entity_id_and_type(self);
|
||||
|
||||
self.user_id.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);
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(router_derive::Setter, Clone, Debug, Insertable, router_derive::DebugAsDisplay)]
|
||||
|
||||
@ -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(),
|
||||
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,7 +563,14 @@ async fn handle_invitation(
|
||||
.err()
|
||||
.unwrap_or(false)
|
||||
{
|
||||
handle_new_user_invitation(state, user_from_token, request, req_state.clone(), auth_id)
|
||||
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<String>,
|
||||
) -> UserResult<InviteMultipleUserResponse> {
|
||||
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 {
|
||||
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?;
|
||||
.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<String>,
|
||||
) -> UserResult<InviteMultipleUserResponse> {
|
||||
@ -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(),
|
||||
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?;
|
||||
.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<dyn EmailData + Send + 'static> =
|
||||
Box::new(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(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(),
|
||||
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,
|
||||
user_from_token.profile_id.as_ref(),
|
||||
UserRoleVersion::V2,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(user_role) => Some(user_role),
|
||||
Err(err) => {
|
||||
if err.current_context().is_db_not_found() {
|
||||
None
|
||||
} else {
|
||||
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
|
||||
.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
|
||||
))
|
||||
} else {
|
||||
e.change_context(UserErrors::InternalServerError)
|
||||
}
|
||||
})?;
|
||||
.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 entity = email_token.get_entity().ok_or(UserErrors::LinkInvalid)?;
|
||||
|
||||
let key_manager_state = &(&state).into();
|
||||
|
||||
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,7 +2351,18 @@ pub async fn list_orgs_for_user(
|
||||
state: SessionState,
|
||||
user_from_token: auth::UserFromToken,
|
||||
) -> UserResponse<Vec<user_api::ListOrgsForUserResponse>> {
|
||||
let orgs = state
|
||||
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(),
|
||||
@ -2299,7 +2378,8 @@ pub async fn list_orgs_for_user(
|
||||
.change_context(UserErrors::InternalServerError)?
|
||||
.into_iter()
|
||||
.filter_map(|user_role| user_role.org_id)
|
||||
.collect::<HashSet<_>>();
|
||||
.collect::<HashSet<_>>(),
|
||||
};
|
||||
|
||||
let resp = futures::future::try_join_all(
|
||||
orgs.iter()
|
||||
@ -2333,24 +2413,16 @@ 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::<Vec<_>>()
|
||||
} else {
|
||||
.change_context(UserErrors::InternalServerError)?,
|
||||
EntityType::Merchant | EntityType::Profile => {
|
||||
let merchant_ids = state
|
||||
.store
|
||||
.list_user_roles_by_user_id(ListUserRolesByUserIdPayload {
|
||||
@ -2370,11 +2442,21 @@ pub async fn list_merchants_for_user_in_org(
|
||||
.collect::<HashSet<_>>()
|
||||
.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::<Vec<_>>()
|
||||
};
|
||||
|
||||
if merchant_accounts.is_empty() {
|
||||
Err(UserErrors::InternalServerError).attach_printable("No merchant found for a user")?;
|
||||
}
|
||||
|
||||
Ok(ApplicationResponse::Json(merchant_accounts))
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn list_profiles_for_user_in_org_and_merchant_account(
|
||||
@ -2415,10 +2491,8 @@ 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
|
||||
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,
|
||||
@ -2426,16 +2500,8 @@ pub async fn list_profiles_for_user_in_org_and_merchant_account(
|
||||
&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::<Vec<_>>()
|
||||
} else {
|
||||
.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::<Vec<_>>()
|
||||
};
|
||||
|
||||
if profiles.is_empty() {
|
||||
Err(UserErrors::InternalServerError).attach_printable("No profile found for a user")?;
|
||||
}
|
||||
|
||||
Ok(ApplicationResponse::Json(profiles))
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn switch_org_for_user(
|
||||
|
||||
@ -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;
|
||||
@ -287,7 +287,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::<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());
|
||||
}
|
||||
|
||||
@ -331,7 +383,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());
|
||||
}
|
||||
|
||||
@ -342,18 +394,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<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
|
||||
.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(),
|
||||
@ -709,14 +823,12 @@ pub async fn list_invitations_for_user(
|
||||
.collect::<HashSet<_>>()
|
||||
.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 {
|
||||
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();
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -1825,10 +1825,23 @@ impl User {
|
||||
web::resource("/invite_multiple").route(web::post().to(invite_multiple_user)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/invite/accept")
|
||||
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))),
|
||||
);
|
||||
|
||||
@ -263,7 +263,9 @@ impl From<Flow> for ApiIdentifier {
|
||||
| Flow::GetAuthorizationInfo
|
||||
| Flow::GetRolesInfo
|
||||
| Flow::AcceptInvitation
|
||||
| Flow::AcceptInvitationsV2
|
||||
| Flow::MerchantSelect
|
||||
| Flow::AcceptInvitationsPreAuth
|
||||
| Flow::DeleteUserRole
|
||||
| Flow::CreateRole
|
||||
| Flow::UpdateRole
|
||||
|
||||
@ -168,6 +168,25 @@ pub async fn accept_invitation(
|
||||
.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(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
@ -189,6 +208,27 @@ pub async fn merchant_select(
|
||||
.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(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
|
||||
@ -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<common_utils::id_type::MerchantId>,
|
||||
flow: domain::Origin,
|
||||
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 {
|
||||
pub async fn new_token(
|
||||
email: domain::UserEmail,
|
||||
merchant_id: Option<common_utils::id_type::MerchantId>,
|
||||
entity: Option<Entity>,
|
||||
flow: domain::Origin,
|
||||
settings: &configs::Settings,
|
||||
) -> CustomResult<String, UserErrors> {
|
||||
@ -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<configs::Settings>,
|
||||
pub subject: &'static str,
|
||||
pub merchant_id: common_utils::id_type::MerchantId,
|
||||
pub entity: Entity,
|
||||
pub auth_id: Option<String>,
|
||||
}
|
||||
|
||||
@ -335,48 +351,7 @@ impl EmailData for InviteUser {
|
||||
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::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()),
|
||||
Some(self.entity.clone()),
|
||||
domain::Origin::AcceptInvitationFromEmail,
|
||||
&self.settings,
|
||||
)
|
||||
|
||||
@ -703,7 +703,6 @@ impl TryFrom<NewUser> 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,
|
||||
@ -931,10 +930,6 @@ impl UserFromStorage {
|
||||
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(
|
||||
&self,
|
||||
state: &SessionState,
|
||||
@ -950,29 +945,6 @@ impl UserFromStorage {
|
||||
.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> {
|
||||
let master_key = state.store.get_master_key();
|
||||
let key_manager_state = &state.into();
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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<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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
ALTER TABLE users ADD COLUMN preferred_merchant_id VARCHAR(64);
|
||||
@ -0,0 +1,2 @@
|
||||
-- Your SQL goes here
|
||||
ALTER TABLE users DROP COLUMN preferred_merchant_id;
|
||||
Reference in New Issue
Block a user