mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +08:00
feat(themes): Create APIs for managing themes (#6658)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -545,5 +545,6 @@ pub(crate) async fn fetch_raw_secrets(
|
||||
.network_tokenization_supported_card_networks,
|
||||
network_tokenization_service,
|
||||
network_tokenization_supported_connectors: conf.network_tokenization_supported_connectors,
|
||||
theme_storage: conf.theme_storage,
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,6 +128,7 @@ pub struct Settings<S: SecretState> {
|
||||
pub network_tokenization_supported_card_networks: NetworkTokenizationSupportedCardNetworks,
|
||||
pub network_tokenization_service: Option<SecretStateContainer<NetworkTokenizationService, S>>,
|
||||
pub network_tokenization_supported_connectors: NetworkTokenizationSupportedConnectors,
|
||||
pub theme_storage: FileStorageConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Default)]
|
||||
@ -886,6 +887,10 @@ impl Settings<SecuredSecret> {
|
||||
.validate()
|
||||
.map_err(|err| ApplicationError::InvalidConfigurationValueError(err.into()))?;
|
||||
|
||||
self.theme_storage
|
||||
.validate()
|
||||
.map_err(|err| ApplicationError::InvalidConfigurationValueError(err.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,6 +96,16 @@ pub enum UserErrors {
|
||||
MaxRecoveryCodeAttemptsReached,
|
||||
#[error("Forbidden tenant id")]
|
||||
ForbiddenTenantId,
|
||||
#[error("Error Uploading file to Theme Storage")]
|
||||
ErrorUploadingFile,
|
||||
#[error("Error Retrieving file from Theme Storage")]
|
||||
ErrorRetrievingFile,
|
||||
#[error("Theme not found")]
|
||||
ThemeNotFound,
|
||||
#[error("Theme with lineage already exists")]
|
||||
ThemeAlreadyExists,
|
||||
#[error("Invalid field: {0} in lineage")]
|
||||
InvalidThemeLineage(String),
|
||||
}
|
||||
|
||||
impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorResponse> for UserErrors {
|
||||
@ -244,57 +254,93 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
|
||||
Self::ForbiddenTenantId => {
|
||||
AER::BadRequest(ApiError::new(sub_code, 50, self.get_error_message(), None))
|
||||
}
|
||||
Self::ErrorUploadingFile => AER::InternalServerError(ApiError::new(
|
||||
sub_code,
|
||||
51,
|
||||
self.get_error_message(),
|
||||
None,
|
||||
)),
|
||||
Self::ErrorRetrievingFile => AER::InternalServerError(ApiError::new(
|
||||
sub_code,
|
||||
52,
|
||||
self.get_error_message(),
|
||||
None,
|
||||
)),
|
||||
Self::ThemeNotFound => {
|
||||
AER::NotFound(ApiError::new(sub_code, 53, self.get_error_message(), None))
|
||||
}
|
||||
Self::ThemeAlreadyExists => {
|
||||
AER::BadRequest(ApiError::new(sub_code, 54, self.get_error_message(), None))
|
||||
}
|
||||
Self::InvalidThemeLineage(_) => {
|
||||
AER::BadRequest(ApiError::new(sub_code, 55, self.get_error_message(), None))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UserErrors {
|
||||
pub fn get_error_message(&self) -> &str {
|
||||
pub fn get_error_message(&self) -> String {
|
||||
match self {
|
||||
Self::InternalServerError => "Something went wrong",
|
||||
Self::InvalidCredentials => "Incorrect email or password",
|
||||
Self::UserNotFound => "Email doesn’t exist. Register",
|
||||
Self::UserExists => "An account already exists with this email",
|
||||
Self::LinkInvalid => "Invalid or expired link",
|
||||
Self::UnverifiedUser => "Kindly verify your account",
|
||||
Self::InvalidOldPassword => "Old password incorrect. Please enter the correct password",
|
||||
Self::EmailParsingError => "Invalid Email",
|
||||
Self::NameParsingError => "Invalid Name",
|
||||
Self::PasswordParsingError => "Invalid Password",
|
||||
Self::UserAlreadyVerified => "User already verified",
|
||||
Self::CompanyNameParsingError => "Invalid Company Name",
|
||||
Self::MerchantAccountCreationError(error_message) => error_message,
|
||||
Self::InvalidEmailError => "Invalid Email",
|
||||
Self::MerchantIdNotFound => "Invalid Merchant ID",
|
||||
Self::MetadataAlreadySet => "Metadata already set",
|
||||
Self::DuplicateOrganizationId => "An Organization with the id already exists",
|
||||
Self::InvalidRoleId => "Invalid Role ID",
|
||||
Self::InvalidRoleOperation => "User Role Operation Not Supported",
|
||||
Self::IpAddressParsingFailed => "Something went wrong",
|
||||
Self::InvalidMetadataRequest => "Invalid Metadata Request",
|
||||
Self::MerchantIdParsingError => "Invalid Merchant Id",
|
||||
Self::ChangePasswordError => "Old and new password cannot be same",
|
||||
Self::InvalidDeleteOperation => "Delete Operation Not Supported",
|
||||
Self::MaxInvitationsError => "Maximum invite count per request exceeded",
|
||||
Self::RoleNotFound => "Role Not Found",
|
||||
Self::InvalidRoleOperationWithMessage(error_message) => error_message,
|
||||
Self::RoleNameParsingError => "Invalid Role Name",
|
||||
Self::RoleNameAlreadyExists => "Role name already exists",
|
||||
Self::TotpNotSetup => "TOTP not setup",
|
||||
Self::InvalidTotp => "Invalid TOTP",
|
||||
Self::TotpRequired => "TOTP required",
|
||||
Self::InvalidRecoveryCode => "Invalid Recovery Code",
|
||||
Self::MaxTotpAttemptsReached => "Maximum attempts reached for TOTP",
|
||||
Self::MaxRecoveryCodeAttemptsReached => "Maximum attempts reached for Recovery Code",
|
||||
Self::TwoFactorAuthRequired => "Two factor auth required",
|
||||
Self::TwoFactorAuthNotSetup => "Two factor auth not setup",
|
||||
Self::TotpSecretNotFound => "TOTP secret not found",
|
||||
Self::UserAuthMethodAlreadyExists => "User auth method already exists",
|
||||
Self::InvalidUserAuthMethodOperation => "Invalid user auth method operation",
|
||||
Self::AuthConfigParsingError => "Auth config parsing error",
|
||||
Self::SSOFailed => "Invalid SSO request",
|
||||
Self::JwtProfileIdMissing => "profile_id missing in JWT",
|
||||
Self::ForbiddenTenantId => "Forbidden tenant id",
|
||||
Self::InternalServerError => "Something went wrong".to_string(),
|
||||
Self::InvalidCredentials => "Incorrect email or password".to_string(),
|
||||
Self::UserNotFound => "Email doesn’t exist. Register".to_string(),
|
||||
Self::UserExists => "An account already exists with this email".to_string(),
|
||||
Self::LinkInvalid => "Invalid or expired link".to_string(),
|
||||
Self::UnverifiedUser => "Kindly verify your account".to_string(),
|
||||
Self::InvalidOldPassword => {
|
||||
"Old password incorrect. Please enter the correct password".to_string()
|
||||
}
|
||||
Self::EmailParsingError => "Invalid Email".to_string(),
|
||||
Self::NameParsingError => "Invalid Name".to_string(),
|
||||
Self::PasswordParsingError => "Invalid Password".to_string(),
|
||||
Self::UserAlreadyVerified => "User already verified".to_string(),
|
||||
Self::CompanyNameParsingError => "Invalid Company Name".to_string(),
|
||||
Self::MerchantAccountCreationError(error_message) => error_message.to_string(),
|
||||
Self::InvalidEmailError => "Invalid Email".to_string(),
|
||||
Self::MerchantIdNotFound => "Invalid Merchant ID".to_string(),
|
||||
Self::MetadataAlreadySet => "Metadata already set".to_string(),
|
||||
Self::DuplicateOrganizationId => {
|
||||
"An Organization with the id already exists".to_string()
|
||||
}
|
||||
Self::InvalidRoleId => "Invalid Role ID".to_string(),
|
||||
Self::InvalidRoleOperation => "User Role Operation Not Supported".to_string(),
|
||||
Self::IpAddressParsingFailed => "Something went wrong".to_string(),
|
||||
Self::InvalidMetadataRequest => "Invalid Metadata Request".to_string(),
|
||||
Self::MerchantIdParsingError => "Invalid Merchant Id".to_string(),
|
||||
Self::ChangePasswordError => "Old and new password cannot be same".to_string(),
|
||||
Self::InvalidDeleteOperation => "Delete Operation Not Supported".to_string(),
|
||||
Self::MaxInvitationsError => "Maximum invite count per request exceeded".to_string(),
|
||||
Self::RoleNotFound => "Role Not Found".to_string(),
|
||||
Self::InvalidRoleOperationWithMessage(error_message) => error_message.to_string(),
|
||||
Self::RoleNameParsingError => "Invalid Role Name".to_string(),
|
||||
Self::RoleNameAlreadyExists => "Role name already exists".to_string(),
|
||||
Self::TotpNotSetup => "TOTP not setup".to_string(),
|
||||
Self::InvalidTotp => "Invalid TOTP".to_string(),
|
||||
Self::TotpRequired => "TOTP required".to_string(),
|
||||
Self::InvalidRecoveryCode => "Invalid Recovery Code".to_string(),
|
||||
Self::MaxTotpAttemptsReached => "Maximum attempts reached for TOTP".to_string(),
|
||||
Self::MaxRecoveryCodeAttemptsReached => {
|
||||
"Maximum attempts reached for Recovery Code".to_string()
|
||||
}
|
||||
Self::TwoFactorAuthRequired => "Two factor auth required".to_string(),
|
||||
Self::TwoFactorAuthNotSetup => "Two factor auth not setup".to_string(),
|
||||
Self::TotpSecretNotFound => "TOTP secret not found".to_string(),
|
||||
Self::UserAuthMethodAlreadyExists => "User auth method already exists".to_string(),
|
||||
Self::InvalidUserAuthMethodOperation => {
|
||||
"Invalid user auth method operation".to_string()
|
||||
}
|
||||
Self::AuthConfigParsingError => "Auth config parsing error".to_string(),
|
||||
Self::SSOFailed => "Invalid SSO request".to_string(),
|
||||
Self::JwtProfileIdMissing => "profile_id missing in JWT".to_string(),
|
||||
Self::ForbiddenTenantId => "Forbidden tenant id".to_string(),
|
||||
Self::ErrorUploadingFile => "Error Uploading file to Theme Storage".to_string(),
|
||||
Self::ErrorRetrievingFile => "Error Retrieving file from Theme Storage".to_string(),
|
||||
Self::ThemeNotFound => "Theme not found".to_string(),
|
||||
Self::ThemeAlreadyExists => "Theme with lineage already exists".to_string(),
|
||||
Self::InvalidThemeLineage(field_name) => {
|
||||
format!("Invalid field: {} in lineage", field_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,6 +44,7 @@ use crate::{
|
||||
pub mod dashboard_metadata;
|
||||
#[cfg(feature = "dummy_connector")]
|
||||
pub mod sample_data;
|
||||
pub mod theme;
|
||||
|
||||
#[cfg(feature = "email")]
|
||||
pub async fn signup_with_merchant_id(
|
||||
|
||||
225
crates/router/src/core/user/theme.rs
Normal file
225
crates/router/src/core/user/theme.rs
Normal file
@ -0,0 +1,225 @@
|
||||
use api_models::user::theme as theme_api;
|
||||
use common_utils::{
|
||||
ext_traits::{ByteSliceExt, Encode},
|
||||
types::theme::ThemeLineage,
|
||||
};
|
||||
use diesel_models::user::theme::ThemeNew;
|
||||
use error_stack::ResultExt;
|
||||
use hyperswitch_domain_models::api::ApplicationResponse;
|
||||
use masking::ExposeInterface;
|
||||
use rdkafka::message::ToBytes;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
core::errors::{StorageErrorExt, UserErrors, UserResponse},
|
||||
routes::SessionState,
|
||||
utils::user::theme as theme_utils,
|
||||
};
|
||||
|
||||
pub async fn get_theme_using_lineage(
|
||||
state: SessionState,
|
||||
lineage: ThemeLineage,
|
||||
) -> UserResponse<theme_api::GetThemeResponse> {
|
||||
let theme = state
|
||||
.global_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 {
|
||||
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,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn get_theme_using_theme_id(
|
||||
state: SessionState,
|
||||
theme_id: String,
|
||||
) -> UserResponse<theme_api::GetThemeResponse> {
|
||||
let theme = state
|
||||
.global_store
|
||||
.find_theme_by_theme_id(theme_id.clone())
|
||||
.await
|
||||
.to_not_found_response(UserErrors::ThemeNotFound)?;
|
||||
|
||||
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 {
|
||||
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,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn upload_file_to_theme_storage(
|
||||
state: SessionState,
|
||||
theme_id: String,
|
||||
request: theme_api::UploadFileRequest,
|
||||
) -> UserResponse<()> {
|
||||
let db_theme = state
|
||||
.global_store
|
||||
.find_theme_by_lineage(request.lineage)
|
||||
.await
|
||||
.to_not_found_response(UserErrors::ThemeNotFound)?;
|
||||
|
||||
if theme_id != db_theme.theme_id {
|
||||
return Err(UserErrors::ThemeNotFound.into());
|
||||
}
|
||||
|
||||
theme_utils::upload_file_to_theme_bucket(
|
||||
&state,
|
||||
&theme_utils::get_specific_file_key(&theme_id, &request.asset_name),
|
||||
request.asset_data.expose(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(ApplicationResponse::StatusOk)
|
||||
}
|
||||
|
||||
pub async fn create_theme(
|
||||
state: SessionState,
|
||||
request: theme_api::CreateThemeRequest,
|
||||
) -> UserResponse<theme_api::GetThemeResponse> {
|
||||
theme_utils::validate_lineage(&state, &request.lineage).await?;
|
||||
|
||||
let new_theme = ThemeNew::new(
|
||||
Uuid::new_v4().to_string(),
|
||||
request.theme_name,
|
||||
request.lineage,
|
||||
);
|
||||
|
||||
let db_theme = state
|
||||
.global_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 {
|
||||
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 update_theme(
|
||||
state: SessionState,
|
||||
theme_id: String,
|
||||
request: theme_api::UpdateThemeRequest,
|
||||
) -> UserResponse<theme_api::GetThemeResponse> {
|
||||
let db_theme = state
|
||||
.global_store
|
||||
.find_theme_by_lineage(request.lineage)
|
||||
.await
|
||||
.to_not_found_response(UserErrors::ThemeNotFound)?;
|
||||
|
||||
if theme_id != db_theme.theme_id {
|
||||
return Err(UserErrors::ThemeNotFound.into());
|
||||
}
|
||||
|
||||
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 {
|
||||
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_theme(
|
||||
state: SessionState,
|
||||
theme_id: String,
|
||||
lineage: ThemeLineage,
|
||||
) -> UserResponse<()> {
|
||||
state
|
||||
.global_store
|
||||
.delete_theme_by_lineage_and_theme_id(theme_id.clone(), lineage)
|
||||
.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)
|
||||
}
|
||||
@ -3723,6 +3723,13 @@ impl ThemeInterface for KafkaStore {
|
||||
self.diesel_store.insert_theme(theme).await
|
||||
}
|
||||
|
||||
async fn find_theme_by_theme_id(
|
||||
&self,
|
||||
theme_id: String,
|
||||
) -> CustomResult<storage::theme::Theme, errors::StorageError> {
|
||||
self.diesel_store.find_theme_by_theme_id(theme_id).await
|
||||
}
|
||||
|
||||
async fn find_theme_by_lineage(
|
||||
&self,
|
||||
lineage: ThemeLineage,
|
||||
|
||||
@ -16,6 +16,11 @@ pub trait ThemeInterface {
|
||||
theme: storage::ThemeNew,
|
||||
) -> CustomResult<storage::Theme, errors::StorageError>;
|
||||
|
||||
async fn find_theme_by_theme_id(
|
||||
&self,
|
||||
theme_id: String,
|
||||
) -> CustomResult<storage::Theme, errors::StorageError>;
|
||||
|
||||
async fn find_theme_by_lineage(
|
||||
&self,
|
||||
lineage: ThemeLineage,
|
||||
@ -41,6 +46,16 @@ impl ThemeInterface for Store {
|
||||
.map_err(|error| report!(errors::StorageError::from(error)))
|
||||
}
|
||||
|
||||
async fn find_theme_by_theme_id(
|
||||
&self,
|
||||
theme_id: String,
|
||||
) -> CustomResult<storage::Theme, errors::StorageError> {
|
||||
let conn = connection::pg_connection_read(self).await?;
|
||||
storage::Theme::find_by_theme_id(&conn, theme_id)
|
||||
.await
|
||||
.map_err(|error| report!(errors::StorageError::from(error)))
|
||||
}
|
||||
|
||||
async fn find_theme_by_lineage(
|
||||
&self,
|
||||
lineage: ThemeLineage,
|
||||
@ -165,6 +180,24 @@ impl ThemeInterface for MockDb {
|
||||
Ok(theme)
|
||||
}
|
||||
|
||||
async fn find_theme_by_theme_id(
|
||||
&self,
|
||||
theme_id: String,
|
||||
) -> CustomResult<storage::Theme, errors::StorageError> {
|
||||
let themes = self.themes.lock().await;
|
||||
themes
|
||||
.iter()
|
||||
.find(|theme| theme.theme_id == theme_id)
|
||||
.cloned()
|
||||
.ok_or(
|
||||
errors::StorageError::ValueNotFound(format!(
|
||||
"Theme with id {} not found",
|
||||
theme_id
|
||||
))
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn find_theme_by_lineage(
|
||||
&self,
|
||||
lineage: ThemeLineage,
|
||||
|
||||
@ -110,6 +110,7 @@ pub struct SessionState {
|
||||
#[cfg(feature = "olap")]
|
||||
pub opensearch_client: Arc<OpenSearchClient>,
|
||||
pub grpc_client: Arc<GrpcClients>,
|
||||
pub theme_storage_client: Arc<dyn FileStorageInterface>,
|
||||
}
|
||||
impl scheduler::SchedulerSessionState for SessionState {
|
||||
fn get_db(&self) -> Box<dyn SchedulerInterface> {
|
||||
@ -208,6 +209,7 @@ pub struct AppState {
|
||||
pub file_storage_client: Arc<dyn FileStorageInterface>,
|
||||
pub encryption_client: Arc<dyn EncryptionManagementInterface>,
|
||||
pub grpc_client: Arc<GrpcClients>,
|
||||
pub theme_storage_client: Arc<dyn FileStorageInterface>,
|
||||
}
|
||||
impl scheduler::SchedulerAppState for AppState {
|
||||
fn get_tenants(&self) -> Vec<id_type::TenantId> {
|
||||
@ -367,6 +369,7 @@ impl AppState {
|
||||
let email_client = Arc::new(create_email_client(&conf).await);
|
||||
|
||||
let file_storage_client = conf.file_storage.get_file_storage_client().await;
|
||||
let theme_storage_client = conf.theme_storage.get_file_storage_client().await;
|
||||
|
||||
let grpc_client = conf.grpc_client.get_grpc_client_interface().await;
|
||||
|
||||
@ -387,6 +390,7 @@ impl AppState {
|
||||
file_storage_client,
|
||||
encryption_client,
|
||||
grpc_client,
|
||||
theme_storage_client,
|
||||
}
|
||||
})
|
||||
.await
|
||||
@ -472,6 +476,7 @@ impl AppState {
|
||||
#[cfg(feature = "olap")]
|
||||
opensearch_client: Arc::clone(&self.opensearch_client),
|
||||
grpc_client: Arc::clone(&self.grpc_client),
|
||||
theme_storage_client: self.theme_storage_client.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -2130,6 +2135,23 @@ impl User {
|
||||
.route(web::delete().to(user::delete_sample_data)),
|
||||
)
|
||||
}
|
||||
|
||||
route = route.service(
|
||||
web::scope("/theme")
|
||||
.service(
|
||||
web::resource("")
|
||||
.route(web::get().to(user::theme::get_theme_using_lineage))
|
||||
.route(web::post().to(user::theme::create_theme)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/{theme_id}")
|
||||
.route(web::get().to(user::theme::get_theme_using_theme_id))
|
||||
.route(web::put().to(user::theme::update_theme))
|
||||
.route(web::post().to(user::theme::upload_file_to_theme_storage))
|
||||
.route(web::delete().to(user::theme::delete_theme)),
|
||||
),
|
||||
);
|
||||
|
||||
route
|
||||
}
|
||||
}
|
||||
|
||||
@ -259,7 +259,13 @@ impl From<Flow> for ApiIdentifier {
|
||||
| Flow::ListMerchantsForUserInOrg
|
||||
| Flow::ListProfileForUserInOrgAndMerchant
|
||||
| Flow::ListInvitationsForUser
|
||||
| Flow::AuthSelect => Self::User,
|
||||
| Flow::AuthSelect
|
||||
| Flow::GetThemeUsingLineage
|
||||
| Flow::GetThemeUsingThemeId
|
||||
| Flow::UploadFileToThemeStorage
|
||||
| Flow::CreateTheme
|
||||
| Flow::UpdateTheme
|
||||
| Flow::DeleteTheme => Self::User,
|
||||
|
||||
Flow::ListRolesV2
|
||||
| Flow::ListInvitableRolesAtEntityLevel
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
pub mod theme;
|
||||
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
#[cfg(feature = "dummy_connector")]
|
||||
use api_models::user::sample_data::SampleDataRequest;
|
||||
|
||||
145
crates/router/src/routes/user/theme.rs
Normal file
145
crates/router/src/routes/user/theme.rs
Normal file
@ -0,0 +1,145 @@
|
||||
use actix_multipart::form::MultipartForm;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use api_models::user::theme as theme_api;
|
||||
use common_utils::types::theme::ThemeLineage;
|
||||
use masking::Secret;
|
||||
use router_env::Flow;
|
||||
|
||||
use crate::{
|
||||
core::{api_locking, user::theme as theme_core},
|
||||
routes::AppState,
|
||||
services::{api, authentication as auth},
|
||||
};
|
||||
|
||||
pub async fn get_theme_using_lineage(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
query: web::Query<ThemeLineage>,
|
||||
) -> HttpResponse {
|
||||
let flow = Flow::GetThemeUsingLineage;
|
||||
let lineage = query.into_inner();
|
||||
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state,
|
||||
&req,
|
||||
lineage,
|
||||
|state, _, lineage, _| theme_core::get_theme_using_lineage(state, lineage),
|
||||
&auth::AdminApiAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_theme_using_theme_id(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
path: web::Path<String>,
|
||||
) -> HttpResponse {
|
||||
let flow = Flow::GetThemeUsingThemeId;
|
||||
let payload = path.into_inner();
|
||||
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state,
|
||||
&req,
|
||||
payload,
|
||||
|state, _, payload, _| theme_core::get_theme_using_theme_id(state, payload),
|
||||
&auth::AdminApiAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn upload_file_to_theme_storage(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
path: web::Path<String>,
|
||||
MultipartForm(payload): MultipartForm<theme_api::UploadFileAssetData>,
|
||||
query: web::Query<ThemeLineage>,
|
||||
) -> HttpResponse {
|
||||
let flow = Flow::UploadFileToThemeStorage;
|
||||
let theme_id = path.into_inner();
|
||||
let payload = theme_api::UploadFileRequest {
|
||||
lineage: query.into_inner(),
|
||||
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, _, payload, _| {
|
||||
theme_core::upload_file_to_theme_storage(state, theme_id.clone(), payload)
|
||||
},
|
||||
&auth::AdminApiAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_theme(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
payload: web::Json<theme_api::CreateThemeRequest>,
|
||||
) -> HttpResponse {
|
||||
let flow = Flow::CreateTheme;
|
||||
let payload = payload.into_inner();
|
||||
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state,
|
||||
&req,
|
||||
payload,
|
||||
|state, _, payload, _| theme_core::create_theme(state, payload),
|
||||
&auth::AdminApiAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update_theme(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
path: web::Path<String>,
|
||||
payload: web::Json<theme_api::UpdateThemeRequest>,
|
||||
) -> HttpResponse {
|
||||
let flow = Flow::UpdateTheme;
|
||||
let theme_id = path.into_inner();
|
||||
let payload = payload.into_inner();
|
||||
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state,
|
||||
&req,
|
||||
payload,
|
||||
|state, _, payload, _| theme_core::update_theme(state, theme_id.clone(), payload),
|
||||
&auth::AdminApiAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_theme(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
path: web::Path<String>,
|
||||
query: web::Query<ThemeLineage>,
|
||||
) -> HttpResponse {
|
||||
let flow = Flow::DeleteTheme;
|
||||
let theme_id = path.into_inner();
|
||||
let lineage = query.into_inner();
|
||||
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state,
|
||||
&req,
|
||||
lineage,
|
||||
|state, _, lineage, _| theme_core::delete_theme(state, theme_id.clone(), lineage),
|
||||
&auth::AdminApiAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
@ -27,6 +27,7 @@ pub mod dashboard_metadata;
|
||||
pub mod password;
|
||||
#[cfg(feature = "dummy_connector")]
|
||||
pub mod sample_data;
|
||||
pub mod theme;
|
||||
pub mod two_factor_auth;
|
||||
|
||||
impl UserFromToken {
|
||||
|
||||
158
crates/router/src/utils/user/theme.rs
Normal file
158
crates/router/src/utils/user/theme.rs
Normal file
@ -0,0 +1,158 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use common_utils::{id_type, types::theme::ThemeLineage};
|
||||
use error_stack::ResultExt;
|
||||
use hyperswitch_domain_models::merchant_key_store::MerchantKeyStore;
|
||||
|
||||
use crate::{
|
||||
core::errors::{StorageErrorExt, UserErrors, UserResult},
|
||||
routes::SessionState,
|
||||
};
|
||||
|
||||
fn get_theme_dir_key(theme_id: &str) -> PathBuf {
|
||||
["themes", theme_id].iter().collect()
|
||||
}
|
||||
|
||||
pub fn get_specific_file_key(theme_id: &str, file_name: &str) -> PathBuf {
|
||||
let mut path = get_theme_dir_key(theme_id);
|
||||
path.push(file_name);
|
||||
path
|
||||
}
|
||||
|
||||
pub fn get_theme_file_key(theme_id: &str) -> PathBuf {
|
||||
get_specific_file_key(theme_id, "theme.json")
|
||||
}
|
||||
|
||||
fn path_buf_to_str(path: &PathBuf) -> UserResult<&str> {
|
||||
path.to_str()
|
||||
.ok_or(UserErrors::InternalServerError)
|
||||
.attach_printable(format!("Failed to convert path {:#?} to string", path))
|
||||
}
|
||||
|
||||
pub async fn retrieve_file_from_theme_bucket(
|
||||
state: &SessionState,
|
||||
path: &PathBuf,
|
||||
) -> UserResult<Vec<u8>> {
|
||||
state
|
||||
.theme_storage_client
|
||||
.retrieve_file(path_buf_to_str(path)?)
|
||||
.await
|
||||
.change_context(UserErrors::ErrorRetrievingFile)
|
||||
}
|
||||
|
||||
pub async fn upload_file_to_theme_bucket(
|
||||
state: &SessionState,
|
||||
path: &PathBuf,
|
||||
data: Vec<u8>,
|
||||
) -> UserResult<()> {
|
||||
state
|
||||
.theme_storage_client
|
||||
.upload_file(path_buf_to_str(path)?, data)
|
||||
.await
|
||||
.change_context(UserErrors::ErrorUploadingFile)
|
||||
}
|
||||
|
||||
pub async fn validate_lineage(state: &SessionState, lineage: &ThemeLineage) -> UserResult<()> {
|
||||
match lineage {
|
||||
ThemeLineage::Organization { tenant_id, org_id } => {
|
||||
validate_tenant(state, tenant_id)?;
|
||||
validate_org(state, org_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
ThemeLineage::Merchant {
|
||||
tenant_id,
|
||||
org_id,
|
||||
merchant_id,
|
||||
} => {
|
||||
validate_tenant(state, tenant_id)?;
|
||||
validate_org(state, org_id).await?;
|
||||
validate_merchant(state, org_id, merchant_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
ThemeLineage::Profile {
|
||||
tenant_id,
|
||||
org_id,
|
||||
merchant_id,
|
||||
profile_id,
|
||||
} => {
|
||||
validate_tenant(state, tenant_id)?;
|
||||
validate_org(state, org_id).await?;
|
||||
let key_store = validate_merchant_and_get_key_store(state, org_id, merchant_id).await?;
|
||||
validate_profile(state, profile_id, merchant_id, &key_store).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_tenant(state: &SessionState, tenant_id: &id_type::TenantId) -> UserResult<()> {
|
||||
if &state.tenant.tenant_id != tenant_id {
|
||||
return Err(UserErrors::InvalidThemeLineage("tenant_id".to_string()).into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn validate_org(state: &SessionState, org_id: &id_type::OrganizationId) -> UserResult<()> {
|
||||
state
|
||||
.store
|
||||
.find_organization_by_org_id(org_id)
|
||||
.await
|
||||
.to_not_found_response(UserErrors::InvalidThemeLineage("org_id".to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn validate_merchant_and_get_key_store(
|
||||
state: &SessionState,
|
||||
org_id: &id_type::OrganizationId,
|
||||
merchant_id: &id_type::MerchantId,
|
||||
) -> UserResult<MerchantKeyStore> {
|
||||
let key_store = state
|
||||
.store
|
||||
.get_merchant_key_store_by_merchant_id(
|
||||
&state.into(),
|
||||
merchant_id,
|
||||
&state.store.get_master_key().to_vec().into(),
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(UserErrors::InvalidThemeLineage("merchant_id".to_string()))?;
|
||||
|
||||
let merchant_account = state
|
||||
.store
|
||||
.find_merchant_account_by_merchant_id(&state.into(), merchant_id, &key_store)
|
||||
.await
|
||||
.to_not_found_response(UserErrors::InvalidThemeLineage("merchant_id".to_string()))?;
|
||||
|
||||
if &merchant_account.organization_id != org_id {
|
||||
return Err(UserErrors::InvalidThemeLineage("merchant_id".to_string()).into());
|
||||
}
|
||||
|
||||
Ok(key_store)
|
||||
}
|
||||
|
||||
async fn validate_merchant(
|
||||
state: &SessionState,
|
||||
org_id: &id_type::OrganizationId,
|
||||
merchant_id: &id_type::MerchantId,
|
||||
) -> UserResult<()> {
|
||||
validate_merchant_and_get_key_store(state, org_id, merchant_id)
|
||||
.await
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
async fn validate_profile(
|
||||
state: &SessionState,
|
||||
profile_id: &id_type::ProfileId,
|
||||
merchant_id: &id_type::MerchantId,
|
||||
key_store: &MerchantKeyStore,
|
||||
) -> UserResult<()> {
|
||||
state
|
||||
.store
|
||||
.find_business_profile_by_merchant_id_profile_id(
|
||||
&state.into(),
|
||||
key_store,
|
||||
merchant_id,
|
||||
profile_id,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(UserErrors::InvalidThemeLineage("profile_id".to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user