mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +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,
|
||||
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 {
|
||||
@ -62,6 +62,7 @@ common_utils::impl_misc_api_event_type!(
|
||||
SignInResponse,
|
||||
UpdateUserAccountDetailsRequest,
|
||||
GetUserDetailsResponse,
|
||||
SignInWithTokenResponse,
|
||||
GetUserRoleDetailsRequest,
|
||||
GetUserRoleDetailsResponse
|
||||
);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use common_enums::{PermissionGroup, RoleScope};
|
||||
use common_enums::{PermissionGroup, RoleScope, TokenPurpose};
|
||||
use common_utils::{crypto::OptionalEncryptableName, pii};
|
||||
use masking::Secret;
|
||||
|
||||
@ -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),
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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)
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
))
|
||||
|
||||
@ -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;
|
||||
@ -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
|
||||
|
||||
@ -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};
|
||||
@ -66,7 +67,7 @@ pub enum AuthenticationType {
|
||||
},
|
||||
SinglePurposeJWT {
|
||||
user_id: String,
|
||||
purpose: Purpose,
|
||||
purpose: TokenPurpose,
|
||||
},
|
||||
MerchantId {
|
||||
merchant_id: String,
|
||||
@ -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 =
|
||||
@ -143,6 +144,7 @@ impl SinglePurposeToken {
|
||||
let token_payload = Self {
|
||||
user_id,
|
||||
purpose,
|
||||
origin,
|
||||
exp,
|
||||
};
|
||||
jwt::generate_jwt(&token_payload, settings).await
|
||||
@ -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]
|
||||
@ -334,6 +337,7 @@ where
|
||||
Ok((
|
||||
UserFromSinglePurposeToken {
|
||||
user_id: payload.user_id.clone(),
|
||||
origin: payload.origin.clone(),
|
||||
},
|
||||
AuthenticationType::SinglePurposeJWT {
|
||||
user_id: payload.user_id,
|
||||
|
||||
@ -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::{
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
@ -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>);
|
||||
@ -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 {
|
||||
@ -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?
|
||||
|
||||
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