feat(user): add user_list and switch_list apis (#3033)

Co-authored-by: Mani Chandra Dulam <mani.dchandra@juspay.in>
This commit is contained in:
Apoorv Dixit
2023-12-01 19:07:17 +05:30
committed by GitHub
parent 95876b0ce0
commit ec15ddd0d0
16 changed files with 356 additions and 52 deletions

View File

@ -7,7 +7,7 @@ use crate::user::{
GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest,
},
ChangePasswordRequest, ConnectAccountRequest, ConnectAccountResponse,
CreateInternalUserRequest, SwitchMerchantIdRequest, UserMerchantCreate,
CreateInternalUserRequest, GetUsersResponse, SwitchMerchantIdRequest, UserMerchantCreate,
};
impl ApiEventMetric for ConnectAccountResponse {
@ -29,7 +29,8 @@ common_utils::impl_misc_api_event_type!(
SetMetaDataRequest,
SwitchMerchantIdRequest,
CreateInternalUserRequest,
UserMerchantCreate
UserMerchantCreate,
GetUsersResponse
);
#[cfg(feature = "dummy_connector")]

View File

@ -1,5 +1,7 @@
use common_utils::pii;
use masking::Secret;
use crate::user_role::UserStatus;
pub mod dashboard_metadata;
#[cfg(feature = "dummy_connector")]
pub mod sample_data;
@ -45,3 +47,18 @@ pub struct CreateInternalUserRequest {
pub struct UserMerchantCreate {
pub company_name: String,
}
#[derive(Debug, serde::Serialize)]
pub struct GetUsersResponse(pub Vec<UserDetails>);
#[derive(Debug, serde::Serialize)]
pub struct UserDetails {
pub user_id: String,
pub email: pii::Email,
pub name: Secret<String>,
pub role_id: String,
pub role_name: String,
pub status: UserStatus,
#[serde(with = "common_utils::custom_serde::iso8601")]
pub last_modified_at: time::PrimitiveDateTime,
}

View File

@ -80,3 +80,9 @@ pub struct UpdateUserRoleRequest {
pub user_id: String,
pub role_id: String,
}
#[derive(Debug, serde::Serialize)]
pub enum UserStatus {
Active,
InvitationSent,
}

View File

@ -5,7 +5,10 @@ use crate::{
enums,
query::generics,
schema::dashboard_metadata::dsl,
user::dashboard_metadata::{DashboardMetadata, DashboardMetadataNew},
user::dashboard_metadata::{
DashboardMetadata, DashboardMetadataNew, DashboardMetadataUpdate,
DashboardMetadataUpdateInternal,
},
PgPooledConn, StorageResult,
};
@ -17,6 +20,31 @@ impl DashboardMetadataNew {
}
impl DashboardMetadata {
pub async fn update(
conn: &PgPooledConn,
user_id: Option<String>,
merchant_id: String,
org_id: String,
data_key: enums::DashboardMetadata,
dashboard_metadata_update: DashboardMetadataUpdate,
) -> StorageResult<Self> {
generics::generic_update_with_unique_predicate_get_result::<
<Self as HasTable>::Table,
_,
_,
_,
>(
conn,
dsl::user_id
.eq(user_id.to_owned())
.and(dsl::merchant_id.eq(merchant_id.to_owned()))
.and(dsl::org_id.eq(org_id.to_owned()))
.and(dsl::data_key.eq(data_key.to_owned())),
DashboardMetadataUpdateInternal::from(dashboard_metadata_update),
)
.await
}
pub async fn find_user_scoped_dashboard_metadata(
conn: &PgPooledConn,
user_id: String,

View File

@ -1,13 +1,24 @@
use diesel::{associations::HasTable, ExpressionMethods};
use error_stack::report;
use router_env::tracing::{self, instrument};
use async_bb8_diesel::AsyncRunQueryDsl;
use diesel::{
associations::HasTable, debug_query, result::Error as DieselError, ExpressionMethods,
JoinOnDsl, QueryDsl,
};
use error_stack::{report, IntoReport};
use router_env::{
logger,
tracing::{self, instrument},
};
pub mod sample_data;
use crate::{
errors::{self},
query::generics,
schema::users::dsl,
schema::{
user_roles::{self, dsl as user_roles_dsl},
users::dsl as users_dsl,
},
user::*,
user_role::UserRole,
PgPooledConn, StorageResult,
};
@ -22,7 +33,7 @@ impl User {
pub async fn find_by_user_email(conn: &PgPooledConn, user_email: &str) -> StorageResult<Self> {
generics::generic_find_one::<<Self as HasTable>::Table, _, _>(
conn,
dsl::email.eq(user_email.to_owned()),
users_dsl::email.eq(user_email.to_owned()),
)
.await
}
@ -30,7 +41,7 @@ impl User {
pub async fn find_by_user_id(conn: &PgPooledConn, user_id: &str) -> StorageResult<Self> {
generics::generic_find_one::<<Self as HasTable>::Table, _, _>(
conn,
dsl::user_id.eq(user_id.to_owned()),
users_dsl::user_id.eq(user_id.to_owned()),
)
.await
}
@ -42,7 +53,7 @@ impl User {
) -> StorageResult<Self> {
generics::generic_update_with_results::<<Self as HasTable>::Table, _, _, _>(
conn,
dsl::user_id.eq(user_id.to_owned()),
users_dsl::user_id.eq(user_id.to_owned()),
UserUpdateInternal::from(user),
)
.await?
@ -56,8 +67,28 @@ impl User {
pub async fn delete_by_user_id(conn: &PgPooledConn, user_id: &str) -> StorageResult<bool> {
generics::generic_delete::<<Self as HasTable>::Table, _>(
conn,
dsl::user_id.eq(user_id.to_owned()),
users_dsl::user_id.eq(user_id.to_owned()),
)
.await
}
pub async fn find_joined_users_and_roles_by_merchant_id(
conn: &PgPooledConn,
mid: &str,
) -> StorageResult<Vec<(Self, UserRole)>> {
let query = Self::table()
.inner_join(user_roles::table.on(user_roles_dsl::user_id.eq(users_dsl::user_id)))
.filter(user_roles_dsl::merchant_id.eq(mid.to_owned()));
logger::debug!(query = %debug_query::<diesel::pg::Pg,_>(&query).to_string());
query
.get_results_async::<(Self, UserRole)>(conn)
.await
.into_report()
.map_err(|err| match err.current_context() {
DieselError::NotFound => err.change_context(errors::DatabaseError::NotFound),
_ => err.change_context(errors::DatabaseError::Others),
})
}
}

View File

@ -33,3 +33,40 @@ pub struct DashboardMetadataNew {
pub last_modified_by: String,
pub last_modified_at: PrimitiveDateTime,
}
#[derive(
router_derive::Setter, Clone, Debug, Insertable, router_derive::DebugAsDisplay, AsChangeset,
)]
#[diesel(table_name = dashboard_metadata)]
pub struct DashboardMetadataUpdateInternal {
pub data_key: enums::DashboardMetadata,
pub data_value: serde_json::Value,
pub last_modified_by: String,
pub last_modified_at: PrimitiveDateTime,
}
pub enum DashboardMetadataUpdate {
UpdateData {
data_key: enums::DashboardMetadata,
data_value: serde_json::Value,
last_modified_by: String,
},
}
impl From<DashboardMetadataUpdate> for DashboardMetadataUpdateInternal {
fn from(metadata_update: DashboardMetadataUpdate) -> Self {
let last_modified_at = common_utils::date_time::now();
match metadata_update {
DashboardMetadataUpdate::UpdateData {
data_key,
data_value,
last_modified_by,
} => Self {
data_key,
data_value,
last_modified_by,
last_modified_at,
},
}
}
}

View File

@ -324,3 +324,29 @@ pub async fn create_merchant_account(
Ok(ApplicationResponse::StatusOk)
}
pub async fn list_merchant_ids_for_user(
state: AppState,
user: auth::UserFromToken,
) -> UserResponse<Vec<String>> {
Ok(ApplicationResponse::Json(
utils::user::get_merchant_ids_for_user(state, &user.user_id).await?,
))
}
pub async fn get_users_for_merchant_account(
state: AppState,
user_from_token: auth::UserFromToken,
) -> UserResponse<user_api::GetUsersResponse> {
let users = state
.store
.find_users_and_roles_by_merchant_id(user_from_token.merchant_id.as_str())
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("No users for given merchant id")?
.into_iter()
.filter_map(|(user, role)| domain::UserAndRoleJoined(user, role).try_into().ok())
.collect();
Ok(ApplicationResponse::Json(user_api::GetUsersResponse(users)))
}

View File

@ -14,6 +14,14 @@ pub trait DashboardMetadataInterface {
&self,
metadata: storage::DashboardMetadataNew,
) -> CustomResult<storage::DashboardMetadata, errors::StorageError>;
async fn update_metadata(
&self,
user_id: Option<String>,
merchant_id: String,
org_id: String,
data_key: enums::DashboardMetadata,
dashboard_metadata_update: storage::DashboardMetadataUpdate,
) -> CustomResult<storage::DashboardMetadata, errors::StorageError>;
async fn find_user_scoped_dashboard_metadata(
&self,
@ -44,6 +52,28 @@ impl DashboardMetadataInterface for Store {
.into_report()
}
async fn update_metadata(
&self,
user_id: Option<String>,
merchant_id: String,
org_id: String,
data_key: enums::DashboardMetadata,
dashboard_metadata_update: storage::DashboardMetadataUpdate,
) -> CustomResult<storage::DashboardMetadata, errors::StorageError> {
let conn = connection::pg_connection_write(self).await?;
storage::DashboardMetadata::update(
&conn,
user_id,
merchant_id,
org_id,
data_key,
dashboard_metadata_update,
)
.await
.map_err(Into::into)
.into_report()
}
async fn find_user_scoped_dashboard_metadata(
&self,
user_id: &str,
@ -121,6 +151,41 @@ impl DashboardMetadataInterface for MockDb {
Ok(metadata_new)
}
async fn update_metadata(
&self,
user_id: Option<String>,
merchant_id: String,
org_id: String,
data_key: enums::DashboardMetadata,
dashboard_metadata_update: storage::DashboardMetadataUpdate,
) -> CustomResult<storage::DashboardMetadata, errors::StorageError> {
let mut dashboard_metadata = self.dashboard_metadata.lock().await;
let dashboard_metadata_to_update = dashboard_metadata
.iter_mut()
.find(|metadata| {
metadata.user_id == user_id
&& metadata.merchant_id == merchant_id
&& metadata.org_id == org_id
&& metadata.data_key == data_key
})
.ok_or(errors::StorageError::MockDbError)?;
match dashboard_metadata_update {
storage::DashboardMetadataUpdate::UpdateData {
data_key,
data_value,
last_modified_by,
} => {
dashboard_metadata_to_update.data_key = data_key;
dashboard_metadata_to_update.data_value = data_value;
dashboard_metadata_to_update.last_modified_by = last_modified_by;
dashboard_metadata_to_update.last_modified_at = common_utils::date_time::now();
}
}
Ok(dashboard_metadata_to_update.clone())
}
async fn find_user_scoped_dashboard_metadata(
&self,
user_id: &str,

View File

@ -1878,6 +1878,15 @@ impl UserInterface for KafkaStore {
) -> CustomResult<bool, errors::StorageError> {
self.diesel_store.delete_user_by_user_id(user_id).await
}
async fn find_users_and_roles_by_merchant_id(
&self,
merchant_id: &str,
) -> CustomResult<Vec<(storage::User, user_storage::UserRole)>, errors::StorageError> {
self.diesel_store
.find_users_and_roles_by_merchant_id(merchant_id)
.await
}
}
impl RedisConnInterface for KafkaStore {
@ -1930,6 +1939,25 @@ impl DashboardMetadataInterface for KafkaStore {
self.diesel_store.insert_metadata(metadata).await
}
async fn update_metadata(
&self,
user_id: Option<String>,
merchant_id: String,
org_id: String,
data_key: enums::DashboardMetadata,
dashboard_metadata_update: storage::DashboardMetadataUpdate,
) -> CustomResult<storage::DashboardMetadata, errors::StorageError> {
self.diesel_store
.update_metadata(
user_id,
merchant_id,
org_id,
data_key,
dashboard_metadata_update,
)
.await
}
async fn find_user_scoped_dashboard_metadata(
&self,
user_id: &str,

View File

@ -1,4 +1,4 @@
use diesel_models::user as storage;
use diesel_models::{user as storage, user_role::UserRole};
use error_stack::{IntoReport, ResultExt};
use masking::Secret;
@ -37,6 +37,11 @@ pub trait UserInterface {
&self,
user_id: &str,
) -> CustomResult<bool, errors::StorageError>;
async fn find_users_and_roles_by_merchant_id(
&self,
merchant_id: &str,
) -> CustomResult<Vec<(storage::User, UserRole)>, errors::StorageError>;
}
#[async_trait::async_trait]
@ -97,6 +102,17 @@ impl UserInterface for Store {
.map_err(Into::into)
.into_report()
}
async fn find_users_and_roles_by_merchant_id(
&self,
merchant_id: &str,
) -> CustomResult<Vec<(storage::User, UserRole)>, errors::StorageError> {
let conn = connection::pg_connection_write(self).await?;
storage::User::find_joined_users_and_roles_by_merchant_id(&conn, merchant_id)
.await
.map_err(Into::into)
.into_report()
}
}
#[async_trait::async_trait]
@ -222,45 +238,11 @@ impl UserInterface for MockDb {
users.remove(user_index);
Ok(true)
}
}
#[cfg(feature = "kafka_events")]
#[async_trait::async_trait]
impl UserInterface for super::KafkaStore {
async fn insert_user(
&self,
user_data: storage::UserNew,
) -> CustomResult<storage::User, errors::StorageError> {
self.diesel_store.insert_user(user_data).await
}
async fn find_user_by_email(
async fn find_users_and_roles_by_merchant_id(
&self,
user_email: &str,
) -> CustomResult<storage::User, errors::StorageError> {
self.diesel_store.find_user_by_email(user_email).await
}
async fn find_user_by_id(
&self,
user_id: &str,
) -> CustomResult<storage::User, errors::StorageError> {
self.diesel_store.find_user_by_id(user_id).await
}
async fn update_user_by_user_id(
&self,
user_id: &str,
user: storage::UserUpdate,
) -> CustomResult<storage::User, errors::StorageError> {
self.diesel_store
.update_user_by_user_id(user_id, user)
.await
}
async fn delete_user_by_user_id(
&self,
user_id: &str,
) -> CustomResult<bool, errors::StorageError> {
self.diesel_store.delete_user_by_user_id(user_id).await
_merchant_id: &str,
) -> CustomResult<Vec<(storage::User, UserRole)>, errors::StorageError> {
Err(errors::StorageError::MockDbError)?
}
}

View File

@ -839,6 +839,8 @@ impl User {
web::resource("/create_merchant")
.route(web::post().to(user_merchant_account_create)),
)
.service(web::resource("/switch/list").route(web::get().to(list_merchant_ids_for_user)))
.service(web::resource("/user/list").route(web::get().to(get_user_details)))
// User Role APIs
.service(web::resource("/permission_info").route(web::get().to(get_authorization_info)))
.service(web::resource("/user/update_role").route(web::post().to(update_user_role)))

View File

@ -157,7 +157,9 @@ impl From<Flow> for ApiIdentifier {
| Flow::SwitchMerchant
| Flow::UserMerchantAccountCreate
| Flow::GenerateSampleData
| Flow::DeleteSampleData => Self::User,
| Flow::DeleteSampleData
| Flow::UserMerchantAccountList
| Flow::GetUserDetails => Self::User,
Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => {
Self::UserRole

View File

@ -204,3 +204,34 @@ pub async fn delete_sample_data(
))
.await
}
pub async fn list_merchant_ids_for_user(
state: web::Data<AppState>,
req: HttpRequest,
) -> HttpResponse {
let flow = Flow::UserMerchantAccountList;
Box::pin(api::server_wrap(
flow,
state,
&req,
(),
|state, user, _| user_core::list_merchant_ids_for_user(state, user),
&auth::DashboardNoPermissionAuth,
api_locking::LockAction::NotApplicable,
))
.await
}
pub async fn get_user_details(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
let flow = Flow::GetUserDetails;
Box::pin(api::server_wrap(
flow,
state.clone(),
&req,
(),
|state, user, _| user_core::get_users_for_merchant_account(state, user),
&auth::JWTAuth(Permission::UsersRead),
api_locking::LockAction::NotApplicable,
))
.await
}

View File

@ -27,7 +27,7 @@ use crate::{
routes::AppState,
services::{
authentication::{AuthToken, UserFromToken},
authorization::info,
authorization::{info, predefined_permissions},
},
types::transformers::ForeignFrom,
utils::user::password,
@ -671,3 +671,30 @@ impl TryFrom<info::PermissionInfo> for user_role_api::PermissionInfo {
})
}
}
pub struct UserAndRoleJoined(pub storage_user::User, pub UserRole);
impl TryFrom<UserAndRoleJoined> for user_api::UserDetails {
type Error = ();
fn try_from(user_and_role: UserAndRoleJoined) -> Result<Self, Self::Error> {
let status = match user_and_role.1.status {
UserStatus::Active => user_role_api::UserStatus::Active,
UserStatus::InvitationSent => user_role_api::UserStatus::InvitationSent,
};
let role_id = user_and_role.1.role_id;
let role_name = predefined_permissions::get_role_name_from_id(role_id.as_str())
.ok_or(())?
.to_string();
Ok(Self {
user_id: user_and_role.0.user_id,
email: user_and_role.0.email,
name: user_and_role.0.name,
role_id,
status,
role_name,
last_modified_at: user_and_role.1.last_modified_at,
})
}
}

View File

@ -1,3 +1,4 @@
use diesel_models::enums::UserStatus;
use error_stack::ResultExt;
use crate::{
@ -51,3 +52,19 @@ impl UserFromToken {
Ok(user)
}
}
pub async fn get_merchant_ids_for_user(state: AppState, user_id: &str) -> UserResult<Vec<String>> {
Ok(state
.store
.list_user_roles_by_user_id(user_id)
.await
.change_context(UserErrors::InternalServerError)?
.into_iter()
.filter_map(|ele| {
if ele.status == UserStatus::Active {
return Some(ele.merchant_id);
}
None
})
.collect())
}

View File

@ -283,6 +283,10 @@ pub enum Flow {
GenerateSampleData,
/// Delete Sample Data
DeleteSampleData,
/// List merchant accounts for user
UserMerchantAccountList,
/// Get users for merchant account
GetUserDetails,
}
///