mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 12:15:40 +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
|
//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)]
|
||||||
|
|||||||
@ -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,
|
||||||
|
}
|
||||||
|
|||||||
@ -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>>>,
|
||||||
|
|||||||
@ -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>>>,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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))),
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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