feat(users): Send email to user if the user already exists (#3705)

This commit is contained in:
Riddhiagrawal001
2024-02-21 19:07:49 +05:30
committed by GitHub
parent 7c63c76011
commit 97252237a9
8 changed files with 205 additions and 9 deletions

View File

@ -10,11 +10,12 @@ use crate::user::{
dashboard_metadata::{
GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest,
},
AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest,
DashboardEntryResponse, ForgotPasswordRequest, GetUsersResponse, InviteUserRequest,
InviteUserResponse, ReInviteUserRequest, ResetPasswordRequest, SendVerifyEmailRequest,
SignInResponse, SignUpRequest, SignUpWithMerchantIdRequest, SwitchMerchantIdRequest,
UpdateUserAccountDetailsRequest, UserMerchantCreate, VerifyEmailRequest,
AcceptInviteFromEmailRequest, AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest,
CreateInternalUserRequest, DashboardEntryResponse, ForgotPasswordRequest, GetUsersResponse,
InviteUserRequest, InviteUserResponse, ReInviteUserRequest, ResetPasswordRequest,
SendVerifyEmailRequest, SignInResponse, SignUpRequest, SignUpWithMerchantIdRequest,
SwitchMerchantIdRequest, UpdateUserAccountDetailsRequest, UserMerchantCreate,
VerifyEmailRequest,
};
impl ApiEventMetric for DashboardEntryResponse {
@ -57,6 +58,7 @@ common_utils::impl_misc_api_event_type!(
ReInviteUserRequest,
VerifyEmailRequest,
SendVerifyEmailRequest,
AcceptInviteFromEmailRequest,
SignInResponse,
UpdateUserAccountDetailsRequest
);

View File

@ -118,6 +118,11 @@ pub struct ReInviteUserRequest {
pub email: pii::Email,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct AcceptInviteFromEmailRequest {
pub token: Secret<String>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct SwitchMerchantIdRequest {
pub merchant_id: String,

View File

@ -464,7 +464,7 @@ pub async fn invite_user(
.store
.insert_user_role(UserRoleNew {
user_id: invitee_user_from_db.get_user_id().to_owned(),
merchant_id: user_from_token.merchant_id,
merchant_id: user_from_token.merchant_id.clone(),
role_id: request.role_id,
org_id: user_from_token.org_id,
status: {
@ -488,8 +488,34 @@ pub async fn invite_user(
}
})?;
let is_email_sent;
#[cfg(feature = "email")]
{
let email_contents = email_types::InviteRegisteredUser {
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,
};
is_email_sent = state
.email_client
.compose_and_send_email(
Box::new(email_contents),
state.conf.proxy.https_url.as_ref(),
)
.await
.map(|email_result| logger::info!(?email_result))
.map_err(|email_result| logger::error!(?email_result))
.is_ok();
}
#[cfg(not(feature = "email"))]
{
is_email_sent = false;
}
Ok(ApplicationResponse::Json(user_api::InviteUserResponse {
is_email_sent: false,
is_email_sent,
password: None,
}))
} else if invitee_user
@ -681,9 +707,37 @@ async fn handle_existing_user_invitation(
}
})?;
let is_email_sent;
#[cfg(feature = "email")]
{
let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?;
let email_contents = email_types::InviteRegisteredUser {
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(),
};
is_email_sent = state
.email_client
.compose_and_send_email(
Box::new(email_contents),
state.conf.proxy.https_url.as_ref(),
)
.await
.map(|email_result| logger::info!(?email_result))
.map_err(|email_result| logger::error!(?email_result))
.is_ok();
}
#[cfg(not(feature = "email"))]
{
is_email_sent = false;
}
Ok(InviteMultipleUserResponse {
email: request.email.clone(),
is_email_sent: false,
is_email_sent,
password: None,
error: None,
})
@ -840,6 +894,67 @@ pub async fn resend_invite(
Ok(ApplicationResponse::StatusOk)
}
#[cfg(feature = "email")]
pub async fn accept_invite_from_email(
state: AppState,
request: user_api::AcceptInviteFromEmailRequest,
) -> UserResponse<user_api::DashboardEntryResponse> {
let token = request.token.expose();
let email_token = auth::decode_jwt::<email_types::EmailToken>(&token, &state)
.await
.change_context(UserErrors::LinkInvalid)?;
auth::blacklist::check_email_token_in_blacklist(&state, &token).await?;
let user: domain::UserFromStorage = state
.store
.find_user_by_email(email_token.get_email())
.await
.change_context(UserErrors::InternalServerError)?
.into();
let merchant_id = email_token
.get_merchant_id()
.ok_or(UserErrors::InternalServerError)?;
let update_status_result = state
.store
.update_user_role_by_user_id_merchant_id(
user.get_user_id(),
merchant_id,
UserRoleUpdate::UpdateStatus {
status: UserStatus::Active,
modified_by: user.get_user_id().to_string(),
},
)
.await
.change_context(UserErrors::InternalServerError)?;
let _ = auth::blacklist::insert_email_token_in_blacklist(&state, &token)
.await
.map_err(|e| logger::error!(?e));
let user_from_db: domain::UserFromStorage = state
.store
.update_user_by_user_id(user.get_user_id(), storage_user::UserUpdate::VerifyUser)
.await
.change_context(UserErrors::InternalServerError)?
.into();
let token =
utils::user::generate_jwt_auth_token(&state, &user_from_db, &update_status_result).await?;
Ok(ApplicationResponse::Json(
utils::user::get_dashboard_entry_response(
&state,
user_from_db,
update_status_result,
token,
)?,
))
}
pub async fn create_internal_user(
state: AppState,
request: user_api::CreateInternalUserRequest,

View File

@ -1020,7 +1020,11 @@ impl User {
web::resource("/verify_email_request")
.route(web::post().to(verify_email_request)),
)
.service(web::resource("/user/resend_invite").route(web::post().to(resend_invite)));
.service(web::resource("/user/resend_invite").route(web::post().to(resend_invite)))
.service(
web::resource("/accept_invite_from_email")
.route(web::post().to(accept_invite_from_email)),
);
}
#[cfg(not(feature = "email"))]
{

View File

@ -188,6 +188,7 @@ impl From<Flow> for ApiIdentifier {
| Flow::UserSignUpWithMerchantId
| Flow::VerifyEmailWithoutInviteChecks
| Flow::VerifyEmail
| Flow::AcceptInviteFromEmail
| Flow::VerifyEmailRequest
| Flow::UpdateUserAccountDetails => Self::User,

View File

@ -420,6 +420,25 @@ pub async fn resend_invite(
.await
}
#[cfg(feature = "email")]
pub async fn accept_invite_from_email(
state: web::Data<AppState>,
req: HttpRequest,
payload: web::Json<user_api::AcceptInviteFromEmailRequest>,
) -> HttpResponse {
let flow = Flow::AcceptInviteFromEmail;
Box::pin(api::server_wrap(
flow,
state.clone(),
&req,
payload.into_inner(),
|state, _, request_payload| user_core::accept_invite_from_email(state, request_payload),
&auth::NoAuth,
api_locking::LockAction::NotApplicable,
))
.await
}
#[cfg(feature = "email")]
pub async fn verify_email_without_invite_checks(
state: web::Data<AppState>,

View File

@ -28,6 +28,10 @@ pub enum EmailBody {
link: String,
user_name: String,
},
AcceptInviteFromEmail {
link: String,
user_name: String,
},
BizEmailProd {
user_name: String,
poc_email: String,
@ -78,6 +82,14 @@ pub mod html {
link = link
)
}
// TODO: Change the linked html for accept invite from email
EmailBody::AcceptInviteFromEmail { link, user_name } => {
format!(
include_str!("assets/invite.html"),
username = user_name,
link = link
)
}
EmailBody::ReconActivation { user_name } => {
format!(
include_str!("assets/recon_activation.html"),
@ -287,6 +299,42 @@ impl EmailData for InviteUser {
})
}
}
pub struct InviteRegisteredUser {
pub recipient_email: domain::UserEmail,
pub user_name: domain::UserName,
pub settings: std::sync::Arc<configs::settings::Settings>,
pub subject: &'static str,
pub merchant_id: 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()),
&self.settings,
)
.await
.change_context(EmailError::TokenGenerationFailure)?;
let invite_user_link = get_link_with_token(
&self.settings.email.base_url,
token,
"accept_invite_from_email",
);
let body = html::get_html_body(EmailBody::AcceptInviteFromEmail {
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 ReconActivation {
pub recipient_email: domain::UserEmail,

View File

@ -339,6 +339,8 @@ pub enum Flow {
InviteMultipleUser,
/// Reinvite user
ReInviteUser,
/// Accept invite from email
AcceptInviteFromEmail,
/// Delete user role
DeleteUserRole,
/// Incremental Authorization flow