diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index d7150af9bc..ec3e2dce9b 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -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 ); diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index c3e6908c73..8f77f72aad 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -118,6 +118,11 @@ pub struct ReInviteUserRequest { pub email: pii::Email, } +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct AcceptInviteFromEmailRequest { + pub token: Secret, +} + #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct SwitchMerchantIdRequest { pub merchant_id: String, diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index e7639af391..dfcfe8dbcd 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -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 { + let token = request.token.expose(); + + let email_token = auth::decode_jwt::(&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, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 41881c60ff..e9e4b67c78 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -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"))] { diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 9393e8ae21..efdbb57b33 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -188,6 +188,7 @@ impl From for ApiIdentifier { | Flow::UserSignUpWithMerchantId | Flow::VerifyEmailWithoutInviteChecks | Flow::VerifyEmail + | Flow::AcceptInviteFromEmail | Flow::VerifyEmailRequest | Flow::UpdateUserAccountDetails => Self::User, diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index a863bc2b66..dacfe0e59a 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -420,6 +420,25 @@ pub async fn resend_invite( .await } +#[cfg(feature = "email")] +pub async fn accept_invite_from_email( + state: web::Data, + req: HttpRequest, + payload: web::Json, +) -> 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, diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index 598a262093..46cfad0878 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -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, + pub subject: &'static str, + pub merchant_id: String, +} + +#[async_trait::async_trait] +impl EmailData for InviteRegisteredUser { + async fn get_email_data(&self) -> CustomResult { + 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, diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 6649b89911..994b134520 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -339,6 +339,8 @@ pub enum Flow { InviteMultipleUser, /// Reinvite user ReInviteUser, + /// Accept invite from email + AcceptInviteFromEmail, /// Delete user role DeleteUserRole, /// Incremental Authorization flow