feat(user): add list org, merchant and profile api (#5662)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Rachit Naithani
2024-08-22 14:34:32 +05:30
committed by GitHub
parent 7f10678c36
commit 98cbf2e71a
9 changed files with 445 additions and 4 deletions

View File

@ -390,3 +390,21 @@ pub struct UserKeyTransferRequest {
pub struct UserTransferKeyResponse { pub struct UserTransferKeyResponse {
pub total_transferred: usize, pub total_transferred: usize,
} }
#[derive(Debug, serde::Serialize)]
pub struct ListOrgsForUserResponse {
pub org_id: id_type::OrganizationId,
pub org_name: Option<String>,
}
#[derive(Debug, serde::Serialize)]
pub struct ListMerchantsForUserInOrgResponse {
pub merchant_id: id_type::MerchantId,
pub merchant_name: OptionalEncryptableName,
}
#[derive(Debug, serde::Serialize)]
pub struct ListProfilesForUserInOrgAndMerchantAccountResponse {
pub profile_id: String,
pub profile_name: String,
}

View File

@ -1,9 +1,14 @@
use async_bb8_diesel::AsyncRunQueryDsl;
use common_utils::id_type; use common_utils::id_type;
use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; use diesel::{
associations::HasTable, debug_query, pg::Pg, result::Error as DieselError,
BoolExpressionMethods, ExpressionMethods, QueryDsl,
};
use error_stack::{report, ResultExt};
use crate::{ use crate::{
enums::UserRoleVersion, query::generics, schema::user_roles::dsl, user_role::*, PgPooledConn, enums::UserRoleVersion, errors, query::generics, schema::user_roles::dsl, user_role::*,
StorageResult, PgPooledConn, StorageResult,
}; };
impl UserRoleNew { impl UserRoleNew {
@ -179,4 +184,55 @@ impl UserRole {
generics::generic_delete_one_with_result::<<Self as HasTable>::Table, _, _>(conn, predicate) generics::generic_delete_one_with_result::<<Self as HasTable>::Table, _, _>(conn, predicate)
.await .await
} }
pub async fn generic_user_roles_list(
conn: &PgPooledConn,
user_id: String,
org_id: Option<id_type::OrganizationId>,
merchant_id: Option<id_type::MerchantId>,
profile_id: Option<String>,
entity_id: Option<String>,
version: Option<UserRoleVersion>,
) -> StorageResult<Vec<Self>> {
let mut query = <Self as HasTable>::table()
.filter(dsl::user_id.eq(user_id))
.into_boxed();
if let Some(org_id) = org_id {
query = query.filter(dsl::org_id.eq(org_id));
}
if let Some(merchant_id) = merchant_id {
query = query.filter(dsl::merchant_id.eq(merchant_id));
}
if let Some(profile_id) = profile_id {
query = query.filter(dsl::profile_id.eq(profile_id));
}
if let Some(entity_id) = entity_id {
query = query.filter(dsl::entity_id.eq(entity_id));
}
if let Some(version) = version {
query = query.filter(dsl::version.eq(version));
}
router_env::logger::debug!(query = %debug_query::<Pg,_>(&query).to_string());
match generics::db_metrics::track_database_call::<Self, _, _>(
query.get_results_async(conn),
generics::db_metrics::DatabaseOperation::Filter,
)
.await
{
Ok(value) => Ok(value),
Err(err) => match err {
DieselError::NotFound => {
Err(report!(err)).change_context(errors::DatabaseError::NotFound)
}
_ => Err(report!(err)).change_context(errors::DatabaseError::Others),
},
}
}
} }

View File

@ -1,14 +1,16 @@
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use api_models::{ use api_models::{
payments::RedirectionResponse, payments::RedirectionResponse,
user::{self as user_api, InviteMultipleUserResponse}, user::{self as user_api, InviteMultipleUserResponse},
}; };
use common_enums::EntityType;
use common_utils::{type_name, types::keymanager::Identifier}; use common_utils::{type_name, types::keymanager::Identifier};
#[cfg(feature = "email")] #[cfg(feature = "email")]
use diesel_models::user_role::UserRoleUpdate; use diesel_models::user_role::UserRoleUpdate;
use diesel_models::{ use diesel_models::{
enums::{TotpStatus, UserRoleVersion, UserStatus}, enums::{TotpStatus, UserRoleVersion, UserStatus},
organization::OrganizationBridge,
user as storage_user, user as storage_user,
user_authentication_method::{UserAuthenticationMethodNew, UserAuthenticationMethodUpdate}, user_authentication_method::{UserAuthenticationMethodNew, UserAuthenticationMethodUpdate},
user_role::UserRoleNew, user_role::UserRoleNew,
@ -2563,3 +2565,198 @@ pub async fn terminate_auth_select(
token, token,
) )
} }
pub async fn list_orgs_for_user(
state: SessionState,
user_from_token: auth::UserFromToken,
) -> UserResponse<Vec<user_api::ListOrgsForUserResponse>> {
let orgs = state
.store
.list_user_roles(
user_from_token.user_id.as_str(),
None,
None,
None,
None,
None,
)
.await
.change_context(UserErrors::InternalServerError)?
.into_iter()
.filter_map(|user_role| {
(user_role.status == UserStatus::Active)
.then_some(user_role.org_id)
.flatten()
})
.collect::<HashSet<_>>();
let resp = futures::future::try_join_all(
orgs.iter()
.map(|org_id| state.store.find_organization_by_org_id(org_id)),
)
.await
.change_context(UserErrors::InternalServerError)?
.into_iter()
.map(|org| user_api::ListOrgsForUserResponse {
org_id: org.get_organization_id(),
org_name: org.get_organization_name(),
})
.collect();
Ok(ApplicationResponse::Json(resp))
}
pub async fn list_merchants_for_user_in_org(
state: SessionState,
user_from_token: auth::UserFromToken,
) -> UserResponse<Vec<user_api::ListMerchantsForUserInOrgResponse>> {
let role_info = roles::RoleInfo::from_role_id(
&state,
&user_from_token.role_id,
&user_from_token.merchant_id,
&user_from_token.org_id,
)
.await
.change_context(UserErrors::InternalServerError)?;
let merchant_accounts = if role_info.get_entity_type() == EntityType::Organization {
state
.store
.list_merchant_accounts_by_organization_id(
&(&state).into(),
user_from_token.org_id.get_string_repr(),
)
.await
.change_context(UserErrors::InternalServerError)?
.into_iter()
.map(
|merchant_account| user_api::ListMerchantsForUserInOrgResponse {
merchant_name: merchant_account.merchant_name.clone(),
merchant_id: merchant_account.get_id().to_owned(),
},
)
.collect()
} else {
let merchant_ids = state
.store
.list_user_roles(
user_from_token.user_id.as_str(),
Some(&user_from_token.org_id),
None,
None,
None,
None,
)
.await
.change_context(UserErrors::InternalServerError)?
.into_iter()
.filter_map(|user_role| {
(user_role.status == UserStatus::Active)
.then_some(user_role.merchant_id)
.flatten()
})
.collect::<HashSet<_>>()
.into_iter()
.collect();
state
.store
.list_multiple_merchant_accounts(&(&state).into(), merchant_ids)
.await
.change_context(UserErrors::InternalServerError)?
.into_iter()
.map(
|merchant_account| user_api::ListMerchantsForUserInOrgResponse {
merchant_name: merchant_account.merchant_name.clone(),
merchant_id: merchant_account.get_id().to_owned(),
},
)
.collect()
};
Ok(ApplicationResponse::Json(merchant_accounts))
}
pub async fn list_profiles_for_user_in_org_and_merchant_account(
state: SessionState,
user_from_token: auth::UserFromToken,
) -> UserResponse<Vec<user_api::ListProfilesForUserInOrgAndMerchantAccountResponse>> {
let role_info = roles::RoleInfo::from_role_id(
&state,
&user_from_token.role_id,
&user_from_token.merchant_id,
&user_from_token.org_id,
)
.await
.change_context(UserErrors::InternalServerError)?;
let key_manager_state = &(&state).into();
let key_store = state
.store
.get_merchant_key_store_by_merchant_id(
key_manager_state,
&user_from_token.merchant_id,
&state.store.get_master_key().to_vec().into(),
)
.await
.change_context(UserErrors::InternalServerError)?;
let user_role_level = role_info.get_entity_type();
let profiles =
if user_role_level == EntityType::Organization || user_role_level == EntityType::Merchant {
state
.store
.list_business_profile_by_merchant_id(
key_manager_state,
&key_store,
&user_from_token.merchant_id,
)
.await
.change_context(UserErrors::InternalServerError)?
.into_iter()
.map(
|profile| user_api::ListProfilesForUserInOrgAndMerchantAccountResponse {
profile_id: profile.profile_id,
profile_name: profile.profile_name,
},
)
.collect()
} else {
let profile_ids = state
.store
.list_user_roles(
user_from_token.user_id.as_str(),
Some(&user_from_token.org_id),
Some(&user_from_token.merchant_id),
None,
None,
None,
)
.await
.change_context(UserErrors::InternalServerError)?
.into_iter()
.filter_map(|user_role| {
(user_role.status == UserStatus::Active)
.then_some(user_role.profile_id)
.flatten()
})
.collect::<HashSet<_>>();
futures::future::try_join_all(profile_ids.iter().map(|profile_id| {
state.store.find_business_profile_by_profile_id(
key_manager_state,
&key_store,
profile_id,
)
}))
.await
.change_context(UserErrors::InternalServerError)?
.into_iter()
.map(
|profile| user_api::ListProfilesForUserInOrgAndMerchantAccountResponse {
profile_id: profile.profile_id,
profile_name: profile.profile_name,
},
)
.collect()
};
Ok(ApplicationResponse::Json(profiles))
}

