feat(users): handle email url for users in different tenancies (#6809)

This commit is contained in:
Apoorv Dixit
2024-12-19 13:10:48 +05:30
committed by GitHub
parent 4bc88a7190
commit 839e69df24
12 changed files with 89 additions and 27 deletions

View File

@ -759,8 +759,15 @@ sdk_eligible_payment_methods = "card"
enabled = false enabled = false
global_tenant = { schema = "public", redis_key_prefix = "", clickhouse_database = "default"} global_tenant = { schema = "public", redis_key_prefix = "", clickhouse_database = "default"}
[multitenancy.tenants] [multitenancy.tenants.public]
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 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] [user_auth_methods]
encryption_key = "" # Encryption key used for encrypting data in user_authentication_methods table encryption_key = "" # Encryption key used for encrypting data in user_authentication_methods table

View File

@ -305,8 +305,14 @@ region = "kms_region" # The AWS region used by the KMS SDK for decrypting data.
enabled = false enabled = false
global_tenant = { schema = "public", redis_key_prefix = "", clickhouse_database = "default"} global_tenant = { schema = "public", redis_key_prefix = "", clickhouse_database = "default"}
[multitenancy.tenants] [multitenancy.tenants.public]
public = { base_url = "http://localhost:8080", schema = "public", redis_key_prefix = "", clickhouse_database = "default" } 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] [user_auth_methods]
encryption_key = "user_auth_table_encryption_key" # Encryption key used for encrypting data in user_authentication_methods table encryption_key = "user_auth_table_encryption_key" # Encryption key used for encrypting data in user_authentication_methods table

View File

@ -775,8 +775,14 @@ sdk_eligible_payment_methods = "card"
enabled = false enabled = false
global_tenant = { schema = "public", redis_key_prefix = "", clickhouse_database = "default"} global_tenant = { schema = "public", redis_key_prefix = "", clickhouse_database = "default"}
[multitenancy.tenants] [multitenancy.tenants.public]
public = { base_url = "http://localhost:8080", schema = "public", redis_key_prefix = "", clickhouse_database = "default"} 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] [user_auth_methods]
encryption_key = "A8EF32E029BC3342E54BF2E172A4D7AA43E8EF9D2C3A624A9F04E2EF79DC698F" encryption_key = "A8EF32E029BC3342E54BF2E172A4D7AA43E8EF9D2C3A624A9F04E2EF79DC698F"

View File

@ -633,8 +633,14 @@ sdk_eligible_payment_methods = "card"
enabled = false enabled = false
global_tenant = { schema = "public", redis_key_prefix = "", clickhouse_database = "default" } global_tenant = { schema = "public", redis_key_prefix = "", clickhouse_database = "default" }
[multitenancy.tenants] [multitenancy.tenants.public]
public = { base_url = "http://localhost:8080", schema = "public", redis_key_prefix = "", clickhouse_database = "default" } 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] [user_auth_methods]
encryption_key = "A8EF32E029BC3342E54BF2E172A4D7AA43E8EF9D2C3A624A9F04E2EF79DC698F" encryption_key = "A8EF32E029BC3342E54BF2E172A4D7AA43E8EF9D2C3A624A9F04E2EF79DC698F"

View File

