mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-01 02:57:02 +08:00
feat(users): Add preferred_merchant_id column and update user details API (#3373)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -13,7 +13,8 @@ use crate::user::{
|
|||||||
AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest,
|
AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest,
|
||||||
DashboardEntryResponse, ForgotPasswordRequest, GetUsersResponse, InviteUserRequest,
|
DashboardEntryResponse, ForgotPasswordRequest, GetUsersResponse, InviteUserRequest,
|
||||||
InviteUserResponse, ResetPasswordRequest, SendVerifyEmailRequest, SignUpRequest,
|
InviteUserResponse, ResetPasswordRequest, SendVerifyEmailRequest, SignUpRequest,
|
||||||
SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, UserMerchantCreate, VerifyEmailRequest,
|
SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, UpdateUserAccountDetailsRequest,
|
||||||
|
UserMerchantCreate, VerifyEmailRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl ApiEventMetric for DashboardEntryResponse {
|
impl ApiEventMetric for DashboardEntryResponse {
|
||||||
@ -54,7 +55,8 @@ common_utils::impl_misc_api_event_type!(
|
|||||||
InviteUserRequest,
|
InviteUserRequest,
|
||||||
InviteUserResponse,
|
InviteUserResponse,
|
||||||
VerifyEmailRequest,
|
VerifyEmailRequest,
|
||||||
SendVerifyEmailRequest
|
SendVerifyEmailRequest,
|
||||||
|
UpdateUserAccountDetailsRequest
|
||||||
);
|
);
|
||||||
|
|
||||||
#[cfg(feature = "dummy_connector")]
|
#[cfg(feature = "dummy_connector")]
|
||||||
|
|||||||
@ -147,3 +147,9 @@ pub struct VerifyTokenResponse {
|
|||||||
pub merchant_id: String,
|
pub merchant_id: String,
|
||||||
pub user_email: pii::Email,
|
pub user_email: pii::Email,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct UpdateUserAccountDetailsRequest {
|
||||||
|
pub name: Option<Secret<String>>,
|
||||||
|
pub preferred_merchant_id: Option<String>,
|
||||||
|
}
|
||||||
|
|||||||
@ -19,6 +19,20 @@ impl UserRole {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn find_by_user_id_merchant_id(
|
||||||
|
conn: &PgPooledConn,
|
||||||
|
user_id: String,
|
||||||
|
merchant_id: String,
|
||||||
|
) -> StorageResult<Self> {
|
||||||
|
generics::generic_find_one::<<Self as HasTable>::Table, _, _>(
|
||||||
|
conn,
|
||||||
|
dsl::user_id
|
||||||
|
.eq(user_id)
|
||||||
|
.and(dsl::merchant_id.eq(merchant_id)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn update_by_user_id_merchant_id(
|
pub async fn update_by_user_id_merchant_id(
|
||||||
conn: &PgPooledConn,
|
conn: &PgPooledConn,
|
||||||
user_id: String,
|
user_id: String,
|
||||||
|
|||||||
@ -1056,6 +1056,8 @@ diesel::table! {
|
|||||||
is_verified -> Bool,
|
is_verified -> Bool,
|
||||||
created_at -> Timestamp,
|
created_at -> Timestamp,
|
||||||
last_modified_at -> Timestamp,
|
last_modified_at -> Timestamp,
|
||||||
|
#[max_length = 64]
|
||||||
|
preferred_merchant_id -> Nullable<Varchar>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@ pub struct User {
|
|||||||
pub is_verified: bool,
|
pub is_verified: bool,
|
||||||
pub created_at: PrimitiveDateTime,
|
pub created_at: PrimitiveDateTime,
|
||||||
pub last_modified_at: PrimitiveDateTime,
|
pub last_modified_at: PrimitiveDateTime,
|
||||||
|
pub preferred_merchant_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
@ -33,6 +34,7 @@ pub struct UserNew {
|
|||||||
pub is_verified: bool,
|
pub is_verified: bool,
|
||||||
pub created_at: Option<PrimitiveDateTime>,
|
pub created_at: Option<PrimitiveDateTime>,
|
||||||
pub last_modified_at: Option<PrimitiveDateTime>,
|
pub last_modified_at: Option<PrimitiveDateTime>,
|
||||||
|
pub preferred_merchant_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)]
|
#[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)]
|
||||||
@ -42,6 +44,7 @@ pub struct UserUpdateInternal {
|
|||||||
password: Option<Secret<String>>,
|
password: Option<Secret<String>>,
|
||||||
is_verified: Option<bool>,
|
is_verified: Option<bool>,
|
||||||
last_modified_at: PrimitiveDateTime,
|
last_modified_at: PrimitiveDateTime,
|
||||||
|
preferred_merchant_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -51,6 +54,7 @@ pub enum UserUpdate {
|
|||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
password: Option<Secret<String>>,
|
password: Option<Secret<String>>,
|
||||||
is_verified: Option<bool>,
|
is_verified: Option<bool>,
|
||||||
|
preferred_merchant_id: Option<String>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,16 +67,19 @@ impl From<UserUpdate> for UserUpdateInternal {
|
|||||||
password: None,
|
password: None,
|
||||||
is_verified: Some(true),
|
is_verified: Some(true),
|
||||||
last_modified_at,
|
last_modified_at,
|
||||||
|
preferred_merchant_id: None,
|
||||||
},
|
},
|
||||||
UserUpdate::AccountUpdate {
|
UserUpdate::AccountUpdate {
|
||||||
name,
|
name,
|
||||||
password,
|
password,
|
||||||
is_verified,
|
is_verified,
|
||||||
|
preferred_merchant_id,
|
||||||
} => Self {
|
} => Self {
|
||||||
name,
|
name,
|
||||||
password,
|
password,
|
||||||
is_verified,
|
is_verified,
|
||||||
last_modified_at,
|
last_modified_at,
|
||||||
|
preferred_merchant_id,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -253,6 +253,7 @@ pub async fn change_password(
|
|||||||
name: None,
|
name: None,
|
||||||
password: Some(new_password_hash),
|
password: Some(new_password_hash),
|
||||||
is_verified: None,
|
is_verified: None,
|
||||||
|
preferred_merchant_id: None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@ -330,6 +331,7 @@ pub async fn reset_password(
|
|||||||
name: None,
|
name: None,
|
||||||
password: Some(hash_password),
|
password: Some(hash_password),
|
||||||
is_verified: Some(true),
|
is_verified: Some(true),
|
||||||
|
preferred_merchant_id: None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@ -786,3 +788,47 @@ pub async fn verify_token(
|
|||||||
user_email: user.email,
|
user_email: user.email,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn update_user_details(
|
||||||
|
state: AppState,
|
||||||
|
user_token: auth::UserFromToken,
|
||||||
|
req: user_api::UpdateUserAccountDetailsRequest,
|
||||||
|
) -> UserResponse<()> {
|
||||||
|
let user: domain::UserFromStorage = state
|
||||||
|
.store
|
||||||
|
.find_user_by_id(&user_token.user_id)
|
||||||
|
.await
|
||||||
|
.change_context(UserErrors::InternalServerError)?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let name = req.name.map(domain::UserName::new).transpose()?;
|
||||||
|
|
||||||
|
if let Some(ref preferred_merchant_id) = req.preferred_merchant_id {
|
||||||
|
let _ = state
|
||||||
|
.store
|
||||||
|
.find_user_role_by_user_id_merchant_id(user.get_user_id(), preferred_merchant_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
if e.current_context().is_db_not_found() {
|
||||||
|
e.change_context(UserErrors::MerchantIdNotFound)
|
||||||
|
} else {
|
||||||
|
e.change_context(UserErrors::InternalServerError)
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_update = storage_user::UserUpdate::AccountUpdate {
|
||||||
|
name: name.map(|x| x.get_secret().expose()),
|
||||||
|
password: None,
|
||||||
|
is_verified: None,
|
||||||
|
preferred_merchant_id: req.preferred_merchant_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
state
|
||||||
|
.store
|
||||||
|
.update_user_by_user_id(user.get_user_id(), user_update)
|
||||||
|
.await
|
||||||
|
.change_context(UserErrors::InternalServerError)?;
|
||||||
|
|
||||||
|
Ok(ApplicationResponse::StatusOk)
|
||||||
|
}
|
||||||
|
|||||||
@ -1927,12 +1927,24 @@ impl UserRoleInterface for KafkaStore {
|
|||||||
) -> CustomResult<user_storage::UserRole, errors::StorageError> {
|
) -> CustomResult<user_storage::UserRole, errors::StorageError> {
|
||||||
self.diesel_store.insert_user_role(user_role).await
|
self.diesel_store.insert_user_role(user_role).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn find_user_role_by_user_id(
|
async fn find_user_role_by_user_id(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
) -> CustomResult<user_storage::UserRole, errors::StorageError> {
|
) -> CustomResult<user_storage::UserRole, errors::StorageError> {
|
||||||
self.diesel_store.find_user_role_by_user_id(user_id).await
|
self.diesel_store.find_user_role_by_user_id(user_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn find_user_role_by_user_id_merchant_id(
|
||||||
|
&self,
|
||||||
|
user_id: &str,
|
||||||
|
merchant_id: &str,
|
||||||
|
) -> CustomResult<user_storage::UserRole, errors::StorageError> {
|
||||||
|
self.diesel_store
|
||||||
|
.find_user_role_by_user_id_merchant_id(user_id, merchant_id)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
async fn update_user_role_by_user_id_merchant_id(
|
async fn update_user_role_by_user_id_merchant_id(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
@ -1943,9 +1955,11 @@ impl UserRoleInterface for KafkaStore {
|
|||||||
.update_user_role_by_user_id_merchant_id(user_id, merchant_id, update)
|
.update_user_role_by_user_id_merchant_id(user_id, merchant_id, update)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_user_role(&self, user_id: &str) -> CustomResult<bool, errors::StorageError> {
|
async fn delete_user_role(&self, user_id: &str) -> CustomResult<bool, errors::StorageError> {
|
||||||
self.diesel_store.delete_user_role(user_id).await
|
self.diesel_store.delete_user_role(user_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_user_roles_by_user_id(
|
async fn list_user_roles_by_user_id(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
|
|||||||
@ -145,6 +145,7 @@ impl UserInterface for MockDb {
|
|||||||
is_verified: user_data.is_verified,
|
is_verified: user_data.is_verified,
|
||||||
created_at: user_data.created_at.unwrap_or(time_now),
|
created_at: user_data.created_at.unwrap_or(time_now),
|
||||||
last_modified_at: user_data.created_at.unwrap_or(time_now),
|
last_modified_at: user_data.created_at.unwrap_or(time_now),
|
||||||
|
preferred_merchant_id: user_data.preferred_merchant_id,
|
||||||
};
|
};
|
||||||
users.push(user.clone());
|
users.push(user.clone());
|
||||||
Ok(user)
|
Ok(user)
|
||||||
@ -207,10 +208,14 @@ impl UserInterface for MockDb {
|
|||||||
name,
|
name,
|
||||||
password,
|
password,
|
||||||
is_verified,
|
is_verified,
|
||||||
|
preferred_merchant_id,
|
||||||
} => storage::User {
|
} => storage::User {
|
||||||
name: name.clone().map(Secret::new).unwrap_or(user.name.clone()),
|
name: name.clone().map(Secret::new).unwrap_or(user.name.clone()),
|
||||||
password: password.clone().unwrap_or(user.password.clone()),
|
password: password.clone().unwrap_or(user.password.clone()),
|
||||||
is_verified: is_verified.unwrap_or(user.is_verified),
|
is_verified: is_verified.unwrap_or(user.is_verified),
|
||||||
|
preferred_merchant_id: preferred_merchant_id
|
||||||
|
.clone()
|
||||||
|
.or(user.preferred_merchant_id.clone()),
|
||||||
..user.to_owned()
|
..user.to_owned()
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,16 +14,25 @@ pub trait UserRoleInterface {
|
|||||||
&self,
|
&self,
|
||||||
user_role: storage::UserRoleNew,
|
user_role: storage::UserRoleNew,
|
||||||
) -> CustomResult<storage::UserRole, errors::StorageError>;
|
) -> CustomResult<storage::UserRole, errors::StorageError>;
|
||||||
|
|
||||||
async fn find_user_role_by_user_id(
|
async fn find_user_role_by_user_id(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
) -> CustomResult<storage::UserRole, errors::StorageError>;
|
) -> CustomResult<storage::UserRole, errors::StorageError>;
|
||||||
|
|
||||||
|
async fn find_user_role_by_user_id_merchant_id(
|
||||||
|
&self,
|
||||||
|
user_id: &str,
|
||||||
|
merchant_id: &str,
|
||||||
|
) -> CustomResult<storage::UserRole, errors::StorageError>;
|
||||||
|
|
||||||
async fn update_user_role_by_user_id_merchant_id(
|
async fn update_user_role_by_user_id_merchant_id(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
merchant_id: &str,
|
merchant_id: &str,
|
||||||
update: storage::UserRoleUpdate,
|
update: storage::UserRoleUpdate,
|
||||||
) -> CustomResult<storage::UserRole, errors::StorageError>;
|
) -> CustomResult<storage::UserRole, errors::StorageError>;
|
||||||
|
|
||||||
async fn delete_user_role(&self, user_id: &str) -> CustomResult<bool, errors::StorageError>;
|
async fn delete_user_role(&self, user_id: &str) -> CustomResult<bool, errors::StorageError>;
|
||||||
|
|
||||||
async fn list_user_roles_by_user_id(
|
async fn list_user_roles_by_user_id(
|
||||||
@ -57,6 +66,22 @@ impl UserRoleInterface for Store {
|
|||||||
.into_report()
|
.into_report()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn find_user_role_by_user_id_merchant_id(
|
||||||
|
&self,
|
||||||
|
user_id: &str,
|
||||||
|
merchant_id: &str,
|
||||||
|
) -> CustomResult<storage::UserRole, errors::StorageError> {
|
||||||
|
let conn = connection::pg_connection_write(self).await?;
|
||||||
|
storage::UserRole::find_by_user_id_merchant_id(
|
||||||
|
&conn,
|
||||||
|
user_id.to_owned(),
|
||||||
|
merchant_id.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
.into_report()
|
||||||
|
}
|
||||||
|
|
||||||
async fn update_user_role_by_user_id_merchant_id(
|
async fn update_user_role_by_user_id_merchant_id(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
@ -148,6 +173,24 @@ impl UserRoleInterface for MockDb {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn find_user_role_by_user_id_merchant_id(
|
||||||
|
&self,
|
||||||
|
user_id: &str,
|
||||||
|
merchant_id: &str,
|
||||||
|
) -> CustomResult<storage::UserRole, errors::StorageError> {
|
||||||
|
let user_roles = self.user_roles.lock().await;
|
||||||
|
user_roles
|
||||||
|
.iter()
|
||||||
|
.find(|user_role| user_role.user_id == user_id && user_role.merchant_id == merchant_id)
|
||||||
|
.cloned()
|
||||||
|
.ok_or(
|
||||||
|
errors::StorageError::ValueNotFound(format!(
|
||||||
|
"No user role available for user_id = {user_id} and merchant_id = {merchant_id}"
|
||||||
|
))
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async fn update_user_role_by_user_id_merchant_id(
|
async fn update_user_role_by_user_id_merchant_id(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
|
|||||||
@ -921,6 +921,7 @@ impl User {
|
|||||||
.service(web::resource("/role/list").route(web::get().to(list_roles)))
|
.service(web::resource("/role/list").route(web::get().to(list_roles)))
|
||||||
.service(web::resource("/role/{role_id}").route(web::get().to(get_role)))
|
.service(web::resource("/role/{role_id}").route(web::get().to(get_role)))
|
||||||
.service(web::resource("/user/invite").route(web::post().to(invite_user)))
|
.service(web::resource("/user/invite").route(web::post().to(invite_user)))
|
||||||
|
.service(web::resource("/update").route(web::post().to(update_user_account_details)))
|
||||||
.service(
|
.service(
|
||||||
web::resource("/data")
|
web::resource("/data")
|
||||||
.route(web::get().to(get_multiple_dashboard_metadata))
|
.route(web::get().to(get_multiple_dashboard_metadata))
|
||||||
|
|||||||
@ -178,7 +178,8 @@ impl From<Flow> for ApiIdentifier {
|
|||||||
| Flow::InviteUser
|
| Flow::InviteUser
|
||||||
| Flow::UserSignUpWithMerchantId
|
| Flow::UserSignUpWithMerchantId
|
||||||
| Flow::VerifyEmail
|
| Flow::VerifyEmail
|
||||||
| Flow::VerifyEmailRequest => Self::User,
|
| Flow::VerifyEmailRequest
|
||||||
|
| Flow::UpdateUserAccountDetails => Self::User,
|
||||||
|
|
||||||
Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => {
|
Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => {
|
||||||
Self::UserRole
|
Self::UserRole
|
||||||
|
|||||||
@ -403,3 +403,21 @@ pub async fn verify_recon_token(state: web::Data<AppState>, http_req: HttpReques
|
|||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn update_user_account_details(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
req: HttpRequest,
|
||||||
|
json_payload: web::Json<user_api::UpdateUserAccountDetailsRequest>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let flow = Flow::UpdateUserAccountDetails;
|
||||||
|
Box::pin(api::server_wrap(
|
||||||
|
flow,
|
||||||
|
state.clone(),
|
||||||
|
&req,
|
||||||
|
json_payload.into_inner(),
|
||||||
|
user_core::update_user_details,
|
||||||
|
&auth::DashboardNoPermissionAuth,
|
||||||
|
api_locking::LockAction::NotApplicable,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|||||||
@ -331,6 +331,8 @@ pub enum Flow {
|
|||||||
VerifyEmail,
|
VerifyEmail,
|
||||||
/// Send verify email
|
/// Send verify email
|
||||||
VerifyEmailRequest,
|
VerifyEmailRequest,
|
||||||
|
/// Update user account details
|
||||||
|
UpdateUserAccountDetails,
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
ALTER TABLE users DROP COLUMN preferred_merchant_id;
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
-- Your SQL goes here
|
||||||
|
ALTER TABLE users ADD COLUMN preferred_merchant_id VARCHAR(64);
|
||||||
Reference in New Issue
Block a user