From c4bd47eca93a158c9daeeeb18afb1e735eea8c94 Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Fri, 1 Dec 2023 15:53:48 +0530 Subject: [PATCH] feat(types): add email types for sending emails (#3020) --- crates/router/src/core/user.rs | 3 +- .../src/services/email/assets/magic_link.html | 32 ++-- crates/router/src/services/email/types.rs | 141 ++++++++++++++++-- 3 files changed, 144 insertions(+), 32 deletions(-) diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 8e7f6c27a7..7c50e0c763 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -81,9 +81,10 @@ pub async fn connect_account( use crate::services::email::types as email_types; - let email_contents = email_types::WelcomeEmail { + let email_contents = email_types::VerifyEmail { recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, settings: state.conf.clone(), + subject: "Welcome to the Hyperswitch community!", }; let send_email_result = state diff --git a/crates/router/src/services/email/assets/magic_link.html b/crates/router/src/services/email/assets/magic_link.html index 6439c83f22..643b6e2306 100644 --- a/crates/router/src/services/email/assets/magic_link.html +++ b/crates/router/src/services/email/assets/magic_link.html @@ -2,20 +2,16 @@ Login to Hyperswitch
Welcome to Hyperswitch!

Dear {user_name},

- We are thrilled to welcome you into our community! + + We are thrilled to welcome you into our community! @@ -140,8 +136,8 @@ align="center" >
- Simply click on the link below, and you'll be granted instant access - to your Hyperswitch account. Note that this link expires in 24 hours + Simply click on the link below, and you'll be granted instant access + to your Hyperswitch account. Note that this link expires in 24 hours and can only be used once.
diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index 8650e1c27c..a4a4681c60 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -5,10 +5,13 @@ use masking::ExposeInterface; use crate::{configs, consts}; #[cfg(feature = "olap")] -use crate::{core::errors::UserErrors, services::jwt, types::domain::UserEmail}; +use crate::{core::errors::UserErrors, services::jwt, types::domain}; pub enum EmailBody { Verify { link: String }, + Reset { link: String, user_name: String }, + MagicLink { link: String, user_name: String }, + InviteUser { link: String, user_name: String }, } pub mod html { @@ -19,6 +22,27 @@ pub mod html { EmailBody::Verify { link } => { format!(include_str!("assets/verify.html"), link = link) } + EmailBody::Reset { link, user_name } => { + format!( + include_str!("assets/reset.html"), + link = link, + username = user_name + ) + } + EmailBody::MagicLink { link, user_name } => { + format!( + include_str!("assets/magic_link.html"), + user_name = user_name, + link = link + ) + } + EmailBody::InviteUser { link, user_name } => { + format!( + include_str!("assets/invite.html"), + username = user_name, + link = link + ) + } } } } @@ -31,7 +55,7 @@ pub struct EmailToken { impl EmailToken { pub async fn new_token( - email: UserEmail, + email: domain::UserEmail, settings: &configs::settings::Settings, ) -> CustomResult { let expiration_duration = std::time::Duration::from_secs(consts::EMAIL_TOKEN_TIME_IN_SECS); @@ -44,35 +68,126 @@ impl EmailToken { } } -pub struct WelcomeEmail { - pub recipient_email: UserEmail, - pub settings: std::sync::Arc, -} - -pub fn get_email_verification_link( +pub fn get_link_with_token( base_url: impl std::fmt::Display, token: impl std::fmt::Display, + action: impl std::fmt::Display, ) -> String { - format!("{base_url}/user/verify_email/?token={token}") + format!("{base_url}/user/{action}/?token={token}") +} + +pub struct VerifyEmail { + pub recipient_email: domain::UserEmail, + pub settings: std::sync::Arc, + pub subject: &'static str, } /// Currently only HTML is supported #[async_trait::async_trait] -impl EmailData for WelcomeEmail { +impl EmailData for VerifyEmail { async fn get_email_data(&self) -> CustomResult { let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) .await .change_context(EmailError::TokenGenerationFailure)?; - let verify_email_link = get_email_verification_link(&self.settings.server.base_url, token); + let verify_email_link = + get_link_with_token(&self.settings.server.base_url, token, "verify_email"); let body = html::get_html_body(EmailBody::Verify { link: verify_email_link, }); - let subject = "Welcome to the Hyperswitch community!".to_string(); Ok(EmailContents { - subject, + subject: self.subject.to_string(), + body: external_services::email::IntermediateString::new(body), + recipient: self.recipient_email.clone().into_inner(), + }) + } +} + +pub struct ResetPassword { + pub recipient_email: domain::UserEmail, + pub user_name: domain::UserName, + pub settings: std::sync::Arc, + pub subject: &'static str, +} + +#[async_trait::async_trait] +impl EmailData for ResetPassword { + async fn get_email_data(&self) -> CustomResult { + let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) + .await + .change_context(EmailError::TokenGenerationFailure)?; + + let reset_password_link = + get_link_with_token(&self.settings.server.base_url, token, "set_password"); + + let body = html::get_html_body(EmailBody::Reset { + link: reset_password_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 MagicLink { + pub recipient_email: domain::UserEmail, + pub user_name: domain::UserName, + pub settings: std::sync::Arc, + pub subject: &'static str, +} + +#[async_trait::async_trait] +impl EmailData for MagicLink { + async fn get_email_data(&self) -> CustomResult { + let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) + .await + .change_context(EmailError::TokenGenerationFailure)?; + + let magic_link_login = get_link_with_token(&self.settings.server.base_url, token, "login"); + + let body = html::get_html_body(EmailBody::MagicLink { + link: magic_link_login, + 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 InviteUser { + pub recipient_email: domain::UserEmail, + pub user_name: domain::UserName, + pub settings: std::sync::Arc, + pub subject: &'static str, +} + +#[async_trait::async_trait] +impl EmailData for InviteUser { + async fn get_email_data(&self) -> CustomResult { + let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) + .await + .change_context(EmailError::TokenGenerationFailure)?; + + let invite_user_link = + get_link_with_token(&self.settings.server.base_url, token, "set_password"); + + let body = html::get_html_body(EmailBody::MagicLink { + 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(), })