Skip to content

Commit

Permalink
feat(user): add support to delete user (#3374)
Browse files Browse the repository at this point in the history
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
  • Loading branch information
apoorvdixit88 and hyperswitch-bot[bot] authored Jan 25, 2024
1 parent f0c7bb9 commit 7777710
Show file tree
Hide file tree
Showing 14 changed files with 271 additions and 21 deletions.
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 @@ -974,7 +974,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
| Flow::UserSignUpWithMerchantId
| Flow::VerifyEmail
| Flow::VerifyEmailRequest
Expand Down
Loading

0 comments on commit 7777710

Please sign in to comment.