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::{ dashboard_metadata::{
GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest, GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest,
}, },
AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest, AcceptInviteFromEmailRequest, AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest,
DashboardEntryResponse, ForgotPasswordRequest, GetUsersResponse, InviteUserRequest, CreateInternalUserRequest, DashboardEntryResponse, ForgotPasswordRequest, GetUsersResponse,
InviteUserResponse, ReInviteUserRequest, ResetPasswordRequest, SendVerifyEmailRequest, InviteUserRequest, InviteUserResponse, ReInviteUserRequest, ResetPasswordRequest,
SignInResponse, SignUpRequest, SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, SendVerifyEmailRequest, SignInResponse, SignUpRequest, SignUpWithMerchantIdRequest,
UpdateUserAccountDetailsRequest, UserMerchantCreate, VerifyEmailRequest, SwitchMerchantIdRequest, UpdateUserAccountDetailsRequest, UserMerchantCreate,
VerifyEmailRequest,
}; };
impl ApiEventMetric for DashboardEntryResponse { impl ApiEventMetric for DashboardEntryResponse {
@ -57,6 +58,7 @@ common_utils::impl_misc_api_event_type!(
ReInviteUserRequest, ReInviteUserRequest,
VerifyEmailRequest, VerifyEmailRequest,
SendVerifyEmailRequest, SendVerifyEmailRequest,
AcceptInviteFromEmailRequest,
SignInResponse, SignInResponse,
UpdateUserAccountDetailsRequest UpdateUserAccountDetailsRequest
); );

View File

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

View File

@ -464,7 +464,7 @@ pub async fn invite_user(
.store .store
.insert_user_role(UserRoleNew { .insert_user_role(UserRoleNew {
user_id: invitee_user_from_db.get_user_id().to_owned(), 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, role_id: request.role_id,
org_id: user_from_token.org_id, org_id: user_from_token.org_id,
status: { 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 { Ok(ApplicationResponse::Json(user_api::InviteUserResponse {
is_email_sent: false, is_email_sent,
password: None, password: None,
})) }))
} else if invitee_user } 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 { Ok(InviteMultipleUserResponse {
email: request.email.clone(), email: request.email.clone(),
is_email_sent: false, is_email_sent,
password: None, password: None,
error: None, error: None,
}) })
@ -840,6 +894,67 @@ pub async fn resend_invite(
Ok(ApplicationResponse::StatusOk) 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( pub async fn create_internal_user(
state: AppState, state: AppState,
request: user_api::CreateInternalUserRequest, request: user_api::CreateInternalUserRequest,

View File

@ -1020,7 +1020,11 @@ impl User {
web::resource("/verify_email_request") web::resource("/verify_email_request")
.route(web::post().to(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"))] #[cfg(not(feature = "email"))]
{ {

View File

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

View File

@ -420,6 +420,25 @@ pub async fn resend_invite(
.await .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")] #[cfg(feature = "email")]
pub async fn verify_email_without_invite_checks( pub async fn verify_email_without_invite_checks(
state: web::Data<AppState>, state: web::Data<AppState>,

View File

@ -28,6 +28,10 @@ pub enum EmailBody {
link: String, link: String,
user_name: String, user_name: String,
}, },
AcceptInviteFromEmail {
link: String,
user_name: String,
},
BizEmailProd { BizEmailProd {
user_name: String, user_name: String,
poc_email: String, poc_email: String,
@ -78,6 +82,14 @@ pub mod html {
link = link 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 } => { EmailBody::ReconActivation { user_name } => {
format!( format!(
include_str!("assets/recon_activation.html"), 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 struct ReconActivation {
pub recipient_email: domain::UserEmail, pub recipient_email: domain::UserEmail,

View File

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