mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 17:19:15 +08:00
feat(users): Send email to user if the user already exists (#3705)
This commit is contained in:
@ -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
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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"))]
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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,
|
||||||
|
|
||||||
|
|||||||
@ -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>,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user