mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-31 01:57:45 +08:00
feat(users): Add transfer org ownership API (#3603)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -2,7 +2,7 @@ use common_utils::events::{ApiEventMetric, ApiEventsType};
|
||||
|
||||
use crate::user_role::{
|
||||
AcceptInvitationRequest, AuthorizationInfoResponse, DeleteUserRoleRequest, GetRoleRequest,
|
||||
ListRolesResponse, RoleInfoResponse, UpdateUserRoleRequest,
|
||||
ListRolesResponse, RoleInfoResponse, TransferOrgOwnershipRequest, UpdateUserRoleRequest,
|
||||
};
|
||||
|
||||
common_utils::impl_misc_api_event_type!(
|
||||
@ -12,5 +12,6 @@ common_utils::impl_misc_api_event_type!(
|
||||
AuthorizationInfoResponse,
|
||||
UpdateUserRoleRequest,
|
||||
AcceptInvitationRequest,
|
||||
DeleteUserRoleRequest
|
||||
DeleteUserRoleRequest,
|
||||
TransferOrgOwnershipRequest
|
||||
);
|
||||
|
||||
@ -108,3 +108,8 @@ pub type AcceptInvitationResponse = DashboardEntryResponse;
|
||||
pub struct DeleteUserRoleRequest {
|
||||
pub email: pii::Email,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct TransferOrgOwnershipRequest {
|
||||
pub email: pii::Email,
|
||||
}
|
||||
|
||||
@ -54,6 +54,20 @@ impl UserRole {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update_by_user_id_org_id(
|
||||
conn: &PgPooledConn,
|
||||
user_id: String,
|
||||
org_id: String,
|
||||
update: UserRoleUpdate,
|
||||
) -> StorageResult<Vec<Self>> {
|
||||
generics::generic_update_with_results::<<Self as HasTable>::Table, _, _, _>(
|
||||
conn,
|
||||
dsl::user_id.eq(user_id).and(dsl::org_id.eq(org_id)),
|
||||
UserRoleUpdateInternal::from(update),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_by_user_id_merchant_id(
|
||||
conn: &PgPooledConn,
|
||||
user_id: String,
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
use api_models::user_role as user_role_api;
|
||||
use api_models::{user as user_api, 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::{
|
||||
consts,
|
||||
core::errors::{StorageErrorExt, UserErrors, UserResponse},
|
||||
routes::AppState,
|
||||
services::{
|
||||
@ -135,6 +136,55 @@ pub async fn update_user_role(
|
||||
Ok(ApplicationResponse::StatusOk)
|
||||
}
|
||||
|
||||
pub async fn transfer_org_ownership(
|
||||
state: AppState,
|
||||
user_from_token: auth::UserFromToken,
|
||||
req: user_role_api::TransferOrgOwnershipRequest,
|
||||
) -> UserResponse<user_api::DashboardEntryResponse> {
|
||||
if user_from_token.role_id != consts::user_role::ROLE_ID_ORGANIZATION_ADMIN {
|
||||
return Err(UserErrors::InvalidRoleOperation.into()).attach_printable(format!(
|
||||
"role_id = {} is not org_admin",
|
||||
user_from_token.role_id
|
||||
));
|
||||
}
|
||||
|
||||
let user_to_be_updated =
|
||||
utils::user::get_user_from_db_by_email(&state, domain::UserEmail::try_from(req.email)?)
|
||||
.await
|
||||
.to_not_found_response(UserErrors::InvalidRoleOperation)
|
||||
.attach_printable("User not found in our records".to_string())?;
|
||||
|
||||
if user_from_token.user_id == user_to_be_updated.get_user_id() {
|
||||
return Err(UserErrors::InvalidRoleOperation.into())
|
||||
.attach_printable("User transferring ownership to themselves".to_string());
|
||||
}
|
||||
|
||||
state
|
||||
.store
|
||||
.transfer_org_ownership_between_users(
|
||||
&user_from_token.user_id,
|
||||
user_to_be_updated.get_user_id(),
|
||||
&user_from_token.org_id,
|
||||
)
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)?;
|
||||
|
||||
auth::blacklist::insert_user_in_blacklist(&state, user_to_be_updated.get_user_id()).await?;
|
||||
auth::blacklist::insert_user_in_blacklist(&state, &user_from_token.user_id).await?;
|
||||
|
||||
let user_from_db = domain::UserFromStorage::from(user_from_token.get_user(&state).await?);
|
||||
let user_role = user_from_db
|
||||
.get_role_from_db_by_merchant_id(&state, &user_from_token.merchant_id)
|
||||
.await
|
||||
.to_not_found_response(UserErrors::InvalidRoleOperation)?;
|
||||
|
||||
let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
|
||||
|
||||
Ok(ApplicationResponse::Json(
|
||||
utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn accept_invitation(
|
||||
state: AppState,
|
||||
user_token: auth::UserWithoutMerchantFromToken,
|
||||
|
||||
@ -1965,6 +1965,17 @@ impl UserRoleInterface for KafkaStore {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn update_user_roles_by_user_id_org_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
org_id: &str,
|
||||
update: user_storage::UserRoleUpdate,
|
||||
) -> CustomResult<Vec<user_storage::UserRole>, errors::StorageError> {
|
||||
self.diesel_store
|
||||
.update_user_roles_by_user_id_org_id(user_id, org_id, update)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn delete_user_role_by_user_id_merchant_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
@ -1981,6 +1992,17 @@ impl UserRoleInterface for KafkaStore {
|
||||
) -> CustomResult<Vec<user_storage::UserRole>, errors::StorageError> {
|
||||
self.diesel_store.list_user_roles_by_user_id(user_id).await
|
||||
}
|
||||
|
||||
async fn transfer_org_ownership_between_users(
|
||||
&self,
|
||||
from_user_id: &str,
|
||||
to_user_id: &str,
|
||||
org_id: &str,
|
||||
) -> CustomResult<(), errors::StorageError> {
|
||||
self.diesel_store
|
||||
.transfer_org_ownership_between_users(from_user_id, to_user_id, org_id)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
use diesel_models::user_role as storage;
|
||||
use std::{collections::HashSet, ops::Not};
|
||||
|
||||
use async_bb8_diesel::AsyncConnection;
|
||||
use diesel_models::{enums, user_role as storage};
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
|
||||
use super::MockDb;
|
||||
use crate::{
|
||||
connection,
|
||||
connection, consts,
|
||||
core::errors::{self, CustomResult},
|
||||
services::Store,
|
||||
};
|
||||
@ -32,6 +35,14 @@ pub trait UserRoleInterface {
|
||||
merchant_id: &str,
|
||||
update: storage::UserRoleUpdate,
|
||||
) -> CustomResult<storage::UserRole, errors::StorageError>;
|
||||
|
||||
async fn update_user_roles_by_user_id_org_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
org_id: &str,
|
||||
update: storage::UserRoleUpdate,
|
||||
) -> CustomResult<Vec<storage::UserRole>, errors::StorageError>;
|
||||
|
||||
async fn delete_user_role_by_user_id_merchant_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
@ -42,6 +53,13 @@ pub trait UserRoleInterface {
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> CustomResult<Vec<storage::UserRole>, errors::StorageError>;
|
||||
|
||||
async fn transfer_org_ownership_between_users(
|
||||
&self,
|
||||
from_user_id: &str,
|
||||
to_user_id: &str,
|
||||
org_id: &str,
|
||||
) -> CustomResult<(), errors::StorageError>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@ -103,6 +121,24 @@ impl UserRoleInterface for Store {
|
||||
.into_report()
|
||||
}
|
||||
|
||||
async fn update_user_roles_by_user_id_org_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
org_id: &str,
|
||||
update: storage::UserRoleUpdate,
|
||||
) -> CustomResult<Vec<storage::UserRole>, errors::StorageError> {
|
||||
let conn = connection::pg_connection_write(self).await?;
|
||||
storage::UserRole::update_by_user_id_org_id(
|
||||
&conn,
|
||||
user_id.to_owned(),
|
||||
org_id.to_owned(),
|
||||
update,
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.into_report()
|
||||
}
|
||||
|
||||
async fn delete_user_role_by_user_id_merchant_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
@ -129,6 +165,86 @@ impl UserRoleInterface for Store {
|
||||
.map_err(Into::into)
|
||||
.into_report()
|
||||
}
|
||||
|
||||
async fn transfer_org_ownership_between_users(
|
||||
&self,
|
||||
from_user_id: &str,
|
||||
to_user_id: &str,
|
||||
org_id: &str,
|
||||
) -> CustomResult<(), errors::StorageError> {
|
||||
let conn = connection::pg_connection_write(self)
|
||||
.await
|
||||
.change_context(errors::StorageError::DatabaseConnectionError)?;
|
||||
|
||||
conn.transaction_async(|conn| async move {
|
||||
let old_org_admin_user_roles = storage::UserRole::update_by_user_id_org_id(
|
||||
&conn,
|
||||
from_user_id.to_owned(),
|
||||
org_id.to_owned(),
|
||||
storage::UserRoleUpdate::UpdateRole {
|
||||
role_id: consts::user_role::ROLE_ID_MERCHANT_ADMIN.to_string(),
|
||||
modified_by: from_user_id.to_owned(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|e| *e.current_context())?;
|
||||
|
||||
let new_org_admin_user_roles = storage::UserRole::update_by_user_id_org_id(
|
||||
&conn,
|
||||
to_user_id.to_owned(),
|
||||
org_id.to_owned(),
|
||||
storage::UserRoleUpdate::UpdateRole {
|
||||
role_id: consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
|
||||
modified_by: from_user_id.to_owned(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|e| *e.current_context())?;
|
||||
|
||||
let new_org_admin_merchant_ids = new_org_admin_user_roles
|
||||
.iter()
|
||||
.map(|user_role| user_role.merchant_id.to_owned())
|
||||
.collect::<HashSet<String>>();
|
||||
|
||||
let now = common_utils::date_time::now();
|
||||
|
||||
let missing_new_user_roles =
|
||||
old_org_admin_user_roles.into_iter().filter_map(|old_role| {
|
||||
new_org_admin_merchant_ids
|
||||
.contains(&old_role.merchant_id)
|
||||
.not()
|
||||
.then_some({
|
||||
storage::UserRoleNew {
|
||||
user_id: to_user_id.to_string(),
|
||||
merchant_id: old_role.merchant_id,
|
||||
role_id: consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
|
||||
org_id: org_id.to_string(),
|
||||
status: enums::UserStatus::Active,
|
||||
created_by: from_user_id.to_string(),
|
||||
last_modified_by: from_user_id.to_string(),
|
||||
created_at: now,
|
||||
last_modified: now,
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
futures::future::try_join_all(missing_new_user_roles.map(|user_role| async {
|
||||
user_role
|
||||
.insert(&conn)
|
||||
.await
|
||||
.map_err(|e| *e.current_context())
|
||||
}))
|
||||
.await?;
|
||||
|
||||
Ok::<_, errors::DatabaseError>(())
|
||||
})
|
||||
.await
|
||||
.into_report()
|
||||
.map_err(Into::into)
|
||||
.into_report()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@ -241,6 +357,107 @@ impl UserRoleInterface for MockDb {
|
||||
)
|
||||
}
|
||||
|
||||
async fn update_user_roles_by_user_id_org_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
org_id: &str,
|
||||
update: storage::UserRoleUpdate,
|
||||
) -> CustomResult<Vec<storage::UserRole>, errors::StorageError> {
|
||||
let mut user_roles = self.user_roles.lock().await;
|
||||
let mut updated_user_roles = Vec::new();
|
||||
for user_role in user_roles.iter_mut() {
|
||||
if user_role.user_id == user_id && user_role.org_id == org_id {
|
||||
match &update {
|
||||
storage::UserRoleUpdate::UpdateRole {
|
||||
role_id,
|
||||
modified_by,
|
||||
} => {
|
||||
user_role.role_id = role_id.to_string();
|
||||
user_role.last_modified_by = modified_by.to_string();
|
||||
}
|
||||
storage::UserRoleUpdate::UpdateStatus {
|
||||
status,
|
||||
modified_by,
|
||||
} => {
|
||||
user_role.status = status.to_owned();
|
||||
user_role.last_modified_by = modified_by.to_owned();
|
||||
}
|
||||
}
|
||||
updated_user_roles.push(user_role.to_owned());
|
||||
}
|
||||
}
|
||||
if updated_user_roles.is_empty() {
|
||||
Err(errors::StorageError::ValueNotFound(format!(
|
||||
"No user role available for user_id = {user_id} and org_id = {org_id}"
|
||||
))
|
||||
.into())
|
||||
} else {
|
||||
Ok(updated_user_roles)
|
||||
}
|
||||
}
|
||||
|
||||
async fn transfer_org_ownership_between_users(
|
||||
&self,
|
||||
from_user_id: &str,
|
||||
to_user_id: &str,
|
||||
org_id: &str,
|
||||
) -> CustomResult<(), errors::StorageError> {
|
||||
let old_org_admin_user_roles = self
|
||||
.update_user_roles_by_user_id_org_id(
|
||||
from_user_id,
|
||||
org_id,
|
||||
storage::UserRoleUpdate::UpdateRole {
|
||||
role_id: consts::user_role::ROLE_ID_MERCHANT_ADMIN.to_string(),
|
||||
modified_by: from_user_id.to_string(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let new_org_admin_user_roles = self
|
||||
.update_user_roles_by_user_id_org_id(
|
||||
to_user_id,
|
||||
org_id,
|
||||
storage::UserRoleUpdate::UpdateRole {
|
||||
role_id: consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
|
||||
modified_by: from_user_id.to_string(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let new_org_admin_merchant_ids = new_org_admin_user_roles
|
||||
.iter()
|
||||
.map(|user_role| user_role.merchant_id.to_owned())
|
||||
.collect::<HashSet<String>>();
|
||||
|
||||
let now = common_utils::date_time::now();
|
||||
|
||||
let missing_new_user_roles = old_org_admin_user_roles
|
||||
.into_iter()
|
||||
.filter_map(|old_roles| {
|
||||
if !new_org_admin_merchant_ids.contains(&old_roles.merchant_id) {
|
||||
Some(storage::UserRoleNew {
|
||||
user_id: to_user_id.to_string(),
|
||||
merchant_id: old_roles.merchant_id,
|
||||
role_id: consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
|
||||
org_id: org_id.to_string(),
|
||||
status: enums::UserStatus::Active,
|
||||
created_by: from_user_id.to_string(),
|
||||
last_modified_by: from_user_id.to_string(),
|
||||
created_at: now,
|
||||
last_modified: now,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
for user_role in missing_new_user_roles {
|
||||
self.insert_user_role(user_role).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_user_role_by_user_id_merchant_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
|
||||
@ -1026,6 +1026,10 @@ impl User {
|
||||
)
|
||||
.service(web::resource("/invite/accept").route(web::post().to(accept_invitation)))
|
||||
.service(web::resource("/update_role").route(web::post().to(update_user_role)))
|
||||
.service(
|
||||
web::resource("/transfer_ownership")
|
||||
.route(web::post().to(transfer_org_ownership)),
|
||||
)
|
||||
.service(web::resource("/delete").route(web::delete().to(delete_user_role))),
|
||||
);
|
||||
|
||||
|
||||
@ -196,7 +196,8 @@ impl From<Flow> for ApiIdentifier {
|
||||
| Flow::UpdateUserRole
|
||||
| Flow::GetAuthorizationInfo
|
||||
| Flow::AcceptInvitation
|
||||
| Flow::DeleteUserRole => Self::UserRole,
|
||||
| Flow::DeleteUserRole
|
||||
| Flow::TransferOrgOwnership => Self::UserRole,
|
||||
|
||||
Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => {
|
||||
Self::ConnectorOnboarding
|
||||
|
||||
@ -97,6 +97,25 @@ pub async fn update_user_role(
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn transfer_org_ownership(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
json_payload: web::Json<user_role_api::TransferOrgOwnershipRequest>,
|
||||
) -> HttpResponse {
|
||||
let flow = Flow::TransferOrgOwnership;
|
||||
let payload = json_payload.into_inner();
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state.clone(),
|
||||
&req,
|
||||
payload,
|
||||
user_role_core::transfer_org_ownership,
|
||||
&auth::JWTAuth(Permission::UsersWrite),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn accept_invitation(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
|
||||
@ -311,6 +311,8 @@ pub enum Flow {
|
||||
GetRoleFromToken,
|
||||
/// Update user role
|
||||
UpdateUserRole,
|
||||
/// Transfer organization ownership
|
||||
TransferOrgOwnership,
|
||||
/// Create merchant account for user in a org
|
||||
UserMerchantAccountCreate,
|
||||
/// Generate Sample Data
|
||||
|
||||
Reference in New Issue
Block a user