feat(user): implement invitations api (#5769)

This commit is contained in:
Rachit Naithani
2024-09-02 20:29:36 +05:30
committed by GitHub
parent 258212d8b0
commit 730c2ba258
11 changed files with 240 additions and 146 deletions

View File

@ -1,5 +1,6 @@
use common_enums::PermissionGroup;
use common_utils::pii;
use masking::Secret;
pub mod role;
@ -138,3 +139,11 @@ pub struct ListUsersInEntityResponse {
pub email: pii::Email,
pub roles: Vec<role::MinimalRoleInfo>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct ListInvitationForUserResponse {
pub entity_id: String,
pub entity_type: common_enums::EntityType,
pub entity_name: Option<Secret<String>>,
pub role_id: String,
}

View File

@ -8,7 +8,11 @@ use error_stack::{report, ResultExt};
use router_env::logger;
use crate::{
enums::UserRoleVersion, errors, query::generics, schema::user_roles::dsl, user_role::*,
enums::{UserRoleVersion, UserStatus},
errors,
query::generics,
schema::user_roles::dsl,
user_role::*,
PgPooledConn, StorageResult,
};
@ -201,6 +205,7 @@ impl UserRole {
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn generic_user_roles_list_for_user(
conn: &PgPooledConn,
user_id: String,
@ -208,7 +213,9 @@ impl UserRole {
merchant_id: Option<id_type::MerchantId>,
profile_id: Option<id_type::ProfileId>,
entity_id: Option<String>,
status: Option<UserStatus>,
version: Option<UserRoleVersion>,
limit: Option<u32>,
) -> StorageResult<Vec<Self>> {
let mut query = <Self as HasTable>::table()
.filter(dsl::user_id.eq(user_id))
@ -234,6 +241,14 @@ impl UserRole {
query = query.filter(dsl::version.eq(version));
}
if let Some(status) = status {
query = query.filter(dsl::status.eq(status));
}
if let Some(limit) = limit {
query = query.limit(limit.into());
}
router_env::logger::debug!(query = %debug_query::<Pg,_>(&query).to_string());
match generics::db_metrics::track_database_call::<Self, _, _>(

View File

@ -26,7 +26,7 @@ pub struct UserRole {
pub version: enums::UserRoleVersion,
}
fn get_entity_id_and_type(user_role: &UserRole) -> (Option<String>, Option<EntityType>) {
pub fn get_entity_id_and_type(user_role: &UserRole) -> (Option<String>, Option<EntityType>) {
match (user_role.version, user_role.role_id.as_str()) {
(enums::UserRoleVersion::V1, consts::ROLE_ID_ORGANIZATION_ADMIN) => (
user_role

View File

@ -33,7 +33,10 @@ use crate::services::email::types as email_types;
use crate::{
consts,
core::encryption::send_request_to_key_service_for_user,
db::domain::user_authentication_method::DEFAULT_USER_AUTH_METHOD,
db::{
domain::user_authentication_method::DEFAULT_USER_AUTH_METHOD,
user_role::ListUserRolesByUserIdPayload,
},
routes::{app::ReqState, SessionState},
services::{authentication as auth, authorization::roles, openidconnect, ApplicationResponse},
types::{domain, transformers::ForeignInto},
@ -2282,22 +2285,20 @@ pub async fn list_orgs_for_user(
) -> UserResponse<Vec<user_api::ListOrgsForUserResponse>> {
let orgs = state
.store
.list_user_roles_by_user_id(
user_from_token.user_id.as_str(),
None,
None,
None,
None,
None,
)
.list_user_roles_by_user_id(ListUserRolesByUserIdPayload {
user_id: user_from_token.user_id.as_str(),
org_id: None,
merchant_id: None,
profile_id: None,
entity_id: None,
version: None,
status: Some(UserStatus::Active),
limit: None,
})
.await
.change_context(UserErrors::InternalServerError)?
.into_iter()
.filter_map(|user_role| {
(user_role.status == UserStatus::Active)
.then_some(user_role.org_id)
.flatten()
})
.filter_map(|user_role| user_role.org_id)
.collect::<HashSet<_>>();
let resp = futures::future::try_join_all(
@ -2311,7 +2312,11 @@ pub async fn list_orgs_for_user(
org_id: org.get_organization_id(),
org_name: org.get_organization_name(),
})
.collect();
.collect::<Vec<_>>();
if resp.is_empty() {
Err(UserErrors::InternalServerError).attach_printable("No orgs found for a user")?;
}
Ok(ApplicationResponse::Json(resp))
}
@ -2344,26 +2349,24 @@ pub async fn list_merchants_for_user_in_org(
merchant_id: merchant_account.get_id().to_owned(),
},
)
.collect()
.collect::<Vec<_>>()
} else {
let merchant_ids = state
.store
.list_user_roles_by_user_id(
user_from_token.user_id.as_str(),
Some(&user_from_token.org_id),
None,
None,
None,
None,
)
.list_user_roles_by_user_id(ListUserRolesByUserIdPayload {
user_id: user_from_token.user_id.as_str(),
org_id: Some(&user_from_token.org_id),
merchant_id: None,
profile_id: None,
entity_id: None,
version: None,
status: Some(UserStatus::Active),
limit: None,
})
.await
.change_context(UserErrors::InternalServerError)?
.into_iter()
.filter_map(|user_role| {
(user_role.status == UserStatus::Active)
.then_some(user_role.merchant_id)
.flatten()
})
.filter_map(|user_role| user_role.merchant_id)
.collect::<HashSet<_>>()
.into_iter()
.collect();
@ -2379,9 +2382,13 @@ pub async fn list_merchants_for_user_in_org(
merchant_id: merchant_account.get_id().to_owned(),
},
)
.collect()
.collect::<Vec<_>>()
};
if merchant_accounts.is_empty() {
Err(UserErrors::InternalServerError).attach_printable("No merchant found for a user")?;
}
Ok(ApplicationResponse::Json(merchant_accounts))
}
@ -2427,26 +2434,24 @@ pub async fn list_profiles_for_user_in_org_and_merchant_account(
profile_name: profile.profile_name,
},
)
.collect()
.collect::<Vec<_>>()
} else {
let profile_ids = state
.store
.list_user_roles_by_user_id(
user_from_token.user_id.as_str(),
Some(&user_from_token.org_id),
Some(&user_from_token.merchant_id),
None,
None,
None,
)
.list_user_roles_by_user_id(ListUserRolesByUserIdPayload {
user_id: user_from_token.user_id.as_str(),
org_id: Some(&user_from_token.org_id),
merchant_id: Some(&user_from_token.merchant_id),
profile_id: None,
entity_id: None,
version: None,
status: Some(UserStatus::Active),
limit: None,
})
.await
.change_context(UserErrors::InternalServerError)?
.into_iter()
.filter_map(|user_role| {
(user_role.status == UserStatus::Active)
.then_some(user_role.profile_id)
.flatten()
})
.filter_map(|user_role| user_role.profile_id)
.collect::<HashSet<_>>();
futures::future::try_join_all(profile_ids.iter().map(|profile_id| {
@ -2465,9 +2470,13 @@ pub async fn list_profiles_for_user_in_org_and_merchant_account(
profile_name: profile.profile_name,
},
)
.collect()
.collect::<Vec<_>>()
};
if profiles.is_empty() {
Err(UserErrors::InternalServerError).attach_printable("No profile found for a user")?;
}
Ok(ApplicationResponse::Json(profiles))
}
@ -2503,23 +2512,23 @@ pub async fn switch_org_for_user(
let user_role = state
.store
.list_user_roles_by_user_id(
&user_from_token.user_id,
Some(&request.org_id),
None,
None,
None,
None,
)
.list_user_roles_by_user_id(ListUserRolesByUserIdPayload {
user_id: &user_from_token.user_id,
org_id: Some(&request.org_id),
merchant_id: None,
profile_id: None,
entity_id: None,
version: None,
status: Some(UserStatus::Active),
limit: Some(1),
})
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("Failed to list user roles by user_id and org_id")?
.into_iter()
.find(|role| role.status == UserStatus::Active)
.pop()
.ok_or(UserErrors::InvalidRoleOperationWithMessage(
"No user role found for the requested org_id".to_string(),
))?
.to_owned();
))?;
let merchant_id = utils::user_role::get_single_merchant_id(&state, &user_role).await?;
@ -2547,7 +2556,7 @@ pub async fn switch_org_for_user(
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("Failed to list business profiles by merchant_id")?
.first()
.pop()
.ok_or(UserErrors::InternalServerError)
.attach_printable("No business profile found for the merchant_id")?
.get_id()
@ -2635,7 +2644,7 @@ pub async fn switch_merchant_for_user_in_org(
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("Failed to list business profiles by merchant_id")?
.first()
.pop()
.ok_or(UserErrors::InternalServerError)
.attach_printable("No business profile found for the given merchant_id")?
.get_id()
@ -2688,12 +2697,11 @@ pub async fn switch_merchant_for_user_in_org(
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("Failed to list business profiles by merchant_id")?
.first()
.pop()
.ok_or(UserErrors::InternalServerError)
.attach_printable("No business profile found for the merchant_id")?
.get_id()
.to_owned();
(
user_from_token.org_id.clone(),
merchant_id,
@ -2705,25 +2713,25 @@ pub async fn switch_merchant_for_user_in_org(
EntityType::Merchant | EntityType::Profile => {
let user_role = state
.store
.list_user_roles_by_user_id(
&user_from_token.user_id,
Some(&user_from_token.org_id),
Some(&request.merchant_id),
None,
None,
None,
)
.list_user_roles_by_user_id(ListUserRolesByUserIdPayload {
user_id: &user_from_token.user_id,
org_id: Some(&user_from_token.org_id),
merchant_id: Some(&request.merchant_id),
profile_id: None,
entity_id: None,
version: None,
status: Some(UserStatus::Active),
limit: Some(1),
})
.await
.change_context(UserErrors::InternalServerError)
.attach_printable(
"Failed to list user roles for the given user_id, org_id and merchant_id",
)?
.into_iter()
.find(|role| role.status == UserStatus::Active)
.pop()
.ok_or(UserErrors::InvalidRoleOperationWithMessage(
"No user role associated with the requested merchant_id".to_string(),
))?
.to_owned();
))?;
let profile_id = if let Some(profile_id) = &user_role.profile_id {
profile_id.clone()
@ -2749,7 +2757,7 @@ pub async fn switch_merchant_for_user_in_org(
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("Failed to list business profiles for the given merchant_id")?
.first()
.pop()
.ok_or(UserErrors::InternalServerError)
.attach_printable("No business profile found for the given merchant_id")?
.get_id()
@ -2846,23 +2854,24 @@ pub async fn switch_profile_for_user_in_org_and_merchant(
EntityType::Profile => {
let user_role = state
.store
.list_user_roles_by_user_id(
&user_from_token.user_id,
Some(&user_from_token.org_id),
Some(&user_from_token.merchant_id),
Some(&request.profile_id),
None,
None,
.list_user_roles_by_user_id(ListUserRolesByUserIdPayload{
user_id:&user_from_token.user_id,
org_id: Some(&user_from_token.org_id),
merchant_id: Some(&user_from_token.merchant_id),
profile_id:Some(&request.profile_id),
entity_id: None,
version:None,
status: Some(UserStatus::Active),
limit: Some(1)
}
)
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("Failed to list user roles for the given user_id, org_id, merchant_id and profile_id")?
.into_iter()
.find(|role| role.status == UserStatus::Active)
.pop()
.ok_or(UserErrors::InvalidRoleOperationWithMessage(
"No user role associated with the profile".to_string(),
))?
.to_owned();
))?;
(request.profile_id, user_role.role_id)
}

View File

@ -3,14 +3,14 @@ use std::collections::{HashMap, HashSet};
use api_models::{user as user_api, user_role as user_role_api};
use diesel_models::{
enums::{UserRoleVersion, UserStatus},
user_role::UserRoleUpdate,
user_role::{get_entity_id_and_type, UserRoleUpdate},
};
use error_stack::{report, ResultExt};
use once_cell::sync::Lazy;
use crate::{
core::errors::{StorageErrorExt, UserErrors, UserResponse},
db::user_role::ListUserRolesByOrgIdPayload,
db::user_role::{ListUserRolesByOrgIdPayload, ListUserRolesByUserIdPayload},
routes::{app::ReqState, SessionState},
services::{
authentication as auth,
@ -687,3 +687,41 @@ pub async fn list_users_in_lineage(
.collect::<Result<Vec<_>, _>>()?,
))
}
pub async fn list_invitations_for_user(
state: SessionState,
user_from_token: auth::UserIdFromAuth,
) -> UserResponse<Vec<user_role_api::ListInvitationForUserResponse>> {
let invitations = state
.store
.list_user_roles_by_user_id(ListUserRolesByUserIdPayload {
user_id: &user_from_token.user_id,
org_id: None,
merchant_id: None,
profile_id: None,
entity_id: None,
version: None,
status: Some(UserStatus::InvitationSent),
limit: None,
})
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("Failed to list user roles by user id and invitation sent")?
.into_iter()
.collect::<HashSet<_>>()
.into_iter()
.filter_map(|user_role| {
let (entity_id, entity_type) = get_entity_id_and_type(&user_role);
entity_id.zip(entity_type).map(|(entity_id, entity_type)| {
user_role_api::ListInvitationForUserResponse {
entity_id,
entity_type,
entity_name: None,
role_id: user_role.role_id,
}
})
})
.collect();
Ok(ApplicationResponse::Json(invitations))
}

View File

@ -35,7 +35,10 @@ use super::{
user::{sample_data::BatchSampleDataInterface, UserInterface},
user_authentication_method::UserAuthenticationMethodInterface,
user_key_store::UserKeyStoreInterface,
user_role::{InsertUserRolePayload, ListUserRolesByOrgIdPayload, UserRoleInterface},
user_role::{
InsertUserRolePayload, ListUserRolesByOrgIdPayload, ListUserRolesByUserIdPayload,
UserRoleInterface,
},
};
#[cfg(feature = "payouts")]
use crate::services::kafka::payout::KafkaPayout;
@ -2872,25 +2875,11 @@ impl UserRoleInterface for KafkaStore {
.await
}
async fn list_user_roles_by_user_id(
async fn list_user_roles_by_user_id<'a>(
&self,
user_id: &str,
org_id: Option<&id_type::OrganizationId>,
merchant_id: Option<&id_type::MerchantId>,
profile_id: Option<&id_type::ProfileId>,
entity_id: Option<&String>,
version: Option<enums::UserRoleVersion>,
payload: ListUserRolesByUserIdPayload<'a>,
) -> CustomResult<Vec<storage::UserRole>, errors::StorageError> {
self.diesel_store
.list_user_roles_by_user_id(
user_id,
org_id,
merchant_id,
profile_id,
entity_id,
version,
)
.await
self.diesel_store.list_user_roles_by_user_id(payload).await
}
async fn list_user_roles_by_org_id<'a>(

View File

@ -1,5 +1,8 @@
use common_utils::id_type;
use diesel_models::{enums, user_role as storage};
use diesel_models::{
enums::{self, UserStatus},
user_role as storage,
};
use error_stack::{report, ResultExt};
use router_env::{instrument, tracing};
@ -33,6 +36,17 @@ pub struct ListUserRolesByOrgIdPayload<'a> {
pub version: Option<enums::UserRoleVersion>,
}
pub struct ListUserRolesByUserIdPayload<'a> {
pub user_id: &'a str,
pub org_id: Option<&'a id_type::OrganizationId>,
pub merchant_id: Option<&'a id_type::MerchantId>,
pub profile_id: Option<&'a id_type::ProfileId>,
pub entity_id: Option<&'a String>,
pub version: Option<enums::UserRoleVersion>,
pub status: Option<UserStatus>,
pub limit: Option<u32>,
}
#[async_trait::async_trait]
pub trait UserRoleInterface {
async fn insert_user_role(
@ -93,14 +107,9 @@ pub trait UserRoleInterface {
version: enums::UserRoleVersion,
) -> CustomResult<storage::UserRole, errors::StorageError>;
async fn list_user_roles_by_user_id(
async fn list_user_roles_by_user_id<'a>(
&self,
user_id: &str,
org_id: Option<&id_type::OrganizationId>,
merchant_id: Option<&id_type::MerchantId>,
profile_id: Option<&id_type::ProfileId>,
entity_id: Option<&String>,
version: Option<enums::UserRoleVersion>,
payload: ListUserRolesByUserIdPayload<'a>,
) -> CustomResult<Vec<storage::UserRole>, errors::StorageError>;
async fn list_user_roles_by_org_id<'a>(
@ -244,24 +253,21 @@ impl UserRoleInterface for Store {
.map_err(|error| report!(errors::StorageError::from(error)))
}
async fn list_user_roles_by_user_id(
async fn list_user_roles_by_user_id<'a>(
&self,
user_id: &str,
org_id: Option<&id_type::OrganizationId>,
merchant_id: Option<&id_type::MerchantId>,
profile_id: Option<&id_type::ProfileId>,
entity_id: Option<&String>,
version: Option<enums::UserRoleVersion>,
payload: ListUserRolesByUserIdPayload<'a>,
) -> CustomResult<Vec<storage::UserRole>, errors::StorageError> {
let conn = connection::pg_connection_read(self).await?;
storage::UserRole::generic_user_roles_list_for_user(
&conn,
user_id.to_owned(),
org_id.cloned(),
merchant_id.cloned(),
profile_id.cloned(),
entity_id.cloned(),
version,
payload.user_id.to_owned(),
payload.org_id.cloned(),
payload.merchant_id.cloned(),
payload.profile_id.cloned(),
payload.entity_id.cloned(),
payload.status,
payload.version,
payload.limit,
)
.await
.map_err(|error| report!(errors::StorageError::from(error)))
@ -552,50 +558,52 @@ impl UserRoleInterface for MockDb {
}
}
async fn list_user_roles_by_user_id(
async fn list_user_roles_by_user_id<'a>(
&self,
user_id: &str,
org_id: Option<&id_type::OrganizationId>,
merchant_id: Option<&id_type::MerchantId>,
profile_id: Option<&id_type::ProfileId>,
entity_id: Option<&String>,
version: Option<enums::UserRoleVersion>,
payload: ListUserRolesByUserIdPayload<'a>,
) -> CustomResult<Vec<storage::UserRole>, errors::StorageError> {
let user_roles = self.user_roles.lock().await;
let filtered_roles: Vec<_> = user_roles
let mut filtered_roles: Vec<_> = user_roles
.iter()
.filter_map(|role| {
let mut filter_condition = role.user_id == user_id;
let mut filter_condition = role.user_id == payload.user_id;
role.org_id
.as_ref()
.zip(org_id)
.zip(payload.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.as_ref().zip(payload.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.as_ref().zip(payload.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)| {
role.entity_id.as_ref().zip(payload.entity_id).inspect(
|(role_entity_id, entity_id)| {
filter_condition = filter_condition && role_entity_id == entity_id
},
);
payload
.version
.inspect(|ver| filter_condition = filter_condition && ver == &role.version);
payload.status.inspect(|status| {
filter_condition = filter_condition && status == &role.status
});
version.inspect(|ver| filter_condition = filter_condition && ver == &role.version);
filter_condition.then(|| role.to_owned())
})
.collect();
if let Some(Ok(limit)) = payload.limit.map(|val| val.try_into()) {
filtered_roles = filtered_roles.into_iter().take(limit).collect();
}
Ok(filtered_roles)
}

View File

@ -1725,6 +1725,9 @@ impl User {
.service(
web::resource("/profile")
.route(web::get().to(list_profiles_for_user_in_org_and_merchant)),
)
.service(
web::resource("/invitation").route(web::get().to(list_invitations_for_user)),
),
);

View File

@ -250,6 +250,7 @@ impl From<Flow> for ApiIdentifier {
| Flow::ListOrgForUser
| Flow::ListMerchantsForUserInOrg
| Flow::ListProfileForUserInOrgAndMerchant
| Flow::ListInvitationsForUser
| Flow::AuthSelect => Self::User,
Flow::ListRoles

View File

@ -310,3 +310,23 @@ pub async fn list_updatable_roles_at_entity_level(
))
.await
}
pub async fn list_invitations_for_user(
state: web::Data<AppState>,
req: HttpRequest,
) -> HttpResponse {
let flow = Flow::ListInvitationsForUser;
Box::pin(api::server_wrap(
flow,
state.clone(),
&req,
(),
|state, user_id_from_token, _, _| {
user_role_core::list_invitations_for_user(state, user_id_from_token)
},
&auth::SinglePurposeOrLoginTokenAuth(TokenPurpose::AcceptInvite),
api_locking::LockAction::NotApplicable,
))
.await
}

View File

@ -474,6 +474,8 @@ pub enum Flow {
ListProfileForUserInOrgAndMerchant,
/// List Users in Org
ListUsersInLineage,
/// List invitations for user
ListInvitationsForUser,
/// List initial webhook delivery attempts
WebhookEventInitialDeliveryAttemptList,
/// List delivery attempts for a webhook event