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:
Mani Chandra
2024-12-04 15:04:13 +05:30
committed by GitHub
parent 248be9c73e
commit 3a3e93cb3b
26 changed files with 946 additions and 52 deletions

View File

@ -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,
}
}

View File

@ -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(())
}
}

View File

@ -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 doesnt 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 doesnt 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)
}
}
}
}

View File

@ -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(

View 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)
}

View File

@ -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,

View File

@ -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,

View File

@ -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
}
}

View File

@ -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

View File

@ -1,3 +1,5 @@
pub mod theme;
use actix_web::{web, HttpRequest, HttpResponse};
#[cfg(feature = "dummy_connector")]
use api_models::user::sample_data::SampleDataRequest;

View 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
}

View File

@ -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 {

View 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(())
}