Skip to content

Commit

Permalink
feat(users): Incorporate themes in user APIs (#6772)
Browse files Browse the repository at this point in the history
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
  • Loading branch information
ThisIsMani and hyperswitch-bot[bot] authored Dec 16, 2024
1 parent da46427 commit 4b989fe
Show file tree
Hide file tree
Showing 27 changed files with 687 additions and 89 deletions.
9 changes: 8 additions & 1 deletion config/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -797,5 +797,12 @@ host = "localhost" # Client Host
port = 7000 # Client Port
service = "dynamo" # Service name

[theme_storage]
[theme.storage]
file_storage_backend = "file_system" # Theme storage backend to be used

[theme.email_config]
entity_name = "Hyperswitch" # Name of the entity to be showed in emails
entity_logo_url = "https://example.com/logo.svg" # Logo URL of the entity to be used in emails
foreground_color = "#000000" # Foreground color of email text
primary_color = "#006DF9" # Primary color of email body
background_color = "#FFFFFF" # Background color of email body
11 changes: 9 additions & 2 deletions config/deployments/env_specific.toml
Original file line number Diff line number Diff line change
Expand Up @@ -328,9 +328,16 @@ host = "localhost" # Client Host
port = 7000 # Client Port
service = "dynamo" # Service name

[theme_storage]
[theme.storage]
file_storage_backend = "aws_s3" # Theme storage backend to be used

[theme_storage.aws_s3]
[theme.storage.aws_s3]
region = "bucket_region" # AWS region where the S3 bucket for theme storage is located
bucket_name = "bucket" # AWS S3 bucket name for theme storage

[theme.email_config]
entity_name = "Hyperswitch" # Name of the entity to be showed in emails
entity_logo_url = "https://example.com/logo.svg" # Logo URL of the entity to be used in emails
foreground_color = "#000000" # Foreground color of email text
primary_color = "#006DF9" # Primary color of email body
background_color = "#FFFFFF" # Background color of email body
11 changes: 9 additions & 2 deletions config/development.toml
Original file line number Diff line number Diff line change
Expand Up @@ -797,5 +797,12 @@ host = "localhost"
port = 7000
service = "dynamo"

[theme_storage]
file_storage_backend = "file_system"
[theme.storage]
file_storage_backend = "file_system" # Theme storage backend to be used

[theme.email_config]
entity_name = "Hyperswitch" # Name of the entity to be showed in emails
entity_logo_url = "https://example.com/logo.svg" # Logo URL of the entity to be used in emails
foreground_color = "#000000" # Foreground color of email text
primary_color = "#006DF9" # Primary color of email body
background_color = "#FFFFFF" # Background color of email body
9 changes: 8 additions & 1 deletion config/docker_compose.toml
Original file line number Diff line number Diff line change
Expand Up @@ -694,5 +694,12 @@ prod_intent_recipient_email = "[email protected]" # Recipient email for prod
email_role_arn = "" # The amazon resource name ( arn ) of the role which has permission to send emails
sts_role_session_name = "" # An identifier for the assumed role session, used to uniquely identify a session.

[theme_storage]
[theme.storage]
file_storage_backend = "file_system" # Theme storage backend to be used

[theme.email_config]
entity_name = "Hyperswitch" # Name of the entity to be showed in emails
entity_logo_url = "https://example.com/logo.svg" # Logo URL of the entity to be used in emails
foreground_color = "#000000" # Foreground color of email text
primary_color = "#006DF9" # Primary color of email body
background_color = "#FFFFFF" # Background color of email body
4 changes: 3 additions & 1 deletion crates/api_models/src/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ pub struct GetUserDetailsResponse {
pub recovery_codes_left: Option<usize>,
pub profile_id: id_type::ProfileId,
pub entity_type: EntityType,
pub theme_id: Option<String>,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
Expand Down Expand Up @@ -345,8 +346,9 @@ pub struct SsoSignInRequest {
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct AuthIdQueryParam {
pub struct AuthIdAndThemeIdQueryParam {
pub auth_id: Option<String>,
pub theme_id: Option<String>,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
Expand Down
8 changes: 7 additions & 1 deletion crates/api_models/src/user/theme.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use actix_multipart::form::{bytes::Bytes, text::Text, MultipartForm};
use common_enums::EntityType;
use common_utils::{id_type, types::theme::ThemeLineage};
use common_utils::{
id_type,
types::theme::{EmailThemeConfig, ThemeLineage},
};
use masking::Secret;
use serde::{Deserialize, Serialize};

Expand All @@ -13,6 +16,7 @@ pub struct GetThemeResponse {
pub org_id: Option<id_type::OrganizationId>,
pub merchant_id: Option<id_type::MerchantId>,
pub profile_id: Option<id_type::ProfileId>,
pub email_config: EmailThemeConfig,
pub theme_data: ThemeData,
}

Expand All @@ -35,12 +39,14 @@ pub struct CreateThemeRequest {
pub lineage: ThemeLineage,
pub theme_name: String,
pub theme_data: ThemeData,
pub email_config: Option<EmailThemeConfig>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct UpdateThemeRequest {
pub lineage: ThemeLineage,
pub theme_data: ThemeData,
// TODO: Add support to update email config
}

// All the below structs are for the theme.json file,
Expand Down
112 changes: 102 additions & 10 deletions crates/common_utils/src/types/theme.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use common_enums::EntityType;
use serde::{Deserialize, Serialize};

use crate::{
events::{ApiEventMetric, ApiEventsType},
Expand All @@ -7,15 +8,14 @@ use crate::{

/// Enum for having all the required lineage for every level.
/// Currently being used for theme related APIs and queries.
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "entity_type", rename_all = "snake_case")]
pub enum ThemeLineage {
// TODO: Add back Tenant variant when we introduce Tenant Variant in EntityType
// /// Tenant lineage variant
// Tenant {
// /// tenant_id: String
// tenant_id: String,
// },
/// Tenant lineage variant
Tenant {
/// tenant_id: TenantId
tenant_id: id_type::TenantId,
},
/// Org lineage variant
Organization {
/// tenant_id: TenantId
Expand Down Expand Up @@ -48,9 +48,35 @@ pub enum ThemeLineage {
impl_api_event_type!(Miscellaneous, (ThemeLineage));

impl ThemeLineage {
/// Constructor for ThemeLineage
pub fn new(
entity_type: EntityType,
tenant_id: id_type::TenantId,
org_id: id_type::OrganizationId,
merchant_id: id_type::MerchantId,
profile_id: id_type::ProfileId,
) -> Self {
match entity_type {
EntityType::Tenant => Self::Tenant { tenant_id },
EntityType::Organization => Self::Organization { tenant_id, org_id },
EntityType::Merchant => Self::Merchant {
tenant_id,
org_id,
merchant_id,
},
EntityType::Profile => Self::Profile {
tenant_id,
org_id,
merchant_id,
profile_id,
},
}
}

/// Get the entity_type from the lineage
pub fn entity_type(&self) -> EntityType {
match self {
Self::Tenant { .. } => EntityType::Tenant,
Self::Organization { .. } => EntityType::Organization,
Self::Merchant { .. } => EntityType::Merchant,
Self::Profile { .. } => EntityType::Profile,
Expand All @@ -60,7 +86,8 @@ impl ThemeLineage {
/// Get the tenant_id from the lineage
pub fn tenant_id(&self) -> &id_type::TenantId {
match self {
Self::Organization { tenant_id, .. }
Self::Tenant { tenant_id }
| Self::Organization { tenant_id, .. }
| Self::Merchant { tenant_id, .. }
| Self::Profile { tenant_id, .. } => tenant_id,
}
Expand All @@ -69,6 +96,7 @@ impl ThemeLineage {
/// Get the org_id from the lineage
pub fn org_id(&self) -> Option<&id_type::OrganizationId> {
match self {
Self::Tenant { .. } => None,
Self::Organization { org_id, .. }
| Self::Merchant { org_id, .. }
| Self::Profile { org_id, .. } => Some(org_id),
Expand All @@ -78,7 +106,7 @@ impl ThemeLineage {
/// Get the merchant_id from the lineage
pub fn merchant_id(&self) -> Option<&id_type::MerchantId> {
match self {
Self::Organization { .. } => None,
Self::Tenant { .. } | Self::Organization { .. } => None,
Self::Merchant { merchant_id, .. } | Self::Profile { merchant_id, .. } => {
Some(merchant_id)
}
Expand All @@ -88,8 +116,72 @@ impl ThemeLineage {
/// Get the profile_id from the lineage
pub fn profile_id(&self) -> Option<&id_type::ProfileId> {
match self {
Self::Organization { .. } | Self::Merchant { .. } => None,
Self::Tenant { .. } | Self::Organization { .. } | Self::Merchant { .. } => None,
Self::Profile { profile_id, .. } => Some(profile_id),
}
}

/// Get higher lineages from the current lineage
pub fn get_same_and_higher_lineages(self) -> Vec<Self> {
match &self {
Self::Tenant { .. } => vec![self],
Self::Organization { tenant_id, .. } => vec![
Self::Tenant {
tenant_id: tenant_id.clone(),
},
self,
],
Self::Merchant {
tenant_id, org_id, ..
} => vec![
Self::Tenant {
tenant_id: tenant_id.clone(),
},
Self::Organization {
tenant_id: tenant_id.clone(),
org_id: org_id.clone(),
},
self,
],
Self::Profile {
tenant_id,
org_id,
merchant_id,
..
} => vec![
Self::Tenant {
tenant_id: tenant_id.clone(),
},
Self::Organization {
tenant_id: tenant_id.clone(),
org_id: org_id.clone(),
},
Self::Merchant {
tenant_id: tenant_id.clone(),
org_id: org_id.clone(),
merchant_id: merchant_id.clone(),
},
self,
],
}
}
}

/// Struct for holding the theme settings for email
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct EmailThemeConfig {
/// The entity name to be used in the email
pub entity_name: String,

/// The URL of the entity logo to be used in the email
pub entity_logo_url: String,

/// The primary color to be used in the email
pub primary_color: String,

/// The foreground color to be used in the email
pub foreground_color: String,

/// The background color to be used in the email
pub background_color: String,
}
65 changes: 54 additions & 11 deletions crates/diesel_models/src/query/user/theme.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
use async_bb8_diesel::AsyncRunQueryDsl;
use common_utils::types::theme::ThemeLineage;
use diesel::{
associations::HasTable,
debug_query,
pg::Pg,
result::Error as DieselError,
sql_types::{Bool, Nullable},
BoolExpressionMethods, ExpressionMethods,
BoolExpressionMethods, ExpressionMethods, NullableExpressionMethods, QueryDsl,
};
use error_stack::{report, ResultExt};
use router_env::logger;

use crate::{
query::generics,
errors::DatabaseError,
query::generics::{
self,
db_metrics::{track_database_call, DatabaseOperation},
},
schema::themes::dsl,
user::theme::{Theme, ThemeNew},
PgPooledConn, StorageResult,
Expand All @@ -27,15 +36,14 @@ impl Theme {
+ 'static,
> {
match lineage {
// TODO: Add back Tenant variant when we introduce Tenant Variant in EntityType
// ThemeLineage::Tenant { tenant_id } => Box::new(
// dsl::tenant_id
// .eq(tenant_id)
// .and(dsl::org_id.is_null())
// .and(dsl::merchant_id.is_null())
// .and(dsl::profile_id.is_null())
// .nullable(),
// ),
ThemeLineage::Tenant { tenant_id } => Box::new(
dsl::tenant_id
.eq(tenant_id)
.and(dsl::org_id.is_null())
.and(dsl::merchant_id.is_null())
.and(dsl::profile_id.is_null())
.nullable(),
),
ThemeLineage::Organization { tenant_id, org_id } => Box::new(
dsl::tenant_id
.eq(tenant_id)
Expand Down Expand Up @@ -77,6 +85,41 @@ impl Theme {
.await
}

pub async fn find_most_specific_theme_in_lineage(
conn: &PgPooledConn,
lineage: ThemeLineage,
) -> StorageResult<Self> {
let query = <Self as HasTable>::table().into_boxed();

let query =
lineage
.get_same_and_higher_lineages()
.into_iter()
.fold(query, |mut query, lineage| {
query = query.or_filter(Self::lineage_filter(lineage));
query
});

logger::debug!(query = %debug_query::<Pg,_>(&query).to_string());

let data: Vec<Self> = match track_database_call::<Self, _, _>(
query.get_results_async(conn),
DatabaseOperation::Filter,
)
.await
{
Ok(value) => Ok(value),
Err(err) => match err {
DieselError::NotFound => Err(report!(err)).change_context(DatabaseError::NotFound),
_ => Err(report!(err)).change_context(DatabaseError::Others),
},
}?;

data.into_iter()
.min_by_key(|theme| theme.entity_type)
.ok_or(report!(DatabaseError::NotFound))
}

pub async fn find_by_lineage(
conn: &PgPooledConn,
lineage: ThemeLineage,
Expand Down
9 changes: 9 additions & 0 deletions crates/diesel_models/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1321,6 +1321,15 @@ diesel::table! {
entity_type -> Varchar,
#[max_length = 64]
theme_name -> Varchar,
#[max_length = 64]
email_primary_color -> Varchar,
#[max_length = 64]
email_foreground_color -> Varchar,
#[max_length = 64]
email_background_color -> Varchar,
#[max_length = 64]
email_entity_name -> Varchar,
email_entity_logo_url -> Text,
}
}

Expand Down
Loading

0 comments on commit 4b989fe

Please sign in to comment.