View File

@ -2871,6 +2871,20 @@ impl UserRoleInterface for KafkaStore {
.list_user_roles_by_merchant_id(merchant_id, version) .list_user_roles_by_merchant_id(merchant_id, version)
.await .await
} }
async fn list_user_roles(
&self,
user_id: &str,
org_id: Option<&id_type::OrganizationId>,
merchant_id: Option<&id_type::MerchantId>,
profile_id: Option<&String>,
entity_id: Option<&String>,
version: Option<enums::UserRoleVersion>,
) -> CustomResult<Vec<storage::UserRole>, errors::StorageError> {
self.diesel_store
.list_user_roles(user_id, org_id, merchant_id, profile_id, entity_id, version)
.await
}
} }
#[async_trait::async_trait] #[async_trait::async_trait]

View File

@ -69,6 +69,16 @@ pub trait UserRoleInterface {
profile_id: Option<&String>, profile_id: Option<&String>,
version: enums::UserRoleVersion, version: enums::UserRoleVersion,
) -> CustomResult<storage::UserRole, errors::StorageError>; ) -> CustomResult<storage::UserRole, errors::StorageError>;
async fn list_user_roles(
&self,
user_id: &str,
org_id: Option<&id_type::OrganizationId>,
merchant_id: Option<&id_type::MerchantId>,
profile_id: Option<&String>,
entity_id: Option<&String>,
version: Option<enums::UserRoleVersion>,
) -> CustomResult<Vec<storage::UserRole>, errors::StorageError>;
} }
#[async_trait::async_trait] #[async_trait::async_trait]
@ -206,6 +216,29 @@ impl UserRoleInterface for Store {
.await .await
.map_err(|error| report!(errors::StorageError::from(error))) .map_err(|error| report!(errors::StorageError::from(error)))
} }
async fn list_user_roles(
&self,
user_id: &str,
org_id: Option<&id_type::OrganizationId>,
merchant_id: Option<&id_type::MerchantId>,
profile_id: Option<&String>,
entity_id: Option<&String>,
version: Option<enums::UserRoleVersion>,
) -> CustomResult<Vec<storage::UserRole>, errors::StorageError> {
let conn = connection::pg_connection_read(self).await?;
storage::UserRole::generic_user_roles_list(
&conn,
user_id.to_owned(),
org_id.cloned(),
merchant_id.cloned(),
profile_id.cloned(),
entity_id.cloned(),
version,
)
.await
.map_err(|error| report!(errors::StorageError::from(error)))
}
} }
#[async_trait::async_trait] #[async_trait::async_trait]
@ -471,4 +504,51 @@ impl UserRoleInterface for MockDb {
.into()), .into()),
} }
} }
async fn list_user_roles(
&self,
user_id: &str,
org_id: Option<&id_type::OrganizationId>,
merchant_id: Option<&id_type::MerchantId>,
profile_id: Option<&String>,
entity_id: Option<&String>,
version: Option<enums::UserRoleVersion>,
) -> CustomResult<Vec<storage::UserRole>, errors::StorageError> {
let user_roles = self.user_roles.lock().await;
let filtered_roles: Vec<_> = user_roles
.iter()
.filter_map(|role| {
let mut filter_condition = role.user_id == user_id;
role.org_id
.as_ref()
.zip(org_id)
.inspect(|(role_org_id, org_id)| {
filter_condition = filter_condition && role_org_id == org_id
});
role.merchant_id.as_ref().zip(merchant_id).inspect(
|(role_merchant_id, merchant_id)| {
filter_condition = filter_condition && role_merchant_id == merchant_id
},
);
role.profile_id.as_ref().zip(profile_id).inspect(
|(role_profile_id, profile_id)| {
filter_condition = filter_condition && role_profile_id == profile_id
},
);
role.entity_id
.as_ref()
.zip(entity_id)
.inspect(|(role_entity_id, entity_id)| {
filter_condition = filter_condition && role_entity_id == entity_id
});
version.inspect(|ver| filter_condition = filter_condition && ver == &role.version);
filter_condition.then(|| role.to_owned())
})
.collect();
Ok(filtered_roles)
}
} }

