Skip to content

Commit

Permalink
feat(users): handle email url for users in different tenancies (#6809)
Browse files Browse the repository at this point in the history
  • Loading branch information
apoorvdixit88 authored Dec 19, 2024
1 parent 4bc88a7 commit 839e69d
Show file tree
Hide file tree
Showing 12 changed files with 89 additions and 27 deletions.
11 changes: 9 additions & 2 deletions config/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -759,8 +759,15 @@ sdk_eligible_payment_methods = "card"
enabled = false
global_tenant = { schema = "public", redis_key_prefix = "", clickhouse_database = "default"}

[multitenancy.tenants]
public = { base_url = "http://localhost:8080", schema = "public", redis_key_prefix = "", clickhouse_database = "default" } # schema -> Postgres db schema, redis_key_prefix -> redis key distinguisher, base_url -> url of the tenant
[multitenancy.tenants.public]
base_url = "http://localhost:8080" # URL of the tenant
schema = "public" # Postgres db schema
redis_key_prefix = "" # Redis key distinguisher
clickhouse_database = "default" # Clickhouse database

[multitenancy.tenants.public.user]
control_center_url = "http://localhost:9000" # Control center URL


[user_auth_methods]
encryption_key = "" # Encryption key used for encrypting data in user_authentication_methods table
Expand Down
10 changes: 8 additions & 2 deletions config/deployments/env_specific.toml
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,14 @@ region = "kms_region" # The AWS region used by the KMS SDK for decrypting data.
enabled = false
global_tenant = { schema = "public", redis_key_prefix = "", clickhouse_database = "default"}

[multitenancy.tenants]
public = { base_url = "http://localhost:8080", schema = "public", redis_key_prefix = "", clickhouse_database = "default" }
[multitenancy.tenants.public]
base_url = "http://localhost:8080"
schema = "public"
redis_key_prefix = ""
clickhouse_database = "default"

[multitenancy.tenants.public.user]
control_center_url = "http://localhost:9000"

[user_auth_methods]
encryption_key = "user_auth_table_encryption_key" # Encryption key used for encrypting data in user_authentication_methods table
Expand Down
10 changes: 8 additions & 2 deletions config/development.toml
Original file line number Diff line number Diff line change
Expand Up @@ -775,8 +775,14 @@ sdk_eligible_payment_methods = "card"
enabled = false
global_tenant = { schema = "public", redis_key_prefix = "", clickhouse_database = "default"}

[multitenancy.tenants]
public = { base_url = "http://localhost:8080", schema = "public", redis_key_prefix = "", clickhouse_database = "default"}
[multitenancy.tenants.public]
base_url = "http://localhost:8080"
schema = "public"
redis_key_prefix = ""
clickhouse_database = "default"

[multitenancy.tenants.public.user]
control_center_url = "http://localhost:9000"

[user_auth_methods]
encryption_key = "A8EF32E029BC3342E54BF2E172A4D7AA43E8EF9D2C3A624A9F04E2EF79DC698F"
Expand Down
10 changes: 8 additions & 2 deletions config/docker_compose.toml
Original file line number Diff line number Diff line change
Expand Up @@ -633,8 +633,14 @@ sdk_eligible_payment_methods = "card"
enabled = false
global_tenant = { schema = "public", redis_key_prefix = "", clickhouse_database = "default" }

[multitenancy.tenants]
public = { base_url = "http://localhost:8080", schema = "public", redis_key_prefix = "", clickhouse_database = "default" }
[multitenancy.tenants.public]
base_url = "http://localhost:8080"
schema = "public"
redis_key_prefix = ""
clickhouse_database = "default"

[multitenancy.tenants.public.user]
control_center_url = "http://localhost:9000"

[user_auth_methods]
encryption_key = "A8EF32E029BC3342E54BF2E172A4D7AA43E8EF9D2C3A624A9F04E2EF79DC698F"
Expand Down
6 changes: 4 additions & 2 deletions crates/external_services/src/email.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub trait EmailService: Sync + Send + dyn_clone::DynClone {
/// Compose and send email using the email data
async fn compose_and_send_email(
&self,
base_url: &str,
email_data: Box<dyn EmailData + Send>,
proxy_url: Option<&String>,
) -> EmailResult<()>;
Expand All @@ -60,10 +61,11 @@ where
{
async fn compose_and_send_email(
&self,
base_url: &str,
email_data: Box<dyn EmailData + Send>,
proxy_url: Option<&String>,
) -> EmailResult<()> {
let email_data = email_data.get_email_data();
let email_data = email_data.get_email_data(base_url);
let email_data = email_data.await?;

let EmailContents {
Expand Down Expand Up @@ -113,7 +115,7 @@ pub struct EmailContents {
#[async_trait::async_trait]
pub trait EmailData {
/// Get the email contents
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError>;
async fn get_email_data(&self, base_url: &str) -> CustomResult<EmailContents, EmailError>;
}

dyn_clone::clone_trait_object!(EmailClient<RichText = Body>);
Expand Down
8 changes: 8 additions & 0 deletions crates/router/src/configs/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@ pub struct Tenant {
pub schema: String,
pub redis_key_prefix: String,
pub clickhouse_database: String,
pub user: TenantUserConfig,
}

#[derive(Debug, Deserialize, Clone)]
pub struct TenantUserConfig {
pub control_center_url: String,
}

impl storage_impl::config::TenantConfig for Tenant {
Expand Down Expand Up @@ -1130,6 +1136,7 @@ impl<'de> Deserialize<'de> for TenantConfig {
schema: String,
redis_key_prefix: String,
clickhouse_database: String,
user: TenantUserConfig,
}

let hashmap = <HashMap<id_type::TenantId, Inner>>::deserialize(deserializer)?;
Expand All @@ -1146,6 +1153,7 @@ impl<'de> Deserialize<'de> for TenantConfig {
schema: value.schema,
redis_key_prefix: value.redis_key_prefix,
clickhouse_database: value.clickhouse_database,
user: value.user,
},
)
})
Expand Down
4 changes: 3 additions & 1 deletion crates/router/src/core/recon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ pub async fn send_recon_request(
state
.email_client
.compose_and_send_email(
email_types::get_base_url(&state),
Box::new(email_contents),
state.conf.proxy.https_url.as_ref(),
)
Expand Down Expand Up @@ -179,7 +180,7 @@ pub async fn recon_merchant_account_update(
let theme = theme_utils::get_most_specific_theme_using_lineage(
&state.clone(),
ThemeLineage::Merchant {
tenant_id: state.tenant.tenant_id,
tenant_id: state.tenant.tenant_id.clone(),
org_id: auth.merchant_account.get_org_id().clone(),
merchant_id: merchant_id.clone(),
},
Expand Down Expand Up @@ -210,6 +211,7 @@ pub async fn recon_merchant_account_update(
let _ = state
.email_client
.compose_and_send_email(
email_types::get_base_url(&state),
Box::new(email_contents),
state.conf.proxy.https_url.as_ref(),
)
Expand Down
9 changes: 9 additions & 0 deletions crates/router/src/core/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ pub async fn signup_with_merchant_id(
let send_email_result = state
.email_client
.compose_and_send_email(
email_types::get_base_url(&state),
Box::new(email_contents),
state.conf.proxy.https_url.as_ref(),
)
Expand Down Expand Up @@ -239,6 +240,7 @@ pub async fn connect_account(
let send_email_result = state
.email_client
.compose_and_send_email(
email_types::get_base_url(&state),
Box::new(email_contents),
state.conf.proxy.https_url.as_ref(),
)
Expand Down Expand Up @@ -294,6 +296,7 @@ pub async fn connect_account(
let magic_link_result = state
.email_client
.compose_and_send_email(
email_types::get_base_url(&state),
Box::new(magic_link_email),
state.conf.proxy.https_url.as_ref(),
)
Expand All @@ -310,6 +313,7 @@ pub async fn connect_account(
let welcome_email_result = state
.email_client
.compose_and_send_email(
email_types::get_base_url(&state),
Box::new(welcome_to_community_email),
state.conf.proxy.https_url.as_ref(),
)
Expand Down Expand Up @@ -438,6 +442,7 @@ pub async fn forgot_password(
state
.email_client
.compose_and_send_email(
email_types::get_base_url(&state),
Box::new(email_contents),
state.conf.proxy.https_url.as_ref(),
)
Expand Down Expand Up @@ -845,6 +850,7 @@ async fn handle_existing_user_invitation(
is_email_sent = state
.email_client
.compose_and_send_email(
email_types::get_base_url(state),
Box::new(email_contents),
state.conf.proxy.https_url.as_ref(),
)
Expand Down Expand Up @@ -1000,6 +1006,7 @@ async fn handle_new_user_invitation(
let send_email_result = state
.email_client
.compose_and_send_email(
email_types::get_base_url(state),
Box::new(email_contents),
state.conf.proxy.https_url.as_ref(),
)
Expand Down Expand Up @@ -1151,6 +1158,7 @@ pub async fn resend_invite(
state
.email_client
.compose_and_send_email(
email_types::get_base_url(&state),
Box::new(email_contents),
state.conf.proxy.https_url.as_ref(),
)
Expand Down Expand Up @@ -1782,6 +1790,7 @@ pub async fn send_verification_mail(
state
.email_client
.compose_and_send_email(
email_types::get_base_url(&state),
Box::new(email_contents),
state.conf.proxy.https_url.as_ref(),
)
Expand Down
1 change: 1 addition & 0 deletions crates/router/src/core/user/dashboard_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ async fn insert_metadata(
let send_email_result = state
.email_client
.compose_and_send_email(
email_types::get_base_url(state),
Box::new(email_contents),
state.conf.proxy.https_url.as_ref(),
)
Expand Down
34 changes: 21 additions & 13 deletions crates/router/src/services/email/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,14 @@ pub fn get_link_with_token(
email_url
}

pub fn get_base_url(state: &SessionState) -> &str {
if !state.conf.multitenancy.enabled {
&state.conf.user.base_url
} else {
&state.tenant.user.control_center_url
}
}

pub struct VerifyEmail {
pub recipient_email: domain::UserEmail,
pub settings: std::sync::Arc<configs::Settings>,
Expand All @@ -237,7 +245,7 @@ pub struct VerifyEmail {
/// Currently only HTML is supported
#[async_trait::async_trait]
impl EmailData for VerifyEmail {
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
async fn get_email_data(&self, base_url: &str) -> CustomResult<EmailContents, EmailError> {
let token = EmailToken::new_token(
self.recipient_email.clone(),
None,
Expand All @@ -248,7 +256,7 @@ impl EmailData for VerifyEmail {
.change_context(EmailError::TokenGenerationFailure)?;

let verify_email_link = get_link_with_token(
&self.settings.user.base_url,
base_url,
token,
"verify_email",
&self.auth_id,
Expand Down Expand Up @@ -279,7 +287,7 @@ pub struct ResetPassword {

#[async_trait::async_trait]
impl EmailData for ResetPassword {
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
async fn get_email_data(&self, base_url: &str) -> CustomResult<EmailContents, EmailError> {
let token = EmailToken::new_token(
self.recipient_email.clone(),
None,
Expand All @@ -290,7 +298,7 @@ impl EmailData for ResetPassword {
.change_context(EmailError::TokenGenerationFailure)?;

let reset_password_link = get_link_with_token(
&self.settings.user.base_url,
base_url,
token,
"set_password",
&self.auth_id,
Expand Down Expand Up @@ -322,7 +330,7 @@ pub struct MagicLink {

#[async_trait::async_trait]
impl EmailData for MagicLink {
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
async fn get_email_data(&self, base_url: &str) -> CustomResult<EmailContents, EmailError> {
let token = EmailToken::new_token(
self.recipient_email.clone(),
None,
Expand All @@ -333,7 +341,7 @@ impl EmailData for MagicLink {
.change_context(EmailError::TokenGenerationFailure)?;

let magic_link_login = get_link_with_token(
&self.settings.user.base_url,
base_url,
token,
"verify_email",
&self.auth_id,
Expand Down Expand Up @@ -366,7 +374,7 @@ pub struct InviteUser {

#[async_trait::async_trait]
impl EmailData for InviteUser {
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
async fn get_email_data(&self, base_url: &str) -> CustomResult<EmailContents, EmailError> {
let token = EmailToken::new_token(
self.recipient_email.clone(),
Some(self.entity.clone()),
Expand All @@ -377,7 +385,7 @@ impl EmailData for InviteUser {
.change_context(EmailError::TokenGenerationFailure)?;

let invite_user_link = get_link_with_token(
&self.settings.user.base_url,
base_url,
token,
"accept_invite_from_email",
&self.auth_id,
Expand Down Expand Up @@ -406,7 +414,7 @@ pub struct ReconActivation {

#[async_trait::async_trait]
impl EmailData for ReconActivation {
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
async fn get_email_data(&self, _base_url: &str) -> CustomResult<EmailContents, EmailError> {
let body = html::get_html_body(EmailBody::ReconActivation {
user_name: self.user_name.clone().get_secret().expose(),
});
Expand Down Expand Up @@ -461,7 +469,7 @@ impl BizEmailProd {

#[async_trait::async_trait]
impl EmailData for BizEmailProd {
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
async fn get_email_data(&self, _base_url: &str) -> CustomResult<EmailContents, EmailError> {
let body = html::get_html_body(EmailBody::BizEmailProd {
user_name: self.user_name.clone().expose(),
poc_email: self.poc_email.clone().expose(),
Expand Down Expand Up @@ -491,7 +499,7 @@ pub struct ProFeatureRequest {

#[async_trait::async_trait]
impl EmailData for ProFeatureRequest {
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
async fn get_email_data(&self, _base_url: &str) -> CustomResult<EmailContents, EmailError> {
let recipient = self.recipient_email.clone().into_inner();

let body = html::get_html_body(EmailBody::ProFeatureRequest {
Expand Down Expand Up @@ -521,7 +529,7 @@ pub struct ApiKeyExpiryReminder {

#[async_trait::async_trait]
impl EmailData for ApiKeyExpiryReminder {
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
async fn get_email_data(&self, _base_url: &str) -> CustomResult<EmailContents, EmailError> {
let recipient = self.recipient_email.clone().into_inner();

let body = html::get_html_body(EmailBody::ApiKeyExpiryReminder {
Expand All @@ -545,7 +553,7 @@ pub struct WelcomeToCommunity {

#[async_trait::async_trait]
impl EmailData for WelcomeToCommunity {
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
async fn get_email_data(&self, _base_url: &str) -> CustomResult<EmailContents, EmailError> {
let body = html::get_html_body(EmailBody::WelcomeToCommunity);

Ok(EmailContents {
Expand Down
3 changes: 2 additions & 1 deletion crates/router/src/workflows/api_key_expiry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
consts, errors,
logger::error,
routes::{metrics, SessionState},
services::email::types::ApiKeyExpiryReminder,
services::email::types::{self as email_types, ApiKeyExpiryReminder},
types::{api, domain::UserEmail, storage},
utils::{user::theme as theme_utils, OptionExt},
};
Expand Down Expand Up @@ -110,6 +110,7 @@ impl ProcessTrackerWorkflow<SessionState> for ApiKeyExpiryWorkflow {
.email_client
.clone()
.compose_and_send_email(
email_types::get_base_url(state),
Box::new(email_contents),
state.conf.proxy.https_url.as_ref(),
)
Expand Down
10 changes: 8 additions & 2 deletions loadtest/config/development.toml
Original file line number Diff line number Diff line change
Expand Up @@ -385,8 +385,14 @@ keys = "accept-language,user-agent"
enabled = false
global_tenant = { schema = "public", redis_key_prefix = "" }

[multitenancy.tenants]
public = { base_url = "http://localhost:8080", schema = "public", redis_key_prefix = "", clickhouse_database = "default"}
[multitenancy.tenants.public]
base_url = "http://localhost:8080"
schema = "public"
redis_key_prefix = ""
clickhouse_database = "default"

[multitenancy.tenants.public.user]
control_center_url = "http://localhost:9000"

[email]
sender_email = "[email protected]"
Expand Down

0 comments on commit 839e69d

Please sign in to comment.