mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 20:23:43 +08:00
feat(users): Create Decision manager for User Flows (#4518)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -14,9 +14,9 @@ use crate::user::{
|
|||||||
CreateInternalUserRequest, DashboardEntryResponse, ForgotPasswordRequest,
|
CreateInternalUserRequest, DashboardEntryResponse, ForgotPasswordRequest,
|
||||||
GetUserDetailsResponse, GetUserRoleDetailsRequest, GetUserRoleDetailsResponse,
|
GetUserDetailsResponse, GetUserRoleDetailsRequest, GetUserRoleDetailsResponse,
|
||||||
InviteUserRequest, ListUsersResponse, ReInviteUserRequest, ResetPasswordRequest,
|
InviteUserRequest, ListUsersResponse, ReInviteUserRequest, ResetPasswordRequest,
|
||||||
SendVerifyEmailRequest, SignInResponse, SignUpRequest, SignUpWithMerchantIdRequest,
|
SendVerifyEmailRequest, SignInResponse, SignInWithTokenResponse, SignUpRequest,
|
||||||
SwitchMerchantIdRequest, UpdateUserAccountDetailsRequest, UserMerchantCreate,
|
SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, UpdateUserAccountDetailsRequest,
|
||||||
VerifyEmailRequest,
|
UserMerchantCreate, VerifyEmailRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl ApiEventMetric for DashboardEntryResponse {
|
impl ApiEventMetric for DashboardEntryResponse {
|
||||||
@ -62,6 +62,7 @@ common_utils::impl_misc_api_event_type!(
|
|||||||
SignInResponse,
|
SignInResponse,
|
||||||
UpdateUserAccountDetailsRequest,
|
UpdateUserAccountDetailsRequest,
|
||||||
GetUserDetailsResponse,
|
GetUserDetailsResponse,
|
||||||
|
SignInWithTokenResponse,
|
||||||
GetUserRoleDetailsRequest,
|
GetUserRoleDetailsRequest,
|
||||||
GetUserRoleDetailsResponse
|
GetUserRoleDetailsResponse
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use common_enums::{PermissionGroup, RoleScope};
|
use common_enums::{PermissionGroup, RoleScope, TokenPurpose};
|
||||||
use common_utils::{crypto::OptionalEncryptableName, pii};
|
use common_utils::{crypto::OptionalEncryptableName, pii};
|
||||||
use masking::Secret;
|
use masking::Secret;
|
||||||
|
|
||||||
@ -213,3 +213,21 @@ pub struct UpdateUserAccountDetailsRequest {
|
|||||||
pub name: Option<Secret<String>>,
|
pub name: Option<Secret<String>>,
|
||||||
pub preferred_merchant_id: Option<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),
|
||||||
|
}
|
||||||
|
|||||||
@ -2706,3 +2706,17 @@ pub enum BankHolderType {
|
|||||||
Personal,
|
Personal,
|
||||||
Business,
|
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,
|
||||||
|
}
|
||||||
|
|||||||
@ -123,7 +123,7 @@ pub async fn signup(
|
|||||||
pub async fn signin(
|
pub async fn signin(
|
||||||
state: AppState,
|
state: AppState,
|
||||||
request: user_api::SignInRequest,
|
request: user_api::SignInRequest,
|
||||||
) -> UserResponse<user_api::SignInResponse> {
|
) -> UserResponse<user_api::SignInWithTokenResponse> {
|
||||||
let user_from_db: domain::UserFromStorage = state
|
let user_from_db: domain::UserFromStorage = state
|
||||||
.store
|
.store
|
||||||
.find_user_by_email(&request.email)
|
.find_user_by_email(&request.email)
|
||||||
@ -161,6 +161,48 @@ pub async fn signin(
|
|||||||
|
|
||||||
let response = signin_strategy.get_signin_response(&state).await?;
|
let response = signin_strategy.get_signin_response(&state).await?;
|
||||||
let token = utils::user::get_token_from_signin_response(&response);
|
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)
|
auth::cookies::set_cookie_response(response, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -76,15 +76,23 @@ pub async fn user_signin(
|
|||||||
state: web::Data<AppState>,
|
state: web::Data<AppState>,
|
||||||
http_req: HttpRequest,
|
http_req: HttpRequest,
|
||||||
json_payload: web::Json<user_api::SignInRequest>,
|
json_payload: web::Json<user_api::SignInRequest>,
|
||||||
|
query: web::Query<user_api::TokenOnlyQueryParam>,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
let flow = Flow::UserSignIn;
|
let flow = Flow::UserSignIn;
|
||||||
let req_payload = json_payload.into_inner();
|
let req_payload = json_payload.into_inner();
|
||||||
|
let is_token_only = query.into_inner().token_only;
|
||||||
Box::pin(api::server_wrap(
|
Box::pin(api::server_wrap(
|
||||||
flow.clone(),
|
flow.clone(),
|
||||||
state,
|
state,
|
||||||
&http_req,
|
&http_req,
|
||||||
req_payload.clone(),
|
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,
|
&auth::NoAuth,
|
||||||
api_locking::LockAction::NotApplicable,
|
api_locking::LockAction::NotApplicable,
|
||||||
))
|
))
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
use actix_web::{web, HttpRequest, HttpResponse};
|
use actix_web::{web, HttpRequest, HttpResponse};
|
||||||
use api_models::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;
|
use router_env::Flow;
|
||||||
|
|
||||||
use super::AppState;
|
use super::AppState;
|
||||||
@ -214,7 +215,7 @@ pub async fn accept_invitation(
|
|||||||
&req,
|
&req,
|
||||||
payload,
|
payload,
|
||||||
user_role_core::accept_invitation,
|
user_role_core::accept_invitation,
|
||||||
&auth::SinglePurposeJWTAuth(auth::Purpose::AcceptInvite),
|
&auth::SinglePurposeJWTAuth(TokenPurpose::AcceptInvite),
|
||||||
api_locking::LockAction::NotApplicable,
|
api_locking::LockAction::NotApplicable,
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
|
|||||||
@ -4,6 +4,7 @@ use api_models::{
|
|||||||
payments,
|
payments,
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use common_enums::TokenPurpose;
|
||||||
use common_utils::date_time;
|
use common_utils::date_time;
|
||||||
use error_stack::{report, ResultExt};
|
use error_stack::{report, ResultExt};
|
||||||
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
|
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
|
||||||
@ -66,7 +67,7 @@ pub enum AuthenticationType {
|
|||||||
},
|
},
|
||||||
SinglePurposeJWT {
|
SinglePurposeJWT {
|
||||||
user_id: String,
|
user_id: String,
|
||||||
purpose: Purpose,
|
purpose: TokenPurpose,
|
||||||
},
|
},
|
||||||
MerchantId {
|
MerchantId {
|
||||||
merchant_id: String,
|
merchant_id: String,
|
||||||
@ -113,28 +114,28 @@ impl AuthenticationType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "olap")]
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct UserFromSinglePurposeToken {
|
pub struct UserFromSinglePurposeToken {
|
||||||
pub user_id: String,
|
pub user_id: String,
|
||||||
|
pub origin: domain::Origin,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "olap")]
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
pub struct SinglePurposeToken {
|
pub struct SinglePurposeToken {
|
||||||
pub user_id: String,
|
pub user_id: String,
|
||||||
pub purpose: Purpose,
|
pub purpose: TokenPurpose,
|
||||||
|
pub origin: domain::Origin,
|
||||||
pub exp: u64,
|
pub exp: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, strum::Display, serde::Deserialize, serde::Serialize)]
|
|
||||||
pub enum Purpose {
|
|
||||||
AcceptInvite,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "olap")]
|
#[cfg(feature = "olap")]
|
||||||
impl SinglePurposeToken {
|
impl SinglePurposeToken {
|
||||||
pub async fn new_token(
|
pub async fn new_token(
|
||||||
user_id: String,
|
user_id: String,
|
||||||
purpose: Purpose,
|
purpose: TokenPurpose,
|
||||||
|
origin: domain::Origin,
|
||||||
settings: &Settings,
|
settings: &Settings,
|
||||||
) -> UserResult<String> {
|
) -> UserResult<String> {
|
||||||
let exp_duration =
|
let exp_duration =
|
||||||
@ -143,6 +144,7 @@ impl SinglePurposeToken {
|
|||||||
let token_payload = Self {
|
let token_payload = Self {
|
||||||
user_id,
|
user_id,
|
||||||
purpose,
|
purpose,
|
||||||
|
origin,
|
||||||
exp,
|
exp,
|
||||||
};
|
};
|
||||||
jwt::generate_jwt(&token_payload, settings).await
|
jwt::generate_jwt(&token_payload, settings).await
|
||||||
@ -308,8 +310,9 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "olap")]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct SinglePurposeJWTAuth(pub Purpose);
|
pub(crate) struct SinglePurposeJWTAuth(pub TokenPurpose);
|
||||||
|
|
||||||
#[cfg(feature = "olap")]
|
#[cfg(feature = "olap")]
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@ -334,6 +337,7 @@ where
|
|||||||
Ok((
|
Ok((
|
||||||
UserFromSinglePurposeToken {
|
UserFromSinglePurposeToken {
|
||||||
user_id: payload.user_id.clone(),
|
user_id: payload.user_id.clone(),
|
||||||
|
origin: payload.origin.clone(),
|
||||||
},
|
},
|
||||||
AuthenticationType::SinglePurposeJWT {
|
AuthenticationType::SinglePurposeJWT {
|
||||||
user_id: payload.user_id,
|
user_id: payload.user_id,
|
||||||
|
|||||||
@ -5,7 +5,9 @@ use common_utils::date_time;
|
|||||||
use error_stack::ResultExt;
|
use error_stack::ResultExt;
|
||||||
use redis_interface::RedisConnectionPool;
|
use redis_interface::RedisConnectionPool;
|
||||||
|
|
||||||
use super::{AuthToken, SinglePurposeToken};
|
use super::AuthToken;
|
||||||
|
#[cfg(feature = "olap")]
|
||||||
|
use super::SinglePurposeToken;
|
||||||
#[cfg(feature = "email")]
|
#[cfg(feature = "email")]
|
||||||
use crate::consts::{EMAIL_TOKEN_BLACKLIST_PREFIX, EMAIL_TOKEN_TIME_IN_SECS};
|
use crate::consts::{EMAIL_TOKEN_BLACKLIST_PREFIX, EMAIL_TOKEN_TIME_IN_SECS};
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -154,6 +156,7 @@ impl BlackList for AuthToken {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "olap")]
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl BlackList for SinglePurposeToken {
|
impl BlackList for SinglePurposeToken {
|
||||||
async fn check_in_blacklist<A>(&self, state: &A) -> RouterResult<bool>
|
async fn check_in_blacklist<A>(&self, state: &A) -> RouterResult<bool>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ use std::{collections::HashSet, ops, str::FromStr};
|
|||||||
use api_models::{
|
use api_models::{
|
||||||
admin as admin_api, organization as api_org, user as user_api, user_role as user_role_api,
|
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 common_utils::{errors::CustomResult, pii};
|
||||||
use diesel_models::{
|
use diesel_models::{
|
||||||
enums::UserStatus,
|
enums::UserStatus,
|
||||||
@ -31,6 +32,8 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub mod dashboard_metadata;
|
pub mod dashboard_metadata;
|
||||||
|
pub mod decision_manager;
|
||||||
|
pub use decision_manager::*;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct UserName(Secret<String>);
|
pub struct UserName(Secret<String>);
|
||||||
@ -810,6 +813,29 @@ impl UserFromStorage {
|
|||||||
.find_user_role_by_user_id_merchant_id(self.get_user_id(), merchant_id)
|
.find_user_role_by_user_id_merchant_id(self.get_user_id(), merchant_id)
|
||||||
.await
|
.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 {
|
impl From<info::ModuleInfo> for user_role_api::ModuleInfo {
|
||||||
@ -937,7 +963,8 @@ impl SignInWithMultipleRolesStrategy {
|
|||||||
email: self.user.get_email(),
|
email: self.user.get_email(),
|
||||||
token: auth::SinglePurposeToken::new_token(
|
token: auth::SinglePurposeToken::new_token(
|
||||||
self.user.get_user_id().to_string(),
|
self.user.get_user_id().to_string(),
|
||||||
auth::Purpose::AcceptInvite,
|
TokenPurpose::AcceptInvite,
|
||||||
|
Origin::SignIn,
|
||||||
&state.conf,
|
&state.conf,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
|
|||||||
257
crates/router/src/types/domain/user/decision_manager.rs
Normal file
257
crates/router/src/types/domain/user/decision_manager.rs
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
use common_enums::TokenPurpose;
|
||||||
|
use diesel_models::{enums::UserStatus, user_role::UserRole};
|
||||||
|
use masking::Secret;
|
||||||
|
|
||||||
|
use super::UserFromStorage;
|
||||||
|
use crate::{
|
||||||
|
core::errors::{UserErrors, UserResult},
|
||||||
|
routes::AppState,
|
||||||
|
services::authentication as auth,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, Clone, Copy)]
|
||||||
|
pub enum UserFlow {
|
||||||
|
SPTFlow(SPTFlow),
|
||||||
|
JWTFlow(JWTFlow),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserFlow {
|
||||||
|
async fn is_required(&self, user: &UserFromStorage, state: &AppState) -> UserResult<bool> {
|
||||||
|
match self {
|
||||||
|
Self::SPTFlow(flow) => flow.is_required(user, state).await,
|
||||||
|
Self::JWTFlow(flow) => flow.is_required(user, state).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, Clone, Copy)]
|
||||||
|
pub enum SPTFlow {
|
||||||
|
TOTP,
|
||||||
|
VerifyEmail,
|
||||||
|
AcceptInvitationFromEmail,
|
||||||
|
ForceSetPassword,
|
||||||
|
MerchantSelect,
|
||||||
|
ResetPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SPTFlow {
|
||||||
|
async fn is_required(&self, user: &UserFromStorage, state: &AppState) -> UserResult<bool> {
|
||||||
|
match self {
|
||||||
|
// TOTP
|
||||||
|
Self::TOTP => Ok(true),
|
||||||
|
// Main email APIs
|
||||||
|
Self::AcceptInvitationFromEmail | Self::ResetPassword => Ok(true),
|
||||||
|
Self::VerifyEmail => Ok(user.0.is_verified),
|
||||||
|
// Final Checks
|
||||||
|
// TODO: this should be based on last_password_modified_at as a placeholder using false
|
||||||
|
Self::ForceSetPassword => Ok(false),
|
||||||
|
Self::MerchantSelect => user
|
||||||
|
.get_roles_from_db(state)
|
||||||
|
.await
|
||||||
|
.map(|roles| !roles.iter().any(|role| role.status == UserStatus::Active)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn generate_spt(
|
||||||
|
self,
|
||||||
|
state: &AppState,
|
||||||
|
next_flow: &NextFlow,
|
||||||
|
) -> UserResult<Secret<String>> {
|
||||||
|
auth::SinglePurposeToken::new_token(
|
||||||
|
next_flow.user.get_user_id().to_string(),
|
||||||
|
self.into(),
|
||||||
|
next_flow.origin.clone(),
|
||||||
|
&state.conf,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|token| token.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, Clone, Copy)]
|
||||||
|
pub enum JWTFlow {
|
||||||
|
UserInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JWTFlow {
|
||||||
|
async fn is_required(&self, _user: &UserFromStorage, _state: &AppState) -> UserResult<bool> {
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn generate_jwt(
|
||||||
|
self,
|
||||||
|
state: &AppState,
|
||||||
|
next_flow: &NextFlow,
|
||||||
|
user_role: &UserRole,
|
||||||
|
) -> UserResult<Secret<String>> {
|
||||||
|
auth::AuthToken::new_token(
|
||||||
|
next_flow.user.get_user_id().to_string(),
|
||||||
|
user_role.merchant_id.clone(),
|
||||||
|
user_role.role_id.clone(),
|
||||||
|
&state.conf,
|
||||||
|
user_role.org_id.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|token| token.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Origin {
|
||||||
|
SignIn,
|
||||||
|
SignUp,
|
||||||
|
MagicLink,
|
||||||
|
VerifyEmail,
|
||||||
|
AcceptInvitationFromEmail,
|
||||||
|
ResetPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Origin {
|
||||||
|
fn get_flows(&self) -> &'static [UserFlow] {
|
||||||
|
match self {
|
||||||
|
Self::SignIn => &SIGNIN_FLOW,
|
||||||
|
Self::SignUp => &SIGNUP_FLOW,
|
||||||
|
Self::VerifyEmail => &VERIFY_EMAIL_FLOW,
|
||||||
|
Self::MagicLink => &MAGIC_LINK_FLOW,
|
||||||
|
Self::AcceptInvitationFromEmail => &ACCEPT_INVITATION_FROM_EMAIL_FLOW,
|
||||||
|
Self::ResetPassword => &RESET_PASSWORD_FLOW,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SIGNIN_FLOW: [UserFlow; 4] = [
|
||||||
|
UserFlow::SPTFlow(SPTFlow::TOTP),
|
||||||
|
UserFlow::SPTFlow(SPTFlow::ForceSetPassword),
|
||||||
|
UserFlow::SPTFlow(SPTFlow::MerchantSelect),
|
||||||
|
UserFlow::JWTFlow(JWTFlow::UserInfo),
|
||||||
|
];
|
||||||
|
|
||||||
|
const SIGNUP_FLOW: [UserFlow; 4] = [
|
||||||
|
UserFlow::SPTFlow(SPTFlow::TOTP),
|
||||||
|
UserFlow::SPTFlow(SPTFlow::ForceSetPassword),
|
||||||
|
UserFlow::SPTFlow(SPTFlow::MerchantSelect),
|
||||||
|
UserFlow::JWTFlow(JWTFlow::UserInfo),
|
||||||
|
];
|
||||||
|
|
||||||
|
const MAGIC_LINK_FLOW: [UserFlow; 5] = [
|
||||||
|
UserFlow::SPTFlow(SPTFlow::TOTP),
|
||||||
|
UserFlow::SPTFlow(SPTFlow::VerifyEmail),
|
||||||
|
UserFlow::SPTFlow(SPTFlow::ForceSetPassword),
|
||||||
|
UserFlow::SPTFlow(SPTFlow::MerchantSelect),
|
||||||
|
UserFlow::JWTFlow(JWTFlow::UserInfo),
|
||||||
|
];
|
||||||
|
|
||||||
|
const VERIFY_EMAIL_FLOW: [UserFlow; 5] = [
|
||||||
|
UserFlow::SPTFlow(SPTFlow::TOTP),
|
||||||
|
UserFlow::SPTFlow(SPTFlow::VerifyEmail),
|
||||||
|
UserFlow::SPTFlow(SPTFlow::ForceSetPassword),
|
||||||
|
UserFlow::SPTFlow(SPTFlow::MerchantSelect),
|
||||||
|
UserFlow::JWTFlow(JWTFlow::UserInfo),
|
||||||
|
];
|
||||||
|
|
||||||
|
const ACCEPT_INVITATION_FROM_EMAIL_FLOW: [UserFlow; 4] = [
|
||||||
|
UserFlow::SPTFlow(SPTFlow::TOTP),
|
||||||
|
UserFlow::SPTFlow(SPTFlow::AcceptInvitationFromEmail),
|
||||||
|
UserFlow::SPTFlow(SPTFlow::ForceSetPassword),
|
||||||
|
UserFlow::JWTFlow(JWTFlow::UserInfo),
|
||||||
|
];
|
||||||
|
|
||||||
|
const RESET_PASSWORD_FLOW: [UserFlow; 2] = [
|
||||||
|
UserFlow::SPTFlow(SPTFlow::TOTP),
|
||||||
|
UserFlow::SPTFlow(SPTFlow::ResetPassword),
|
||||||
|
];
|
||||||
|
|
||||||
|
pub struct CurrentFlow {
|
||||||
|
origin: Origin,
|
||||||
|
current_flow_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CurrentFlow {
|
||||||
|
pub fn new(origin: Origin, current_flow: UserFlow) -> UserResult<Self> {
|
||||||
|
let flows = origin.get_flows();
|
||||||
|
let index = flows
|
||||||
|
.iter()
|
||||||
|
.position(|flow| flow == ¤t_flow)
|
||||||
|
.ok_or(UserErrors::InternalServerError)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
origin,
|
||||||
|
current_flow_index: index,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn next(&self, user: UserFromStorage, state: &AppState) -> UserResult<NextFlow> {
|
||||||
|
let flows = self.origin.get_flows();
|
||||||
|
let remaining_flows = flows.iter().skip(self.current_flow_index + 1);
|
||||||
|
for flow in remaining_flows {
|
||||||
|
if flow.is_required(&user, state).await? {
|
||||||
|
return Ok(NextFlow {
|
||||||
|
origin: self.origin.clone(),
|
||||||
|
next_flow: *flow,
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(UserErrors::InternalServerError.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NextFlow {
|
||||||
|
origin: Origin,
|
||||||
|
next_flow: UserFlow,
|
||||||
|
user: UserFromStorage,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NextFlow {
|
||||||
|
pub async fn from_origin(
|
||||||
|
origin: Origin,
|
||||||
|
user: UserFromStorage,
|
||||||
|
state: &AppState,
|
||||||
|
) -> UserResult<Self> {
|
||||||
|
let flows = origin.get_flows();
|
||||||
|
for flow in flows {
|
||||||
|
if flow.is_required(&user, state).await? {
|
||||||
|
return Ok(Self {
|
||||||
|
origin,
|
||||||
|
next_flow: *flow,
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(UserErrors::InternalServerError.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_flow(&self) -> UserFlow {
|
||||||
|
self.next_flow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UserFlow> for TokenPurpose {
|
||||||
|
fn from(value: UserFlow) -> Self {
|
||||||
|
match value {
|
||||||
|
UserFlow::SPTFlow(flow) => flow.into(),
|
||||||
|
UserFlow::JWTFlow(flow) => flow.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SPTFlow> for TokenPurpose {
|
||||||
|
fn from(value: SPTFlow) -> Self {
|
||||||
|
match value {
|
||||||
|
SPTFlow::TOTP => Self::TOTP,
|
||||||
|
SPTFlow::VerifyEmail => Self::VerifyEmail,
|
||||||
|
SPTFlow::AcceptInvitationFromEmail => Self::AcceptInvitationFromEmail,
|
||||||
|
SPTFlow::MerchantSelect => Self::AcceptInvite,
|
||||||
|
SPTFlow::ResetPassword | SPTFlow::ForceSetPassword => Self::ResetPassword,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<JWTFlow> for TokenPurpose {
|
||||||
|
fn from(value: JWTFlow) -> Self {
|
||||||
|
match value {
|
||||||
|
JWTFlow::UserInfo => Self::UserInfo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user