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:
Mani Chandra
2024-01-18 18:44:20 +05:30
committed by GitHub
parent 975986d966
commit 862a1b5303
15 changed files with 168 additions and 3 deletions

View File

@ -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")]

View File

@ -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>,
}

View File

@ -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,

View File

@ -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>,
} }
} }

View File

@ -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,
}, },
} }
} }

View File

@ -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)
}

View File

@ -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,

View File

@ -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()
}, },
}; };

View File

@ -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,

View File

@ -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))

View File

@ -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

View File

@ -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
}

View File

@ -331,6 +331,8 @@ pub enum Flow {
VerifyEmail, VerifyEmail,
/// Send verify email /// Send verify email
VerifyEmailRequest, VerifyEmailRequest,
/// Update user account details
UpdateUserAccountDetails,
} }
/// ///

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
ALTER TABLE users DROP COLUMN preferred_merchant_id;

View File

@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE users ADD COLUMN preferred_merchant_id VARCHAR(64);