From ea8eb91a4c92607fcf88bd7fe91d4e1faaf20a68 Mon Sep 17 00:00:00 2001 From: Apoorv Dixit Date: Thu, 1 Feb 2024 17:42:26 +0530 Subject: [PATCH 1/3] feat(user): add support for resend invite --- crates/api_models/src/events/user.rs | 5 +- crates/api_models/src/user.rs | 5 ++ crates/router/src/core/user.rs | 61 ++++++++++++++++++++++++ crates/router/src/routes/app.rs | 64 +++++++++++++------------- crates/router/src/routes/lock_utils.rs | 1 + crates/router/src/routes/user.rs | 19 ++++++++ crates/router_env/src/logger/types.rs | 2 + 7 files changed, 124 insertions(+), 33 deletions(-) diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 04aabc071ae..d7150af9bc7 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -12,8 +12,8 @@ use crate::user::{ }, AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest, DashboardEntryResponse, ForgotPasswordRequest, GetUsersResponse, InviteUserRequest, - InviteUserResponse, ResetPasswordRequest, SendVerifyEmailRequest, SignInResponse, - SignUpRequest, SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, + InviteUserResponse, ReInviteUserRequest, ResetPasswordRequest, SendVerifyEmailRequest, + SignInResponse, SignUpRequest, SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, UpdateUserAccountDetailsRequest, UserMerchantCreate, VerifyEmailRequest, }; @@ -54,6 +54,7 @@ common_utils::impl_misc_api_event_type!( ResetPasswordRequest, InviteUserRequest, InviteUserResponse, + ReInviteUserRequest, VerifyEmailRequest, SendVerifyEmailRequest, SignInResponse, diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 89f42f58c39..b29ce811be3 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -113,6 +113,11 @@ pub struct InviteMultipleUserResponse { pub error: Option, } +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct ReInviteUserRequest { + pub email: pii::Email, +} + #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct SwitchMerchantIdRequest { pub merchant_id: String, diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 7050c9f0024..39696a125b4 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -706,6 +706,67 @@ async fn handle_new_user_invitation( }) } +#[cfg(feature = "email")] +pub async fn resend_invite( + state: AppState, + user_from_token: auth::UserFromToken, + request: user_api::ReInviteUserRequest, +) -> UserResponse { + let invitee_email = domain::UserEmail::from_pii_email(request.email)?; + let user: domain::UserFromStorage = state + .store + .find_user_by_email(invitee_email.clone().get_secret().expose().as_str()) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::InvalidRoleOperation) + .attach_printable("User not found in the records") + } else { + e.change_context(UserErrors::InternalServerError) + } + })? + .into(); + let user_role = state + .store + .find_user_role_by_user_id_merchant_id(user.get_user_id(), &user_from_token.merchant_id) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::InvalidRoleOperation) + .attach_printable("User role with given UserId MerchantId not found") + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + + if !matches!(user_role.status, UserStatus::InvitationSent) { + return Err(UserErrors::InvalidRoleOperation.into()) + .attach_printable("Invalid Status for Reinvitation"); + } + + let email_contents = email_types::InviteUser { + recipient_email: invitee_email, + user_name: domain::UserName::new(user.get_name())?, + settings: state.conf.clone(), + subject: "You have been invited to join Hyperswitch Community!", + merchant_id: user_from_token.merchant_id, + }; + let send_email_result = state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await; + router_env::logger::info!(?send_email_result); + let is_email_sent = send_email_result.is_ok(); + + Ok(ApplicationResponse::Json(user_api::InviteUserResponse { + is_email_sent, + password: None, + })) +} + pub async fn create_internal_user( state: AppState, request: user_api::CreateInternalUserRequest, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 9e8bee73c28..c130da8dbb4 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -976,41 +976,12 @@ impl User { .service(web::resource("/switch/list").route(web::get().to(list_merchant_ids_for_user))) .service(web::resource("/permission_info").route(web::get().to(get_authorization_info))) .service(web::resource("/update").route(web::post().to(update_user_account_details))) - .service( - web::resource("/user/invite_multiple").route(web::post().to(invite_multiple_user)), - ) .service( 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::delete().to(delete_user_role))); - - // User management - route = route.service( - web::scope("/user") - .service(web::resource("/list").route(web::get().to(get_user_details))) - .service(web::resource("/invite").route(web::post().to(invite_user))) - .service(web::resource("/invite/accept").route(web::post().to(accept_invitation))) - .service(web::resource("/update_role").route(web::post().to(update_user_role))), - ); - - // Role information - route = route.service( - web::scope("/role") - .service(web::resource("").route(web::get().to(get_role_from_token))) - .service(web::resource("/list").route(web::get().to(list_all_roles))) - .service(web::resource("/{role_id}").route(web::get().to(get_role))), - ); + ); - #[cfg(feature = "dummy_connector")] - { - route = route.service( - web::resource("/sample_data") - .route(web::post().to(generate_sample_data)) - .route(web::delete().to(delete_sample_data)), - ) - } #[cfg(feature = "email")] { route = route @@ -1031,12 +1002,43 @@ impl User { .service( web::resource("/verify_email_request") .route(web::post().to(verify_email_request)), - ); + ) + .service(web::resource("/user/resend_invite").route(web::post().to(resend_invite))); } #[cfg(not(feature = "email"))] { route = route.service(web::resource("/signup").route(web::post().to(user_signup))) } + + // User management + route = route.service( + web::scope("/user") + .service(web::resource("/list").route(web::get().to(get_user_details))) + .service(web::resource("/invite").route(web::post().to(invite_user))) + .service( + web::resource("/invite_multiple").route(web::post().to(invite_multiple_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("/delete").route(web::delete().to(delete_user_role))), + ); + + // Role information + route = route.service( + web::scope("/role") + .service(web::resource("").route(web::get().to(get_role_from_token))) + .service(web::resource("/list").route(web::get().to(list_all_roles))) + .service(web::resource("/{role_id}").route(web::get().to(get_role))), + ); + + #[cfg(feature = "dummy_connector")] + { + route = route.service( + web::resource("/sample_data") + .route(web::post().to(generate_sample_data)) + .route(web::delete().to(delete_sample_data)), + ) + } route } } diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 0dfc3b1b339..042e89fdd52 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -182,6 +182,7 @@ impl From for ApiIdentifier { | Flow::ResetPassword | Flow::InviteUser | Flow::InviteMultipleUser + | Flow::ReInviteUser | Flow::UserSignUpWithMerchantId | Flow::VerifyEmailWithoutInviteChecks | Flow::VerifyEmail diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 0c2694dc70f..a863bc2b662 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -401,6 +401,25 @@ pub async fn invite_multiple_user( .await } +#[cfg(feature = "email")] +pub async fn resend_invite( + state: web::Data, + req: HttpRequest, + payload: web::Json, +) -> HttpResponse { + let flow = Flow::ReInviteUser; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload.into_inner(), + user_core::resend_invite, + &auth::JWTAuth(Permission::UsersWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} + #[cfg(feature = "email")] pub async fn verify_email_without_invite_checks( state: web::Data, diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 55e7bafd8e0..11e6f9c0ed8 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -331,6 +331,8 @@ pub enum Flow { InviteUser, /// Invite multiple users InviteMultipleUser, + /// Reinvite user + ReInviteUser, /// Delete user role DeleteUserRole, /// Incremental Authorization flow From fcef022416f9aaef6c766ac1d9d89c164ac3e5cc Mon Sep 17 00:00:00 2001 From: Apoorv Dixit Date: Thu, 1 Feb 2024 19:33:01 +0530 Subject: [PATCH 2/3] fix: status for resend invite --- crates/router/src/core/user.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 39696a125b4..fae57bc0ee2 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -711,7 +711,7 @@ pub async fn resend_invite( state: AppState, user_from_token: auth::UserFromToken, request: user_api::ReInviteUserRequest, -) -> UserResponse { +) -> UserResponse<()> { let invitee_email = domain::UserEmail::from_pii_email(request.email)?; let user: domain::UserFromStorage = state .store @@ -751,20 +751,15 @@ pub async fn resend_invite( subject: "You have been invited to join Hyperswitch Community!", merchant_id: user_from_token.merchant_id, }; - let send_email_result = state + state .email_client .compose_and_send_email( Box::new(email_contents), state.conf.proxy.https_url.as_ref(), ) - .await; - router_env::logger::info!(?send_email_result); - let is_email_sent = send_email_result.is_ok(); + .await.change_context(UserErrors::InternalServerError)?; - Ok(ApplicationResponse::Json(user_api::InviteUserResponse { - is_email_sent, - password: None, - })) + Ok(ApplicationResponse::StatusOk) } pub async fn create_internal_user( From e24b5a9edeb465465386852c08473e20da1b5000 Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Thu, 1 Feb 2024 14:03:36 +0000 Subject: [PATCH 3/3] chore: run formatter --- crates/router/src/core/user.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index fae57bc0ee2..41a407bd670 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -757,7 +757,8 @@ pub async fn resend_invite( Box::new(email_contents), state.conf.proxy.https_url.as_ref(), ) - .await.change_context(UserErrors::InternalServerError)?; + .await + .change_context(UserErrors::InternalServerError)?; Ok(ApplicationResponse::StatusOk) }