Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(user): add support to delete user #3374

Merged
merged 12 commits into from
Jan 25, 2024
7 changes: 4 additions & 3 deletions crates/api_models/src/events/user_role.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use common_utils::events::{ApiEventMetric, ApiEventsType};

use crate::user_role::{
AcceptInvitationRequest, AuthorizationInfoResponse, GetRoleRequest, ListRolesResponse,
RoleInfoResponse, UpdateUserRoleRequest,
AcceptInvitationRequest, AuthorizationInfoResponse, DeleteUserRoleRequest, GetRoleRequest,
ListRolesResponse, RoleInfoResponse, UpdateUserRoleRequest,
};

common_utils::impl_misc_api_event_type!(
Expand All @@ -11,5 +11,6 @@ common_utils::impl_misc_api_event_type!(
GetRoleRequest,
AuthorizationInfoResponse,
UpdateUserRoleRequest,
AcceptInvitationRequest
AcceptInvitationRequest,
DeleteUserRoleRequest
);
7 changes: 7 additions & 0 deletions crates/api_models/src/user_role.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use common_utils::pii;

use crate::user::DashboardEntryResponse;

#[derive(Debug, serde::Serialize)]
Expand Down Expand Up @@ -101,3 +103,8 @@ pub struct AcceptInvitationRequest {
}

pub type AcceptInvitationResponse = DashboardEntryResponse;

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct DeleteUserRoleRequest {
pub email: pii::Email,
}
14 changes: 14 additions & 0 deletions crates/diesel_models/src/query/dashboard_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,18 @@ impl DashboardMetadata {
)
.await
}

pub async fn delete_user_scoped_dashboard_metadata_by_merchant_id(
conn: &PgPooledConn,
user_id: String,
merchant_id: String,
) -> StorageResult<bool> {
generics::generic_delete::<<Self as HasTable>::Table, _>(
conn,
dsl::user_id
.eq(user_id)
.and(dsl::merchant_id.eq(merchant_id)),
)
.await
}
}
15 changes: 12 additions & 3 deletions crates/diesel_models/src/query/user_role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,18 @@ impl UserRole {
.await
}

pub async fn delete_by_user_id(conn: &PgPooledConn, user_id: String) -> StorageResult<bool> {
generics::generic_delete::<<Self as HasTable>::Table, _>(conn, dsl::user_id.eq(user_id))
.await
pub async fn delete_by_user_id_merchant_id(
conn: &PgPooledConn,
user_id: String,
merchant_id: String,
) -> StorageResult<bool> {
generics::generic_delete::<<Self as HasTable>::Table, _>(
conn,
dsl::user_id
.eq(user_id)
.and(dsl::merchant_id.eq(merchant_id)),
)
.await
}

pub async fn list_by_user_id(conn: &PgPooledConn, user_id: String) -> StorageResult<Vec<Self>> {
Expand Down
8 changes: 8 additions & 0 deletions crates/router/src/core/errors/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ pub enum UserErrors {
MerchantIdParsingError,
#[error("ChangePasswordError")]
ChangePasswordError,
#[error("InvalidDeleteOperation")]
InvalidDeleteOperation,
}

impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorResponse> for UserErrors {
Expand Down Expand Up @@ -157,6 +159,12 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
"Old and new password cannot be same",
None,
)),
Self::InvalidDeleteOperation => AER::BadRequest(ApiError::new(
sub_code,
30,
"Delete Operation Not Supported",
None,
)),
}
}
}
87 changes: 87 additions & 0 deletions crates/router/src/core/user_role.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use api_models::user_role as user_role_api;
use diesel_models::{enums::UserStatus, user_role::UserRoleUpdate};
use error_stack::ResultExt;
use masking::ExposeInterface;
use router_env::logger;

use crate::{
Expand All @@ -11,6 +12,7 @@ use crate::{
authorization::{info, predefined_permissions},
ApplicationResponse,
},
types::domain,
utils,
};

Expand Down Expand Up @@ -161,3 +163,88 @@ pub async fn accept_invitation(

Ok(ApplicationResponse::StatusOk)
}