View File

@ -1632,6 +1632,18 @@ impl User {
.service(web::resource("/transfer").route(web::post().to(transfer_user_key))), .service(web::resource("/transfer").route(web::post().to(transfer_user_key))),
); );
route = route.service(
web::scope("/list")
.service(web::resource("/org").route(web::get().to(list_orgs_for_user)))
.service(
web::resource("/merchant").route(web::get().to(list_merchants_for_user_in_org)),
)
.service(
web::resource("/profile")
.route(web::get().to(list_profiles_for_user_in_org_and_merchant)),
),
);
// Two factor auth routes // Two factor auth routes
route = route.service( route = route.service(
web::scope("/2fa") web::scope("/2fa")

View File

@ -244,6 +244,9 @@ impl From<Flow> for ApiIdentifier {
| Flow::UserTransferKey | Flow::UserTransferKey
| Flow::GetSsoAuthUrl | Flow::GetSsoAuthUrl
| Flow::SignInWithSso | Flow::SignInWithSso
| Flow::ListOrgForUser
| Flow::ListMerchantsForUserInOrg
| Flow::ListProfileForUserInOrgAndMerchant
| Flow::AuthSelect => Self::User, | Flow::AuthSelect => Self::User,
Flow::ListRoles Flow::ListRoles

View File

@ -914,3 +914,58 @@ pub async fn transfer_user_key(
)) ))
.await .await
} }
pub async fn list_orgs_for_user(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
let flow = Flow::ListOrgForUser;
Box::pin(api::server_wrap(
flow,
state.clone(),
&req,
(),
|state, user_from_token, _, _| user_core::list_orgs_for_user(state, user_from_token),
&auth::DashboardNoPermissionAuth,
api_locking::LockAction::NotApplicable,
))
.await
}
pub async fn list_merchants_for_user_in_org(
state: web::Data<AppState>,
req: HttpRequest,
) -> HttpResponse {
let flow = Flow::ListMerchantsForUserInOrg;
Box::pin(api::server_wrap(
flow,
state.clone(),
&req,
(),
|state, user_from_token, _, _| {
user_core::list_merchants_for_user_in_org(state, user_from_token)
},
&auth::DashboardNoPermissionAuth,
api_locking::LockAction::NotApplicable,
))
.await
}
pub async fn list_profiles_for_user_in_org_and_merchant(
state: web::Data<AppState>,
req: HttpRequest,
) -> HttpResponse {
let flow = Flow::ListProfileForUserInOrgAndMerchant;
Box::pin(api::server_wrap(
flow,
state.clone(),
&req,
(),
|state, user_from_token, _, _| {
user_core::list_profiles_for_user_in_org_and_merchant_account(state, user_from_token)
},
&auth::DashboardNoPermissionAuth,
api_locking::LockAction::NotApplicable,
))
.await
}

View File

@ -456,6 +456,12 @@ pub enum Flow {
SignInWithSso, SignInWithSso,
/// Auth Select /// Auth Select
AuthSelect, AuthSelect,
/// List Orgs for user
ListOrgForUser,
/// List Merchants for user in org
ListMerchantsForUserInOrg,
/// List Profile for user in org and merchant
ListProfileForUserInOrgAndMerchant,
/// 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