Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion crates/api_models/src/events/user_role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ use crate::user_role::{
UpdateRoleRequest,
},
AcceptInvitationRequest, AuthorizationInfoResponse, DeleteUserRoleRequest,
TransferOrgOwnershipRequest, UpdateUserRoleRequest,
MerchantSelectRequest, TransferOrgOwnershipRequest, UpdateUserRoleRequest,
};

common_utils::impl_misc_api_event_type!(
RoleInfoWithPermissionsResponse,
GetRoleRequest,
AuthorizationInfoResponse,
UpdateUserRoleRequest,
MerchantSelectRequest,
AcceptInvitationRequest,
DeleteUserRoleRequest,
TransferOrgOwnershipRequest,
Expand Down
6 changes: 5 additions & 1 deletion crates/api_models/src/user_role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,15 @@ pub enum UserStatus {
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct AcceptInvitationRequest {
pub struct MerchantSelectRequest {
pub merchant_ids: Vec<String>,
// TODO: Remove this once the token only api is being used
pub need_dashboard_entry_response: Option<bool>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct AcceptInvitationRequest {
pub merchant_ids: Vec<String>,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct DeleteUserRoleRequest {
Expand Down
52 changes: 38 additions & 14 deletions crates/router/src/core/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use diesel_models::{
user_role::UserRoleNew,
};
use error_stack::{report, ResultExt};
#[cfg(feature = "email")]
use external_services::email::EmailData;
use masking::{ExposeInterface, PeekInterface};
#[cfg(feature = "email")]
use router_env::env;
Expand Down Expand Up @@ -570,14 +572,16 @@ pub async fn invite_multiple_user(
user_from_token: auth::UserFromToken,
requests: Vec<user_api::InviteUserRequest>,
req_state: ReqState,
is_token_only: Option<bool>,
) -> UserResponse<Vec<InviteMultipleUserResponse>> {
if requests.len() > 10 {
return Err(report!(UserErrors::MaxInvitationsError))
.attach_printable("Number of invite requests must not exceed 10");
}

let responses = futures::future::join_all(requests.iter().map(|request| async {
match handle_invitation(&state, &user_from_token, request, &req_state).await {
match handle_invitation(&state, &user_from_token, request, &req_state, is_token_only).await
{
Ok(response) => response,
Err(error) => InviteMultipleUserResponse {
email: request.email.clone(),
Expand All @@ -597,6 +601,7 @@ async fn handle_invitation(
user_from_token: &auth::UserFromToken,
request: &user_api::InviteUserRequest,
req_state: &ReqState,
is_token_only: Option<bool>,
) -> UserResult<InviteMultipleUserResponse> {
let inviter_user = user_from_token.get_user_from_db(state).await?;

Expand Down Expand Up @@ -635,7 +640,14 @@ async fn handle_invitation(
.err()
.unwrap_or(false)
{
handle_new_user_invitation(state, user_from_token, request, req_state.clone()).await
handle_new_user_invitation(
state,
user_from_token,
request,
req_state.clone(),
is_token_only,
)
.await
} else {
Err(UserErrors::InternalServerError.into())
}
Expand Down Expand Up @@ -718,6 +730,7 @@ async fn handle_new_user_invitation(
user_from_token: &auth::UserFromToken,
request: &user_api::InviteUserRequest,
req_state: ReqState,
is_token_only: Option<bool>,
) -> UserResult<InviteMultipleUserResponse> {
let new_user = domain::NewUser::try_from((request.clone(), user_from_token.clone()))?;

Expand Down Expand Up @@ -756,25 +769,36 @@ async fn handle_new_user_invitation(
})?;

let is_email_sent;
// TODO: Adding this to avoid clippy lints, remove this once the token only flow is being used
let _ = is_token_only;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😂


#[cfg(feature = "email")]
{
// TODO: Adding this to avoid clippy lints
// Will be adding actual usage for this variable later
let _ = req_state.clone();
let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?;
let email_contents = email_types::InviteUser {
recipient_email: invitee_email,
user_name: domain::UserName::new(new_user.get_name())?,
settings: state.conf.clone(),
subject: "You have been invited to join Hyperswitch Community!",
merchant_id: user_from_token.merchant_id.clone(),
let email_contents: Box<dyn EmailData + Send + 'static> = if let Some(true) = is_token_only
{
Box::new(email_types::InviteRegisteredUser {
recipient_email: invitee_email,
user_name: domain::UserName::new(new_user.get_name())?,
settings: state.conf.clone(),
subject: "You have been invited to join Hyperswitch Community!",
merchant_id: user_from_token.merchant_id.clone(),
})
} else {
Box::new(email_types::InviteUser {
recipient_email: invitee_email,
user_name: domain::UserName::new(new_user.get_name())?,
settings: state.conf.clone(),
subject: "You have been invited to join Hyperswitch Community!",
merchant_id: user_from_token.merchant_id.clone(),
})
};
let send_email_result = state
.email_client
.compose_and_send_email(
Box::new(email_contents),
state.conf.proxy.https_url.as_ref(),
)
.compose_and_send_email(email_contents, state.conf.proxy.https_url.as_ref())
.await;
logger::info!(?send_email_result);
is_email_sent = send_email_result.is_ok();
Expand Down Expand Up @@ -1203,11 +1227,11 @@ pub async fn create_merchant_account(

pub async fn list_merchants_for_user(
state: AppState,
user_from_token: auth::UserFromToken,
user_from_token: Box<dyn auth::GetUserIdFromAuth>,
) -> UserResponse<Vec<user_api::UserMerchantAccount>> {
let user_roles = state
.store
.list_user_roles_by_user_id(user_from_token.user_id.as_str())
.list_user_roles_by_user_id(user_from_token.get_user_id().as_str())
.await
.change_context(UserErrors::InternalServerError)?;

Expand Down
37 changes: 33 additions & 4 deletions crates/router/src/core/user_role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,38 @@ pub async fn transfer_org_ownership(

pub async fn accept_invitation(
state: AppState,
user_token: auth::UserFromSinglePurposeToken,
user_token: auth::UserFromToken,
req: user_role_api::AcceptInvitationRequest,
) -> UserResponse<()> {
futures::future::join_all(req.merchant_ids.iter().map(|merchant_id| async {
state
.store
.update_user_role_by_user_id_merchant_id(
user_token.user_id.as_str(),
merchant_id,
UserRoleUpdate::UpdateStatus {
status: UserStatus::Active,
modified_by: user_token.user_id.clone(),
},
)
.await
.map_err(|e| {
logger::error!("Error while accepting invitation {}", e);
})
.ok()
}))
.await
.into_iter()
.reduce(Option::or)
.flatten()
.ok_or(UserErrors::MerchantIdNotFound.into())
.map(|_| ApplicationResponse::StatusOk)
}

pub async fn merchant_select(
state: AppState,
user_token: auth::UserFromSinglePurposeToken,
req: user_role_api::MerchantSelectRequest,
) -> UserResponse<user_api::TokenOrPayloadResponse<user_api::DashboardEntryResponse>> {
let user_role = futures::future::join_all(req.merchant_ids.iter().map(|merchant_id| async {
state
Expand Down Expand Up @@ -207,7 +237,6 @@ pub async fn accept_invitation(
utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await;

let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;

let response = utils::user::get_dashboard_entry_response(
&state,
user_from_db,
Expand All @@ -223,10 +252,10 @@ pub async fn accept_invitation(
Ok(ApplicationResponse::StatusOk)
}

pub async fn accept_invitation_token_only_flow(
pub async fn merchant_select_token_only_flow(
state: AppState,
user_token: auth::UserFromSinglePurposeToken,
req: user_role_api::AcceptInvitationRequest,
req: user_role_api::MerchantSelectRequest,
) -> UserResponse<user_api::TokenOrPayloadResponse<user_api::DashboardEntryResponse>> {
let user_role = futures::future::join_all(req.merchant_ids.iter().map(|merchant_id| async {
state
Expand Down
11 changes: 10 additions & 1 deletion crates/router/src/routes/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1192,6 +1192,11 @@ impl User {
// TODO: Remove this endpoint once migration to /merchants/list is done
.service(web::resource("/switch/list").route(web::get().to(list_merchants_for_user)))
.service(web::resource("/merchants/list").route(web::get().to(list_merchants_for_user)))
// The route is utilized to select an invitation from a list of merchants in an intermediate state
.service(
web::resource("/merchants_select/list")
.route(web::get().to(list_merchants_for_user_with_spt)),
)
.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(
Expand Down Expand Up @@ -1241,7 +1246,11 @@ impl 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("/invite/accept")
.route(web::post().to(merchant_select))
.route(web::put().to(accept_invitation)),
)
.service(web::resource("/update_role").route(web::post().to(update_user_role)))
.service(
web::resource("/transfer_ownership")
Expand Down
1 change: 1 addition & 0 deletions crates/router/src/routes/lock_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ impl From<Flow> for ApiIdentifier {
| Flow::UpdateUserRole
| Flow::GetAuthorizationInfo
| Flow::AcceptInvitation
| Flow::MerchantSelect
| Flow::DeleteUserRole
| Flow::TransferOrgOwnership
| Flow::CreateRole
Expand Down
23 changes: 22 additions & 1 deletion crates/router/src/routes/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,23 @@ pub async fn list_merchants_for_user(state: web::Data<AppState>, req: HttpReques
.await
}

pub async fn list_merchants_for_user_with_spt(
state: web::Data<AppState>,
req: HttpRequest,
) -> HttpResponse {
let flow = Flow::UserMerchantAccountList;
Box::pin(api::server_wrap(
flow,
state,
&req,
(),
|state, user, _, _| user_core::list_merchants_for_user(state, user),
&auth::SinglePurposeJWTAuth(TokenPurpose::AcceptInvite),
api_locking::LockAction::NotApplicable,
))
.await
}

pub async fn get_user_role_details(
state: web::Data<AppState>,
req: HttpRequest,
Expand Down Expand Up @@ -435,14 +452,18 @@ pub async fn invite_multiple_user(
state: web::Data<AppState>,
req: HttpRequest,
payload: web::Json<Vec<user_api::InviteUserRequest>>,
query: web::Query<user_api::TokenOnlyQueryParam>,
) -> HttpResponse {
let flow = Flow::InviteMultipleUser;
let is_token_only = query.into_inner().token_only;
Box::pin(api::server_wrap(
flow,
state.clone(),
&req,
payload.into_inner(),
user_core::invite_multiple_user,
|state, user, payload, req_state| {
user_core::invite_multiple_user(state, user, payload, req_state, is_token_only)
},
&auth::JWTAuth(Permission::UsersWrite),
api_locking::LockAction::NotApplicable,
))
Expand Down
25 changes: 22 additions & 3 deletions crates/router/src/routes/user_role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,29 @@ pub async fn accept_invitation(
state: web::Data<AppState>,
req: HttpRequest,
json_payload: web::Json<user_role_api::AcceptInvitationRequest>,
query: web::Query<user_api::TokenOnlyQueryParam>,
) -> HttpResponse {
let flow = Flow::AcceptInvitation;
let payload = json_payload.into_inner();
Box::pin(api::server_wrap(
flow,
state.clone(),
&req,
payload,
|state, user, req_body, _| user_role_core::accept_invitation(state, user, req_body),
&auth::DashboardNoPermissionAuth,
api_locking::LockAction::NotApplicable,
))
.await
}

pub async fn merchant_select(
state: web::Data<AppState>,
req: HttpRequest,
json_payload: web::Json<user_role_api::MerchantSelectRequest>,
query: web::Query<user_api::TokenOnlyQueryParam>,
) -> HttpResponse {
let flow = Flow::MerchantSelect;
let payload = json_payload.into_inner();
let is_token_only = query.into_inner().token_only;
Box::pin(api::server_wrap(
flow,
Expand All @@ -221,9 +240,9 @@ pub async fn accept_invitation(
payload,
|state, user, req_body, _| async move {
if let Some(true) = is_token_only {
user_role_core::accept_invitation_token_only_flow(state, user, req_body).await
user_role_core::merchant_select_token_only_flow(state, user, req_body).await
} else {
user_role_core::accept_invitation(state, user, req_body).await
user_role_core::merchant_select(state, user, req_body).await
}
},
&auth::SinglePurposeJWTAuth(TokenPurpose::AcceptInvite),
Expand Down
Loading