mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 17:19:15 +08:00
feat(themes): Create user APIs for managing themes (#8387)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -4,7 +4,8 @@ use common_utils::events::{ApiEventMetric, ApiEventsType};
|
|||||||
use crate::user::sample_data::SampleDataRequest;
|
use crate::user::sample_data::SampleDataRequest;
|
||||||
#[cfg(feature = "control_center_theme")]
|
#[cfg(feature = "control_center_theme")]
|
||||||
use crate::user::theme::{
|
use crate::user::theme::{
|
||||||
CreateThemeRequest, GetThemeResponse, UpdateThemeRequest, UploadFileRequest,
|
CreateThemeRequest, CreateUserThemeRequest, GetThemeResponse, UpdateThemeRequest,
|
||||||
|
UploadFileRequest,
|
||||||
};
|
};
|
||||||
use crate::user::{
|
use crate::user::{
|
||||||
dashboard_metadata::{
|
dashboard_metadata::{
|
||||||
@ -83,6 +84,7 @@ common_utils::impl_api_event_type!(
|
|||||||
GetThemeResponse,
|
GetThemeResponse,
|
||||||
UploadFileRequest,
|
UploadFileRequest,
|
||||||
CreateThemeRequest,
|
CreateThemeRequest,
|
||||||
|
CreateUserThemeRequest,
|
||||||
UpdateThemeRequest
|
UpdateThemeRequest
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@ -41,6 +41,14 @@ pub struct CreateThemeRequest {
|
|||||||
pub email_config: Option<EmailThemeConfig>,
|
pub email_config: Option<EmailThemeConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct CreateUserThemeRequest {
|
||||||
|
pub entity_type: EntityType,
|
||||||
|
pub theme_name: String,
|
||||||
|
pub theme_data: ThemeData,
|
||||||
|
pub email_config: Option<EmailThemeConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct UpdateThemeRequest {
|
pub struct UpdateThemeRequest {
|
||||||
pub theme_data: Option<ThemeData>,
|
pub theme_data: Option<ThemeData>,
|
||||||
@ -137,3 +145,9 @@ struct Urls {
|
|||||||
favicon_url: Option<String>,
|
favicon_url: Option<String>,
|
||||||
logo_url: Option<String>,
|
logo_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub struct EntityTypeQueryParam {
|
||||||
|
pub entity_type: EntityType,
|
||||||
|
}
|
||||||
|
|||||||
@ -7534,6 +7534,8 @@ pub enum PermissionGroup {
|
|||||||
ReconOpsView,
|
ReconOpsView,
|
||||||
ReconOpsManage,
|
ReconOpsManage,
|
||||||
InternalManage,
|
InternalManage,
|
||||||
|
ThemeView,
|
||||||
|
ThemeManage,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, serde::Serialize, PartialEq, Eq, Hash, strum::EnumIter)]
|
#[derive(Clone, Debug, serde::Serialize, PartialEq, Eq, Hash, strum::EnumIter)]
|
||||||
@ -7547,6 +7549,7 @@ pub enum ParentGroup {
|
|||||||
ReconReports,
|
ReconReports,
|
||||||
Account,
|
Account,
|
||||||
Internal,
|
Internal,
|
||||||
|
Theme,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, serde::Serialize)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, serde::Serialize)]
|
||||||
@ -7577,6 +7580,7 @@ pub enum Resource {
|
|||||||
ReconConfig,
|
ReconConfig,
|
||||||
RevenueRecovery,
|
RevenueRecovery,
|
||||||
InternalConnector,
|
InternalConnector,
|
||||||
|
Theme,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, Hash)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, Hash)]
|
||||||
|
|||||||
@ -9,7 +9,6 @@ crate::impl_id_type_methods!(ProfileId, "profile_id");
|
|||||||
// This is to display the `ProfileId` as ProfileId(abcd)
|
// This is to display the `ProfileId` as ProfileId(abcd)
|
||||||
crate::impl_debug_id_type!(ProfileId);
|
crate::impl_debug_id_type!(ProfileId);
|
||||||
crate::impl_try_from_cow_str_id_type!(ProfileId, "profile_id");
|
crate::impl_try_from_cow_str_id_type!(ProfileId, "profile_id");
|
||||||
|
|
||||||
crate::impl_generate_id_id_type!(ProfileId, "pro");
|
crate::impl_generate_id_id_type!(ProfileId, "pro");
|
||||||
crate::impl_serializable_secret_id_type!(ProfileId);
|
crate::impl_serializable_secret_id_type!(ProfileId);
|
||||||
crate::impl_queryable_id_type!(ProfileId);
|
crate::impl_queryable_id_type!(ProfileId);
|
||||||
|
|||||||
@ -8,7 +8,7 @@ use crate::{
|
|||||||
|
|
||||||
/// Enum for having all the required lineage for every level.
|
/// Enum for having all the required lineage for every level.
|
||||||
/// Currently being used for theme related APIs and queries.
|
/// Currently being used for theme related APIs and queries.
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
#[serde(tag = "entity_type", rename_all = "snake_case")]
|
#[serde(tag = "entity_type", rename_all = "snake_case")]
|
||||||
pub enum ThemeLineage {
|
pub enum ThemeLineage {
|
||||||
/// Tenant lineage variant
|
/// Tenant lineage variant
|
||||||
|
|||||||
@ -77,6 +77,48 @@ impl Theme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Matches all themes that belong to the specified hierarchy level or below
|
||||||
|
fn lineage_hierarchy_filter(
|
||||||
|
lineage: ThemeLineage,
|
||||||
|
) -> Box<
|
||||||
|
dyn diesel::BoxableExpression<<Self as HasTable>::Table, Pg, SqlType = Nullable<Bool>>
|
||||||
|
+ 'static,
|
||||||
|
> {
|
||||||
|
match lineage {
|
||||||
|
ThemeLineage::Tenant { tenant_id } => Box::new(dsl::tenant_id.eq(tenant_id).nullable()),
|
||||||
|
ThemeLineage::Organization { tenant_id, org_id } => Box::new(
|
||||||
|
dsl::tenant_id
|
||||||
|
.eq(tenant_id)
|
||||||
|
.and(dsl::org_id.eq(org_id))
|
||||||
|
.nullable(),
|
||||||
|
),
|
||||||
|
ThemeLineage::Merchant {
|
||||||
|
tenant_id,
|
||||||
|
org_id,
|
||||||
|
merchant_id,
|
||||||
|
} => Box::new(
|
||||||
|
dsl::tenant_id
|
||||||
|
.eq(tenant_id)
|
||||||
|
.and(dsl::org_id.eq(org_id))
|
||||||
|
.and(dsl::merchant_id.eq(merchant_id))
|
||||||
|
.nullable(),
|
||||||
|
),
|
||||||
|
ThemeLineage::Profile {
|
||||||
|
tenant_id,
|
||||||
|
org_id,
|
||||||
|
merchant_id,
|
||||||
|
profile_id,
|
||||||
|
} => Box::new(
|
||||||
|
dsl::tenant_id
|
||||||
|
.eq(tenant_id)
|
||||||
|
.and(dsl::org_id.eq(org_id))
|
||||||
|
.and(dsl::merchant_id.eq(merchant_id))
|
||||||
|
.and(dsl::profile_id.eq(profile_id))
|
||||||
|
.nullable(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn find_by_theme_id(conn: &PgPooledConn, theme_id: String) -> StorageResult<Self> {
|
pub async fn find_by_theme_id(conn: &PgPooledConn, theme_id: String) -> StorageResult<Self> {
|
||||||
generics::generic_find_one::<<Self as HasTable>::Table, _, _>(
|
generics::generic_find_one::<<Self as HasTable>::Table, _, _>(
|
||||||
conn,
|
conn,
|
||||||
@ -161,4 +203,35 @@ impl Theme {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
pub async fn delete_by_theme_id(conn: &PgPooledConn, theme_id: String) -> StorageResult<Self> {
|
||||||
|
generics::generic_delete_one_with_result::<<Self as HasTable>::Table, _, _>(
|
||||||
|
conn,
|
||||||
|
dsl::theme_id.eq(theme_id),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
/// Finds all themes that match the specified lineage hierarchy.
|
||||||
|
pub async fn find_all_by_lineage_hierarchy(
|
||||||
|
conn: &PgPooledConn,
|
||||||
|
lineage: ThemeLineage,
|
||||||
|
) -> StorageResult<Vec<Self>> {
|
||||||
|
let filter = Self::lineage_hierarchy_filter(lineage);
|
||||||
|
|
||||||
|
let query = <Self as HasTable>::table().filter(filter).into_boxed();
|
||||||
|
|
||||||
|
logger::debug!(query = %debug_query::<Pg,_>(&query).to_string());
|
||||||
|
|
||||||
|
match track_database_call::<Self, _, _>(
|
||||||
|
query.get_results_async(conn),
|
||||||
|
DatabaseOperation::Filter,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(themes) => Ok(themes),
|
||||||
|
Err(err) => match err {
|
||||||
|
DieselError::NotFound => Err(report!(err)).change_context(DatabaseError::NotFound),
|
||||||
|
_ => Err(report!(err)).change_context(DatabaseError::Others),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
use api_models::user::theme as theme_api;
|
use api_models::user::theme as theme_api;
|
||||||
|
use common_enums::EntityType;
|
||||||
use common_utils::{
|
use common_utils::{
|
||||||
ext_traits::{ByteSliceExt, Encode},
|
ext_traits::{ByteSliceExt, Encode},
|
||||||
types::user::ThemeLineage,
|
types::user::ThemeLineage,
|
||||||
@ -13,9 +14,11 @@ use uuid::Uuid;
|
|||||||
use crate::{
|
use crate::{
|
||||||
core::errors::{StorageErrorExt, UserErrors, UserResponse},
|
core::errors::{StorageErrorExt, UserErrors, UserResponse},
|
||||||
routes::SessionState,
|
routes::SessionState,
|
||||||
|
services::authentication::UserFromToken,
|
||||||
utils::user::theme as theme_utils,
|
utils::user::theme as theme_utils,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: To be deprecated
|
||||||
pub async fn get_theme_using_lineage(
|
pub async fn get_theme_using_lineage(
|
||||||
state: SessionState,
|
state: SessionState,
|
||||||
lineage: ThemeLineage,
|
lineage: ThemeLineage,
|
||||||
@ -50,6 +53,7 @@ pub async fn get_theme_using_lineage(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: To be deprecated
|
||||||
pub async fn get_theme_using_theme_id(
|
pub async fn get_theme_using_theme_id(
|
||||||
state: SessionState,
|
state: SessionState,
|
||||||
theme_id: String,
|
theme_id: String,
|
||||||
@ -84,6 +88,7 @@ pub async fn get_theme_using_theme_id(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: To be deprecated
|
||||||
pub async fn upload_file_to_theme_storage(
|
pub async fn upload_file_to_theme_storage(
|
||||||
state: SessionState,
|
state: SessionState,
|
||||||
theme_id: String,
|
theme_id: String,
|
||||||
@ -105,6 +110,7 @@ pub async fn upload_file_to_theme_storage(
|
|||||||
Ok(ApplicationResponse::StatusOk)
|
Ok(ApplicationResponse::StatusOk)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: To be deprecated
|
||||||
pub async fn create_theme(
|
pub async fn create_theme(
|
||||||
state: SessionState,
|
state: SessionState,
|
||||||
request: theme_api::CreateThemeRequest,
|
request: theme_api::CreateThemeRequest,
|
||||||
@ -166,6 +172,7 @@ pub async fn create_theme(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: To be deprecated
|
||||||
pub async fn update_theme(
|
pub async fn update_theme(
|
||||||
state: SessionState,
|
state: SessionState,
|
||||||
theme_id: String,
|
theme_id: String,
|
||||||
@ -223,14 +230,11 @@ pub async fn update_theme(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_theme(
|
// TODO: To be deprecated
|
||||||
state: SessionState,
|
pub async fn delete_theme(state: SessionState, theme_id: String) -> UserResponse<()> {
|
||||||
theme_id: String,
|
|
||||||
lineage: ThemeLineage,
|
|
||||||
) -> UserResponse<()> {
|
|
||||||
state
|
state
|
||||||
.store
|
.store
|
||||||
.delete_theme_by_lineage_and_theme_id(theme_id.clone(), lineage)
|
.delete_theme_by_theme_id(theme_id.clone())
|
||||||
.await
|
.await
|
||||||
.to_not_found_response(UserErrors::ThemeNotFound)?;
|
.to_not_found_response(UserErrors::ThemeNotFound)?;
|
||||||
|
|
||||||
@ -240,3 +244,331 @@ pub async fn delete_theme(
|
|||||||
|
|
||||||
Ok(ApplicationResponse::StatusOk)
|
Ok(ApplicationResponse::StatusOk)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn create_user_theme(
|
||||||
|
state: SessionState,
|
||||||
|
user_from_token: UserFromToken,
|
||||||
|
request: theme_api::CreateUserThemeRequest,
|
||||||
|
) -> UserResponse<theme_api::GetThemeResponse> {
|
||||||
|
let email_config = if cfg!(feature = "email") {
|
||||||
|
request.email_config.ok_or(UserErrors::MissingEmailConfig)?
|
||||||
|
} else {
|
||||||
|
request
|
||||||
|
.email_config
|
||||||
|
.unwrap_or(state.conf.theme.email_config.clone())
|
||||||
|
};
|
||||||
|
let lineage = theme_utils::get_theme_lineage_from_user_token(
|
||||||
|
&user_from_token,
|
||||||
|
&state,
|
||||||
|
&request.entity_type,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let new_theme = ThemeNew::new(
|
||||||
|
Uuid::new_v4().to_string(),
|
||||||
|
request.theme_name,
|
||||||
|
lineage,
|
||||||
|
email_config,
|
||||||
|
);
|
||||||
|
|
||||||
|
let db_theme = state
|
||||||
|
.store
|
||||||
|
.insert_theme(new_theme)
|
||||||
|
.await
|
||||||
|
.to_duplicate_response(UserErrors::ThemeAlreadyExists)?;
|
||||||
|
|
||||||
|
theme_utils::upload_file_to_theme_bucket(
|
||||||
|
&state,
|
||||||
|
&theme_utils::get_theme_file_key(&db_theme.theme_id),
|
||||||
|
request
|
||||||
|
.theme_data
|
||||||
|
.encode_to_vec()
|
||||||
|
.change_context(UserErrors::InternalServerError)?,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let file = theme_utils::retrieve_file_from_theme_bucket(
|
||||||
|
&state,
|
||||||
|
&theme_utils::get_theme_file_key(&db_theme.theme_id),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let parsed_data = file
|
||||||
|
.to_bytes()
|
||||||
|
.parse_struct("ThemeData")
|
||||||
|
.change_context(UserErrors::InternalServerError)?;
|
||||||
|
|
||||||
|
Ok(ApplicationResponse::Json(theme_api::GetThemeResponse {
|
||||||
|
email_config: db_theme.email_config(),
|
||||||
|
theme_id: db_theme.theme_id,
|
||||||
|
entity_type: db_theme.entity_type,
|
||||||
|
tenant_id: db_theme.tenant_id,
|
||||||
|
org_id: db_theme.org_id,
|
||||||
|
merchant_id: db_theme.merchant_id,
|
||||||
|
profile_id: db_theme.profile_id,
|
||||||
|
theme_name: db_theme.theme_name,
|
||||||
|
theme_data: parsed_data,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_user_theme(
|
||||||
|
state: SessionState,
|
||||||
|
user_from_token: UserFromToken,
|
||||||
|
theme_id: String,
|
||||||
|
) -> UserResponse<()> {
|
||||||
|
let db_theme = state
|
||||||
|
.store
|
||||||
|
.find_theme_by_theme_id(theme_id.clone())
|
||||||
|
.await
|
||||||
|
.to_not_found_response(UserErrors::ThemeNotFound)?;
|
||||||
|
|
||||||
|
let user_role_info = user_from_token
|
||||||
|
.get_role_info_from_db(&state)
|
||||||
|
.await
|
||||||
|
.attach_printable("Invalid role_id in JWT")?;
|
||||||
|
let user_entity_type = user_role_info.get_entity_type();
|
||||||
|
|
||||||
|
theme_utils::can_user_access_theme(&user_from_token, &user_entity_type, &db_theme).await?;
|
||||||
|
state
|
||||||
|
.store
|
||||||
|
.delete_theme_by_theme_id(theme_id.clone())
|
||||||
|
.await
|
||||||
|
.to_not_found_response(UserErrors::ThemeNotFound)?;
|
||||||
|
// TODO (#6717): Delete theme folder from the theme storage.
|
||||||
|
// Currently there is no simple or easy way to delete a whole folder from S3.
|
||||||
|
// So, we are not deleting the theme folder from the theme storage.
|
||||||
|
|
||||||
|
Ok(ApplicationResponse::StatusOk)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_user_theme(
|
||||||
|
state: SessionState,
|
||||||
|
theme_id: String,
|
||||||
|
user_from_token: UserFromToken,
|
||||||
|
request: theme_api::UpdateThemeRequest,
|
||||||
|
) -> UserResponse<theme_api::GetThemeResponse> {
|
||||||
|
let db_theme = state
|
||||||
|
.store
|
||||||
|
.find_theme_by_theme_id(theme_id.clone())
|
||||||
|
.await
|
||||||
|
.to_not_found_response(UserErrors::ThemeNotFound)?;
|
||||||
|
|
||||||
|
let user_role_info = user_from_token
|
||||||
|
.get_role_info_from_db(&state)
|
||||||
|
.await
|
||||||
|
.attach_printable("Invalid role_id in JWT")?;
|
||||||
|
let user_entity_type = user_role_info.get_entity_type();
|
||||||
|
|
||||||
|
theme_utils::can_user_access_theme(&user_from_token, &user_entity_type, &db_theme).await?;
|
||||||
|
|
||||||
|
let db_theme = match request.email_config {
|
||||||
|
Some(email_config) => {
|
||||||
|
let theme_update = ThemeUpdate::EmailConfig { email_config };
|
||||||
|
state
|
||||||
|
.store
|
||||||
|
.update_theme_by_theme_id(theme_id.clone(), theme_update)
|
||||||
|
.await
|
||||||
|
.change_context(UserErrors::InternalServerError)?
|
||||||
|
}
|
||||||
|
None => db_theme,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(theme_data) = request.theme_data {
|
||||||
|
theme_utils::upload_file_to_theme_bucket(
|
||||||
|
&state,
|
||||||
|
&theme_utils::get_theme_file_key(&db_theme.theme_id),
|
||||||
|
theme_data
|
||||||
|
.encode_to_vec()
|
||||||
|
.change_context(UserErrors::InternalServerError)
|
||||||
|
.attach_printable("Failed to parse ThemeData")?,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = theme_utils::retrieve_file_from_theme_bucket(
|
||||||
|
&state,
|
||||||
|
&theme_utils::get_theme_file_key(&db_theme.theme_id),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let parsed_data = file
|
||||||
|
.to_bytes()
|
||||||
|
.parse_struct("ThemeData")
|
||||||
|
.change_context(UserErrors::InternalServerError)?;
|
||||||
|
|
||||||
|
Ok(ApplicationResponse::Json(theme_api::GetThemeResponse {
|
||||||
|
email_config: db_theme.email_config(),
|
||||||
|
theme_id: db_theme.theme_id,
|
||||||
|
entity_type: db_theme.entity_type,
|
||||||
|
tenant_id: db_theme.tenant_id,
|
||||||
|
org_id: db_theme.org_id,
|
||||||
|
merchant_id: db_theme.merchant_id,
|
||||||
|
profile_id: db_theme.profile_id,
|
||||||
|
theme_name: db_theme.theme_name,
|
||||||
|
theme_data: parsed_data,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn upload_file_to_user_theme_storage(
|
||||||
|
state: SessionState,
|
||||||
|
theme_id: String,
|
||||||
|
user_from_token: UserFromToken,
|
||||||
|
request: theme_api::UploadFileRequest,
|
||||||
|
) -> UserResponse<()> {
|
||||||
|
let db_theme = state
|
||||||
|
.store
|
||||||
|
.find_theme_by_theme_id(theme_id)
|
||||||
|
.await
|
||||||
|
.to_not_found_response(UserErrors::ThemeNotFound)?;
|
||||||
|
|
||||||
|
let user_role_info = user_from_token
|
||||||
|
.get_role_info_from_db(&state)
|
||||||
|
.await
|
||||||
|
.attach_printable("Invalid role_id in JWT")?;
|
||||||
|
let user_entity_type = user_role_info.get_entity_type();
|
||||||
|
|
||||||
|
theme_utils::can_user_access_theme(&user_from_token, &user_entity_type, &db_theme).await?;
|
||||||
|
theme_utils::upload_file_to_theme_bucket(
|
||||||
|
&state,
|
||||||
|
&theme_utils::get_specific_file_key(&db_theme.theme_id, &request.asset_name),
|
||||||
|
request.asset_data.expose(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(ApplicationResponse::StatusOk)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_all_themes_in_lineage(
|
||||||
|
state: SessionState,
|
||||||
|
user: UserFromToken,
|
||||||
|
entity_type: EntityType,
|
||||||
|
) -> UserResponse<Vec<theme_api::GetThemeResponse>> {
|
||||||
|
let lineage =
|
||||||
|
theme_utils::get_theme_lineage_from_user_token(&user, &state, &entity_type).await?;
|
||||||
|
|
||||||
|
let db_themes = state
|
||||||
|
.store
|
||||||
|
.list_themes_at_and_under_lineage(lineage)
|
||||||
|
.await
|
||||||
|
.change_context(UserErrors::InternalServerError)?;
|
||||||
|
|
||||||
|
let mut themes = Vec::new();
|
||||||
|
for theme in db_themes {
|
||||||
|
match theme_utils::retrieve_file_from_theme_bucket(
|
||||||
|
&state,
|
||||||
|
&theme_utils::get_theme_file_key(&theme.theme_id),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(file) => {
|
||||||
|
match file
|
||||||
|
.to_bytes()
|
||||||
|
.parse_struct("ThemeData")
|
||||||
|
.change_context(UserErrors::InternalServerError)
|
||||||
|
{
|
||||||
|
Ok(parsed_data) => {
|
||||||
|
themes.push(theme_api::GetThemeResponse {
|
||||||
|
email_config: theme.email_config(),
|
||||||
|
theme_id: theme.theme_id,
|
||||||
|
theme_name: theme.theme_name,
|
||||||
|
entity_type: theme.entity_type,
|
||||||
|
tenant_id: theme.tenant_id,
|
||||||
|
org_id: theme.org_id,
|
||||||
|
merchant_id: theme.merchant_id,
|
||||||
|
profile_id: theme.profile_id,
|
||||||
|
theme_data: parsed_data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
return Err(UserErrors::ErrorRetrievingFile.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
return Err(UserErrors::ErrorRetrievingFile.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ApplicationResponse::Json(themes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_user_theme_using_theme_id(
|
||||||
|
state: SessionState,
|
||||||
|
user_from_token: UserFromToken,
|
||||||
|
theme_id: String,
|
||||||
|
) -> UserResponse<theme_api::GetThemeResponse> {
|
||||||
|
let db_theme = state
|
||||||
|
.store
|
||||||
|
.find_theme_by_theme_id(theme_id.clone())
|
||||||
|
.await
|
||||||
|
.to_not_found_response(UserErrors::ThemeNotFound)?;
|
||||||
|
|
||||||
|
let user_role_info = user_from_token
|
||||||
|
.get_role_info_from_db(&state)
|
||||||
|
.await
|
||||||
|
.attach_printable("Invalid role_id in JWT")?;
|
||||||
|
|
||||||
|
let user_role_entity = user_role_info.get_entity_type();
|
||||||
|
|
||||||
|
theme_utils::can_user_access_theme(&user_from_token, &user_role_entity, &db_theme).await?;
|
||||||
|
let file = theme_utils::retrieve_file_from_theme_bucket(
|
||||||
|
&state,
|
||||||
|
&theme_utils::get_theme_file_key(&theme_id),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let parsed_data = file
|
||||||
|
.to_bytes()
|
||||||
|
.parse_struct("ThemeData")
|
||||||
|
.change_context(UserErrors::InternalServerError)?;
|
||||||
|
|
||||||
|
Ok(ApplicationResponse::Json(theme_api::GetThemeResponse {
|
||||||
|
email_config: db_theme.email_config(),
|
||||||
|
theme_id: db_theme.theme_id,
|
||||||
|
theme_name: db_theme.theme_name,
|
||||||
|
entity_type: db_theme.entity_type,
|
||||||
|
tenant_id: db_theme.tenant_id,
|
||||||
|
org_id: db_theme.org_id,
|
||||||
|
merchant_id: db_theme.merchant_id,
|
||||||
|
profile_id: db_theme.profile_id,
|
||||||
|
theme_data: parsed_data,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_user_theme_using_lineage(
|
||||||
|
state: SessionState,
|
||||||
|
user_from_token: UserFromToken,
|
||||||
|
entity_type: EntityType,
|
||||||
|
) -> UserResponse<theme_api::GetThemeResponse> {
|
||||||
|
let lineage =
|
||||||
|
theme_utils::get_theme_lineage_from_user_token(&user_from_token, &state, &entity_type)
|
||||||
|
.await?;
|
||||||
|
let theme = state
|
||||||
|
.store
|
||||||
|
.find_theme_by_lineage(lineage)
|
||||||
|
.await
|
||||||
|
.to_not_found_response(UserErrors::ThemeNotFound)?;
|
||||||
|
|
||||||
|
let file = theme_utils::retrieve_file_from_theme_bucket(
|
||||||
|
&state,
|
||||||
|
&theme_utils::get_theme_file_key(&theme.theme_id),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let parsed_data = file
|
||||||
|
.to_bytes()
|
||||||
|
.parse_struct("ThemeData")
|
||||||
|
.change_context(UserErrors::InternalServerError)?;
|
||||||
|
|
||||||
|
Ok(ApplicationResponse::Json(theme_api::GetThemeResponse {
|
||||||
|
email_config: theme.email_config(),
|
||||||
|
theme_id: theme.theme_id,
|
||||||
|
theme_name: theme.theme_name,
|
||||||
|
entity_type: theme.entity_type,
|
||||||
|
tenant_id: theme.tenant_id,
|
||||||
|
org_id: theme.org_id,
|
||||||
|
merchant_id: theme.merchant_id,
|
||||||
|
profile_id: theme.profile_id,
|
||||||
|
theme_data: parsed_data,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|||||||
@ -4152,13 +4152,19 @@ impl ThemeInterface for KafkaStore {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_theme_by_lineage_and_theme_id(
|
async fn delete_theme_by_theme_id(
|
||||||
&self,
|
&self,
|
||||||
theme_id: String,
|
theme_id: String,
|
||||||
lineage: ThemeLineage,
|
|
||||||
) -> CustomResult<storage::theme::Theme, errors::StorageError> {
|
) -> CustomResult<storage::theme::Theme, errors::StorageError> {
|
||||||
|
self.diesel_store.delete_theme_by_theme_id(theme_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_themes_at_and_under_lineage(
|
||||||
|
&self,
|
||||||
|
lineage: ThemeLineage,
|
||||||
|
) -> CustomResult<Vec<storage::theme::Theme>, errors::StorageError> {
|
||||||
self.diesel_store
|
self.diesel_store
|
||||||
.delete_theme_by_lineage_and_theme_id(theme_id, lineage)
|
.list_themes_at_and_under_lineage(lineage)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,11 +37,15 @@ pub trait ThemeInterface {
|
|||||||
theme_update: ThemeUpdate,
|
theme_update: ThemeUpdate,
|
||||||
) -> CustomResult<storage::Theme, errors::StorageError>;
|
) -> CustomResult<storage::Theme, errors::StorageError>;
|
||||||
|
|
||||||
async fn delete_theme_by_lineage_and_theme_id(
|
async fn delete_theme_by_theme_id(
|
||||||
&self,
|
&self,
|
||||||
theme_id: String,
|
theme_id: String,
|
||||||
lineage: ThemeLineage,
|
|
||||||
) -> CustomResult<storage::Theme, errors::StorageError>;
|
) -> CustomResult<storage::Theme, errors::StorageError>;
|
||||||
|
|
||||||
|
async fn list_themes_at_and_under_lineage(
|
||||||
|
&self,
|
||||||
|
lineage: ThemeLineage,
|
||||||
|
) -> CustomResult<Vec<storage::Theme>, errors::StorageError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
@ -98,13 +102,21 @@ impl ThemeInterface for Store {
|
|||||||
.map_err(|error| report!(errors::StorageError::from(error)))
|
.map_err(|error| report!(errors::StorageError::from(error)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_theme_by_lineage_and_theme_id(
|
async fn delete_theme_by_theme_id(
|
||||||
&self,
|
&self,
|
||||||
theme_id: String,
|
theme_id: String,
|
||||||
lineage: ThemeLineage,
|
|
||||||
) -> CustomResult<storage::Theme, errors::StorageError> {
|
) -> CustomResult<storage::Theme, errors::StorageError> {
|
||||||
let conn = connection::pg_connection_write(self).await?;
|
let conn = connection::pg_connection_write(self).await?;
|
||||||
storage::Theme::delete_by_theme_id_and_lineage(&conn, theme_id, lineage)
|
storage::Theme::delete_by_theme_id(&conn, theme_id)
|
||||||
|
.await
|
||||||
|
.map_err(|error| report!(errors::StorageError::from(error)))
|
||||||
|
}
|
||||||
|
async fn list_themes_at_and_under_lineage(
|
||||||
|
&self,
|
||||||
|
lineage: ThemeLineage,
|
||||||
|
) -> CustomResult<Vec<storage::Theme>, errors::StorageError> {
|
||||||
|
let conn = connection::pg_connection_read(self).await?;
|
||||||
|
storage::Theme::find_all_by_lineage_hierarchy(&conn, lineage)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| report!(errors::StorageError::from(error)))
|
.map_err(|error| report!(errors::StorageError::from(error)))
|
||||||
}
|
}
|
||||||
@ -166,6 +178,57 @@ fn check_theme_with_lineage(theme: &storage::Theme, lineage: &ThemeLineage) -> b
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn check_theme_belongs_to_lineage_hierarchy(
|
||||||
|
theme: &storage::Theme,
|
||||||
|
lineage: &ThemeLineage,
|
||||||
|
) -> bool {
|
||||||
|
match lineage {
|
||||||
|
ThemeLineage::Tenant { tenant_id } => &theme.tenant_id == tenant_id,
|
||||||
|
ThemeLineage::Organization { tenant_id, org_id } => {
|
||||||
|
&theme.tenant_id == tenant_id
|
||||||
|
&& theme
|
||||||
|
.org_id
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|org_id_inner| org_id_inner == org_id)
|
||||||
|
}
|
||||||
|
ThemeLineage::Merchant {
|
||||||
|
tenant_id,
|
||||||
|
org_id,
|
||||||
|
merchant_id,
|
||||||
|
} => {
|
||||||
|
&theme.tenant_id == tenant_id
|
||||||
|
&& theme
|
||||||
|
.org_id
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|org_id_inner| org_id_inner == org_id)
|
||||||
|
&& theme
|
||||||
|
.merchant_id
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|merchant_id_inner| merchant_id_inner == merchant_id)
|
||||||
|
}
|
||||||
|
ThemeLineage::Profile {
|
||||||
|
tenant_id,
|
||||||
|
org_id,
|
||||||
|
merchant_id,
|
||||||
|
profile_id,
|
||||||
|
} => {
|
||||||
|
&theme.tenant_id == tenant_id
|
||||||
|
&& theme
|
||||||
|
.org_id
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|org_id_inner| org_id_inner == org_id)
|
||||||
|
&& theme
|
||||||
|
.merchant_id
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|merchant_id_inner| merchant_id_inner == merchant_id)
|
||||||
|
&& theme
|
||||||
|
.profile_id
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|profile_id_inner| profile_id_inner == profile_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl ThemeInterface for MockDb {
|
impl ThemeInterface for MockDb {
|
||||||
async fn insert_theme(
|
async fn insert_theme(
|
||||||
@ -297,23 +360,32 @@ impl ThemeInterface for MockDb {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_theme_by_lineage_and_theme_id(
|
async fn delete_theme_by_theme_id(
|
||||||
&self,
|
&self,
|
||||||
theme_id: String,
|
theme_id: String,
|
||||||
lineage: ThemeLineage,
|
|
||||||
) -> CustomResult<storage::Theme, errors::StorageError> {
|
) -> CustomResult<storage::Theme, errors::StorageError> {
|
||||||
let mut themes = self.themes.lock().await;
|
let mut themes = self.themes.lock().await;
|
||||||
let index = themes
|
let index = themes
|
||||||
.iter()
|
.iter()
|
||||||
.position(|theme| {
|
.position(|theme| theme.theme_id == theme_id)
|
||||||
theme.theme_id == theme_id && check_theme_with_lineage(theme, &lineage)
|
|
||||||
})
|
|
||||||
.ok_or(errors::StorageError::ValueNotFound(format!(
|
.ok_or(errors::StorageError::ValueNotFound(format!(
|
||||||
"Theme with id {theme_id} and lineage {lineage:?} not found",
|
"Theme with id {theme_id} not found"
|
||||||
)))?;
|
)))?;
|
||||||
|
|
||||||
let theme = themes.remove(index);
|
let theme = themes.remove(index);
|
||||||
|
|
||||||
Ok(theme)
|
Ok(theme)
|
||||||
}
|
}
|
||||||
|
async fn list_themes_at_and_under_lineage(
|
||||||
|
&self,
|
||||||
|
lineage: ThemeLineage,
|
||||||
|
) -> CustomResult<Vec<storage::Theme>, errors::StorageError> {
|
||||||
|
let themes = self.themes.lock().await;
|
||||||
|
let matching_themes: Vec<storage::Theme> = themes
|
||||||
|
.iter()
|
||||||
|
.filter(|theme| check_theme_belongs_to_lineage_hierarchy(theme, &lineage))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(matching_themes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2663,9 +2663,10 @@ impl User {
|
|||||||
.route(web::delete().to(user::delete_sample_data)),
|
.route(web::delete().to(user::delete_sample_data)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// Admin Theme
|
||||||
|
// TODO: To be deprecated
|
||||||
route = route.service(
|
route = route.service(
|
||||||
web::scope("/theme")
|
web::scope("/admin/theme")
|
||||||
.service(
|
.service(
|
||||||
web::resource("")
|
web::resource("")
|
||||||
.route(web::get().to(user::theme::get_theme_using_lineage))
|
.route(web::get().to(user::theme::get_theme_using_lineage))
|
||||||
@ -2679,7 +2680,26 @@ impl User {
|
|||||||
.route(web::delete().to(user::theme::delete_theme)),
|
.route(web::delete().to(user::theme::delete_theme)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
// User Theme
|
||||||
|
route = route.service(
|
||||||
|
web::scope("/theme")
|
||||||
|
.service(
|
||||||
|
web::resource("")
|
||||||
|
.route(web::post().to(user::theme::create_user_theme))
|
||||||
|
.route(web::get().to(user::theme::get_user_theme_using_lineage)),
|
||||||
|
)
|
||||||
|
.service(
|
||||||
|
web::resource("/list")
|
||||||
|
.route(web::get().to(user::theme::list_all_themes_in_lineage)),
|
||||||
|
)
|
||||||
|
.service(
|
||||||
|
web::resource("/{theme_id}")
|
||||||
|
.route(web::get().to(user::theme::get_user_theme_using_theme_id))
|
||||||
|
.route(web::put().to(user::theme::update_user_theme))
|
||||||
|
.route(web::post().to(user::theme::upload_file_to_user_theme_storage))
|
||||||
|
.route(web::delete().to(user::theme::delete_user_theme)),
|
||||||
|
),
|
||||||
|
);
|
||||||
route
|
route
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -302,6 +302,13 @@ impl From<Flow> for ApiIdentifier {
|
|||||||
| Flow::CreateTheme
|
| Flow::CreateTheme
|
||||||
| Flow::UpdateTheme
|
| Flow::UpdateTheme
|
||||||
| Flow::DeleteTheme
|
| Flow::DeleteTheme
|
||||||
|
| Flow::CreateUserTheme
|
||||||
|
| Flow::UpdateUserTheme
|
||||||
|
| Flow::DeleteUserTheme
|
||||||
|
| Flow::GetUserThemeUsingThemeId
|
||||||
|
| Flow::UploadFileToUserThemeStorage
|
||||||
|
| Flow::GetUserThemeUsingLineage
|
||||||
|
| Flow::ListAllThemesInLineage
|
||||||
| Flow::CloneConnector => Self::User,
|
| Flow::CloneConnector => Self::User,
|
||||||
|
|
||||||
Flow::GetDataFromHyperswitchAiFlow => Self::AiWorkflow,
|
Flow::GetDataFromHyperswitchAiFlow => Self::AiWorkflow,
|
||||||
|
|||||||
@ -8,7 +8,7 @@ use router_env::Flow;
|
|||||||
use crate::{
|
use crate::{
|
||||||
core::{api_locking, user::theme as theme_core},
|
core::{api_locking, user::theme as theme_core},
|
||||||
routes::AppState,
|
routes::AppState,
|
||||||
services::{api, authentication as auth},
|
services::{api, authentication as auth, authorization::permissions::Permission},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn get_theme_using_lineage(
|
pub async fn get_theme_using_lineage(
|
||||||
@ -124,20 +124,190 @@ pub async fn delete_theme(
|
|||||||
state: web::Data<AppState>,
|
state: web::Data<AppState>,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
path: web::Path<String>,
|
path: web::Path<String>,
|
||||||
query: web::Query<ThemeLineage>,
|
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
let flow = Flow::DeleteTheme;
|
let flow = Flow::DeleteTheme;
|
||||||
let theme_id = path.into_inner();
|
let theme_id = path.into_inner();
|
||||||
let lineage = query.into_inner();
|
|
||||||
|
|
||||||
Box::pin(api::server_wrap(
|
Box::pin(api::server_wrap(
|
||||||
flow,
|
flow,
|
||||||
state,
|
state,
|
||||||
&req,
|
&req,
|
||||||
lineage,
|
theme_id,
|
||||||
|state, _, lineage, _| theme_core::delete_theme(state, theme_id.clone(), lineage),
|
|state, _, theme_id, _| theme_core::delete_theme(state, theme_id),
|
||||||
&auth::AdminApiAuth,
|
&auth::AdminApiAuth,
|
||||||
api_locking::LockAction::NotApplicable,
|
api_locking::LockAction::NotApplicable,
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
pub async fn create_user_theme(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
req: HttpRequest,
|
||||||
|
payload: web::Json<theme_api::CreateUserThemeRequest>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let flow = Flow::CreateUserTheme;
|
||||||
|
let payload = payload.into_inner();
|
||||||
|
Box::pin(api::server_wrap(
|
||||||
|
flow,
|
||||||
|
state,
|
||||||
|
&req,
|
||||||
|
payload,
|
||||||
|
|state, user: auth::UserFromToken, payload, _| {
|
||||||
|
theme_core::create_user_theme(state, user, payload)
|
||||||
|
},
|
||||||
|
&auth::JWTAuth {
|
||||||
|
permission: Permission::OrganizationThemeWrite,
|
||||||
|
},
|
||||||
|
api_locking::LockAction::NotApplicable,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
pub async fn get_user_theme_using_theme_id(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
req: HttpRequest,
|
||||||
|
path: web::Path<String>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let flow = Flow::GetUserThemeUsingThemeId;
|
||||||
|
let payload = path.into_inner();
|
||||||
|
|
||||||
|
Box::pin(api::server_wrap(
|
||||||
|
flow,
|
||||||
|
state,
|
||||||
|
&req,
|
||||||
|
payload,
|
||||||
|
|state, user: auth::UserFromToken, payload, _| {
|
||||||
|
theme_core::get_user_theme_using_theme_id(state, user, payload)
|
||||||
|
},
|
||||||
|
&auth::JWTAuth {
|
||||||
|
permission: Permission::OrganizationThemeRead,
|
||||||
|
},
|
||||||
|
api_locking::LockAction::NotApplicable,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_user_theme(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
req: HttpRequest,
|
||||||
|
path: web::Path<String>,
|
||||||
|
payload: web::Json<theme_api::UpdateThemeRequest>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let flow = Flow::UpdateUserTheme;
|
||||||
|
let theme_id = path.into_inner();
|
||||||
|
let payload = payload.into_inner();
|
||||||
|
|
||||||
|
Box::pin(api::server_wrap(
|
||||||
|
flow,
|
||||||
|
state,
|
||||||
|
&req,
|
||||||
|
payload,
|
||||||
|
|state, user: auth::UserFromToken, payload, _| {
|
||||||
|
theme_core::update_user_theme(state, theme_id.clone(), user, payload)
|
||||||
|
},
|
||||||
|
&auth::JWTAuth {
|
||||||
|
permission: Permission::OrganizationThemeWrite,
|
||||||
|
},
|
||||||
|
api_locking::LockAction::NotApplicable,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_user_theme(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
req: HttpRequest,
|
||||||
|
path: web::Path<String>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let flow = Flow::DeleteUserTheme;
|
||||||
|
let theme_id = path.into_inner();
|
||||||
|
|
||||||
|
Box::pin(api::server_wrap(
|
||||||
|
flow,
|
||||||
|
state,
|
||||||
|
&req,
|
||||||
|
theme_id,
|
||||||
|
|state, user: auth::UserFromToken, theme_id, _| {
|
||||||
|
theme_core::delete_user_theme(state, user, theme_id)
|
||||||
|
},
|
||||||
|
&auth::JWTAuth {
|
||||||
|
permission: Permission::OrganizationThemeWrite,
|
||||||
|
},
|
||||||
|
api_locking::LockAction::NotApplicable,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn upload_file_to_user_theme_storage(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
req: HttpRequest,
|
||||||
|
path: web::Path<String>,
|
||||||
|
MultipartForm(payload): MultipartForm<theme_api::UploadFileAssetData>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let flow = Flow::UploadFileToUserThemeStorage;
|
||||||
|
let theme_id = path.into_inner();
|
||||||
|
let payload = theme_api::UploadFileRequest {
|
||||||
|
asset_name: payload.asset_name.into_inner(),
|
||||||
|
asset_data: Secret::new(payload.asset_data.data.to_vec()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Box::pin(api::server_wrap(
|
||||||
|
flow,
|
||||||
|
state,
|
||||||
|
&req,
|
||||||
|
payload,
|
||||||
|
|state, user: auth::UserFromToken, payload, _| {
|
||||||
|
theme_core::upload_file_to_user_theme_storage(state, theme_id.clone(), user, payload)
|
||||||
|
},
|
||||||
|
&auth::JWTAuth {
|
||||||
|
permission: Permission::OrganizationThemeWrite,
|
||||||
|
},
|
||||||
|
api_locking::LockAction::NotApplicable,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_all_themes_in_lineage(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
req: HttpRequest,
|
||||||
|
query: web::Query<theme_api::EntityTypeQueryParam>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let flow = Flow::ListAllThemesInLineage;
|
||||||
|
let entity_type = query.into_inner().entity_type;
|
||||||
|
|
||||||
|
Box::pin(api::server_wrap(
|
||||||
|
flow,
|
||||||
|
state,
|
||||||
|
&req,
|
||||||
|
(),
|
||||||
|
|state, user: auth::UserFromToken, _payload, _| {
|
||||||
|
theme_core::list_all_themes_in_lineage(state, user, entity_type)
|
||||||
|
},
|
||||||
|
&auth::JWTAuth {
|
||||||
|
permission: Permission::OrganizationThemeRead,
|
||||||
|
},
|
||||||
|
api_locking::LockAction::NotApplicable,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_user_theme_using_lineage(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
req: HttpRequest,
|
||||||
|
query: web::Query<theme_api::EntityTypeQueryParam>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let flow = Flow::GetUserThemeUsingLineage;
|
||||||
|
let entity_type = query.into_inner().entity_type;
|
||||||
|
|
||||||
|
Box::pin(api::server_wrap(
|
||||||
|
flow,
|
||||||
|
state,
|
||||||
|
&req,
|
||||||
|
(),
|
||||||
|
|state, user: auth::UserFromToken, _payload, _| {
|
||||||
|
theme_core::get_user_theme_using_lineage(state, user, entity_type)
|
||||||
|
},
|
||||||
|
&auth::JWTAuth {
|
||||||
|
permission: Permission::OrganizationThemeRead,
|
||||||
|
},
|
||||||
|
api_locking::LockAction::NotApplicable,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|||||||
@ -48,6 +48,8 @@ fn get_group_description(group: PermissionGroup) -> Option<&'static str> {
|
|||||||
PermissionGroup::ReconReportsManage => Some("Manage reconciliation reports"),
|
PermissionGroup::ReconReportsManage => Some("Manage reconciliation reports"),
|
||||||
PermissionGroup::ReconOpsView => Some("View and access all reconciliation operations including reports and analytics"),
|
PermissionGroup::ReconOpsView => Some("View and access all reconciliation operations including reports and analytics"),
|
||||||
PermissionGroup::ReconOpsManage => Some("Manage all reconciliation operations including reports and analytics"),
|
PermissionGroup::ReconOpsManage => Some("Manage all reconciliation operations including reports and analytics"),
|
||||||
|
PermissionGroup::ThemeView => Some("View Themes"),
|
||||||
|
PermissionGroup::ThemeManage => Some("Manage Themes"),
|
||||||
PermissionGroup::InternalManage => None, // Internal group, no user-facing description
|
PermissionGroup::InternalManage => None, // Internal group, no user-facing description
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,6 +64,7 @@ pub fn get_parent_group_description(group: ParentGroup) -> Option<&'static str>
|
|||||||
ParentGroup::Account => Some("Create, modify and delete Merchant Details like api keys, webhooks, etc"),
|
ParentGroup::Account => Some("Create, modify and delete Merchant Details like api keys, webhooks, etc"),
|
||||||
ParentGroup::ReconOps => Some("View, manage reconciliation operations like upload and process files, run reconciliation etc"),
|
ParentGroup::ReconOps => Some("View, manage reconciliation operations like upload and process files, run reconciliation etc"),
|
||||||
ParentGroup::ReconReports => Some("View, manage reconciliation reports and analytics"),
|
ParentGroup::ReconReports => Some("View, manage reconciliation reports and analytics"),
|
||||||
|
ParentGroup::Theme => Some("Manage and view themes for the organization"),
|
||||||
ParentGroup::Internal => None, // Internal group, no user-facing description
|
ParentGroup::Internal => None, // Internal group, no user-facing description
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,8 @@ impl PermissionGroupExt for PermissionGroup {
|
|||||||
| Self::MerchantDetailsView
|
| Self::MerchantDetailsView
|
||||||
| Self::AccountView
|
| Self::AccountView
|
||||||
| Self::ReconOpsView
|
| Self::ReconOpsView
|
||||||
| Self::ReconReportsView => PermissionScope::Read,
|
| Self::ReconReportsView
|
||||||
|
| Self::ThemeView => PermissionScope::Read,
|
||||||
|
|
||||||
Self::OperationsManage
|
Self::OperationsManage
|
||||||
| Self::ConnectorsManage
|
| Self::ConnectorsManage
|
||||||
@ -34,7 +35,8 @@ impl PermissionGroupExt for PermissionGroup {
|
|||||||
| Self::AccountManage
|
| Self::AccountManage
|
||||||
| Self::ReconOpsManage
|
| Self::ReconOpsManage
|
||||||
| Self::ReconReportsManage
|
| Self::ReconReportsManage
|
||||||
| Self::InternalManage => PermissionScope::Write,
|
| Self::InternalManage
|
||||||
|
| Self::ThemeManage => PermissionScope::Write,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,6 +52,8 @@ impl PermissionGroupExt for PermissionGroup {
|
|||||||
| Self::MerchantDetailsManage
|
| Self::MerchantDetailsManage
|
||||||
| Self::AccountView
|
| Self::AccountView
|
||||||
| Self::AccountManage => ParentGroup::Account,
|
| Self::AccountManage => ParentGroup::Account,
|
||||||
|
|
||||||
|
Self::ThemeView | Self::ThemeManage => ParentGroup::Theme,
|
||||||
Self::ReconOpsView | Self::ReconOpsManage => ParentGroup::ReconOps,
|
Self::ReconOpsView | Self::ReconOpsManage => ParentGroup::ReconOps,
|
||||||
Self::ReconReportsView | Self::ReconReportsManage => ParentGroup::ReconReports,
|
Self::ReconReportsView | Self::ReconReportsManage => ParentGroup::ReconReports,
|
||||||
Self::InternalManage => ParentGroup::Internal,
|
Self::InternalManage => ParentGroup::Internal,
|
||||||
@ -103,6 +107,8 @@ impl PermissionGroupExt for PermissionGroup {
|
|||||||
Self::AccountManage => vec![Self::AccountView, Self::AccountManage],
|
Self::AccountManage => vec![Self::AccountView, Self::AccountManage],
|
||||||
|
|
||||||
Self::InternalManage => vec![Self::InternalManage],
|
Self::InternalManage => vec![Self::InternalManage],
|
||||||
|
Self::ThemeView => vec![Self::ThemeView, Self::AccountView],
|
||||||
|
Self::ThemeManage => vec![Self::ThemeManage, Self::AccountView],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -127,6 +133,7 @@ impl ParentGroupExt for ParentGroup {
|
|||||||
Self::ReconOps => RECON_OPS.to_vec(),
|
Self::ReconOps => RECON_OPS.to_vec(),
|
||||||
Self::ReconReports => RECON_REPORTS.to_vec(),
|
Self::ReconReports => RECON_REPORTS.to_vec(),
|
||||||
Self::Internal => INTERNAL.to_vec(),
|
Self::Internal => INTERNAL.to_vec(),
|
||||||
|
Self::Theme => THEME.to_vec(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,3 +217,5 @@ pub static RECON_REPORTS: [Resource; 4] = [
|
|||||||
Resource::ReconReports,
|
Resource::ReconReports,
|
||||||
Resource::Account,
|
Resource::Account,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
pub static THEME: [Resource; 1] = [Resource::Theme];
|
||||||
|
|||||||
@ -102,6 +102,10 @@ generate_permissions! {
|
|||||||
InternalConnector: {
|
InternalConnector: {
|
||||||
scopes: [Write],
|
scopes: [Write],
|
||||||
entities: [Merchant]
|
entities: [Merchant]
|
||||||
|
},
|
||||||
|
Theme: {
|
||||||
|
scopes: [Read,Write],
|
||||||
|
entities: [Organization]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -137,6 +141,7 @@ pub fn get_resource_name(resource: Resource, entity_type: EntityType) -> Option<
|
|||||||
(Resource::Account, EntityType::Merchant) => Some("Merchant Account"),
|
(Resource::Account, EntityType::Merchant) => Some("Merchant Account"),
|
||||||
(Resource::Account, EntityType::Organization) => Some("Organization Account"),
|
(Resource::Account, EntityType::Organization) => Some("Organization Account"),
|
||||||
(Resource::Account, EntityType::Tenant) => Some("Tenant Account"),
|
(Resource::Account, EntityType::Tenant) => Some("Tenant Account"),
|
||||||
|
(Resource::Theme, _) => Some("Themes"),
|
||||||
(Resource::InternalConnector, _) => None,
|
(Resource::InternalConnector, _) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -150,6 +150,8 @@ pub static PREDEFINED_ROLES: LazyLock<HashMap<&'static str, RoleInfo>> = LazyLoc
|
|||||||
PermissionGroup::ReconOpsManage,
|
PermissionGroup::ReconOpsManage,
|
||||||
PermissionGroup::ReconReportsView,
|
PermissionGroup::ReconReportsView,
|
||||||
PermissionGroup::ReconReportsManage,
|
PermissionGroup::ReconReportsManage,
|
||||||
|
PermissionGroup::ThemeView,
|
||||||
|
PermissionGroup::ThemeManage,
|
||||||
],
|
],
|
||||||
role_id: common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
|
role_id: common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
|
||||||
role_name: "organization_admin".to_string(),
|
role_name: "organization_admin".to_string(),
|
||||||
|
|||||||
@ -224,3 +224,81 @@ pub async fn get_theme_using_optional_theme_id(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub async fn get_theme_lineage_from_user_token(
|
||||||
|
user_from_token: &UserFromToken,
|
||||||
|
state: &SessionState,
|
||||||
|
request_entity_type: &EntityType,
|
||||||
|
) -> UserResult<ThemeLineage> {
|
||||||
|
let tenant_id = user_from_token
|
||||||
|
.tenant_id
|
||||||
|
.clone()
|
||||||
|
.unwrap_or(state.tenant.tenant_id.clone());
|
||||||
|
let org_id = user_from_token.org_id.clone();
|
||||||
|
let merchant_id = user_from_token.merchant_id.clone();
|
||||||
|
let profile_id = user_from_token.profile_id.clone();
|
||||||
|
|
||||||
|
Ok(ThemeLineage::new(
|
||||||
|
*request_entity_type,
|
||||||
|
tenant_id,
|
||||||
|
org_id,
|
||||||
|
merchant_id,
|
||||||
|
profile_id,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn can_user_access_theme(
|
||||||
|
user: &UserFromToken,
|
||||||
|
user_entity_type: &EntityType,
|
||||||
|
theme: &Theme,
|
||||||
|
) -> UserResult<()> {
|
||||||
|
if user_entity_type < &theme.entity_type {
|
||||||
|
return Err(UserErrors::ThemeNotFound.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
match theme.entity_type {
|
||||||
|
EntityType::Tenant => {
|
||||||
|
if user.tenant_id.as_ref() == Some(&theme.tenant_id)
|
||||||
|
&& theme.org_id.is_none()
|
||||||
|
&& theme.merchant_id.is_none()
|
||||||
|
&& theme.profile_id.is_none()
|
||||||
|
{
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(UserErrors::ThemeNotFound.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EntityType::Organization => {
|
||||||
|
if user.tenant_id.as_ref() == Some(&theme.tenant_id)
|
||||||
|
&& theme.org_id.as_ref() == Some(&user.org_id)
|
||||||
|
&& theme.merchant_id.is_none()
|
||||||
|
&& theme.profile_id.is_none()
|
||||||
|
{
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(UserErrors::ThemeNotFound.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EntityType::Merchant => {
|
||||||
|
if user.tenant_id.as_ref() == Some(&theme.tenant_id)
|
||||||
|
&& theme.org_id.as_ref() == Some(&user.org_id)
|
||||||
|
&& theme.merchant_id.as_ref() == Some(&user.merchant_id)
|
||||||
|
&& theme.profile_id.is_none()
|
||||||
|
{
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(UserErrors::ThemeNotFound.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EntityType::Profile => {
|
||||||
|
if user.tenant_id.as_ref() == Some(&theme.tenant_id)
|
||||||
|
&& theme.org_id.as_ref() == Some(&user.org_id)
|
||||||
|
&& theme.merchant_id.as_ref() == Some(&user.merchant_id)
|
||||||
|
&& theme.profile_id.as_ref() == Some(&user.profile_id)
|
||||||
|
{
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(UserErrors::ThemeNotFound.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -523,6 +523,20 @@ pub enum Flow {
|
|||||||
UpdateTheme,
|
UpdateTheme,
|
||||||
/// Delete theme
|
/// Delete theme
|
||||||
DeleteTheme,
|
DeleteTheme,
|
||||||
|
/// Create user theme
|
||||||
|
CreateUserTheme,
|
||||||
|
/// Update user theme
|
||||||
|
UpdateUserTheme,
|
||||||
|
/// Delete user theme
|
||||||
|
DeleteUserTheme,
|
||||||
|
/// Upload file to user theme storage
|
||||||
|
UploadFileToUserThemeStorage,
|
||||||
|
/// Get user theme using theme id
|
||||||
|
GetUserThemeUsingThemeId,
|
||||||
|
///List All Themes In Lineage
|
||||||
|
ListAllThemesInLineage,
|
||||||
|
/// Get user theme using lineage
|
||||||
|
GetUserThemeUsingLineage,
|
||||||
/// List initial webhook delivery attempts
|
/// List initial webhook delivery attempts
|
||||||
WebhookEventInitialDeliveryAttemptList,
|
WebhookEventInitialDeliveryAttemptList,
|
||||||
/// List delivery attempts for a webhook event
|
/// List delivery attempts for a webhook event
|
||||||
|
|||||||
Reference in New Issue
Block a user