refactor(users): Add V2 user_roles data support (#5763)

Co-authored-by: Apoorv Dixit <apoorv.dixit@juspay.in>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Mani Chandra
2024-09-01 19:09:25 +05:30
committed by GitHub
parent 379831932a
commit 6b410505da
31 changed files with 768 additions and 2042 deletions

View File

@ -16,11 +16,11 @@ use crate::user::{
GetSsoAuthUrlRequest, GetUserAuthenticationMethodsRequest, GetUserDetailsResponse,
GetUserRoleDetailsRequest, GetUserRoleDetailsResponse, InviteUserRequest, ListUsersResponse,
ReInviteUserRequest, RecoveryCodes, ResetPasswordRequest, RotatePasswordRequest,
SendVerifyEmailRequest, SignInResponse, SignUpRequest, SignUpWithMerchantIdRequest,
SsoSignInRequest, SwitchMerchantRequest, SwitchOrganizationRequest, SwitchProfileRequest,
TokenOrPayloadResponse, TokenResponse, TwoFactorAuthStatusResponse,
UpdateUserAccountDetailsRequest, UpdateUserAuthenticationMethodRequest, UserFromEmailRequest,
UserMerchantCreate, VerifyEmailRequest, VerifyRecoveryCodeRequest, VerifyTotpRequest,
SendVerifyEmailRequest, SignUpRequest, SignUpWithMerchantIdRequest, SsoSignInRequest,
SwitchMerchantRequest, SwitchOrganizationRequest, SwitchProfileRequest, TokenResponse,
TwoFactorAuthStatusResponse, UpdateUserAccountDetailsRequest,
UpdateUserAuthenticationMethodRequest, UserFromEmailRequest, UserMerchantCreate,
VerifyEmailRequest, VerifyRecoveryCodeRequest, VerifyTotpRequest,
};
impl ApiEventMetric for DashboardEntryResponse {
@ -40,12 +40,6 @@ impl ApiEventMetric for VerifyTokenResponse {
}
}
impl<T> ApiEventMetric for TokenOrPayloadResponse<T> {
fn get_api_event_type(&self) -> Option<ApiEventsType> {
Some(ApiEventsType::Miscellaneous)
}
}
common_utils::impl_api_event_type!(
Miscellaneous,
(
@ -72,7 +66,6 @@ common_utils::impl_api_event_type!(
VerifyEmailRequest,
SendVerifyEmailRequest,
AcceptInviteFromEmailRequest,
SignInResponse,
UpdateUserAccountDetailsRequest,
GetUserDetailsResponse,
GetUserRoleDetailsRequest,

View File

@ -2,9 +2,8 @@ use common_utils::events::{ApiEventMetric, ApiEventsType};
use crate::user_role::{
role::{
CreateRoleRequest, GetRoleFromTokenResponse, GetRoleRequest, ListRolesResponse,
RoleInfoResponse, RoleInfoWithGroupsResponse, RoleInfoWithPermissionsResponse,
UpdateRoleRequest,
CreateRoleRequest, GetRoleRequest, ListRolesResponse, RoleInfoWithGroupsResponse,
RoleInfoWithPermissionsResponse, UpdateRoleRequest,
},
AcceptInvitationRequest, AuthorizationInfoResponse, DeleteUserRoleRequest,
MerchantSelectRequest, UpdateUserRoleRequest,
@ -23,8 +22,6 @@ common_utils::impl_api_event_type!(
CreateRoleRequest,
UpdateRoleRequest,
ListRolesResponse,
RoleInfoResponse,
GetRoleFromTokenResponse,
RoleInfoWithGroupsResponse
)
);

View File

@ -23,8 +23,6 @@ pub struct SignUpRequest {
pub password: Secret<String>,
}
pub type SignUpResponse = DashboardEntryResponse;
#[derive(serde::Serialize, Debug, Clone)]
pub struct DashboardEntryResponse {
pub token: Secret<String>,
@ -40,22 +38,6 @@ pub struct DashboardEntryResponse {
pub type SignInRequest = SignUpRequest;
#[derive(Debug, serde::Serialize)]
#[serde(tag = "flow_type", rename_all = "snake_case")]
pub enum SignInResponse {
MerchantSelect(MerchantSelectResponse),
DashboardEntry(DashboardEntryResponse),
}
#[derive(Debug, serde::Serialize)]
pub struct MerchantSelectResponse {
pub token: Secret<String>,
pub name: Secret<String>,
pub email: pii::Email,
pub verification_days_left: Option<i64>,
pub merchants: Vec<UserMerchantAccount>,
}
#[derive(serde::Deserialize, Debug, Clone, serde::Serialize)]
pub struct ConnectAccountRequest {
pub email: pii::Email,
@ -202,8 +184,6 @@ pub struct VerifyEmailRequest {
pub token: Secret<String>,
}
pub type VerifyEmailResponse = SignInResponse;
#[derive(serde::Deserialize, Debug, serde::Serialize)]
pub struct SendVerifyEmailRequest {
pub email: pii::Email,
@ -232,11 +212,6 @@ pub struct UpdateUserAccountDetailsRequest {
pub preferred_merchant_id: Option<id_type::MerchantId>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct TokenOnlyQueryParam {
pub token_only: Option<bool>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct SkipTwoFactorAuthQueryParam {
pub skip_two_factor_auth: Option<bool>,
@ -254,12 +229,6 @@ pub struct TwoFactorAuthStatusResponse {
pub recovery_code: bool,
}
#[derive(Debug, serde::Serialize)]
#[serde(untagged)]
pub enum TokenOrPayloadResponse<T> {
Token(TokenResponse),
Payload(T),
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct UserFromEmailRequest {
pub token: Secret<String>,

View File

@ -121,9 +121,8 @@ pub enum UserStatus {
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct MerchantSelectRequest {
pub merchant_ids: Vec<common_utils::id_type::MerchantId>,
// 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<common_utils::id_type::MerchantId>,

View File

@ -1,4 +1,5 @@
use common_enums::{EntityType, PermissionGroup, RoleScope};
pub use common_enums::PermissionGroup;
use common_enums::{EntityType, RoleScope};
use super::Permission;
@ -17,26 +18,7 @@ pub struct UpdateRoleRequest {
}
#[derive(Debug, serde::Serialize)]
pub struct ListRolesResponse(pub Vec<RoleInfoResponse>);
#[derive(Debug, serde::Deserialize)]
pub struct GetGroupsQueryParam {
pub groups: Option<bool>,
}
#[derive(Debug, serde::Serialize)]
#[serde(untagged)]
pub enum GetRoleFromTokenResponse {
Permissions(Vec<Permission>),
Groups(Vec<PermissionGroup>),
}
#[derive(Debug, serde::Serialize)]
#[serde(untagged)]
pub enum RoleInfoResponse {
Permissions(RoleInfoWithPermissionsResponse),
Groups(RoleInfoWithGroupsResponse),
}
pub struct ListRolesResponse(pub Vec<RoleInfoWithGroupsResponse>);
#[derive(Debug, serde::Serialize)]
pub struct RoleInfoWithPermissionsResponse {

View File

@ -121,42 +121,10 @@ pub async fn get_user_details(
))
}
pub async fn signup(
state: SessionState,
request: user_api::SignUpRequest,
) -> UserResponse<user_api::TokenOrPayloadResponse<user_api::SignUpResponse>> {
let new_user = domain::NewUser::try_from(request)?;
new_user
.get_new_merchant()
.get_new_organization()
.insert_org_in_db(state.clone())
.await?;
let user_from_db = new_user
.insert_user_and_merchant_in_db(state.clone())
.await?;
let user_role = new_user
.insert_org_level_user_role_in_db(
state.clone(),
common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
UserStatus::Active,
None,
)
.await?;
utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await;
let token =
utils::user::generate_jwt_auth_token_without_profile(&state, &user_from_db, &user_role)
.await?;
let response =
utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token.clone())?;
auth::cookies::set_cookie_response(user_api::TokenOrPayloadResponse::Payload(response), token)
}
pub async fn signup_token_only_flow(
state: SessionState,
request: user_api::SignUpRequest,
) -> UserResponse<user_api::TokenOrPayloadResponse<user_api::SignUpResponse>> {
) -> UserResponse<user_api::TokenResponse> {
let new_user = domain::NewUser::try_from(request)?;
new_user
.get_new_merchant()
@ -182,61 +150,17 @@ pub async fn signup_token_only_flow(
.get_token_with_user_role(&state, &user_role)
.await?;
let response = user_api::TokenOrPayloadResponse::Token(user_api::TokenResponse {
let response = user_api::TokenResponse {
token: token.clone(),
token_type: next_flow.get_flow().into(),
});
auth::cookies::set_cookie_response(response, token)
}
pub async fn signin(
state: SessionState,
request: user_api::SignInRequest,
) -> UserResponse<user_api::TokenOrPayloadResponse<user_api::SignInResponse>> {
let user_from_db: domain::UserFromStorage = state
.global_store
.find_user_by_email(&request.email)
.await
.map_err(|e| {
if e.current_context().is_db_not_found() {
e.change_context(UserErrors::InvalidCredentials)
} else {
e.change_context(UserErrors::InternalServerError)
}
})?
.into();
user_from_db.compare_password(&request.password)?;
let signin_strategy =
if let Some(preferred_merchant_id) = user_from_db.get_preferred_merchant_id() {
let preferred_role = user_from_db
.get_role_from_db_by_merchant_id(&state, &preferred_merchant_id)
.await
.to_not_found_response(UserErrors::InternalServerError)
.attach_printable("User role with preferred_merchant_id not found")?;
domain::SignInWithRoleStrategyType::SingleRole(domain::SignInWithSingleRoleStrategy {
user: user_from_db,
user_role: Box::new(preferred_role),
})
} else {
let user_roles = user_from_db.get_roles_from_db(&state).await?;
domain::SignInWithRoleStrategyType::decide_signin_strategy_by_user_roles(
user_from_db,
user_roles,
)
.await?
};
let response = signin_strategy.get_signin_response(&state).await?;
let token = utils::user::get_token_from_signin_response(&response);
auth::cookies::set_cookie_response(user_api::TokenOrPayloadResponse::Payload(response), token)
auth::cookies::set_cookie_response(response, token)
}
pub async fn signin_token_only_flow(
state: SessionState,
request: user_api::SignInRequest,
) -> UserResponse<user_api::TokenOrPayloadResponse<user_api::SignInResponse>> {
) -> UserResponse<user_api::TokenResponse> {
let user_from_db: domain::UserFromStorage = state
.global_store
.find_user_by_email(&request.email)
@ -251,10 +175,10 @@ pub async fn signin_token_only_flow(
let token = next_flow.get_token(&state).await?;
let response = user_api::TokenOrPayloadResponse::Token(user_api::TokenResponse {
let response = user_api::TokenResponse {
token: token.clone(),
token_type: next_flow.get_flow().into(),
});
};
auth::cookies::set_cookie_response(response, token)
}
@ -573,105 +497,11 @@ pub async fn reset_password_token_only_flow(
auth::cookies::remove_cookie_response()
}
#[cfg(feature = "email")]
pub async fn reset_password(
state: SessionState,
request: user_api::ResetPasswordRequest,
) -> UserResponse<()> {
let token = request.token.expose();
let email_token = auth::decode_jwt::<email_types::EmailToken>(&token, &state)
.await
.change_context(UserErrors::LinkInvalid)?;
auth::blacklist::check_email_token_in_blacklist(&state, &token).await?;
let password = domain::UserPassword::new(request.password)?;
let hash_password = utils::user::password::generate_password_hash(password.get_secret())?;
let user = state
.global_store
.update_user_by_email(
&email_token
.get_email()
.change_context(UserErrors::InternalServerError)?,
storage_user::UserUpdate::PasswordUpdate {
password: hash_password,
},
)
.await
.change_context(UserErrors::InternalServerError)?;
if let Some(inviter_merchant_id) = email_token.get_merchant_id() {
let key_manager_state = &(&state).into();
let key_store = state
.store
.get_merchant_key_store_by_merchant_id(
key_manager_state,
inviter_merchant_id,
&state.store.get_master_key().to_vec().into(),
)
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("merchant_key_store not found")?;
let merchant_account = state
.store
.find_merchant_account_by_merchant_id(
key_manager_state,
inviter_merchant_id,
&key_store,
)
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("merchant_account not found")?;
let (update_v1_result, update_v2_result) =
utils::user_role::update_v1_and_v2_user_roles_in_db(
&state,
user.user_id.clone().as_str(),
&merchant_account.organization_id,
inviter_merchant_id,
None,
UserRoleUpdate::UpdateStatus {
status: UserStatus::Active,
modified_by: user.user_id.clone(),
},
)
.await;
if update_v1_result
.as_ref()
.is_err_and(|err| !err.current_context().is_db_not_found())
|| update_v2_result
.as_ref()
.is_err_and(|err| !err.current_context().is_db_not_found())
{
return Err(report!(UserErrors::InternalServerError));
}
if update_v1_result.is_err() && update_v2_result.is_err() {
return Err(report!(UserErrors::InvalidRoleOperation))
.attach_printable("User not found in the organization")?;
}
}
let _ = auth::blacklist::insert_email_token_in_blacklist(&state, &token)
.await
.map_err(|error| logger::error!(?error));
let _ = auth::blacklist::insert_user_in_blacklist(&state, &user.user_id)
.await
.map_err(|error| logger::error!(?error));
auth::cookies::remove_cookie_response()
}
pub async fn invite_multiple_user(
state: SessionState,
user_from_token: auth::UserFromToken,
requests: Vec<user_api::InviteUserRequest>,
req_state: ReqState,
is_token_only: Option<bool>,
auth_id: Option<String>,
) -> UserResponse<Vec<InviteMultipleUserResponse>> {
if requests.len() > 10 {
@ -680,16 +510,7 @@ pub async fn invite_multiple_user(
}
let responses = futures::future::join_all(requests.iter().map(|request| async {
match handle_invitation(
&state,
&user_from_token,
request,
&req_state,
is_token_only,
&auth_id,
)
.await
{
match handle_invitation(&state, &user_from_token, request, &req_state, &auth_id).await {
Ok(response) => response,
Err(error) => InviteMultipleUserResponse {
email: request.email.clone(),
@ -709,7 +530,6 @@ async fn handle_invitation(
user_from_token: &auth::UserFromToken,
request: &user_api::InviteUserRequest,
req_state: &ReqState,
is_token_only: Option<bool>,
auth_id: &Option<String>,
) -> UserResult<InviteMultipleUserResponse> {
let inviter_user = user_from_token.get_user_from_db(state).await?;
@ -756,14 +576,7 @@ async fn handle_invitation(
.err()
.unwrap_or(false)
{
handle_new_user_invitation(
state,
user_from_token,
request,
req_state.clone(),
is_token_only,
auth_id,
)
handle_new_user_invitation(state, user_from_token, request, req_state.clone(), auth_id)
.await
} else {
Err(UserErrors::InternalServerError.into())
@ -877,7 +690,6 @@ async fn handle_new_user_invitation(
user_from_token: &auth::UserFromToken,
request: &user_api::InviteUserRequest,
req_state: ReqState,
is_token_only: Option<bool>,
auth_id: &Option<String>,
) -> UserResult<InviteMultipleUserResponse> {
let new_user = domain::NewUser::try_from((request.clone(), user_from_token.clone()))?;
@ -912,8 +724,6 @@ async fn handle_new_user_invitation(
.await?;
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;
#[cfg(feature = "email")]
{
@ -921,8 +731,7 @@ async fn handle_new_user_invitation(
// 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: Box<dyn EmailData + Send + 'static> = if let Some(true) = is_token_only
{
let email_contents: Box<dyn EmailData + Send + 'static> =
Box::new(email_types::InviteRegisteredUser {
recipient_email: invitee_email,
user_name: domain::UserName::new(new_user.get_name())?,
@ -930,17 +739,7 @@ async fn handle_new_user_invitation(
subject: "You have been invited to join Hyperswitch Community!",
merchant_id: user_from_token.merchant_id.clone(),
auth_id: auth_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(),
auth_id: auth_id.clone(),
})
};
});
let send_email_result = state
.email_client
.compose_and_send_email(email_contents, state.conf.proxy.https_url.as_ref())
@ -1047,115 +846,12 @@ pub async fn resend_invite(
Ok(ApplicationResponse::StatusOk)
}
#[cfg(feature = "email")]
pub async fn accept_invite_from_email(
state: SessionState,
request: user_api::AcceptInviteFromEmailRequest,
) -> UserResponse<user_api::DashboardEntryResponse> {
let token = request.token.expose();
let email_token = auth::decode_jwt::<email_types::EmailToken>(&token, &state)
.await
.change_context(UserErrors::LinkInvalid)?;
auth::blacklist::check_email_token_in_blacklist(&state, &token).await?;
let user: domain::UserFromStorage = state
.global_store
.find_user_by_email(
&email_token
.get_email()
.change_context(UserErrors::InternalServerError)?,
)
.await
.change_context(UserErrors::InternalServerError)?
.into();
let merchant_id = email_token
.get_merchant_id()
.ok_or(UserErrors::InternalServerError)?;
let key_manager_state = &(&state).into();
let key_store = state
.store
.get_merchant_key_store_by_merchant_id(
key_manager_state,
merchant_id,
&state.store.get_master_key().to_vec().into(),
)
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("merchant_key_store not found")?;
let merchant_account = state
.store
.find_merchant_account_by_merchant_id(key_manager_state, merchant_id, &key_store)
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("merchant_account not found")?;
let (update_v1_result, update_v2_result) = utils::user_role::update_v1_and_v2_user_roles_in_db(
&state,
user.get_user_id(),
&merchant_account.organization_id,
merchant_id,
None,
UserRoleUpdate::UpdateStatus {
status: UserStatus::Active,
modified_by: user.get_user_id().to_string(),
},
)
.await;
if update_v1_result
.as_ref()
.is_err_and(|err| !err.current_context().is_db_not_found())
|| update_v2_result
.as_ref()
.is_err_and(|err| !err.current_context().is_db_not_found())
{
return Err(report!(UserErrors::InternalServerError));
}
if update_v1_result.is_err() && update_v2_result.is_err() {
return Err(report!(UserErrors::InvalidRoleOperation))
.attach_printable("User not found in the organization")?;
}
let _ = auth::blacklist::insert_email_token_in_blacklist(&state, &token)
.await
.map_err(|error| logger::error!(?error));
let user_from_db: domain::UserFromStorage = state
.global_store
.update_user_by_user_id(user.get_user_id(), storage_user::UserUpdate::VerifyUser)
.await
.change_context(UserErrors::InternalServerError)?
.into();
let user_role = user_from_db
.get_preferred_or_active_user_role_from_db(&state)
.await
.change_context(UserErrors::InternalServerError)?;
let token =
utils::user::generate_jwt_auth_token_without_profile(&state, &user_from_db, &user_role)
.await?;
utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await;
let response =
utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token.clone())?;
auth::cookies::set_cookie_response(response, token)
}
#[cfg(feature = "email")]
pub async fn accept_invite_from_email_token_only_flow(
state: SessionState,
user_token: auth::UserFromSinglePurposeToken,
request: user_api::AcceptInviteFromEmailRequest,
) -> UserResponse<user_api::TokenOrPayloadResponse<user_api::DashboardEntryResponse>> {
) -> UserResponse<user_api::TokenResponse> {
let token = request.token.expose();
let email_token = auth::decode_jwt::<email_types::EmailToken>(&token, &state)
@ -1261,10 +957,10 @@ pub async fn accept_invite_from_email_token_only_flow(
.get_token_with_user_role(&state, &user_role)
.await?;
let response = user_api::TokenOrPayloadResponse::Token(user_api::TokenResponse {
let response = user_api::TokenResponse {
token: token.clone(),
token_type: next_flow.get_flow().into(),
});
};
auth::cookies::set_cookie_response(response, token)
}
@ -1503,7 +1199,7 @@ pub async fn list_merchants_for_user(
.await
.change_context(UserErrors::InternalServerError)?;
let merchant_accounts = state
let merchant_accounts_map = state
.store
.list_multiple_merchant_accounts(
&(&state).into(),
@ -1517,17 +1213,62 @@ pub async fn list_merchants_for_user(
.collect::<Result<Vec<_>, _>>()?,
)
.await
.change_context(UserErrors::InternalServerError)?;
.change_context(UserErrors::InternalServerError)?
.into_iter()
.map(|merchant_account| (merchant_account.get_id().clone(), merchant_account))
.collect::<HashMap<_, _>>();
let roles =
utils::user_role::get_multiple_role_info_for_user_roles(&state, &user_roles).await?;
let roles_map = futures::future::try_join_all(user_roles.iter().map(|user_role| async {
let Some(merchant_id) = &user_role.merchant_id else {
return Err(report!(UserErrors::InternalServerError))
.attach_printable("merchant_id not found for user_role");
};
let Some(org_id) = &user_role.org_id else {
return Err(report!(UserErrors::InternalServerError)
.attach_printable("org_id not found in user_role"));
};
roles::RoleInfo::from_role_id(&state, &user_role.role_id, merchant_id, org_id)
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("Unable to find role info for user role")
}))
.await?
.into_iter()
.map(|role_info| (role_info.get_role_id().to_owned(), role_info))
.collect::<HashMap<_, _>>();
Ok(ApplicationResponse::Json(
utils::user::get_multiple_merchant_details_with_status(
user_roles,
merchant_accounts,
roles,
)?,
user_roles
.into_iter()
.map(|user_role| {
let Some(merchant_id) = &user_role.merchant_id else {
return Err(report!(UserErrors::InternalServerError))
.attach_printable("merchant_id not found for user_role");
};
let Some(org_id) = &user_role.org_id else {
return Err(report!(UserErrors::InternalServerError)
.attach_printable("org_id not found in user_role"));
};
let merchant_account = merchant_accounts_map
.get(merchant_id)
.ok_or(UserErrors::InternalServerError)
.attach_printable("Merchant account for user role doesn't exist")?;
let role_info = roles_map
.get(&user_role.role_id)
.ok_or(UserErrors::InternalServerError)
.attach_printable("Role info for user role doesn't exist")?;
Ok(user_api::UserMerchantAccount {
merchant_id: merchant_id.to_owned(),
merchant_name: merchant_account.merchant_name.clone(),
is_active: user_role.status == UserStatus::Active,
role_id: user_role.role_id,
role_name: role_info.get_role_name().to_string(),
org_id: org_id.to_owned(),
})
})
.collect::<Result<Vec<_>, _>>()?,
))
}
@ -1650,71 +1391,12 @@ pub async fn list_users_for_merchant_account(
)))
}
#[cfg(feature = "email")]
pub async fn verify_email(
state: SessionState,
req: user_api::VerifyEmailRequest,
) -> UserResponse<user_api::SignInResponse> {
let token = req.token.clone().expose();
let email_token = auth::decode_jwt::<email_types::EmailToken>(&token, &state)
.await
.change_context(UserErrors::LinkInvalid)?;
auth::blacklist::check_email_token_in_blacklist(&state, &token).await?;
let user = state
.global_store
.find_user_by_email(
&email_token
.get_email()
.change_context(UserErrors::InternalServerError)?,
)
.await
.change_context(UserErrors::InternalServerError)?;
let user = state
.global_store
.update_user_by_user_id(user.user_id.as_str(), storage_user::UserUpdate::VerifyUser)
.await
.change_context(UserErrors::InternalServerError)?;
let user_from_db: domain::UserFromStorage = user.into();
let signin_strategy =
if let Some(preferred_merchant_id) = user_from_db.get_preferred_merchant_id() {
let preferred_role = user_from_db
.get_role_from_db_by_merchant_id(&state, &preferred_merchant_id)
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("User role with preferred_merchant_id not found")?;
domain::SignInWithRoleStrategyType::SingleRole(domain::SignInWithSingleRoleStrategy {
user: user_from_db,
user_role: Box::new(preferred_role),
})
} else {
let user_roles = user_from_db.get_roles_from_db(&state).await?;
domain::SignInWithRoleStrategyType::decide_signin_strategy_by_user_roles(
user_from_db,
user_roles,
)
.await?
};
let _ = auth::blacklist::insert_email_token_in_blacklist(&state, &token)
.await
.map_err(|error| logger::error!(?error));
let response = signin_strategy.get_signin_response(&state).await?;
let token = utils::user::get_token_from_signin_response(&response);
auth::cookies::set_cookie_response(response, token)
}
#[cfg(feature = "email")]
pub async fn verify_email_token_only_flow(
state: SessionState,
user_token: auth::UserFromSinglePurposeToken,
req: user_api::VerifyEmailRequest,
) -> UserResponse<user_api::TokenOrPayloadResponse<user_api::SignInResponse>> {
) -> UserResponse<user_api::TokenResponse> {
let token = req.token.clone().expose();
let email_token = auth::decode_jwt::<email_types::EmailToken>(&token, &state)
.await
@ -1754,10 +1436,10 @@ pub async fn verify_email_token_only_flow(
let next_flow = current_flow.next(user_from_db, &state).await?;
let token = next_flow.get_token(&state).await?;
let response = user_api::TokenOrPayloadResponse::Token(user_api::TokenResponse {
let response = user_api::TokenResponse {
token: token.clone(),
token_type: next_flow.get_flow().into(),
});
};
auth::cookies::set_cookie_response(response, token)
}
@ -2839,24 +2521,7 @@ pub async fn switch_org_for_user(
))?
.to_owned();
let merchant_id = if let Some(merchant_id) = &user_role.merchant_id {
merchant_id.clone()
} else {
state
.store
.list_merchant_accounts_by_organization_id(
key_manager_state,
request.org_id.get_string_repr(),
)
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("Failed to list merchant accounts by organization_id")?
.first()
.ok_or(UserErrors::InternalServerError)
.attach_printable("No merchant account found for the given organization_id")?
.get_id()
.clone()
};
let merchant_id = utils::user_role::get_single_merchant_id(&state, &user_role).await?;
let profile_id = if let Some(profile_id) = &user_role.profile_id {
profile_id.clone()

View File

@ -24,20 +24,6 @@ pub mod role;
use common_enums::{EntityType, PermissionGroup};
use strum::IntoEnumIterator;
// TODO: To be deprecated once groups are stable
pub async fn get_authorization_info_with_modules(
_state: SessionState,
) -> UserResponse<user_role_api::AuthorizationInfoResponse> {
Ok(ApplicationResponse::Json(
user_role_api::AuthorizationInfoResponse(
info::get_module_authorization_info()
.into_iter()
.map(|module_info| user_role_api::AuthorizationInfo::Module(module_info.into()))
.collect(),
),
))
}
pub async fn get_authorization_info_with_groups(
_state: SessionState,
) -> UserResponse<user_role_api::AuthorizationInfoResponse> {
@ -50,6 +36,7 @@ pub async fn get_authorization_info_with_groups(
),
))
}
pub async fn get_authorization_info_with_group_tag(
) -> UserResponse<user_role_api::AuthorizationInfoResponse> {
static GROUPS_WITH_PARENT_TAGS: Lazy<Vec<user_role_api::ParentInfo>> = Lazy::new(|| {
@ -309,85 +296,11 @@ pub async fn accept_invitation(
Ok(ApplicationResponse::StatusOk)
}
pub async fn merchant_select(
state: SessionState,
user_token: auth::UserFromSinglePurposeToken,
req: user_role_api::MerchantSelectRequest,
) -> UserResponse<user_api::TokenOrPayloadResponse<user_api::DashboardEntryResponse>> {
let merchant_accounts = state
.store
.list_multiple_merchant_accounts(&(&state).into(), req.merchant_ids)
.await
.change_context(UserErrors::InternalServerError)?;
let update_result =
futures::future::join_all(merchant_accounts.iter().map(|merchant_account| async {
let (update_v1_result, update_v2_result) =
utils::user_role::update_v1_and_v2_user_roles_in_db(
&state,
user_token.user_id.as_str(),
&merchant_account.organization_id,
merchant_account.get_id(),
None,
UserRoleUpdate::UpdateStatus {
status: UserStatus::Active,
modified_by: user_token.user_id.clone(),
},
)
.await;
if update_v1_result.is_err_and(|err| !err.current_context().is_db_not_found())
|| update_v2_result.is_err_and(|err| !err.current_context().is_db_not_found())
{
Err(report!(UserErrors::InternalServerError))
} else {
Ok(())
}
}))
.await;
if update_result.iter().all(Result::is_err) {
return Err(UserErrors::MerchantIdNotFound.into());
}
if let Some(true) = req.need_dashboard_entry_response {
let user_from_db: domain::UserFromStorage = state
.global_store
.find_user_by_id(user_token.user_id.as_str())
.await
.change_context(UserErrors::InternalServerError)?
.into();
let user_role = user_from_db
.get_preferred_or_active_user_role_from_db(&state)
.await
.change_context(UserErrors::InternalServerError)?;
utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await;
let token =
utils::user::generate_jwt_auth_token_without_profile(&state, &user_from_db, &user_role)
.await?;
let response = utils::user::get_dashboard_entry_response(
&state,
user_from_db,
user_role,
token.clone(),
)?;
return auth::cookies::set_cookie_response(
user_api::TokenOrPayloadResponse::Payload(response),
token,
);
}
Ok(ApplicationResponse::StatusOk)
}
pub async fn merchant_select_token_only_flow(
state: SessionState,
user_token: auth::UserFromSinglePurposeToken,
req: user_role_api::MerchantSelectRequest,
) -> UserResponse<user_api::TokenOrPayloadResponse<user_api::DashboardEntryResponse>> {
) -> UserResponse<user_api::TokenResponse> {
let merchant_accounts = state
.store
.list_multiple_merchant_accounts(&(&state).into(), req.merchant_ids)
@ -444,10 +357,10 @@ pub async fn merchant_select_token_only_flow(
.get_token_with_user_role(&state, &user_role)
.await?;
let response = user_api::TokenOrPayloadResponse::Token(user_api::TokenResponse {
let response = user_api::TokenResponse {
token: token.clone(),
token_type: next_flow.get_flow().into(),
});
};
auth::cookies::set_cookie_response(response, token)
}

View File

@ -16,30 +16,10 @@ use crate::{
utils,
};
pub async fn get_role_from_token_with_permissions(
state: SessionState,
user_from_token: UserFromToken,
) -> UserResponse<role_api::GetRoleFromTokenResponse> {
let role_info = user_from_token
.get_role_info_from_db(&state)
.await
.attach_printable("Invalid role_id in JWT")?;
let permissions = role_info
.get_permissions_set()
.into_iter()
.map(Into::into)
.collect();
Ok(ApplicationResponse::Json(
role_api::GetRoleFromTokenResponse::Permissions(permissions),
))
}
pub async fn get_role_from_token_with_groups(
state: SessionState,
user_from_token: UserFromToken,
) -> UserResponse<role_api::GetRoleFromTokenResponse> {
) -> UserResponse<Vec<role_api::PermissionGroup>> {
let role_info = user_from_token
.get_role_info_from_db(&state)
.await
@ -47,9 +27,7 @@ pub async fn get_role_from_token_with_groups(
let permissions = role_info.get_permission_groups().to_vec();
Ok(ApplicationResponse::Json(
role_api::GetRoleFromTokenResponse::Groups(permissions),
))
Ok(ApplicationResponse::Json(permissions))
}
pub async fn create_role(
@ -105,56 +83,6 @@ pub async fn create_role(
))
}
// TODO: To be deprecated once groups are stable
pub async fn list_invitable_roles_with_permissions(
state: SessionState,
user_from_token: UserFromToken,
) -> UserResponse<role_api::ListRolesResponse> {
let predefined_roles_map = PREDEFINED_ROLES
.iter()
.filter(|(_, role_info)| role_info.is_invitable())
.map(|(role_id, role_info)| {
role_api::RoleInfoResponse::Permissions(role_api::RoleInfoWithPermissionsResponse {
permissions: role_info
.get_permissions_set()
.into_iter()
.map(Into::into)
.collect(),
role_id: role_id.to_string(),
role_name: role_info.get_role_name().to_string(),
role_scope: role_info.get_scope(),
})
});
let custom_roles_map = state
.store
.list_all_roles(&user_from_token.merchant_id, &user_from_token.org_id)
.await
.change_context(UserErrors::InternalServerError)?
.into_iter()
.filter_map(|role| {
let role_info = roles::RoleInfo::from(role);
role_info
.is_invitable()
.then_some(role_api::RoleInfoResponse::Permissions(
role_api::RoleInfoWithPermissionsResponse {
permissions: role_info
.get_permissions_set()
.into_iter()
.map(Into::into)
.collect(),
role_id: role_info.get_role_id().to_string(),
role_name: role_info.get_role_name().to_string(),
role_scope: role_info.get_scope(),
},
))
});
Ok(ApplicationResponse::Json(role_api::ListRolesResponse(
predefined_roles_map.chain(custom_roles_map).collect(),
)))
}
pub async fn list_invitable_roles_with_groups(
state: SessionState,
user_from_token: UserFromToken,
@ -162,14 +90,14 @@ pub async fn list_invitable_roles_with_groups(
let predefined_roles_map = PREDEFINED_ROLES
.iter()
.filter(|(_, role_info)| role_info.is_invitable())
.map(|(role_id, role_info)| {
role_api::RoleInfoResponse::Groups(role_api::RoleInfoWithGroupsResponse {
.map(
|(role_id, role_info)| role_api::RoleInfoWithGroupsResponse {
groups: role_info.get_permission_groups().to_vec(),
role_id: role_id.to_string(),
role_name: role_info.get_role_name().to_string(),
role_scope: role_info.get_scope(),
})
});
},
);
let custom_roles_map = state
.store
@ -181,14 +109,12 @@ pub async fn list_invitable_roles_with_groups(
let role_info = roles::RoleInfo::from(role);
role_info
.is_invitable()
.then_some(role_api::RoleInfoResponse::Groups(
role_api::RoleInfoWithGroupsResponse {
.then_some(role_api::RoleInfoWithGroupsResponse {
groups: role_info.get_permission_groups().to_vec(),
role_id: role_info.get_role_id().to_string(),
role_name: role_info.get_role_name().to_string(),
role_scope: role_info.get_scope(),
},
))
})
});
Ok(ApplicationResponse::Json(role_api::ListRolesResponse(
@ -196,46 +122,11 @@ pub async fn list_invitable_roles_with_groups(
)))
}
// TODO: To be deprecated once groups are stable
pub async fn get_role_with_permissions(
state: SessionState,
user_from_token: UserFromToken,
role: role_api::GetRoleRequest,
) -> UserResponse<role_api::RoleInfoResponse> {
let role_info = roles::RoleInfo::from_role_id(
&state,
&role.role_id,
&user_from_token.merchant_id,
&user_from_token.org_id,
)
.await
.to_not_found_response(UserErrors::InvalidRoleId)?;
if role_info.is_internal() {
return Err(UserErrors::InvalidRoleId.into());
}
let permissions = role_info
.get_permissions_set()
.into_iter()
.map(Into::into)
.collect();
Ok(ApplicationResponse::Json(
role_api::RoleInfoResponse::Permissions(role_api::RoleInfoWithPermissionsResponse {
permissions,
role_id: role.role_id,
role_name: role_info.get_role_name().to_string(),
role_scope: role_info.get_scope(),
}),
))
}
pub async fn get_role_with_groups(
state: SessionState,
user_from_token: UserFromToken,
role: role_api::GetRoleRequest,
) -> UserResponse<role_api::RoleInfoResponse> {
) -> UserResponse<role_api::RoleInfoWithGroupsResponse> {
let role_info = roles::RoleInfo::from_role_id(
&state,
&role.role_id,
@ -250,12 +141,12 @@ pub async fn get_role_with_groups(
}
Ok(ApplicationResponse::Json(
role_api::RoleInfoResponse::Groups(role_api::RoleInfoWithGroupsResponse {
role_api::RoleInfoWithGroupsResponse {
groups: role_info.get_permission_groups().to_vec(),
role_id: role.role_id,
role_name: role_info.get_role_name().to_string(),
role_scope: role_info.get_scope(),
}),
},
))
}

View File

@ -357,11 +357,7 @@ impl UserRoleInterface for MockDb {
for user_role in user_roles.iter() {
let Some(user_role_merchant_id) = &user_role.merchant_id else {
return Err(errors::StorageError::DatabaseError(
report!(errors::DatabaseError::Others)
.attach_printable("merchant_id not found for user_role"),
)
.into());
continue;
};
if user_role.user_id == user_id
&& user_role_merchant_id == merchant_id

View File

@ -1681,6 +1681,7 @@ impl User {
route = route
.service(web::resource("").route(web::get().to(get_user_details)))
.service(web::resource("/signin").route(web::post().to(user_signin)))
.service(web::resource("/v2/signin").route(web::post().to(user_signin)))
// signin/signup with sso using openidconnect
.service(web::resource("/oidc").route(web::post().to(sso_sign)))
@ -1796,6 +1797,7 @@ impl User {
web::resource("/signup_with_merchant_id")
.route(web::post().to(user_signup_with_merchant_id)),
)
.service(web::resource("/verify_email").route(web::post().to(verify_email)))
.service(web::resource("/v2/verify_email").route(web::post().to(verify_email)))
.service(
web::resource("/verify_email_request")

View File

@ -62,22 +62,16 @@ pub async fn user_signup(
state: web::Data<AppState>,
http_req: HttpRequest,
json_payload: web::Json<user_api::SignUpRequest>,
query: web::Query<user_api::TokenOnlyQueryParam>,
) -> HttpResponse {
let flow = Flow::UserSignUp;
let req_payload = json_payload.into_inner();
let is_token_only = query.into_inner().token_only;
Box::pin(api::server_wrap(
flow.clone(),
state,
&http_req,
req_payload.clone(),
|state, _: (), req_body, _| async move {
if let Some(true) = is_token_only {
user_core::signup_token_only_flow(state, req_body).await
} else {
user_core::signup(state, req_body).await
}
},
&auth::NoAuth,
api_locking::LockAction::NotApplicable,
@ -89,22 +83,16 @@ pub async fn user_signin(
state: web::Data<AppState>,
http_req: HttpRequest,
json_payload: web::Json<user_api::SignInRequest>,
query: web::Query<user_api::TokenOnlyQueryParam>,
) -> HttpResponse {
let flow = Flow::UserSignIn;
let req_payload = json_payload.into_inner();
let is_token_only = query.into_inner().token_only;
Box::pin(api::server_wrap(
flow.clone(),
state,
&http_req,
req_payload.clone(),
|state, _: (), req_body, _| async move {
if let Some(true) = is_token_only {
user_core::signin_token_only_flow(state, req_body).await
} else {
user_core::signin(state, req_body).await
}
},
&auth::NoAuth,
api_locking::LockAction::NotApplicable,
@ -409,46 +397,27 @@ pub async fn reset_password(
state: web::Data<AppState>,
req: HttpRequest,
payload: web::Json<user_api::ResetPasswordRequest>,
query: web::Query<user_api::TokenOnlyQueryParam>,
) -> HttpResponse {
let flow = Flow::ResetPassword;
let is_token_only = query.into_inner().token_only;
if let Some(true) = is_token_only {
Box::pin(api::server_wrap(
flow,
state.clone(),
&req,
payload.into_inner(),
|state, user, payload, _| {
user_core::reset_password_token_only_flow(state, user, payload)
},
|state, user, payload, _| user_core::reset_password_token_only_flow(state, user, payload),
&auth::SinglePurposeJWTAuth(TokenPurpose::ResetPassword),
api_locking::LockAction::NotApplicable,
))
.await
} else {
Box::pin(api::server_wrap(
flow,
state.clone(),
&req,
payload.into_inner(),
|state, _: (), payload, _| user_core::reset_password(state, payload),
&auth::NoAuth,
api_locking::LockAction::NotApplicable,
))
.await
}
}
pub async fn invite_multiple_user(
state: web::Data<AppState>,
req: HttpRequest,
payload: web::Json<Vec<user_api::InviteUserRequest>>,
token_only_query_param: web::Query<user_api::TokenOnlyQueryParam>,
auth_id_query_param: web::Query<user_api::AuthIdQueryParam>,
) -> HttpResponse {
let flow = Flow::InviteMultipleUser;
let is_token_only = token_only_query_param.into_inner().token_only;
let auth_id = auth_id_query_param.into_inner().auth_id;
Box::pin(api::server_wrap(
flow,
@ -456,14 +425,7 @@ pub async fn invite_multiple_user(
&req,
payload.into_inner(),
|state, user, payload, req_state| {
user_core::invite_multiple_user(
state,
user,
payload,
req_state,
is_token_only,
auth_id.clone(),
)
user_core::invite_multiple_user(state, user, payload, req_state, auth_id.clone())
},
&auth::JWTAuth(Permission::UsersWrite),
api_locking::LockAction::NotApplicable,
@ -499,11 +461,8 @@ pub async fn accept_invite_from_email(
state: web::Data<AppState>,
req: HttpRequest,
payload: web::Json<user_api::AcceptInviteFromEmailRequest>,
query: web::Query<user_api::TokenOnlyQueryParam>,
) -> HttpResponse {
let flow = Flow::AcceptInviteFromEmail;
let is_token_only = query.into_inner().token_only;
if let Some(true) = is_token_only {
Box::pin(api::server_wrap(
flow.clone(),
state,
@ -516,20 +475,6 @@ pub async fn accept_invite_from_email(
api_locking::LockAction::NotApplicable,
))
.await
} else {
Box::pin(api::server_wrap(
flow,
state.clone(),
&req,
payload.into_inner(),
|state, _: (), request_payload, _| {
user_core::accept_invite_from_email(state, request_payload)
},
&auth::NoAuth,
api_locking::LockAction::NotApplicable,
))
.await
}
}
#[cfg(feature = "email")]
@ -537,11 +482,8 @@ pub async fn verify_email(
state: web::Data<AppState>,
http_req: HttpRequest,
json_payload: web::Json<user_api::VerifyEmailRequest>,
query: web::Query<user_api::TokenOnlyQueryParam>,
) -> HttpResponse {
let flow = Flow::VerifyEmail;
let is_token_only = query.into_inner().token_only;
if let Some(true) = is_token_only {
Box::pin(api::server_wrap(
flow.clone(),
state,
@ -554,18 +496,6 @@ pub async fn verify_email(
api_locking::LockAction::NotApplicable,
))
.await
} else {
Box::pin(api::server_wrap(
flow.clone(),
state,
&http_req,
json_payload.into_inner(),
|state, _: (), req_payload, _| user_core::verify_email(state, req_payload),
&auth::NoAuth,
api_locking::LockAction::NotApplicable,
))
.await
}
}
#[cfg(feature = "email")]

View File

@ -1,8 +1,5 @@
use actix_web::{web, HttpRequest, HttpResponse};
use api_models::{
user as user_api,
user_role::{self as user_role_api, role as role_api},
};
use api_models::user_role::{self as user_role_api, role as role_api};
use common_enums::TokenPurpose;
use router_env::Flow;
@ -22,22 +19,15 @@ use crate::{
pub async fn get_authorization_info(
state: web::Data<AppState>,
http_req: HttpRequest,
query: web::Query<role_api::GetGroupsQueryParam>,
) -> HttpResponse {
let flow = Flow::GetAuthorizationInfo;
let respond_with_groups = query.into_inner().groups.unwrap_or(false);
Box::pin(api::server_wrap(
flow,
state.clone(),
&http_req,
(),
|state, _: (), _, _| async move {
// TODO: Permissions to be deprecated once groups are stable
if respond_with_groups {
user_role_core::get_authorization_info_with_groups(state).await
} else {
user_role_core::get_authorization_info_with_modules(state).await
}
},
&auth::JWTAuth(Permission::UsersRead),
api_locking::LockAction::NotApplicable,
@ -45,13 +35,8 @@ pub async fn get_authorization_info(
.await
}
pub async fn get_role_from_token(
state: web::Data<AppState>,
req: HttpRequest,
query: web::Query<role_api::GetGroupsQueryParam>,
) -> HttpResponse {
pub async fn get_role_from_token(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
let flow = Flow::GetRoleFromToken;
let respond_with_groups = query.into_inner().groups.unwrap_or(false);
Box::pin(api::server_wrap(
flow,
@ -59,12 +44,7 @@ pub async fn get_role_from_token(
&req,
(),
|state, user, _, _| async move {
// TODO: Permissions to be deprecated once groups are stable
if respond_with_groups {
role_core::get_role_from_token_with_groups(state, user).await
} else {
role_core::get_role_from_token_with_permissions(state, user).await
}
},
&auth::DashboardNoPermissionAuth,
api_locking::LockAction::NotApplicable,
@ -90,25 +70,15 @@ pub async fn create_role(
.await
}
pub async fn list_all_roles(
state: web::Data<AppState>,
req: HttpRequest,
query: web::Query<role_api::GetGroupsQueryParam>,
) -> HttpResponse {
pub async fn list_all_roles(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
let flow = Flow::ListRoles;
let respond_with_groups = query.into_inner().groups.unwrap_or(false);
Box::pin(api::server_wrap(
flow,
state.clone(),
&req,
(),
|state, user, _, _| async move {
// TODO: Permissions to be deprecated once groups are stable
if respond_with_groups {
role_core::list_invitable_roles_with_groups(state, user).await
} else {
role_core::list_invitable_roles_with_permissions(state, user).await
}
},
&auth::JWTAuth(Permission::UsersRead),
api_locking::LockAction::NotApplicable,
@ -120,25 +90,18 @@ pub async fn get_role(
state: web::Data<AppState>,
req: HttpRequest,
path: web::Path<String>,
query: web::Query<role_api::GetGroupsQueryParam>,
) -> HttpResponse {
let flow = Flow::GetRole;
let request_payload = user_role_api::role::GetRoleRequest {
role_id: path.into_inner(),
};
let respond_with_groups = query.into_inner().groups.unwrap_or(false);
Box::pin(api::server_wrap(
flow,
state.clone(),
&req,
request_payload,
|state, user, payload, _| async move {
// TODO: Permissions to be deprecated once groups are stable
if respond_with_groups {
role_core::get_role_with_groups(state, user, payload).await
} else {
role_core::get_role_with_permissions(state, user, payload).await
}
},
&auth::JWTAuth(Permission::UsersRead),
api_locking::LockAction::NotApplicable,
@ -209,22 +172,16 @@ 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,
state.clone(),
&req,
payload,
|state, user, req_body, _| async move {
if let Some(true) = is_token_only {
user_role_core::merchant_select_token_only_flow(state, user, req_body).await
} else {
user_role_core::merchant_select(state, user, req_body).await
}
},
&auth::SinglePurposeJWTAuth(TokenPurpose::AcceptInvite),
api_locking::LockAction::NotApplicable,

View File

@ -1,346 +0,0 @@
use std::collections::HashMap;
#[cfg(feature = "olap")]
use error_stack::ResultExt;
use once_cell::sync::Lazy;
use super::permissions::Permission;
use crate::consts;
#[cfg(feature = "olap")]
use crate::core::errors::{UserErrors, UserResult};
#[allow(dead_code)]
pub struct RoleInfo {
permissions: Vec<Permission>,
name: Option<&'static str>,
is_invitable: bool,
is_deletable: bool,
is_updatable: bool,
}
impl RoleInfo {
pub fn get_permissions(&self) -> &Vec<Permission> {
&self.permissions
}
pub fn get_name(&self) -> Option<&'static str> {
self.name
}
pub fn is_invitable(&self) -> bool {
self.is_invitable
}
}
pub static PREDEFINED_PERMISSIONS: Lazy<HashMap<&'static str, RoleInfo>> = Lazy::new(|| {
let mut roles = HashMap::new();
roles.insert(
consts::user_role::ROLE_ID_INTERNAL_ADMIN,
RoleInfo {
permissions: vec![
Permission::PaymentRead,
Permission::PaymentWrite,
Permission::RefundRead,
Permission::RefundWrite,
Permission::ApiKeyRead,
Permission::ApiKeyWrite,
Permission::MerchantAccountRead,
Permission::MerchantAccountWrite,
Permission::MerchantConnectorAccountRead,
Permission::MerchantConnectorAccountWrite,
Permission::RoutingRead,
Permission::RoutingWrite,
Permission::ThreeDsDecisionManagerWrite,
Permission::ThreeDsDecisionManagerRead,
Permission::SurchargeDecisionManagerWrite,
Permission::SurchargeDecisionManagerRead,
Permission::DisputeRead,
Permission::DisputeWrite,
Permission::MandateRead,
Permission::MandateWrite,
Permission::CustomerRead,
Permission::CustomerWrite,
Permission::Analytics,
Permission::UsersRead,
Permission::UsersWrite,
Permission::MerchantAccountCreate,
Permission::PayoutRead,
Permission::PayoutWrite,
],
name: None,
is_invitable: false,
is_deletable: false,
is_updatable: false,
},
);
roles.insert(
consts::user_role::ROLE_ID_INTERNAL_VIEW_ONLY_USER,
RoleInfo {
permissions: vec![
Permission::PaymentRead,
Permission::RefundRead,
Permission::ApiKeyRead,
Permission::MerchantAccountRead,
Permission::MerchantConnectorAccountRead,
Permission::RoutingRead,
Permission::ThreeDsDecisionManagerRead,
Permission::SurchargeDecisionManagerRead,
Permission::Analytics,
Permission::DisputeRead,
Permission::MandateRead,
Permission::CustomerRead,
Permission::UsersRead,
Permission::PayoutRead,
],
name: None,
is_invitable: false,
is_deletable: false,
is_updatable: false,
},
);
roles.insert(
consts::user_role::ROLE_ID_ORGANIZATION_ADMIN,
RoleInfo {
permissions: vec![
Permission::PaymentRead,
Permission::PaymentWrite,
Permission::RefundRead,
Permission::RefundWrite,
Permission::ApiKeyRead,
Permission::ApiKeyWrite,
Permission::MerchantAccountRead,
Permission::MerchantAccountWrite,
Permission::MerchantConnectorAccountRead,
Permission::MerchantConnectorAccountWrite,
Permission::RoutingRead,
Permission::RoutingWrite,
Permission::ThreeDsDecisionManagerWrite,
Permission::ThreeDsDecisionManagerRead,
Permission::SurchargeDecisionManagerWrite,
Permission::SurchargeDecisionManagerRead,
Permission::DisputeRead,
Permission::DisputeWrite,
Permission::MandateRead,
Permission::MandateWrite,
Permission::CustomerRead,
Permission::CustomerWrite,
Permission::Analytics,
Permission::UsersRead,
Permission::UsersWrite,
Permission::MerchantAccountCreate,
Permission::PayoutRead,
Permission::PayoutWrite,
],
name: Some("Organization Admin"),
is_invitable: false,
is_deletable: false,
is_updatable: false,
},
);
// MERCHANT ROLES
roles.insert(
consts::user_role::ROLE_ID_MERCHANT_ADMIN,
RoleInfo {
permissions: vec![
Permission::PaymentRead,
Permission::PaymentWrite,
Permission::RefundRead,
Permission::RefundWrite,
Permission::ApiKeyRead,
Permission::ApiKeyWrite,
Permission::MerchantAccountRead,
Permission::MerchantAccountWrite,
Permission::MerchantConnectorAccountRead,
Permission::MerchantConnectorAccountWrite,
Permission::RoutingRead,
Permission::RoutingWrite,
Permission::ThreeDsDecisionManagerWrite,
Permission::ThreeDsDecisionManagerRead,
Permission::SurchargeDecisionManagerWrite,
Permission::SurchargeDecisionManagerRead,
Permission::DisputeRead,
Permission::DisputeWrite,
Permission::MandateRead,
Permission::MandateWrite,
Permission::CustomerRead,
Permission::CustomerWrite,
Permission::Analytics,
Permission::UsersRead,
Permission::UsersWrite,
Permission::PayoutRead,
Permission::PayoutWrite,
],
name: Some("Admin"),
is_invitable: true,
is_deletable: true,
is_updatable: true,
},
);
roles.insert(
consts::user_role::ROLE_ID_MERCHANT_VIEW_ONLY,
RoleInfo {
permissions: vec![
Permission::PaymentRead,
Permission::RefundRead,
Permission::ApiKeyRead,
Permission::MerchantAccountRead,
Permission::MerchantConnectorAccountRead,
Permission::RoutingRead,
Permission::ThreeDsDecisionManagerRead,
Permission::SurchargeDecisionManagerRead,
Permission::DisputeRead,
Permission::MandateRead,
Permission::CustomerRead,
Permission::Analytics,
Permission::UsersRead,
Permission::PayoutRead,
],
name: Some("View Only"),
is_invitable: true,
is_deletable: true,
is_updatable: true,
},
);
roles.insert(
consts::user_role::ROLE_ID_MERCHANT_IAM_ADMIN,
RoleInfo {
permissions: vec![
Permission::PaymentRead,
Permission::RefundRead,
Permission::ApiKeyRead,
Permission::MerchantAccountRead,
Permission::MerchantConnectorAccountRead,
Permission::RoutingRead,
Permission::ThreeDsDecisionManagerRead,
Permission::SurchargeDecisionManagerRead,
Permission::DisputeRead,
Permission::MandateRead,
Permission::CustomerRead,
Permission::Analytics,
Permission::UsersRead,
Permission::UsersWrite,
Permission::PayoutRead,
],
name: Some("IAM"),
is_invitable: true,
is_deletable: true,
is_updatable: true,
},
);
roles.insert(
consts::user_role::ROLE_ID_MERCHANT_DEVELOPER,
RoleInfo {
permissions: vec![
Permission::PaymentRead,
Permission::RefundRead,
Permission::ApiKeyRead,
Permission::ApiKeyWrite,
Permission::MerchantAccountRead,
Permission::MerchantConnectorAccountRead,
Permission::RoutingRead,
Permission::ThreeDsDecisionManagerRead,
Permission::SurchargeDecisionManagerRead,
Permission::DisputeRead,
Permission::MandateRead,
Permission::CustomerRead,
Permission::Analytics,
Permission::UsersRead,
Permission::PayoutRead,
],
name: Some("Developer"),
is_invitable: true,
is_deletable: true,
is_updatable: true,
},
);
roles.insert(
consts::user_role::ROLE_ID_MERCHANT_OPERATOR,
RoleInfo {
permissions: vec![
Permission::PaymentRead,
Permission::PaymentWrite,
Permission::RefundRead,
Permission::RefundWrite,
Permission::ApiKeyRead,
Permission::MerchantAccountRead,
Permission::MerchantConnectorAccountRead,
Permission::MerchantConnectorAccountWrite,
Permission::RoutingRead,
Permission::RoutingWrite,
Permission::ThreeDsDecisionManagerRead,
Permission::ThreeDsDecisionManagerWrite,
Permission::SurchargeDecisionManagerRead,
Permission::SurchargeDecisionManagerWrite,
Permission::DisputeRead,
Permission::MandateRead,
Permission::CustomerRead,
Permission::Analytics,
Permission::UsersRead,
Permission::PayoutRead,
Permission::PayoutWrite,
],
name: Some("Operator"),
is_invitable: true,
is_deletable: true,
is_updatable: true,
},
);
roles.insert(
consts::user_role::ROLE_ID_MERCHANT_CUSTOMER_SUPPORT,
RoleInfo {
permissions: vec![
Permission::PaymentRead,
Permission::RefundRead,
Permission::RefundWrite,
Permission::DisputeRead,
Permission::DisputeWrite,
Permission::MerchantAccountRead,
Permission::MerchantConnectorAccountRead,
Permission::MandateRead,
Permission::CustomerRead,
Permission::Analytics,
Permission::PayoutRead,
],
name: Some("Customer Support"),
is_invitable: true,
is_deletable: true,
is_updatable: true,
},
);
roles
});
pub fn get_role_name_from_id(role_id: &str) -> Option<&'static str> {
PREDEFINED_PERMISSIONS
.get(role_id)
.and_then(|role_info| role_info.name)
}
#[cfg(feature = "olap")]
pub fn is_role_invitable(role_id: &str) -> UserResult<bool> {
PREDEFINED_PERMISSIONS
.get(role_id)
.map(|role_info| role_info.is_invitable)
.ok_or(UserErrors::InvalidRoleId.into())
.attach_printable(format!("role_id = {} doesn't exist", role_id))
}
#[cfg(feature = "olap")]
pub fn is_role_deletable(role_id: &str) -> UserResult<bool> {
PREDEFINED_PERMISSIONS
.get(role_id)
.map(|role_info| role_info.is_deletable)
.ok_or(UserErrors::InvalidRoleId.into())
.attach_printable(format!("role_id = {} doesn't exist", role_id))
}
#[cfg(feature = "olap")]
pub fn is_role_updatable(role_id: &str) -> UserResult<bool> {
PREDEFINED_PERMISSIONS
.get(role_id)
.map(|role_info| role_info.is_updatable)
.ok_or(UserErrors::InvalidRoleId.into())
.attach_printable(format!("role_id = {} doesn't exist", role_id))
}

View File

@ -3,7 +3,7 @@ use std::{collections::HashSet, ops, str::FromStr};
use api_models::{
admin as admin_api, organization as api_org, user as user_api, user_role as user_role_api,
};
use common_enums::{EntityType, TokenPurpose};
use common_enums::EntityType;
use common_utils::{
crypto::Encryptable, errors::CustomResult, id_type, new_type::MerchantName, pii, type_name,
types::keymanager::Identifier,
@ -32,13 +32,9 @@ use crate::{
},
db::{user_role::InsertUserRolePayload, GlobalStorageInterface},
routes::SessionState,
services::{
self,
authentication::{self as auth, UserFromToken},
authorization::info,
},
services::{self, authentication::UserFromToken, authorization::info},
types::transformers::ForeignFrom,
utils::{self, user::password},
utils::user::password,
};
pub mod dashboard_metadata;
@ -1115,130 +1111,6 @@ impl From<info::PermissionModule> for user_role_api::PermissionModule {
}
}
pub enum SignInWithRoleStrategyType {
SingleRole(SignInWithSingleRoleStrategy),
MultipleRoles(SignInWithMultipleRolesStrategy),
}
impl SignInWithRoleStrategyType {
pub async fn decide_signin_strategy_by_user_roles(
user: UserFromStorage,
user_roles: Vec<UserRole>,
) -> UserResult<Self> {
if user_roles.is_empty() {
return Err(UserErrors::InternalServerError.into());
}
if let Some(user_role) = user_roles
.iter()
.find(|role| role.status == UserStatus::Active)
{
Ok(Self::SingleRole(SignInWithSingleRoleStrategy {
user,
user_role: Box::new(user_role.clone()),
}))
} else {
Ok(Self::MultipleRoles(SignInWithMultipleRolesStrategy {
user,
user_roles,
}))
}
}
pub async fn get_signin_response(
self,
state: &SessionState,
) -> UserResult<user_api::SignInResponse> {
match self {
Self::SingleRole(strategy) => strategy.get_signin_response(state).await,
Self::MultipleRoles(strategy) => strategy.get_signin_response(state).await,
}
}
}
pub struct SignInWithSingleRoleStrategy {
pub user: UserFromStorage,
pub user_role: Box<UserRole>,
}
impl SignInWithSingleRoleStrategy {
async fn get_signin_response(
self,
state: &SessionState,
) -> UserResult<user_api::SignInResponse> {
let token = utils::user::generate_jwt_auth_token_without_profile(
state,
&self.user,
&self.user_role,
)
.await?;
utils::user_role::set_role_permissions_in_cache_by_user_role(state, &self.user_role).await;
let dashboard_entry_response =
utils::user::get_dashboard_entry_response(state, self.user, *self.user_role, token)?;
Ok(user_api::SignInResponse::DashboardEntry(
dashboard_entry_response,
))
}
}
pub struct SignInWithMultipleRolesStrategy {
pub user: UserFromStorage,
pub user_roles: Vec<UserRole>,
}
impl SignInWithMultipleRolesStrategy {
async fn get_signin_response(
self,
state: &SessionState,
) -> UserResult<user_api::SignInResponse> {
let merchant_accounts = state
.store
.list_multiple_merchant_accounts(
&state.into(),
self.user_roles
.iter()
.map(|role| {
role.merchant_id
.clone()
.ok_or(UserErrors::InternalServerError)
})
.collect::<Result<Vec<_>, _>>()?,
)
.await
.change_context(UserErrors::InternalServerError)?;
let roles =
utils::user_role::get_multiple_role_info_for_user_roles(state, &self.user_roles)
.await?;
let merchant_details = utils::user::get_multiple_merchant_details_with_status(
self.user_roles,
merchant_accounts,
roles,
)?;
Ok(user_api::SignInResponse::MerchantSelect(
user_api::MerchantSelectResponse {
name: self.user.get_name(),
email: self.user.get_email(),
token: auth::SinglePurposeToken::new_token(
self.user.get_user_id().to_string(),
TokenPurpose::AcceptInvite,
Origin::SignIn,
&state.conf,
vec![],
)
.await?
.into(),
merchants: merchant_details,
verification_days_left: utils::user::get_verification_days_left(state, &self.user)?,
},
))
}
}
impl ForeignFrom<UserStatus> for user_role_api::UserStatus {
fn foreign_from(value: UserStatus) -> Self {
match value {

View File

@ -1,6 +1,6 @@
use common_enums::TokenPurpose;
use diesel_models::{enums::UserStatus, user_role::UserRole};
use error_stack::report;
use error_stack::{report, ResultExt};
use masking::Secret;
use super::UserFromStorage;
@ -109,16 +109,14 @@ impl JWTFlow {
) -> UserResult<Secret<String>> {
auth::AuthToken::new_token(
next_flow.user.get_user_id().to_string(),
user_role
.merchant_id
.clone()
.ok_or(report!(UserErrors::InternalServerError))?,
utils::user_role::get_single_merchant_id(state, user_role).await?,
user_role.role_id.clone(),
&state.conf,
user_role
.org_id
.clone()
.ok_or(report!(UserErrors::InternalServerError))?,
.ok_or(report!(UserErrors::InternalServerError))
.attach_printable("org_id not found")?,
None,
)
.await

View File

@ -1,11 +1,11 @@
use std::{collections::HashMap, sync::Arc};
use std::sync::Arc;
use api_models::user as user_api;
use common_enums::UserAuthType;
use common_utils::{
encryption::Encryption, errors::CustomResult, id_type, type_name, types::keymanager::Identifier,
};
use diesel_models::{enums::UserStatus, user_role::UserRole};
use diesel_models::user_role::UserRole;
use error_stack::{report, ResultExt};
use masking::{ExposeInterface, Secret};
use redis_interface::RedisConnectionPool;
@ -128,28 +128,6 @@ pub async fn generate_jwt_auth_token_with_attributes(
Ok(Secret::new(token))
}
pub fn get_dashboard_entry_response(
state: &SessionState,
user: UserFromStorage,
user_role: UserRole,
token: Secret<String>,
) -> UserResult<user_api::DashboardEntryResponse> {
let verification_days_left = get_verification_days_left(state, &user)?;
Ok(user_api::DashboardEntryResponse {
merchant_id: user_role.merchant_id.ok_or(
report!(UserErrors::InternalServerError)
.attach_printable("merchant_id not found for user_role"),
)?,
token,
name: user.get_name(),
email: user.get_email(),
user_id: user.get_user_id().to_string(),
verification_days_left,
user_role: user_role.role_id,
})
}
#[allow(unused_variables)]
pub fn get_verification_days_left(
state: &SessionState,
@ -161,54 +139,6 @@ pub fn get_verification_days_left(
return Ok(None);
}
pub fn get_multiple_merchant_details_with_status(
user_roles: Vec<UserRole>,
merchant_accounts: Vec<MerchantAccount>,
roles: Vec<RoleInfo>,
) -> UserResult<Vec<user_api::UserMerchantAccount>> {
let merchant_account_map = merchant_accounts
.into_iter()
.map(|merchant_account| (merchant_account.get_id().clone(), merchant_account))
.collect::<HashMap<_, _>>();
let role_map = roles
.into_iter()
.map(|role_info| (role_info.get_role_id().to_string(), role_info))
.collect::<HashMap<_, _>>();
user_roles
.into_iter()
.map(|user_role| {
let Some(merchant_id) = &user_role.merchant_id else {
return Err(report!(UserErrors::InternalServerError))
.attach_printable("merchant_id not found for user_role");
};
let Some(org_id) = &user_role.org_id else {
return Err(report!(UserErrors::InternalServerError)
.attach_printable("org_id not found in user_role"));
};
let merchant_account = merchant_account_map
.get(merchant_id)
.ok_or(UserErrors::InternalServerError)
.attach_printable("Merchant account for user role doesn't exist")?;
let role_info = role_map
.get(&user_role.role_id)
.ok_or(UserErrors::InternalServerError)
.attach_printable("Role info for user role doesn't exist")?;
Ok(user_api::UserMerchantAccount {
merchant_id: merchant_id.to_owned(),
merchant_name: merchant_account.merchant_name.clone(),
is_active: user_role.status == UserStatus::Active,
role_id: user_role.role_id,
role_name: role_info.get_role_name().to_string(),
org_id: org_id.to_owned(),
})
})
.collect()
}
pub async fn get_user_from_db_by_email(
state: &SessionState,
email: domain::UserEmail,
@ -220,13 +150,6 @@ pub async fn get_user_from_db_by_email(
.map(UserFromStorage::from)
}
pub fn get_token_from_signin_response(resp: &user_api::SignInResponse) -> Secret<String> {
match resp {
user_api::SignInResponse::DashboardEntry(data) => data.token.clone(),
user_api::SignInResponse::MerchantSelect(data) => data.token.clone(),
}
}
pub fn get_redis_connection(state: &SessionState) -> UserResult<Arc<RedisConnectionPool>> {
state
.store

View File

@ -1,7 +1,7 @@
use std::collections::HashSet;
use api_models::user_role as user_role_api;
use common_enums::PermissionGroup;
use common_enums::{EntityType, PermissionGroup};
use common_utils::id_type;
use diesel_models::{
enums::UserRoleVersion,
@ -13,7 +13,7 @@ use storage_impl::errors::StorageError;
use crate::{
consts,
core::errors::{StorageErrorExt, UserErrors, UserResult},
core::errors::{UserErrors, UserResult},
routes::SessionState,
services::authorization::{self as authz, permissions::Permission, roles},
types::domain,
@ -167,28 +167,6 @@ pub async fn set_role_permissions_in_cache_if_required(
.attach_printable("Error setting permissions in redis")
}
pub async fn get_multiple_role_info_for_user_roles(
state: &SessionState,
user_roles: &[UserRole],
) -> UserResult<Vec<roles::RoleInfo>> {
futures::future::try_join_all(user_roles.iter().map(|user_role| async {
let Some(merchant_id) = &user_role.merchant_id else {
return Err(report!(UserErrors::InternalServerError))
.attach_printable("merchant_id not found for user_role");
};
let Some(org_id) = &user_role.org_id else {
return Err(report!(UserErrors::InternalServerError)
.attach_printable("org_id not found in user_role"));
};
let role = roles::RoleInfo::from_role_id(state, &user_role.role_id, merchant_id, org_id)
.await
.to_not_found_response(UserErrors::InternalServerError)
.attach_printable("Role for user role doesn't exist")?;
Ok::<_, Report<UserErrors>>(role)
}))
.await
}
pub async fn update_v1_and_v2_user_roles_in_db(
state: &SessionState,
user_id: &str,
@ -234,3 +212,38 @@ pub async fn update_v1_and_v2_user_roles_in_db(
(updated_v1_role, updated_v2_role)
}
pub async fn get_single_merchant_id(
state: &SessionState,
user_role: &UserRole,
) -> UserResult<id_type::MerchantId> {
match user_role.entity_type {
Some(EntityType::Organization) => Ok(state
.store
.list_merchant_accounts_by_organization_id(
&state.into(),
user_role
.org_id
.as_ref()
.ok_or(UserErrors::InternalServerError)
.attach_printable("org_id not found")?
.get_string_repr(),
)
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("Failed to get merchant list for org")?
.first()
.ok_or(UserErrors::InternalServerError)
.attach_printable("No merchants found for org_id")?
.get_id()
.clone()),
Some(EntityType::Merchant)
| Some(EntityType::Internal)
| Some(EntityType::Profile)
| None => user_role
.merchant_id
.clone()
.ok_or(UserErrors::InternalServerError)
.attach_printable("merchant_id not found"),
}
}

View File

@ -2,7 +2,7 @@
"childrenOrder": [
"Signin",
"Signin Wrong",
"Signin Token Only",
"Signin Token Only Wrong"
"Signin V2 - To be deprecated",
"Signin V2 Wrong"
]
}

View File

@ -1,24 +1,24 @@
console.log("[LOG]::x-request-id - " + pm.response.headers.get("x-request-id"));
// Validate status 2xx
pm.test("[POST]::user/v2/signin?token_only=true - Status code is 2xx", function () {
pm.test("[POST]::user/v2/signin - Status code is 2xx", function () {
pm.response.to.be.success;
});
// Validate if response header has matching content-type
pm.test("[POST]::user/v2/signin?token_only=true - Content-Type is application/json", function () {
pm.test("[POST]::user/v2/signin - Content-Type is application/json", function () {
pm.expect(pm.response.headers.get("Content-Type")).to.include(
"application/json",
);
});
// Validate if response has JSON Body
pm.test("[POST]::user/v2/signin?token_only=true - Response has JSON Body", function () {
pm.test("[POST]::user/v2/signin - Response has JSON Body", function () {
pm.response.to.have.jsonBody();
});
// Validate specific JSON response content
pm.test("[POST]::user/v2/signin?token_only=true - Response contains token", function () {
pm.test("[POST]::user/v2/signin - Response contains token", function () {
var jsonData = pm.response.json();
pm.expect(jsonData).to.have.property("token");
pm.expect(jsonData.token).to.be.a("string").and.to.not.be.empty;

View File

@ -18,7 +18,7 @@
}
},
"url": {
"raw": "{{baseUrl}}/user/v2/signin?token_only=true",
"raw": "{{baseUrl}}/user/v2/signin",
"host": [
"{{baseUrl}}"
],
@ -26,12 +26,6 @@
"user",
"v2",
"signin"
],
"query": [
{
"key": "token_only",
"value": "true"
}
]
}
}

View File

@ -1,18 +1,18 @@
console.log("[LOG]::x-request-id - " + pm.response.headers.get("x-request-id"));
// Validate status 4xx
pm.test("[POST]::/user/v2/signin?token_only=true - Status code is 401", function () {
pm.test("[POST]::/user/v2/signin - Status code is 401", function () {
pm.response.to.have.status(401);
});
// Validate if response header has matching content-type
pm.test("[POST]::user/v2/signin?token_only=true - Content-Type is application/json", function () {
pm.test("[POST]::user/v2/signin - Content-Type is application/json", function () {
pm.expect(pm.response.headers.get("Content-Type")).to.include(
"application/json",
);
});
// Validate if response has JSON Body
pm.test("[POST]::user/v2/signin?token_only=true - Response has JSON Body", function () {
pm.test("[POST]::user/v2/signin - Response has JSON Body", function () {
pm.response.to.have.jsonBody();
});

View File

@ -18,7 +18,7 @@
}
},
"url": {
"raw": "{{baseUrl}}/user/v2/signin?token_only=true",
"raw": "{{baseUrl}}/user/v2/signin",
"host": [
"{{baseUrl}}"
],
@ -26,12 +26,6 @@
"user",
"v2",
"signin"
],
"query": [
{
"key": "token_only",
"value": "true"
}
]
}
}

View File

@ -1,18 +1,18 @@
console.log("[LOG]::x-request-id - " + pm.response.headers.get("x-request-id"));
// Validate status code is 4xx Bad Request
pm.test("[POST]::/user/v2/signin - Status code is 401", function () {
pm.test("[POST]::/user/signin - Status code is 401", function () {
pm.response.to.have.status(401);
});
// Validate if response header has matching content-type
pm.test("[POST]::/user/v2/signin - Content-Type is application/json", function () {
pm.test("[POST]::/user/signin - Content-Type is application/json", function () {
pm.expect(pm.response.headers.get("Content-Type")).to.include(
"application/json",
);
});
// Validate if response has JSON Body
pm.test("[POST]::/user/v2/signin - Response has JSON Body", function () {
pm.test("[POST]::/user/signin - Response has JSON Body", function () {
pm.response.to.have.jsonBody();
});

View File

@ -18,13 +18,12 @@
}
},
"url": {
"raw": "{{baseUrl}}/user/v2/signin",
"raw": "{{baseUrl}}/user/signin",
"host": [
"{{baseUrl}}"
],
"path": [
"user",
"v2",
"signin"
]
}

View File

@ -1,24 +1,24 @@
console.log("[LOG]::x-request-id - " + pm.response.headers.get("x-request-id"));
// Validate status 2xx
pm.test("[POST]::/user/v2/signin - Status code is 2xx", function () {
pm.test("[POST]::/user/signin - Status code is 2xx", function () {
pm.response.to.be.success;
});
// Validate if response header has matching content-type
pm.test("[POST]::/user/v2/signin - Content-Type is application/json", function () {
pm.test("[POST]::/user/signin - Content-Type is application/json", function () {
pm.expect(pm.response.headers.get("Content-Type")).to.include(
"application/json",
);
});
// Validate if response has JSON Body
pm.test("[POST]::/user/v2/signin - Response has JSON Body", function () {
pm.test("[POST]::/user/signin - Response has JSON Body", function () {
pm.response.to.have.jsonBody();
});
// Validate specific JSON response content
pm.test("[POST]::/user/v2/signin - Response contains token", function () {
pm.test("[POST]::/user/signin - Response contains token", function () {
var jsonData = pm.response.json();
pm.expect(jsonData).to.have.property("token");
pm.expect(jsonData.token).to.be.a("string").and.to.not.be.empty;

View File

@ -18,13 +18,12 @@
}
},
"url": {
"raw": "{{baseUrl}}/user/v2/signin",
"raw": "{{baseUrl}}/user/signin",
"host": [
"{{baseUrl}}"
],
"path": [
"user",
"v2",
"signin"
]
}

View File

@ -1,24 +1,11 @@
{
"event": [
{
"listen": "prerequest",
"script": {
"exec": [
""
],
"type": "text/javascript"
}
"info": {
"_postman_id": "b5b40c9a-7e58-42c7-8b89-0adb208c45c9",
"name": "users",
"description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "26710321"
},
{
"listen": "test",
"script": {
"exec": [
""
],
"type": "text/javascript"
}
}
],
"item": [
{
"name": "Health check",
@ -133,7 +120,7 @@
],
"body": {
"mode": "raw",
"raw": "{\"email\":\"{{unique_email}}\"}"
"raw": "{\n \"email\": \"{{unique_email}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/user/connect_account",
@ -163,24 +150,24 @@
"console.log(\"[LOG]::x-request-id - \" + pm.response.headers.get(\"x-request-id\"));",
"",
"// Validate status 2xx",
"pm.test(\"[POST]::/user/v2/signin - Status code is 2xx\", function () {",
"pm.test(\"[POST]::/user/signin - Status code is 2xx\", function () {",
" pm.response.to.be.success;",
"});",
"",
"// Validate if response header has matching content-type",
"pm.test(\"[POST]::/user/v2/signin - Content-Type is application/json\", function () {",
"pm.test(\"[POST]::/user/signin - Content-Type is application/json\", function () {",
" pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(",
" \"application/json\",",
" );",
"});",
"",
"// Validate if response has JSON Body",
"pm.test(\"[POST]::/user/v2/signin - Response has JSON Body\", function () {",
"pm.test(\"[POST]::/user/signin - Response has JSON Body\", function () {",
" pm.response.to.have.jsonBody();",
"});",
"",
"// Validate specific JSON response content",
"pm.test(\"[POST]::/user/v2/signin - Response contains token\", function () {",
"pm.test(\"[POST]::/user/signin - Response contains token\", function () {",
" var jsonData = pm.response.json();",
" pm.expect(jsonData).to.have.property(\"token\");",
" pm.expect(jsonData.token).to.be.a(\"string\").and.to.not.be.empty;",
@ -213,16 +200,15 @@
],
"body": {
"mode": "raw",
"raw": "{\"email\":\"{{user_email}}\",\"password\":\"{{user_password}}\"}"
"raw": "{\n \"email\": \"{{user_email}}\",\n \"password\": \"{{user_password}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/user/v2/signin",
"raw": "{{baseUrl}}/user/signin",
"host": [
"{{baseUrl}}"
],
"path": [
"user",
"v2",
"signin"
]
}
@ -239,19 +225,19 @@
"console.log(\"[LOG]::x-request-id - \" + pm.response.headers.get(\"x-request-id\"));",
"",
"// Validate status code is 4xx Bad Request",
"pm.test(\"[POST]::/user/v2/signin - Status code is 401\", function () {",
"pm.test(\"[POST]::/user/signin - Status code is 401\", function () {",
" pm.response.to.have.status(401);",
"});",
"",
"// Validate if response header has matching content-type",
"pm.test(\"[POST]::/user/v2/signin - Content-Type is application/json\", function () {",
"pm.test(\"[POST]::/user/signin - Content-Type is application/json\", function () {",
" pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(",
" \"application/json\",",
" );",
"});",
"",
"// Validate if response has JSON Body",
"pm.test(\"[POST]::/user/v2/signin - Response has JSON Body\", function () {",
"pm.test(\"[POST]::/user/signin - Response has JSON Body\", function () {",
" pm.response.to.have.jsonBody();",
"});"
],
@ -273,16 +259,15 @@
],
"body": {
"mode": "raw",
"raw": "{\"email\":\"{{user_email}}\",\"password\":\"{{wrong_password}}\"}"
"raw": "{\n \"email\": \"{{user_email}}\",\n \"password\": \"{{wrong_password}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/user/v2/signin",
"raw": "{{baseUrl}}/user/signin",
"host": [
"{{baseUrl}}"
],
"path": [
"user",
"v2",
"signin"
]
}
@ -290,7 +275,7 @@
"response": []
},
{
"name": "Signin Token Only",
"name": "Signin V2 - To be deprecated",
"event": [
{
"listen": "test",
@ -299,24 +284,24 @@
"console.log(\"[LOG]::x-request-id - \" + pm.response.headers.get(\"x-request-id\"));",
"",
"// Validate status 2xx",
"pm.test(\"[POST]::user/v2/signin?token_only=true - Status code is 2xx\", function () {",
"pm.test(\"[POST]::user/v2/signin - Status code is 2xx\", function () {",
" pm.response.to.be.success;",
"});",
"",
"// Validate if response header has matching content-type",
"pm.test(\"[POST]::user/v2/signin?token_only=true - Content-Type is application/json\", function () {",
"pm.test(\"[POST]::user/v2/signin - Content-Type is application/json\", function () {",
" pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(",
" \"application/json\",",
" );",
"});",
"",
"// Validate if response has JSON Body",
"pm.test(\"[POST]::user/v2/signin?token_only=true - Response has JSON Body\", function () {",
"pm.test(\"[POST]::user/v2/signin - Response has JSON Body\", function () {",
" pm.response.to.have.jsonBody();",
"});",
"",
"// Validate specific JSON response content",
"pm.test(\"[POST]::user/v2/signin?token_only=true - Response contains token\", function () {",
"pm.test(\"[POST]::user/v2/signin - Response contains token\", function () {",
" var jsonData = pm.response.json();",
" pm.expect(jsonData).to.have.property(\"token\");",
" pm.expect(jsonData.token).to.be.a(\"string\").and.to.not.be.empty;",
@ -340,10 +325,10 @@
],
"body": {
"mode": "raw",
"raw": "{\"email\":\"{{user_email}}\",\"password\":\"{{user_password}}\"}"
"raw": "{\n \"email\": \"{{user_email}}\",\n \"password\": \"{{user_password}}\" \n}"
},
"url": {
"raw": "{{baseUrl}}/user/v2/signin?token_only=true",
"raw": "{{baseUrl}}/user/v2/signin",
"host": [
"{{baseUrl}}"
],
@ -351,19 +336,13 @@
"user",
"v2",
"signin"
],
"query": [
{
"key": "token_only",
"value": "true"
}
]
}
},
"response": []
},
{
"name": "Signin Token Only Wrong",
"name": "Signin V2 Wrong",
"event": [
{
"listen": "test",
@ -372,19 +351,19 @@
"console.log(\"[LOG]::x-request-id - \" + pm.response.headers.get(\"x-request-id\"));",
"",
"// Validate status 4xx",
"pm.test(\"[POST]::/user/v2/signin?token_only=true - Status code is 401\", function () {",
"pm.test(\"[POST]::/user/v2/signin - Status code is 401\", function () {",
" pm.response.to.have.status(401);",
"});",
"",
"// Validate if response header has matching content-type",
"pm.test(\"[POST]::user/v2/signin?token_only=true - Content-Type is application/json\", function () {",
"pm.test(\"[POST]::user/v2/signin - Content-Type is application/json\", function () {",
" pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(",
" \"application/json\",",
" );",
"});",
"",
"// Validate if response has JSON Body",
"pm.test(\"[POST]::user/v2/signin?token_only=true - Response has JSON Body\", function () {",
"pm.test(\"[POST]::user/v2/signin - Response has JSON Body\", function () {",
" pm.response.to.have.jsonBody();",
"});"
],
@ -406,10 +385,10 @@
],
"body": {
"mode": "raw",
"raw": "{\"email\":\"{{user_email}}\",\"password\":\"{{wrong_password}}\"}"
"raw": "{\n \"email\": \"{{user_email}}\",\n \"password\": \"{{wrong_password}}\" \n}"
},
"url": {
"raw": "{{baseUrl}}/user/v2/signin?token_only=true",
"raw": "{{baseUrl}}/user/v2/signin",
"host": [
"{{baseUrl}}"
],
@ -417,12 +396,6 @@
"user",
"v2",
"signin"
],
"query": [
{
"key": "token_only",
"value": "true"
}
]
}
},
@ -433,13 +406,26 @@
]
}
],
"info": {
"_postman_id": "b5b40c9a-7e58-42c7-8b89-0adb208c45c9",
"name": "users",
"description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "26710321"
"event": [
{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
},
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
}
],
"variable": [
{
"key": "baseUrl",