feat(user): support multiple invites (#3422)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Apoorv Dixit
2024-01-25 18:54:13 +05:30
committed by GitHub
parent ec859eabbf
commit a59ac7d5b9
7 changed files with 296 additions and 69 deletions

View File

@ -1,4 +1,4 @@
use api_models::user as user_api;
use api_models::user::{self as user_api, InviteMultipleUserResponse};
use diesel_models::{enums::UserStatus, user as storage_user, user_role::UserRoleNew};
#[cfg(feature = "email")]
use error_stack::IntoReport;
@ -9,7 +9,7 @@ use router_env::env;
#[cfg(feature = "email")]
use router_env::logger;
use super::errors::{UserErrors, UserResponse};
use super::errors::{UserErrors, UserResponse, UserResult};
#[cfg(feature = "email")]
use crate::services::email::types as email_types;
use crate::{
@ -407,6 +407,12 @@ pub async fn invite_user(
.await
.change_context(UserErrors::InternalServerError)?;
let invitation_status = if cfg!(feature = "email") {
UserStatus::InvitationSent
} else {
UserStatus::Active
};
let now = common_utils::date_time::now();
state
.store
@ -415,7 +421,7 @@ pub async fn invite_user(
merchant_id: user_from_token.merchant_id,
role_id: request.role_id,
org_id: user_from_token.org_id,
status: UserStatus::InvitationSent,
status: invitation_status,
created_by: user_from_token.user_id.clone(),
last_modified_by: user_from_token.user_id,
created_at: now,
@ -467,6 +473,181 @@ pub async fn invite_user(
}
}
pub async fn invite_multiple_user(
state: AppState,
user_from_token: auth::UserFromToken,
requests: Vec<user_api::InviteUserRequest>,
) -> UserResponse<Vec<InviteMultipleUserResponse>> {
if requests.len() > 10 {
return Err(UserErrors::MaxInvitationsError.into())
.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).await {
Ok(response) => response,
Err(error) => InviteMultipleUserResponse {
email: request.email.clone(),
is_email_sent: false,
password: None,
error: Some(error.current_context().get_error_message().to_string()),
},
}
}))
.await;
Ok(ApplicationResponse::Json(responses))
}
async fn handle_invitation(
state: &AppState,
user_from_token: &auth::UserFromToken,
request: &user_api::InviteUserRequest,
) -> UserResult<InviteMultipleUserResponse> {
let inviter_user = user_from_token.get_user(state.clone()).await?;
if inviter_user.email == request.email {
return Err(UserErrors::InvalidRoleOperation.into())
.attach_printable("User Inviting themself");
}
utils::user_role::validate_role_id(request.role_id.as_str())?;
let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?;
let invitee_user = state
.store
.find_user_by_email(invitee_email.clone().get_secret().expose().as_str())
.await;
if let Ok(invitee_user) = invitee_user {
handle_existing_user_invitation(state, user_from_token, request, invitee_user.into()).await
} else if invitee_user
.as_ref()
.map_err(|e| e.current_context().is_db_not_found())
.err()
.unwrap_or(false)
{
handle_new_user_invitation(state, user_from_token, request).await
} else {
Err(UserErrors::InternalServerError.into())
}
}
//TODO: send email
async fn handle_existing_user_invitation(
state: &AppState,
user_from_token: &auth::UserFromToken,
request: &user_api::InviteUserRequest,
invitee_user_from_db: domain::UserFromStorage,
) -> UserResult<InviteMultipleUserResponse> {
let now = common_utils::date_time::now();
state
.store
.insert_user_role(UserRoleNew {
user_id: invitee_user_from_db.get_user_id().to_owned(),
merchant_id: user_from_token.merchant_id.clone(),
role_id: request.role_id.clone(),
org_id: user_from_token.org_id.clone(),
status: UserStatus::Active,
created_by: user_from_token.user_id.clone(),
last_modified_by: user_from_token.user_id.clone(),
created_at: now,
last_modified: now,
})
.await
.map_err(|e| {
if e.current_context().is_db_unique_violation() {
e.change_context(UserErrors::UserExists)
} else {
e.change_context(UserErrors::InternalServerError)
}
})?;
Ok(InviteMultipleUserResponse {
email: request.email.clone(),
is_email_sent: false,
password: None,
error: None,
})
}
async fn handle_new_user_invitation(
state: &AppState,
user_from_token: &auth::UserFromToken,
request: &user_api::InviteUserRequest,
) -> UserResult<InviteMultipleUserResponse> {
let new_user = domain::NewUser::try_from((request.clone(), user_from_token.clone()))?;
new_user
.insert_user_in_db(state.store.as_ref())
.await
.change_context(UserErrors::InternalServerError)?;
let invitation_status = if cfg!(feature = "email") {
UserStatus::InvitationSent
} else {
UserStatus::Active
};
let now = common_utils::date_time::now();
state
.store
.insert_user_role(UserRoleNew {
user_id: new_user.get_user_id().to_owned(),
merchant_id: user_from_token.merchant_id.clone(),
role_id: request.role_id.clone(),
org_id: user_from_token.org_id.clone(),
status: invitation_status,
created_by: user_from_token.user_id.clone(),
last_modified_by: user_from_token.user_id.clone(),
created_at: now,
last_modified: now,
})
.await
.map_err(|e| {
if e.current_context().is_db_unique_violation() {
e.change_context(UserErrors::UserExists)
} else {
e.change_context(UserErrors::InternalServerError)
}
})?;
let is_email_sent;
#[cfg(feature = "email")]
{
let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?;
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!",
};
let send_email_result = state
.email_client
.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();
}
#[cfg(not(feature = "email"))]
{
is_email_sent = false;
}
Ok(InviteMultipleUserResponse {
is_email_sent,
password: if cfg!(not(feature = "email")) {
Some(new_user.get_password().get_secret())
} else {
None
},
email: request.email.clone(),
error: None,
})
}
pub async fn create_internal_user(
state: AppState,
request: user_api::CreateInternalUserRequest,