diff --git a/config/config.example.toml b/config/config.example.toml index 5c55d57cf77b..4d9950226451 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -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 diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml index c470f364fe74..967b847dae51 100644 --- a/config/deployments/env_specific.toml +++ b/config/deployments/env_specific.toml @@ -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 diff --git a/config/development.toml b/config/development.toml index fd84d626d377..6250e431bd4f 100644 --- a/config/development.toml +++ b/config/development.toml @@ -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" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index dbd5286b2900..b861e47e5945 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -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" diff --git a/crates/external_services/src/email.rs b/crates/external_services/src/email.rs index 2e05a26e1f1c..a98b03c0b545 100644 --- a/crates/external_services/src/email.rs +++ b/crates/external_services/src/email.rs @@ -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, proxy_url: Option<&String>, ) -> EmailResult<()>; @@ -60,10 +61,11 @@ where { async fn compose_and_send_email( &self, + base_url: &str, email_data: Box, 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 { @@ -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; + async fn get_email_data(&self, base_url: &str) -> CustomResult; } dyn_clone::clone_trait_object!(EmailClient); diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 4ab2a8fd5c77..ccd43d25b8ee 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -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 { @@ -1130,6 +1136,7 @@ impl<'de> Deserialize<'de> for TenantConfig { schema: String, redis_key_prefix: String, clickhouse_database: String, + user: TenantUserConfig, } let hashmap = >::deserialize(deserializer)?; @@ -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, }, ) }) diff --git a/crates/router/src/core/recon.rs b/crates/router/src/core/recon.rs index c2f289d4cc02..3b2aacd4514d 100644 --- a/crates/router/src/core/recon.rs +++ b/crates/router/src/core/recon.rs @@ -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(), ) @@ -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(), }, @@ -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(), ) diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 642473bdf8e0..19f257ef4f37 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -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(), ) @@ -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(), ) @@ -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(), ) @@ -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(), ) @@ -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(), ) @@ -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(), ) @@ -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(), ) @@ -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(), ) @@ -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(), ) diff --git a/crates/router/src/core/user/dashboard_metadata.rs b/crates/router/src/core/user/dashboard_metadata.rs index 88380363b4e3..aa67a70223ff 100644 --- a/crates/router/src/core/user/dashboard_metadata.rs +++ b/crates/router/src/core/user/dashboard_metadata.rs @@ -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(), ) diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index 128250a61aa8..476cd1292f6a 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -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, @@ -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 { + async fn get_email_data(&self, base_url: &str) -> CustomResult { let token = EmailToken::new_token( self.recipient_email.clone(), None, @@ -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, @@ -279,7 +287,7 @@ pub struct ResetPassword { #[async_trait::async_trait] impl EmailData for ResetPassword { - async fn get_email_data(&self) -> CustomResult { + async fn get_email_data(&self, base_url: &str) -> CustomResult { let token = EmailToken::new_token( self.recipient_email.clone(), None, @@ -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, @@ -322,7 +330,7 @@ pub struct MagicLink { #[async_trait::async_trait] impl EmailData for MagicLink { - async fn get_email_data(&self) -> CustomResult { + async fn get_email_data(&self, base_url: &str) -> CustomResult { let token = EmailToken::new_token( self.recipient_email.clone(), None, @@ -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, @@ -366,7 +374,7 @@ pub struct InviteUser { #[async_trait::async_trait] impl EmailData for InviteUser { - async fn get_email_data(&self) -> CustomResult { + async fn get_email_data(&self, base_url: &str) -> CustomResult { let token = EmailToken::new_token( self.recipient_email.clone(), Some(self.entity.clone()), @@ -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, @@ -406,7 +414,7 @@ pub struct ReconActivation { #[async_trait::async_trait] impl EmailData for ReconActivation { - async fn get_email_data(&self) -> CustomResult { + async fn get_email_data(&self, _base_url: &str) -> CustomResult { let body = html::get_html_body(EmailBody::ReconActivation { user_name: self.user_name.clone().get_secret().expose(), }); @@ -461,7 +469,7 @@ impl BizEmailProd { #[async_trait::async_trait] impl EmailData for BizEmailProd { - async fn get_email_data(&self) -> CustomResult { + async fn get_email_data(&self, _base_url: &str) -> CustomResult { let body = html::get_html_body(EmailBody::BizEmailProd { user_name: self.user_name.clone().expose(), poc_email: self.poc_email.clone().expose(), @@ -491,7 +499,7 @@ pub struct ProFeatureRequest { #[async_trait::async_trait] impl EmailData for ProFeatureRequest { - async fn get_email_data(&self) -> CustomResult { + async fn get_email_data(&self, _base_url: &str) -> CustomResult { let recipient = self.recipient_email.clone().into_inner(); let body = html::get_html_body(EmailBody::ProFeatureRequest { @@ -521,7 +529,7 @@ pub struct ApiKeyExpiryReminder { #[async_trait::async_trait] impl EmailData for ApiKeyExpiryReminder { - async fn get_email_data(&self) -> CustomResult { + async fn get_email_data(&self, _base_url: &str) -> CustomResult { let recipient = self.recipient_email.clone().into_inner(); let body = html::get_html_body(EmailBody::ApiKeyExpiryReminder { @@ -545,7 +553,7 @@ pub struct WelcomeToCommunity { #[async_trait::async_trait] impl EmailData for WelcomeToCommunity { - async fn get_email_data(&self) -> CustomResult { + async fn get_email_data(&self, _base_url: &str) -> CustomResult { let body = html::get_html_body(EmailBody::WelcomeToCommunity); Ok(EmailContents { diff --git a/crates/router/src/workflows/api_key_expiry.rs b/crates/router/src/workflows/api_key_expiry.rs index cdc4959c4562..2ff527a046e5 100644 --- a/crates/router/src/workflows/api_key_expiry.rs +++ b/crates/router/src/workflows/api_key_expiry.rs @@ -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}, }; @@ -110,6 +110,7 @@ impl ProcessTrackerWorkflow 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(), ) diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 51981a298e31..09b52bc357d2 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -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 = "example@example.com"