diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index c0743c8b8fc0..40d082d1cade 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -13,7 +13,8 @@ use crate::user::{ AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest, DashboardEntryResponse, ForgotPasswordRequest, GetUsersResponse, InviteUserRequest, InviteUserResponse, ResetPasswordRequest, SendVerifyEmailRequest, SignUpRequest, - SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, UserMerchantCreate, VerifyEmailRequest, + SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, UpdateUserAccountDetailsRequest, + UserMerchantCreate, VerifyEmailRequest, }; impl ApiEventMetric for DashboardEntryResponse { @@ -54,7 +55,8 @@ common_utils::impl_misc_api_event_type!( InviteUserRequest, InviteUserResponse, VerifyEmailRequest, - SendVerifyEmailRequest + SendVerifyEmailRequest, + UpdateUserAccountDetailsRequest ); #[cfg(feature = "dummy_connector")] diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index a04c4fef6601..8de6a3c0b4fa 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -147,3 +147,9 @@ pub struct VerifyTokenResponse { pub merchant_id: String, pub user_email: pii::Email, } + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct UpdateUserAccountDetailsRequest { + pub name: Option>, + pub preferred_merchant_id: Option, +} diff --git a/crates/diesel_models/src/query/user_role.rs b/crates/diesel_models/src/query/user_role.rs index d2f9564a5309..6b408038ef55 100644 --- a/crates/diesel_models/src/query/user_role.rs +++ b/crates/diesel_models/src/query/user_role.rs @@ -19,6 +19,20 @@ impl UserRole { .await } + pub async fn find_by_user_id_merchant_id( + conn: &PgPooledConn, + user_id: String, + merchant_id: String, + ) -> StorageResult { + generics::generic_find_one::<::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( conn: &PgPooledConn, user_id: String, diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 131d2b182661..c9887e1770fc 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -1056,6 +1056,8 @@ diesel::table! { is_verified -> Bool, created_at -> Timestamp, last_modified_at -> Timestamp, + #[max_length = 64] + preferred_merchant_id -> Nullable, } } diff --git a/crates/diesel_models/src/user.rs b/crates/diesel_models/src/user.rs index c608f2654c6a..84fe8710060e 100644 --- a/crates/diesel_models/src/user.rs +++ b/crates/diesel_models/src/user.rs @@ -19,6 +19,7 @@ pub struct User { pub is_verified: bool, pub created_at: PrimitiveDateTime, pub last_modified_at: PrimitiveDateTime, + pub preferred_merchant_id: Option, } #[derive( @@ -33,6 +34,7 @@ pub struct UserNew { pub is_verified: bool, pub created_at: Option, pub last_modified_at: Option, + pub preferred_merchant_id: Option, } #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] @@ -42,6 +44,7 @@ pub struct UserUpdateInternal { password: Option>, is_verified: Option, last_modified_at: PrimitiveDateTime, + preferred_merchant_id: Option, } #[derive(Debug)] @@ -51,6 +54,7 @@ pub enum UserUpdate { name: Option, password: Option>, is_verified: Option, + preferred_merchant_id: Option, }, } @@ -63,16 +67,19 @@ impl From for UserUpdateInternal { password: None, is_verified: Some(true), last_modified_at, + preferred_merchant_id: None, }, UserUpdate::AccountUpdate { name, password, is_verified, + preferred_merchant_id, } => Self { name, password, is_verified, last_modified_at, + preferred_merchant_id, }, } } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 27a4f67618e4..729cef65c20a 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -253,6 +253,7 @@ pub async fn change_password( name: None, password: Some(new_password_hash), is_verified: None, + preferred_merchant_id: None, }, ) .await @@ -330,6 +331,7 @@ pub async fn reset_password( name: None, password: Some(hash_password), is_verified: Some(true), + preferred_merchant_id: None, }, ) .await @@ -786,3 +788,47 @@ pub async fn verify_token( 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) +} diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 19a83088a06f..8398c153156d 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -1927,12 +1927,24 @@ impl UserRoleInterface for KafkaStore { ) -> CustomResult { self.diesel_store.insert_user_role(user_role).await } + async fn find_user_role_by_user_id( &self, user_id: &str, ) -> CustomResult { 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 { + 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( &self, user_id: &str, @@ -1943,9 +1955,11 @@ 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 { self.diesel_store.delete_user_role(user_id).await } + async fn list_user_roles_by_user_id( &self, user_id: &str, diff --git a/crates/router/src/db/user.rs b/crates/router/src/db/user.rs index e3dda965f9c9..ecd71f7e2c9b 100644 --- a/crates/router/src/db/user.rs +++ b/crates/router/src/db/user.rs @@ -145,6 +145,7 @@ impl UserInterface for MockDb { is_verified: user_data.is_verified, created_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()); Ok(user) @@ -207,10 +208,14 @@ impl UserInterface for MockDb { name, password, is_verified, + preferred_merchant_id, } => storage::User { name: name.clone().map(Secret::new).unwrap_or(user.name.clone()), password: password.clone().unwrap_or(user.password.clone()), 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() }, }; diff --git a/crates/router/src/db/user_role.rs b/crates/router/src/db/user_role.rs index bf84ae134ea7..d8938f9683da 100644 --- a/crates/router/src/db/user_role.rs +++ b/crates/router/src/db/user_role.rs @@ -14,16 +14,25 @@ pub trait UserRoleInterface { &self, user_role: storage::UserRoleNew, ) -> CustomResult; + async fn find_user_role_by_user_id( &self, user_id: &str, ) -> CustomResult; + + async fn find_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult; + async fn update_user_role_by_user_id_merchant_id( &self, user_id: &str, merchant_id: &str, update: storage::UserRoleUpdate, ) -> CustomResult; + async fn delete_user_role(&self, user_id: &str) -> CustomResult; async fn list_user_roles_by_user_id( @@ -57,6 +66,22 @@ impl UserRoleInterface for Store { .into_report() } + async fn find_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult { + 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( &self, 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 { + 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( &self, user_id: &str, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 0c489dbe63a7..0807fb0800e1 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -921,6 +921,7 @@ impl User { .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("/user/invite").route(web::post().to(invite_user))) + .service(web::resource("/update").route(web::post().to(update_user_account_details))) .service( web::resource("/data") .route(web::get().to(get_multiple_dashboard_metadata)) diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 12cf76be4759..805fb1152647 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -178,7 +178,8 @@ impl From for ApiIdentifier { | Flow::InviteUser | Flow::UserSignUpWithMerchantId | Flow::VerifyEmail - | Flow::VerifyEmailRequest => Self::User, + | Flow::VerifyEmailRequest + | Flow::UpdateUserAccountDetails => Self::User, Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => { Self::UserRole diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 976fd5c9f564..eca32318adf6 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -403,3 +403,21 @@ pub async fn verify_recon_token(state: web::Data, http_req: HttpReques )) .await } + +pub async fn update_user_account_details( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> 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 +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 0d6636e567da..c4e0aa3f3ea2 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -331,6 +331,8 @@ pub enum Flow { VerifyEmail, /// Send verify email VerifyEmailRequest, + /// Update user account details + UpdateUserAccountDetails, } /// diff --git a/migrations/2024-01-02-111223_users_preferred_merchant_column/down.sql b/migrations/2024-01-02-111223_users_preferred_merchant_column/down.sql new file mode 100644 index 000000000000..b9160b6f1052 --- /dev/null +++ b/migrations/2024-01-02-111223_users_preferred_merchant_column/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE users DROP COLUMN preferred_merchant_id; diff --git a/migrations/2024-01-02-111223_users_preferred_merchant_column/up.sql b/migrations/2024-01-02-111223_users_preferred_merchant_column/up.sql new file mode 100644 index 000000000000..77567ce93faf --- /dev/null +++ b/migrations/2024-01-02-111223_users_preferred_merchant_column/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE users ADD COLUMN preferred_merchant_id VARCHAR(64);