pub async fn delete_user_role(
state: AppState,
user_from_token: auth::UserFromToken,
request: user_role_api::DeleteUserRoleRequest,
) -> UserResponse<()> {
let user_from_db: domain::UserFromStorage = state
.store
.find_user_by_email(
domain::UserEmail::from_pii_email(request.email)?
.get_secret()
.expose()
.as_str(),
)
.await
.map_err(|e| {
if e.current_context().is_db_not_found() {
e.change_context(UserErrors::InvalidRoleOperation)
.attach_printable("User not found in records")
} else {
e.change_context(UserErrors::InternalServerError)
}
})?
.into();

if user_from_db.get_user_id() == user_from_token.user_id {
return Err(UserErrors::InvalidDeleteOperation.into())
.attach_printable("User deleting himself");
}

let user_roles = state
.store
.list_user_roles_by_user_id(user_from_db.get_user_id())
.await
.change_context(UserErrors::InternalServerError)?;

match user_roles
.iter()
.find(|&role| role.merchant_id == user_from_token.merchant_id.as_str())
{
Some(user_role) => {
if !predefined_permissions::is_role_deletable(&user_role.role_id) {
return Err(UserErrors::InvalidRoleId.into())
.attach_printable("Deletion not allowed for users with specific role id");
}
}
None => {
return Err(UserErrors::InvalidDeleteOperation.into())
.attach_printable("User is not associated with the merchant");
}
};

if user_roles.len() > 1 {
state
.store
.delete_user_role_by_user_id_merchant_id(
user_from_db.get_user_id(),
user_from_token.merchant_id.as_str(),
)
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("Error while deleting user role")?;

Ok(ApplicationResponse::StatusOk)
} else {
state
.store
.delete_user_by_user_id(user_from_db.get_user_id())
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("Error while deleting user entry")?;

state
.store
.delete_user_role_by_user_id_merchant_id(
user_from_db.get_user_id(),
user_from_token.merchant_id.as_str(),
)
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("Error while deleting user role")?;

Ok(ApplicationResponse::StatusOk)
}
}
48 changes: 48 additions & 0 deletions crates/router/src/db/dashboard_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ pub trait DashboardMetadataInterface {
org_id: &str,
data_keys: Vec<enums::DashboardMetadata>,
) -> CustomResult<Vec<storage::DashboardMetadata>, errors::StorageError>;

async fn delete_user_scoped_dashboard_metadata_by_merchant_id(
&self,
user_id: &str,
merchant_id: &str,
) -> CustomResult<bool, errors::StorageError>;
}

#[async_trait::async_trait]
Expand Down Expand Up @@ -111,6 +117,21 @@ impl DashboardMetadataInterface for Store {
.map_err(Into::into)
.into_report()
}
async fn delete_user_scoped_dashboard_metadata_by_merchant_id(
&self,
user_id: &str,
merchant_id: &str,
) -> CustomResult<bool, errors::StorageError> {
let conn = connection::pg_connection_write(self).await?;
storage::DashboardMetadata::delete_user_scoped_dashboard_metadata_by_merchant_id(
&conn,
user_id.to_owned(),
merchant_id.to_owned(),
)
.await
.map_err(Into::into)
.into_report()
}
}

#[async_trait::async_trait]
Expand Down Expand Up @@ -246,4 +267,31 @@ impl DashboardMetadataInterface for MockDb {
}
Ok(query_result)
}
async fn delete_user_scoped_dashboard_metadata_by_merchant_id(
&self,
user_id: &str,
merchant_id: &str,
) -> CustomResult<bool, errors::StorageError> {
let mut dashboard_metadata = self.dashboard_metadata.lock().await;

let initial_len = dashboard_metadata.len();

dashboard_metadata.retain(|metadata_inner| {
!(metadata_inner
.user_id
.clone()
.map(|user_id_inner| user_id_inner == user_id)
.unwrap_or(false)
&& metadata_inner.merchant_id == merchant_id)
});

if dashboard_metadata.len() == initial_len {
return Err(errors::StorageError::ValueNotFound(format!(
"No user available for user_id = {user_id} and merchant id = {merchant_id}"
))
.into());
}

Ok(true)
}
}
21 changes: 18 additions & 3 deletions crates/router/src/db/kafka_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1955,9 +1955,14 @@ impl UserRoleInterface for KafkaStore {
.update_user_role_by_user_id_merchant_id(user_id, merchant_id, update)
.await
}

