Skip to content

Commit

Permalink
feat(users): Create Decision manager for User Flows (#4518)
Browse files Browse the repository at this point in the history
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
  • Loading branch information
ThisIsMani and hyperswitch-bot[bot] authored May 2, 2024
1 parent 3ed0e8b commit 4b3faf6
Show file tree
Hide file tree
Showing 10 changed files with 393 additions and 18 deletions.
7 changes: 4 additions & 3 deletions crates/api_models/src/events/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ use crate::user::{
CreateInternalUserRequest, DashboardEntryResponse, ForgotPasswordRequest,
GetUserDetailsResponse, GetUserRoleDetailsRequest, GetUserRoleDetailsResponse,
InviteUserRequest, ListUsersResponse, ReInviteUserRequest, ResetPasswordRequest,
SendVerifyEmailRequest, SignInResponse, SignUpRequest, SignUpWithMerchantIdRequest,
SwitchMerchantIdRequest, UpdateUserAccountDetailsRequest, UserMerchantCreate,
VerifyEmailRequest,
SendVerifyEmailRequest, SignInResponse, SignInWithTokenResponse, SignUpRequest,
SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, UpdateUserAccountDetailsRequest,
UserMerchantCreate, VerifyEmailRequest,
};

impl ApiEventMetric for DashboardEntryResponse {
Expand Down Expand Up @@ -62,6 +62,7 @@ common_utils::impl_misc_api_event_type!(
SignInResponse,
UpdateUserAccountDetailsRequest,
GetUserDetailsResponse,
SignInWithTokenResponse,
GetUserRoleDetailsRequest,
GetUserRoleDetailsResponse
);
Expand Down
20 changes: 19 additions & 1 deletion crates/api_models/src/user.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use common_enums::{PermissionGroup, RoleScope};
use common_enums::{PermissionGroup, RoleScope, TokenPurpose};
use common_utils::{crypto::OptionalEncryptableName, pii};
use masking::Secret;

Expand Down Expand Up @@ -213,3 +213,21 @@ pub struct UpdateUserAccountDetailsRequest {
pub name: Option<Secret<String>>,
pub preferred_merchant_id: Option<String>,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct TokenOnlyQueryParam {
pub token_only: Option<bool>,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct TokenResponse {
pub token: Secret<String>,
pub token_type: TokenPurpose,
}

#[derive(Debug, serde::Serialize)]
#[serde(untagged)]
pub enum SignInWithTokenResponse {
Token(TokenResponse),
SignInResponse(SignInResponse),
}
14 changes: 14 additions & 0 deletions crates/common_enums/src/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2706,3 +2706,17 @@ pub enum BankHolderType {
Personal,
Business,
}

#[derive(Debug, Clone, PartialEq, Eq, strum::Display, serde::Deserialize, serde::Serialize)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum TokenPurpose {
#[serde(rename = "totp")]
#[strum(serialize = "totp")]
TOTP,
VerifyEmail,
AcceptInvitationFromEmail,
ResetPassword,
AcceptInvite,
UserInfo,
}
44 changes: 43 additions & 1 deletion crates/router/src/core/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ pub async fn signup(
pub async fn signin(
state: AppState,
request: user_api::SignInRequest,
) -> UserResponse<user_api::SignInResponse> {
) -> UserResponse<user_api::SignInWithTokenResponse> {
let user_from_db: domain::UserFromStorage = state
.store
.find_user_by_email(&request.email)
Expand Down Expand Up @@ -161,6 +161,48 @@ pub async fn signin(

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::SignInWithTokenResponse::SignInResponse(response),
token,
)
}

pub async fn signin_token_only_flow(
state: AppState,
request: user_api::SignInRequest,
) -> UserResponse<user_api::SignInWithTokenResponse> {
let user_from_db: domain::UserFromStorage = state
.store
.find_user_by_email(&request.email)
.await
.to_not_found_response(UserErrors::InvalidCredentials)?
.into();

user_from_db.compare_password(request.password)?;

let next_flow =
domain::NextFlow::from_origin(domain::Origin::SignIn, user_from_db.clone(), &state).await?;

let token = match next_flow.get_flow() {
domain::UserFlow::SPTFlow(spt_flow) => spt_flow.generate_spt(&state, &next_flow).await,
domain::UserFlow::JWTFlow(jwt_flow) => {
#[cfg(feature = "email")]
{
user_from_db.get_verification_days_left(&state)?;
}

let user_role = user_from_db
.get_preferred_or_active_user_role_from_db(&state)
.await
.to_not_found_response(UserErrors::InternalServerError)?;
jwt_flow.generate_jwt(&state, &next_flow, &user_role).await
}
}?;

let response = user_api::SignInWithTokenResponse::Token(user_api::TokenResponse {
token: token.clone(),
token_type: next_flow.get_flow().into(),
});
auth::cookies::set_cookie_response(response, token)
}

Expand Down
10 changes: 9 additions & 1 deletion crates/router/src/routes/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,23 @@ 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, _| user_core::signin(state, req_body),
|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,
))
Expand Down
3 changes: 2 additions & 1 deletion crates/router/src/routes/user_role.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use actix_web::{web, HttpRequest, HttpResponse};
use api_models::user_role::{self as user_role_api, role as role_api};
use common_enums::TokenPurpose;
use router_env::Flow;

use super::AppState;
Expand Down Expand Up @@ -214,7 +215,7 @@ pub async fn accept_invitation(
&req,
payload,
user_role_core::accept_invitation,
&auth::SinglePurposeJWTAuth(auth::Purpose::AcceptInvite),
&auth::SinglePurposeJWTAuth(TokenPurpose::AcceptInvite),
api_locking::LockAction::NotApplicable,
))
.await
Expand Down
22 changes: 13 additions & 9 deletions crates/router/src/services/authentication.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use api_models::{
payments,
};
use async_trait::async_trait;
use common_enums::TokenPurpose;
use common_utils::date_time;
use error_stack::{report, ResultExt};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
Expand Down Expand Up @@ -66,7 +67,7 @@ pub enum AuthenticationType {
},
SinglePurposeJWT {
user_id: String,
purpose: Purpose,
purpose: TokenPurpose,
},
MerchantId {
merchant_id: String,
Expand Down Expand Up @@ -113,28 +114,28 @@ impl AuthenticationType {
}
}

#[cfg(feature = "olap")]
#[derive(Clone, Debug)]
pub struct UserFromSinglePurposeToken {
pub user_id: String,
pub origin: domain::Origin,
}

#[cfg(feature = "olap")]
#[derive(serde::Serialize, serde::Deserialize)]
pub struct SinglePurposeToken {
pub user_id: String,
pub purpose: Purpose,
pub purpose: TokenPurpose,
pub origin: domain::Origin,
pub exp: u64,
}

#[derive(Debug, Clone, PartialEq, Eq, strum::Display, serde::Deserialize, serde::Serialize)]
pub enum Purpose {
AcceptInvite,
}

#[cfg(feature = "olap")]
impl SinglePurposeToken {
pub async fn new_token(
user_id: String,
purpose: Purpose,
purpose: TokenPurpose,
origin: domain::Origin,
settings: &Settings,
) -> UserResult<String> {
let exp_duration =
Expand All @@ -143,6 +144,7 @@ impl SinglePurposeToken {
let token_payload = Self {
user_id,
purpose,
origin,
exp,
};
jwt::generate_jwt(&token_payload, settings).await
Expand Down Expand Up @@ -308,8 +310,9 @@ where
}
}

#[cfg(feature = "olap")]
#[derive(Debug)]
pub(crate) struct SinglePurposeJWTAuth(pub Purpose);
pub(crate) struct SinglePurposeJWTAuth(pub TokenPurpose);

#[cfg(feature = "olap")]
#[async_trait]
Expand All @@ -334,6 +337,7 @@ where
Ok((
UserFromSinglePurposeToken {
user_id: payload.user_id.clone(),
origin: payload.origin.clone(),
},
AuthenticationType::SinglePurposeJWT {
user_id: payload.user_id,
Expand Down
5 changes: 4 additions & 1 deletion crates/router/src/services/authentication/blacklist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use common_utils::date_time;
use error_stack::ResultExt;
use redis_interface::RedisConnectionPool;

use super::{AuthToken, SinglePurposeToken};
use super::AuthToken;
#[cfg(feature = "olap")]
use super::SinglePurposeToken;
#[cfg(feature = "email")]
use crate::consts::{EMAIL_TOKEN_BLACKLIST_PREFIX, EMAIL_TOKEN_TIME_IN_SECS};
use crate::{
Expand Down Expand Up @@ -154,6 +156,7 @@ impl BlackList for AuthToken {
}
}

#[cfg(feature = "olap")]
#[async_trait::async_trait]
impl BlackList for SinglePurposeToken {
async fn check_in_blacklist<A>(&self, state: &A) -> RouterResult<bool>
Expand Down
29 changes: 28 additions & 1 deletion crates/router/src/types/domain/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +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::TokenPurpose;
use common_utils::{errors::CustomResult, pii};
use diesel_models::{
enums::UserStatus,
Expand Down Expand Up @@ -31,6 +32,8 @@ use crate::{
};

pub mod dashboard_metadata;
pub mod decision_manager;
pub use decision_manager::*;

#[derive(Clone)]
pub struct UserName(Secret<String>);
Expand Down Expand Up @@ -810,6 +813,29 @@ impl UserFromStorage {
.find_user_role_by_user_id_merchant_id(self.get_user_id(), merchant_id)
.await
}

pub async fn get_preferred_or_active_user_role_from_db(
&self,
state: &AppState,
) -> CustomResult<UserRole, errors::StorageError> {
if let Some(preferred_merchant_id) = self.get_preferred_merchant_id() {
self.get_role_from_db_by_merchant_id(state, &preferred_merchant_id)
.await
} else {
state
.store
.list_user_roles_by_user_id(&self.0.user_id)
.await?
.into_iter()
.find(|role| role.status == UserStatus::Active)
.ok_or(
errors::StorageError::ValueNotFound(
"No active role found for user".to_string(),
)
.into(),
)
}
}
}

impl From<info::ModuleInfo> for user_role_api::ModuleInfo {
Expand Down Expand Up @@ -937,7 +963,8 @@ impl SignInWithMultipleRolesStrategy {
email: self.user.get_email(),
token: auth::SinglePurposeToken::new_token(
self.user.get_user_id().to_string(),
auth::Purpose::AcceptInvite,
TokenPurpose::AcceptInvite,
Origin::SignIn,
&state.conf,
)
.await?
Expand Down
Loading

0 comments on commit 4b3faf6

Please sign in to comment.