@ -47,6 +47,7 @@ pub trait EmailService: Sync + Send + dyn_clone::DynClone {
/// Compose and send email using the email data /// Compose and send email using the email data
async fn compose_and_send_email( async fn compose_and_send_email(
&self, &self,
base_url: &str,
email_data: Box<dyn EmailData + Send>, email_data: Box<dyn EmailData + Send>,
proxy_url: Option<&String>, proxy_url: Option<&String>,
) -> EmailResult<()>; ) -> EmailResult<()>;
@ -60,10 +61,11 @@ where
{ {
async fn compose_and_send_email( async fn compose_and_send_email(
&self, &self,
base_url: &str,
email_data: Box<dyn EmailData + Send>, email_data: Box<dyn EmailData + Send>,
proxy_url: Option<&String>, proxy_url: Option<&String>,
) -> EmailResult<()> { ) -> 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 email_data = email_data.await?;
let EmailContents { let EmailContents {
@ -113,7 +115,7 @@ pub struct EmailContents {
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait EmailData { pub trait EmailData {
/// Get the email contents /// 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>); dyn_clone::clone_trait_object!(EmailClient<RichText = Body>);

View File

@ -169,6 +169,12 @@ pub struct Tenant {
pub schema: String, pub schema: String,
pub redis_key_prefix: String, pub redis_key_prefix: String,
pub clickhouse_database: 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 { impl storage_impl::config::TenantConfig for Tenant {
@ -1130,6 +1136,7 @@ impl<'de> Deserialize<'de> for TenantConfig {
schema: String, schema: String,
redis_key_prefix: String, redis_key_prefix: String,
clickhouse_database: String, clickhouse_database: String,
user: TenantUserConfig,
} }
let hashmap = <HashMap<id_type::TenantId, Inner>>::deserialize(deserializer)?; let hashmap = <HashMap<id_type::TenantId, Inner>>::deserialize(deserializer)?;
@ -1146,6 +1153,7 @@ impl<'de> Deserialize<'de> for TenantConfig {
schema: value.schema, schema: value.schema,
redis_key_prefix: value.redis_key_prefix, redis_key_prefix: value.redis_key_prefix,
clickhouse_database: value.clickhouse_database, clickhouse_database: value.clickhouse_database,
user: value.user,
}, },
) )
}) })

View File

@ -80,6 +80,7 @@ pub async fn send_recon_request(
state state
.email_client .email_client
.compose_and_send_email( .compose_and_send_email(
email_types::get_base_url(&state),
Box::new(email_contents), Box::new(email_contents),
state.conf.proxy.https_url.as_ref(), 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( let theme = theme_utils::get_most_specific_theme_using_lineage(
&state.clone(), &state.clone(),
ThemeLineage::Merchant { ThemeLineage::Merchant {
tenant_id: state.tenant.tenant_id, tenant_id: state.tenant.tenant_id.clone(),
org_id: auth.merchant_account.get_org_id().clone(), org_id: auth.merchant_account.get_org_id().clone(),
merchant_id: merchant_id.clone(), merchant_id: merchant_id.clone(),
}, },
@ -210,6 +211,7 @@ pub async fn recon_merchant_account_update(
let _ = state let _ = state
.email_client .email_client
.compose_and_send_email( .compose_and_send_email(
email_types::get_base_url(&state),
Box::new(email_contents), Box::new(email_contents),
state.conf.proxy.https_url.as_ref(), state.conf.proxy.https_url.as_ref(),
) )

View File

@ -96,6 +96,7 @@ pub async fn signup_with_merchant_id(
let send_email_result = state let send_email_result = state
.email_client .email_client
.compose_and_send_email( .compose_and_send_email(
email_types::get_base_url(&state),
Box::new(email_contents), Box::new(email_contents),
state.conf.proxy.https_url.as_ref(), state.conf.proxy.https_url.as_ref(),
) )
@ -239,6 +240,7 @@ pub async fn connect_account(
let send_email_result = state let send_email_result = state
.email_client .email_client
.compose_and_send_email( .compose_and_send_email(
email_types::get_base_url(&state),
Box::new(email_contents), Box::new(email_contents),
state.conf.proxy.https_url.as_ref(), state.conf.proxy.https_url.as_ref(),
) )
@ -294,6 +296,7 @@ pub async fn connect_account(
let magic_link_result = state let magic_link_result = state
.email_client .email_client
.compose_and_send_email( .compose_and_send_email(
email_types::get_base_url(&state),
Box::new(magic_link_email), Box::new(magic_link_email),
state.conf.proxy.https_url.as_ref(), state.conf.proxy.https_url.as_ref(),
) )
@ -310,6 +313,7 @@ pub async fn connect_account(
let welcome_email_result = state let welcome_email_result = state
.email_client .email_client
.compose_and_send_email( .compose_and_send_email(
email_types::get_base_url(&state),
Box::new(welcome_to_community_email), Box::new(welcome_to_community_email),
state.conf.proxy.https_url.as_ref(), state.conf.proxy.https_url.as_ref(),
) )
@ -438,6 +442,7 @@ pub async fn forgot_password(
state state
.email_client .email_client
.compose_and_send_email( .compose_and_send_email(
email_types::get_base_url(&state),
Box::new(email_contents), Box::new(email_contents),
state.conf.proxy.https_url.as_ref(), state.conf.proxy.https_url.as_ref(),
) )
@ -845,6 +850,7 @@ async fn handle_existing_user_invitation(
is_email_sent = state is_email_sent = state
.email_client .email_client
.compose_and_send_email( .compose_and_send_email(
email_types::get_base_url(state),
Box::new(email_contents), Box::new(email_contents),
state.conf.proxy.https_url.as_ref(), state.conf.proxy.https_url.as_ref(),
) )
@ -1000,6 +1006,7 @@ async fn handle_new_user_invitation(
let send_email_result = state let send_email_result = state
.email_client .email_client
.compose_and_send_email( .compose_and_send_email(
email_types::get_base_url(state),
Box::new(email_contents), Box::new(email_contents),
state.conf.proxy.https_url.as_ref(), state.conf.proxy.https_url.as_ref(),
) )
@ -1151,6 +1158,7 @@ pub async fn resend_invite(
state state
.email_client .email_client
.compose_and_send_email( .compose_and_send_email(
email_types::get_base_url(&state),
Box::new(email_contents), Box::new(email_contents),
state.conf.proxy.https_url.as_ref(), state.conf.proxy.https_url.as_ref(),
) )
@ -1782,6 +1790,7 @@ pub async fn send_verification_mail(
state state
.email_client .email_client
.compose_and_send_email( .compose_and_send_email(
email_types::get_base_url(&state),
Box::new(email_contents), Box::new(email_contents),
state.conf.proxy.https_url.as_ref(), state.conf.proxy.https_url.as_ref(),
) )

View File

@ -496,6 +496,7 @@ async fn insert_metadata(
let send_email_result = state let send_email_result = state
.email_client .email_client
.compose_and_send_email( .compose_and_send_email(
email_types::get_base_url(state),
Box::new(email_contents), Box::new(email_contents),
state.conf.proxy.https_url.as_ref(), state.conf.proxy.https_url.as_ref(),
) )

View File

@ -225,6 +225,14 @@ pub fn get_link_with_token(
email_url 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 struct VerifyEmail {
pub recipient_email: domain::UserEmail, pub recipient_email: domain::UserEmail,
pub settings: std::sync::Arc<configs::Settings>, pub settings: std::sync::Arc<configs::Settings>,
@ -237,7 +245,7 @@ pub struct VerifyEmail {
/// Currently only HTML is supported /// Currently only HTML is supported
#[async_trait::async_trait] #[async_trait::async_trait]
impl EmailData for VerifyEmail { 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( let token = EmailToken::new_token(
self.recipient_email.clone(), self.recipient_email.clone(),
None, None,
@ -248,7 +256,7 @@ impl EmailData for VerifyEmail {
.change_context(EmailError::TokenGenerationFailure)?; .change_context(EmailError::TokenGenerationFailure)?;
let verify_email_link = get_link_with_token( let verify_email_link = get_link_with_token(
&self.settings.user.base_url, base_url,
token, token,
"verify_email", "verify_email",
&self.auth_id, &self.auth_id,
@ -279,7 +287,7 @@ pub struct ResetPassword {
#[async_trait::async_trait] #[async_trait::async_trait]
impl EmailData for ResetPassword { 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( let token = EmailToken::new_token(
self.recipient_email.clone(), self.recipient_email.clone(),
None, None,
@ -290,7 +298,7 @@ impl EmailData for ResetPassword {
.change_context(EmailError::TokenGenerationFailure)?; .change_context(EmailError::TokenGenerationFailure)?;
let reset_password_link = get_link_with_token( let reset_password_link = get_link_with_token(
&self.settings.user.base_url, base_url,
token, token,
"set_password", "set_password",
&self.auth_id, &self.auth_id,
@ -322,7 +330,7 @@ pub struct MagicLink {
#[async_trait::async_trait] #[async_trait::async_trait]
impl EmailData for MagicLink { 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( let token = EmailToken::new_token(
self.recipient_email.clone(), self.recipient_email.clone(),
None, None,
@ -333,7 +341,7 @@ impl EmailData for MagicLink {
.change_context(EmailError::TokenGenerationFailure)?; .change_context(EmailError::TokenGenerationFailure)?;
let magic_link_login = get_link_with_token( let magic_link_login = get_link_with_token(
&self.settings.user.base_url, base_url,
token, token,
"verify_email", "verify_email",
&self.auth_id, &self.auth_id,
@ -366,7 +374,7 @@ pub struct InviteUser {
#[async_trait::async_trait] #[async_trait::async_trait]
impl EmailData for InviteUser { 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( let token = EmailToken::new_token(
self.recipient_email.clone(), self.recipient_email.clone(),
Some(self.entity.clone()), Some(self.entity.clone()),
@ -377,7 +385,7 @@ impl EmailData for InviteUser {
.change_context(EmailError::TokenGenerationFailure)?; .change_context(EmailError::TokenGenerationFailure)?;
let invite_user_link = get_link_with_token( let invite_user_link = get_link_with_token(
&self.settings.user.base_url, base_url,
token, token,
"accept_invite_from_email", "accept_invite_from_email",
&self.auth_id, &self.auth_id,
@ -406,7 +414,7 @@ pub struct ReconActivation {
#[async_trait::async_trait] #[async_trait::async_trait]
impl EmailData for ReconActivation { 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 { let body = html::get_html_body(EmailBody::ReconActivation {
user_name: self.user_name.clone().get_secret().expose(), user_name: self.user_name.clone().get_secret().expose(),
}); });
@ -461,7 +469,7 @@ impl BizEmailProd {
#[async_trait::async_trait] #[async_trait::async_trait]
impl EmailData for BizEmailProd { 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 { let body = html::get_html_body(EmailBody::BizEmailProd {
user_name: self.user_name.clone().expose(), user_name: self.user_name.clone().expose(),
poc_email: self.poc_email.clone().expose(), poc_email: self.poc_email.clone().expose(),
@ -491,7 +499,7 @@ pub struct ProFeatureRequest {
#[async_trait::async_trait] #[async_trait::async_trait]
impl EmailData for ProFeatureRequest { 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 recipient = self.recipient_email.clone().into_inner();
let body = html::get_html_body(EmailBody::ProFeatureRequest { let body = html::get_html_body(EmailBody::ProFeatureRequest {
@ -521,7 +529,7 @@ pub struct ApiKeyExpiryReminder {
#[async_trait::async_trait] #[async_trait::async_trait]
impl EmailData for ApiKeyExpiryReminder { 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 recipient = self.recipient_email.clone().into_inner();
let body = html::get_html_body(EmailBody::ApiKeyExpiryReminder { let body = html::get_html_body(EmailBody::ApiKeyExpiryReminder {
@ -545,7 +553,7 @@ pub struct WelcomeToCommunity {
#[async_trait::async_trait] #[async_trait::async_trait]
impl EmailData for WelcomeToCommunity { 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); let body = html::get_html_body(EmailBody::WelcomeToCommunity);
Ok(EmailContents { Ok(EmailContents {

View File

@ -9,7 +9,7 @@ use crate::{
consts, errors, consts, errors,
logger::error, logger::error,
routes::{metrics, SessionState}, routes::{metrics, SessionState},
services::email::types::ApiKeyExpiryReminder, services::email::types::{self as email_types, ApiKeyExpiryReminder},
types::{api, domain::UserEmail, storage}, types::{api, domain::UserEmail, storage},
utils::{user::theme as theme_utils, OptionExt}, utils::{user::theme as theme_utils, OptionExt},
}; };
@ -110,6 +110,7 @@ impl ProcessTrackerWorkflow<SessionState> for ApiKeyExpiryWorkflow {
.email_client .email_client
.clone() .clone()
.compose_and_send_email( .compose_and_send_email(
email_types::get_base_url(state),
Box::new(email_contents), Box::new(email_contents),
state.conf.proxy.https_url.as_ref(), state.conf.proxy.https_url.as_ref(),
) )

View File

@ -385,8 +385,14 @@ keys = "accept-language,user-agent"
enabled = false enabled = false
global_tenant = { schema = "public", redis_key_prefix = "" } global_tenant = { schema = "public", redis_key_prefix = "" }
[multitenancy.tenants] [multitenancy.tenants.public]
public = { base_url = "http://localhost:8080", schema = "public", redis_key_prefix = "", clickhouse_database = "default"} base_url = "http://localhost:8080"
schema = "public"
redis_key_prefix = ""
clickhouse_database = "default"
[multitenancy.tenants.public.user]
control_center_url = "http://localhost:9000"
[email] [email]
sender_email = "example@example.com" sender_email = "example@example.com"