async fn delete_user_role(&self, user_id: &str) -> CustomResult<bool, errors::StorageError> {
self.diesel_store.delete_user_role(user_id).await
async fn delete_user_role_by_user_id_merchant_id(
&self,
user_id: &str,
merchant_id: &str,
) -> CustomResult<bool, errors::StorageError> {
self.diesel_store
.delete_user_role_by_user_id_merchant_id(user_id, merchant_id)
.await
}

async fn list_user_roles_by_user_id(
Expand Down Expand Up @@ -2017,6 +2022,16 @@ impl DashboardMetadataInterface for KafkaStore {
.find_merchant_scoped_dashboard_metadata(merchant_id, org_id, data_keys)
.await
}

async fn delete_user_scoped_dashboard_metadata_by_merchant_id(
&self,
user_id: &str,
merchant_id: &str,
) -> CustomResult<bool, errors::StorageError> {
self.diesel_store
.delete_user_scoped_dashboard_metadata_by_merchant_id(user_id, merchant_id)
.await
}
}

#[async_trait::async_trait]
Expand Down
45 changes: 34 additions & 11 deletions crates/router/src/db/user_role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@ pub trait UserRoleInterface {
merchant_id: &str,
update: storage::UserRoleUpdate,
) -> CustomResult<storage::UserRole, errors::StorageError>;

async fn delete_user_role(&self, user_id: &str) -> CustomResult<bool, errors::StorageError>;
async fn delete_user_role_by_user_id_merchant_id(
&self,
user_id: &str,
merchant_id: &str,
) -> CustomResult<bool, errors::StorageError>;

async fn list_user_roles_by_user_id(
&self,
Expand Down Expand Up @@ -100,12 +103,20 @@ impl UserRoleInterface for Store {
.into_report()
}

async fn delete_user_role(&self, user_id: &str) -> CustomResult<bool, errors::StorageError> {
async fn delete_user_role_by_user_id_merchant_id(
&self,
user_id: &str,
merchant_id: &str,
) -> CustomResult<bool, errors::StorageError> {
let conn = connection::pg_connection_write(self).await?;
storage::UserRole::delete_by_user_id(&conn, user_id.to_owned())
.await
.map_err(Into::into)
.into_report()
storage::UserRole::delete_by_user_id_merchant_id(
&conn,
user_id.to_owned(),
merchant_id.to_owned(),
)
.await
.map_err(Into::into)
.into_report()
}

async fn list_user_roles_by_user_id(
Expand Down Expand Up @@ -230,11 +241,17 @@ impl UserRoleInterface for MockDb {
)
}

async fn delete_user_role(&self, user_id: &str) -> CustomResult<bool, errors::StorageError> {
async fn delete_user_role_by_user_id_merchant_id(
&self,
user_id: &str,
merchant_id: &str,
) -> CustomResult<bool, errors::StorageError> {
let mut user_roles = self.user_roles.lock().await;
let user_role_index = user_roles
.iter()
.position(|user_role| user_role.user_id == user_id)
.position(|user_role| {
user_role.user_id == user_id && user_role.merchant_id == merchant_id
})
.ok_or(errors::StorageError::ValueNotFound(format!(
"No user available for user_id = {user_id}"
)))?;
Expand Down Expand Up @@ -286,8 +303,14 @@ impl UserRoleInterface for super::KafkaStore {
) -> CustomResult<storage::UserRole, errors::StorageError> {
self.diesel_store.find_user_role_by_user_id(user_id).await
}
async fn delete_user_role(&self, user_id: &str) -> CustomResult<bool, errors::StorageError> {
self.diesel_store.delete_user_role(user_id).await
async fn delete_user_role_by_user_id_merchant_id(
&self,
user_id: &str,
merchant_id: &str,
) -> CustomResult<bool, errors::StorageError> {
self.diesel_store
.delete_user_role_by_user_id_merchant_id(user_id, merchant_id)
.await
}
async fn list_user_roles_by_user_id(
&self,
Expand Down
3 changes: 2 additions & 1 deletion crates/router/src/routes/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -928,7 +928,8 @@ impl User {
web::resource("/data")
.route(web::get().to(get_multiple_dashboard_metadata))
.route(web::post().to(set_dashboard_metadata)),
);
)
.service(web::resource("/user/delete").route(web::delete().to(delete_user_role)));

#[cfg(feature = "dummy_connector")]
{
Expand Down
1 change: 1 addition & 0 deletions crates/router/src/routes/lock_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ impl From<Flow> for ApiIdentifier {
| Flow::ForgotPassword
| Flow::ResetPassword
| Flow::InviteUser
| Flow::DeleteUser
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put this in UserRoles

| Flow::UserSignUpWithMerchantId
| Flow::VerifyEmail
| Flow::VerifyEmailRequest
Expand Down
Loading
Loading