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
8 changes: 5 additions & 3 deletions crates/api_models/src/events/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ use crate::user::{
GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest,
},
AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest,
DashboardEntryResponse, ForgotPasswordRequest, GetUsersResponse, InviteUserRequest,
InviteUserResponse, ResetPasswordRequest, SendVerifyEmailRequest, SignUpRequest,
SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, UserMerchantCreate, VerifyEmailRequest,
DashboardEntryResponse, DeleteUserRequest, ForgotPasswordRequest, GetUsersResponse,
InviteUserRequest, InviteUserResponse, ResetPasswordRequest, SendVerifyEmailRequest,
SignUpRequest, SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, UserMerchantCreate,
VerifyEmailRequest,
};

impl ApiEventMetric for DashboardEntryResponse {
Expand Down Expand Up @@ -53,6 +54,7 @@ common_utils::impl_misc_api_event_type!(
ResetPasswordRequest,
InviteUserRequest,
InviteUserResponse,
DeleteUserRequest,
VerifyEmailRequest,
SendVerifyEmailRequest
);
Expand Down
5 changes: 5 additions & 0 deletions crates/api_models/src/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ pub struct InviteUserResponse {
pub password: Option<Secret<String>>,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct DeleteUserRequest {
pub email: pii::Email,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct SwitchMerchantIdRequest {
pub merchant_id: String,
Expand Down
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(
Copy link
Contributor

Choose a reason for hiding this comment

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

rename to 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 @@ -40,9 +40,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
16 changes: 16 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,10 @@ pub enum UserErrors {
MerchantIdParsingError,
#[error("ChangePasswordError")]
ChangePasswordError,
#[error("UserNotExist")]
UserNotExist,
#[error("InvalidDeleteOperation")]
InvalidDeleteOperation,
}

impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorResponse> for UserErrors {
Expand Down Expand Up @@ -157,6 +161,18 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
"Old and new password cannot be same",
None,
)),
Self::UserNotExist => AER::BadRequest(ApiError::new(
sub_code,
30,
"User does not exist in records",
None,
)),
Copy link
Contributor

Choose a reason for hiding this comment

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

not needed

Self::InvalidDeleteOperation => AER::BadRequest(ApiError::new(
sub_code,
31,
"Delete Operation Not Supported",
None,
)),
}
}
}
77 changes: 77 additions & 0 deletions crates/router/src/core/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::{
services::{authentication as auth, ApplicationResponse},
types::domain,
utils,
utils::user::can_delete_user_role,
};
pub mod dashboard_metadata;
#[cfg(feature = "dummy_connector")]
Expand Down Expand Up @@ -467,6 +468,82 @@ pub async fn invite_user(
}
}

pub async fn delete_user(
state: AppState,
request: user_api::DeleteUserRequest,
user_from_token: auth::UserFromToken,
) -> UserResponse<()> {
let user_from_db: domain::UserFromStorage = state
.store
.find_user_by_email(request.email.to_owned().expose().expose().as_str())
.await
.map_err(|e| {
if e.current_context().is_db_not_found() {
e.change_context(UserErrors::UserNotExist)
} 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) => {
let _ = can_delete_user_role(&user_role.role_id);
Copy link
Contributor

Choose a reason for hiding this comment

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

error handling ?

}
None => {
return Err(UserErrors::InvalidDeleteOperation.into())
.attach_printable("User role not found");
}
};

if user_roles.len() > 1 {
let _ = 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 {
let _ = 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");

let _ = 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)
}
}

pub async fn create_internal_user(
state: AppState,
request: user_api::CreateInternalUserRequest,
Expand Down
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(
&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(
&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(
&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(
&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)
}
}
20 changes: 18 additions & 2 deletions crates/router/src/db/kafka_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1943,8 +1943,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(
&self,
Expand Down Expand Up @@ -2003,6 +2009,16 @@ impl DashboardMetadataInterface for KafkaStore {
.find_merchant_scoped_dashboard_metadata(merchant_id, org_id, data_keys)
.await
}

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

#[async_trait::async_trait]
Expand Down
44 changes: 34 additions & 10 deletions crates/router/src/db/user_role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,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 @@ -75,12 +79,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 @@ -187,11 +199,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 @@ -243,8 +261,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 @@ -925,7 +925,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::post().to(delete_user)));

#[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 => Self::User,
Expand Down
Loading
Loading