diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 3ac65830eb..8b7cd02c93 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -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")] diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index e6e8546c67..36d730f511 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -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); + +#[derive(Debug, serde::Serialize)] +pub struct UserDetails { + pub user_id: String, + pub email: pii::Email, + pub name: Secret, + pub role_id: String, + pub role_name: String, + pub status: UserStatus, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub last_modified_at: time::PrimitiveDateTime, +} diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs index 521d17e734..735cd240b6 100644 --- a/crates/api_models/src/user_role.rs +++ b/crates/api_models/src/user_role.rs @@ -80,3 +80,9 @@ pub struct UpdateUserRoleRequest { pub user_id: String, pub role_id: String, } + +#[derive(Debug, serde::Serialize)] +pub enum UserStatus { + Active, + InvitationSent, +} diff --git a/crates/diesel_models/src/query/dashboard_metadata.rs b/crates/diesel_models/src/query/dashboard_metadata.rs index 03e4a2dab3..44fd24c7ac 100644 --- a/crates/diesel_models/src/query/dashboard_metadata.rs +++ b/crates/diesel_models/src/query/dashboard_metadata.rs @@ -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, + merchant_id: String, + org_id: String, + data_key: enums::DashboardMetadata, + dashboard_metadata_update: DashboardMetadataUpdate, + ) -> StorageResult { + generics::generic_update_with_unique_predicate_get_result::< + ::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, diff --git a/crates/diesel_models/src/query/user.rs b/crates/diesel_models/src/query/user.rs index aa1d8471d2..b4d5976ba2 100644 --- a/crates/diesel_models/src/query/user.rs +++ b/crates/diesel_models/src/query/user.rs @@ -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 { generics::generic_find_one::<::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 { generics::generic_find_one::<::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 { generics::generic_update_with_results::<::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 { generics::generic_delete::<::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> { + 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::(&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), + }) + } } diff --git a/crates/diesel_models/src/user/dashboard_metadata.rs b/crates/diesel_models/src/user/dashboard_metadata.rs index 018808f1c0..1eeb61d613 100644 --- a/crates/diesel_models/src/user/dashboard_metadata.rs +++ b/crates/diesel_models/src/user/dashboard_metadata.rs @@ -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 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, + }, + } + } +} diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index b38fb4cf4a..7d0d599cc4 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -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> { + 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 { + 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))) +} diff --git a/crates/router/src/db/dashboard_metadata.rs b/crates/router/src/db/dashboard_metadata.rs index 2e8129398c..ec24b4ed07 100644 --- a/crates/router/src/db/dashboard_metadata.rs +++ b/crates/router/src/db/dashboard_metadata.rs @@ -14,6 +14,14 @@ pub trait DashboardMetadataInterface { &self, metadata: storage::DashboardMetadataNew, ) -> CustomResult; + async fn update_metadata( + &self, + user_id: Option, + merchant_id: String, + org_id: String, + data_key: enums::DashboardMetadata, + dashboard_metadata_update: storage::DashboardMetadataUpdate, + ) -> CustomResult; 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, + merchant_id: String, + org_id: String, + data_key: enums::DashboardMetadata, + dashboard_metadata_update: storage::DashboardMetadataUpdate, + ) -> CustomResult { + 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, + merchant_id: String, + org_id: String, + data_key: enums::DashboardMetadata, + dashboard_metadata_update: storage::DashboardMetadataUpdate, + ) -> CustomResult { + 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, diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 60a2fb4c2b..32548e36b6 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -1878,6 +1878,15 @@ impl UserInterface for KafkaStore { ) -> CustomResult { 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, 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, + merchant_id: String, + org_id: String, + data_key: enums::DashboardMetadata, + dashboard_metadata_update: storage::DashboardMetadataUpdate, + ) -> CustomResult { + 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, diff --git a/crates/router/src/db/user.rs b/crates/router/src/db/user.rs index be0554ec69..e3dda965f9 100644 --- a/crates/router/src/db/user.rs +++ b/crates/router/src/db/user.rs @@ -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; + + async fn find_users_and_roles_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult, 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, 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 { - 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 { - self.diesel_store.find_user_by_email(user_email).await - } - - async fn find_user_by_id( - &self, - user_id: &str, - ) -> CustomResult { - 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 { - self.diesel_store - .update_user_by_user_id(user_id, user) - .await - } - - async fn delete_user_by_user_id( - &self, - user_id: &str, - ) -> CustomResult { - self.diesel_store.delete_user_by_user_id(user_id).await + _merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + Err(errors::StorageError::MockDbError)? } } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 9c83583bc6..a145f3e7e5 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -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))) diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 04b2b0dc95..6aa2bbad0b 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -157,7 +157,9 @@ impl From 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 diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 78aecea244..97bd7054da 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -204,3 +204,34 @@ pub async fn delete_sample_data( )) .await } + +pub async fn list_merchant_ids_for_user( + state: web::Data, + 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, 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 +} diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 0c7760f84d..082b29d809 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -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 for user_role_api::PermissionInfo { }) } } + +pub struct UserAndRoleJoined(pub storage_user::User, pub UserRole); + +impl TryFrom for user_api::UserDetails { + type Error = (); + fn try_from(user_and_role: UserAndRoleJoined) -> Result { + 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, + }) + } +} diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index c29e78c714..696aa40900 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -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> { + 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()) +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index c844a6aede..f54a5a82ba 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -283,6 +283,10 @@ pub enum Flow { GenerateSampleData, /// Delete Sample Data DeleteSampleData, + /// List merchant accounts for user + UserMerchantAccountList, + /// Get users for merchant account + GetUserDetails, } ///