refactor(users): Separate signup and signin (#2921)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Mani Chandra
2023-12-04 17:03:44 +05:30
committed by GitHub
parent 9d93533219
commit 80efeb76b1
9 changed files with 453 additions and 109 deletions

View File

@ -6,11 +6,12 @@ use crate::user::{
dashboard_metadata::{
GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest,
},
ChangePasswordRequest, ConnectAccountRequest, ConnectAccountResponse,
CreateInternalUserRequest, GetUsersResponse, SwitchMerchantIdRequest, UserMerchantCreate,
AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest,
DashboardEntryResponse, GetUsersResponse, SignUpRequest, SignUpWithMerchantIdRequest,
SwitchMerchantIdRequest, UserMerchantCreate,
};
impl ApiEventMetric for ConnectAccountResponse {
impl ApiEventMetric for DashboardEntryResponse {
fn get_api_event_type(&self) -> Option<ApiEventsType> {
Some(ApiEventsType::User {
merchant_id: self.merchant_id.clone(),
@ -19,9 +20,9 @@ impl ApiEventMetric for ConnectAccountResponse {
}
}
impl ApiEventMetric for ConnectAccountRequest {}
common_utils::impl_misc_api_event_type!(
SignUpRequest,
SignUpWithMerchantIdRequest,
ChangePasswordRequest,
GetMultipleMetaDataPayload,
GetMetaDataResponse,
@ -30,7 +31,9 @@ common_utils::impl_misc_api_event_type!(
SwitchMerchantIdRequest,
CreateInternalUserRequest,
UserMerchantCreate,
GetUsersResponse
GetUsersResponse,
AuthorizeResponse,
ConnectAccountRequest
);
#[cfg(feature = "dummy_connector")]

View File

@ -7,13 +7,25 @@ pub mod dashboard_metadata;
pub mod sample_data;
#[derive(serde::Deserialize, Debug, Clone, serde::Serialize)]
pub struct ConnectAccountRequest {
pub struct SignUpWithMerchantIdRequest {
pub name: Secret<String>,
pub email: pii::Email,
pub password: Secret<String>,
pub company_name: String,
}
pub type SignUpWithMerchantIdResponse = AuthorizeResponse;
#[derive(serde::Deserialize, Debug, Clone, serde::Serialize)]
pub struct SignUpRequest {
pub email: pii::Email,
pub password: Secret<String>,
}
pub type SignUpResponse = DashboardEntryResponse;
#[derive(serde::Serialize, Debug, Clone)]
pub struct ConnectAccountResponse {
pub struct DashboardEntryResponse {
pub token: Secret<String>,
pub merchant_id: String,
pub name: Secret<String>,
@ -25,6 +37,28 @@ pub struct ConnectAccountResponse {
pub user_id: String,
}
pub type SignInRequest = SignUpRequest;
pub type SignInResponse = DashboardEntryResponse;
#[derive(serde::Deserialize, Debug, Clone, serde::Serialize)]
pub struct ConnectAccountRequest {
pub email: pii::Email,
}
pub type ConnectAccountResponse = AuthorizeResponse;
#[derive(serde::Serialize, Debug, Clone)]
pub struct AuthorizeResponse {
pub is_email_sent: bool,
//this field is added for audit/debug reasons
#[serde(skip_serializing)]
pub user_id: String,
//this field is added for audit/debug reasons
#[serde(skip_serializing)]
pub merchant_id: String,
}
#[derive(serde::Deserialize, Debug, serde::Serialize)]
pub struct ChangePasswordRequest {
pub new_password: Secret<String>,
@ -36,6 +70,8 @@ pub struct SwitchMerchantIdRequest {
pub merchant_id: String,
}
pub type SwitchMerchantResponse = DashboardEntryResponse;
#[derive(serde::Deserialize, Debug, serde::Serialize)]
pub struct CreateInternalUserRequest {
pub name: Secret<String>,

View File

@ -1,10 +1,17 @@
use api_models::user as user_api;
use diesel_models::{enums::UserStatus, user as storage_user};
use error_stack::{IntoReport, ResultExt};
use masking::{ExposeInterface, Secret};
#[cfg(feature = "email")]
use error_stack::IntoReport;
use error_stack::ResultExt;
use masking::ExposeInterface;
#[cfg(feature = "email")]
use router_env::env;
#[cfg(feature = "email")]
use router_env::logger;
use super::errors::{UserErrors, UserResponse};
#[cfg(feature = "email")]
use crate::services::email::types as email_types;
use crate::{
consts,
db::user::UserInterface,
@ -13,11 +20,112 @@ use crate::{
types::domain,
utils,
};
pub mod dashboard_metadata;
#[cfg(feature = "dummy_connector")]
pub mod sample_data;
pub mod dashboard_metadata;
#[cfg(feature = "email")]
pub async fn signup_with_merchant_id(
state: AppState,
request: user_api::SignUpWithMerchantIdRequest,
) -> UserResponse<user_api::SignUpWithMerchantIdResponse> {
let new_user = domain::NewUser::try_from(request.clone())?;
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_user_role_in_db(
state.clone(),
consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
UserStatus::Active,
)
.await?;
let email_contents = email_types::ResetPassword {
recipient_email: user_from_db.get_email().try_into()?,
user_name: domain::UserName::new(user_from_db.get_name())?,
settings: state.conf.clone(),
subject: "Get back to Hyperswitch - Reset Your Password Now",
};
let send_email_result = state
.email_client
.compose_and_send_email(
Box::new(email_contents),
state.conf.proxy.https_url.as_ref(),
)
.await;
logger::info!(?send_email_result);
Ok(ApplicationResponse::Json(user_api::AuthorizeResponse {
is_email_sent: send_email_result.is_ok(),
user_id: user_from_db.get_user_id().to_string(),
merchant_id: user_role.merchant_id,
}))
}
pub async fn signup(
state: AppState,
request: user_api::SignUpRequest,
) -> UserResponse<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_user_role_in_db(
state.clone(),
consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
UserStatus::Active,
)
.await?;
let token = utils::user::generate_jwt_auth_token(state, &user_from_db, &user_role).await?;
Ok(ApplicationResponse::Json(
utils::user::get_dashboard_entry_response(user_from_db, user_role, token),
))
}
pub async fn signin(
state: AppState,
request: user_api::SignInRequest,
) -> UserResponse<user_api::SignInResponse> {
let user_from_db: domain::UserFromStorage = state
.store
.find_user_by_email(request.email.clone().expose().expose().as_str())
.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 user_role = user_from_db.get_role_from_db(state.clone()).await?;
let token = utils::user::generate_jwt_auth_token(state, &user_from_db, &user_role).await?;
Ok(ApplicationResponse::Json(
utils::user::get_dashboard_entry_response(user_from_db, user_role, token),
))
}
#[cfg(feature = "email")]
pub async fn connect_account(
state: AppState,
request: user_api::ConnectAccountRequest,
@ -29,26 +137,34 @@ pub async fn connect_account(
if let Ok(found_user) = find_user {
let user_from_db: domain::UserFromStorage = found_user.into();
user_from_db.compare_password(request.password)?;
let user_role = user_from_db.get_role_from_db(state.clone()).await?;
let jwt_token = user_from_db
.get_jwt_auth_token(state.clone(), user_role.org_id)
.await?;
let email_contents = email_types::MagicLink {
recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?,
settings: state.conf.clone(),
user_name: domain::UserName::new(user_from_db.get_name())?,
subject: "Unlock Hyperswitch: Use Your Magic Link to Sign In",
};
let send_email_result = state
.email_client
.compose_and_send_email(
Box::new(email_contents),
state.conf.proxy.https_url.as_ref(),
)
.await;
logger::info!(?send_email_result);
return Ok(ApplicationResponse::Json(
user_api::ConnectAccountResponse {
token: Secret::new(jwt_token),
merchant_id: user_role.merchant_id,
name: user_from_db.get_name(),
email: user_from_db.get_email(),
verification_days_left: None,
user_role: user_role.role_id,
is_email_sent: send_email_result.is_ok(),
user_id: user_from_db.get_user_id().to_string(),
merchant_id: user_role.merchant_id,
},
));
} else if find_user
.as_ref()
.map_err(|e| e.current_context().is_db_not_found())
.err()
.unwrap_or(false)
@ -73,15 +189,6 @@ pub async fn connect_account(
UserStatus::Active,
)
.await?;
let jwt_token = user_from_db
.get_jwt_auth_token(state.clone(), user_role.org_id)
.await?;
#[cfg(feature = "email")]
{
use router_env::logger;
use crate::services::email::types as email_types;
let email_contents = email_types::VerifyEmail {
recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?,
@ -98,21 +205,19 @@ pub async fn connect_account(
.await;
logger::info!(?send_email_result);
}
return Ok(ApplicationResponse::Json(
user_api::ConnectAccountResponse {
token: Secret::new(jwt_token),
merchant_id: user_role.merchant_id,
name: user_from_db.get_name(),
email: user_from_db.get_email(),
verification_days_left: None,
user_role: user_role.role_id,
is_email_sent: send_email_result.is_ok(),
user_id: user_from_db.get_user_id().to_string(),
merchant_id: user_role.merchant_id,
},
));
} else {
Err(UserErrors::InternalServerError.into())
Err(find_user
.err()
.map(|e| e.change_context(UserErrors::InternalServerError))
.unwrap_or(UserErrors::InternalServerError.into()))
}
}
@ -215,7 +320,7 @@ pub async fn switch_merchant_id(
state: AppState,
request: user_api::SwitchMerchantIdRequest,
user_from_token: auth::UserFromToken,
) -> UserResponse<user_api::ConnectAccountResponse> {
) -> UserResponse<user_api::SwitchMerchantResponse> {
if !utils::user_role::is_internal_role(&user_from_token.role_id) {
let merchant_list =
utils::user_role::get_merchant_ids_for_user(state.clone(), &user_from_token.user_id)
@ -252,7 +357,7 @@ pub async fn switch_merchant_id(
}
})?;
let org_id = state
let _org_id = state
.store
.find_merchant_account_by_merchant_id(request.merchant_id.as_str(), &key_store)
.await
@ -272,23 +377,23 @@ pub async fn switch_merchant_id(
.await
.change_context(UserErrors::InternalServerError)?;
let token = Box::pin(user.get_jwt_auth_token_with_custom_merchant_id(
state.clone(),
let token = utils::user::generate_jwt_auth_token_with_custom_merchant_id(
state,
&user,
&user_role,
request.merchant_id.clone(),
org_id,
))
.await?
.into();
)
.await?;
Ok(ApplicationResponse::Json(
user_api::ConnectAccountResponse {
merchant_id: request.merchant_id,
user_api::SwitchMerchantResponse {
token,
name: user.get_name(),
email: user.get_email(),
user_id: user.get_user_id().to_string(),
verification_days_left: None,
user_role: user_role.role_id,
merchant_id: user_role.merchant_id,
},
))
}

View File

@ -823,10 +823,7 @@ impl User {
let mut route = web::scope("/user").app_data(web::Data::new(state));
route = route
.service(web::resource("/signin").route(web::post().to(user_connect_account)))
.service(web::resource("/signup").route(web::post().to(user_connect_account)))
.service(web::resource("/v2/signin").route(web::post().to(user_connect_account)))
.service(web::resource("/v2/signup").route(web::post().to(user_connect_account)))
.service(web::resource("/signin").route(web::post().to(user_signin)))
.service(web::resource("/change_password").route(web::post().to(change_password)))
.service(
web::resource("/data/merchant")
@ -841,7 +838,6 @@ impl User {
)
.service(web::resource("/switch/list").route(web::get().to(list_merchant_ids_for_user)))
.service(web::resource("/user/list").route(web::get().to(get_user_details)))
// User Role APIs
.service(web::resource("/permission_info").route(web::get().to(get_authorization_info)))
.service(web::resource("/user/update_role").route(web::post().to(update_user_role)))
.service(web::resource("/role/list").route(web::get().to(list_roles)))
@ -855,6 +851,21 @@ impl User {
.route(web::delete().to(delete_sample_data)),
)
}
#[cfg(feature = "email")]
{
route = route
.service(
web::resource("/connect_account").route(web::post().to(user_connect_account)),
)
.service(
web::resource("/signup_with_merchant_id")
.route(web::post().to(user_signup_with_merchant_id)),
);
}
#[cfg(not(feature = "email"))]
{
route = route.service(web::resource("/signup").route(web::post().to(user_signup)))
}
route
}
}

View File

@ -149,6 +149,8 @@ impl From<Flow> for ApiIdentifier {
| Flow::GsmRuleDelete => Self::Gsm,
Flow::UserConnectAccount
| Flow::UserSignUp
| Flow::UserSignIn
| Flow::ChangePassword
| Flow::SetDashboardMetadata
| Flow::GetMutltipleDashboardMetadata
@ -159,7 +161,8 @@ impl From<Flow> for ApiIdentifier {
| Flow::GenerateSampleData
| Flow::DeleteSampleData
| Flow::UserMerchantAccountList
| Flow::GetUserDetails => Self::User,
| Flow::GetUserDetails
| Flow::UserSignUpWithMerchantId => Self::User,
Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => {
Self::UserRole

View File

@ -19,6 +19,65 @@ use crate::{
utils::user::dashboard_metadata::{parse_string_to_enums, set_ip_address_if_required},
};
#[cfg(feature = "email")]
pub async fn user_signup_with_merchant_id(
state: web::Data<AppState>,
http_req: HttpRequest,
json_payload: web::Json<user_api::SignUpWithMerchantIdRequest>,
) -> HttpResponse {
let flow = Flow::UserSignUpWithMerchantId;
let req_payload = json_payload.into_inner();
Box::pin(api::server_wrap(
flow.clone(),
state,
&http_req,
req_payload.clone(),
|state, _, req_body| user_core::signup_with_merchant_id(state, req_body),
&auth::NoAuth,
api_locking::LockAction::NotApplicable,
))
.await
}
pub async fn user_signup(
state: web::Data<AppState>,
http_req: HttpRequest,
json_payload: web::Json<user_api::SignUpRequest>,
) -> HttpResponse {
let flow = Flow::UserSignUp;
let req_payload = json_payload.into_inner();
Box::pin(api::server_wrap(
flow.clone(),
state,
&http_req,
req_payload.clone(),
|state, _, req_body| user_core::signup(state, req_body),
&auth::NoAuth,
api_locking::LockAction::NotApplicable,
))
.await
}
pub async fn user_signin(
state: web::Data<AppState>,
http_req: HttpRequest,
json_payload: web::Json<user_api::SignInRequest>,
) -> HttpResponse {
let flow = Flow::UserSignIn;
let req_payload = json_payload.into_inner();
Box::pin(api::server_wrap(
flow.clone(),
state,
&http_req,
req_payload.clone(),
|state, _, req_body| user_core::signin(state, req_body),
&auth::NoAuth,
api_locking::LockAction::NotApplicable,
))
.await
}
#[cfg(feature = "email")]
pub async fn user_connect_account(
state: web::Data<AppState>,
http_req: HttpRequest,

View File

@ -26,7 +26,7 @@ use crate::{
db::StorageInterface,
routes::AppState,
services::{
authentication::{AuthToken, UserFromToken},
authentication::UserFromToken,
authorization::{info, predefined_permissions},
},
types::transformers::ForeignFrom,
@ -215,6 +215,25 @@ impl NewUserOrganization {
}
}
impl TryFrom<user_api::SignUpWithMerchantIdRequest> for NewUserOrganization {
type Error = error_stack::Report<UserErrors>;
fn try_from(value: user_api::SignUpWithMerchantIdRequest) -> UserResult<Self> {
let new_organization = api_org::OrganizationNew::new(Some(
UserCompanyName::new(value.company_name)?.get_secret(),
));
let db_organization = ForeignFrom::foreign_from(new_organization);
Ok(Self(db_organization))
}
}
impl From<user_api::SignUpRequest> for NewUserOrganization {
fn from(_value: user_api::SignUpRequest) -> Self {
let new_organization = api_org::OrganizationNew::new(None);
let db_organization = ForeignFrom::foreign_from(new_organization);
Self(db_organization)
}
}
impl From<user_api::ConnectAccountRequest> for NewUserOrganization {
fn from(_value: user_api::ConnectAccountRequest) -> Self {
let new_organization = api_org::OrganizationNew::new(None);
@ -334,6 +353,24 @@ impl NewUserMerchant {
}
}
impl TryFrom<user_api::SignUpRequest> for NewUserMerchant {
type Error = error_stack::Report<UserErrors>;
fn try_from(value: user_api::SignUpRequest) -> UserResult<Self> {
let merchant_id = MerchantId::new(format!(
"merchant_{}",
common_utils::date_time::now_unix_timestamp()
))?;
let new_organization = NewUserOrganization::from(value);
Ok(Self {
company_name: None,
merchant_id,
new_organization,
})
}
}
impl TryFrom<user_api::ConnectAccountRequest> for NewUserMerchant {
type Error = error_stack::Report<UserErrors>;
@ -352,6 +389,21 @@ impl TryFrom<user_api::ConnectAccountRequest> for NewUserMerchant {
}
}
impl TryFrom<user_api::SignUpWithMerchantIdRequest> for NewUserMerchant {
type Error = error_stack::Report<UserErrors>;
fn try_from(value: user_api::SignUpWithMerchantIdRequest) -> UserResult<Self> {
let company_name = Some(UserCompanyName::new(value.company_name.clone())?);
let merchant_id = MerchantId::new(value.company_name.clone())?;
let new_organization = NewUserOrganization::try_from(value)?;
Ok(Self {
company_name,
merchant_id,
new_organization,
})
}
}
impl TryFrom<user_api::CreateInternalUserRequest> for NewUserMerchant {
type Error = error_stack::Report<UserErrors>;
@ -434,10 +486,23 @@ impl NewUser {
.attach_printable("Error while inserting user")
}
pub async fn check_if_already_exists_in_db(&self, state: AppState) -> UserResult<()> {
if state
.store
.find_user_by_email(self.get_email().into_inner().expose().expose().as_str())
.await
.is_ok()
{
return Err(UserErrors::UserExists).into_report();
}
Ok(())
}
pub async fn insert_user_and_merchant_in_db(
&self,
state: AppState,
) -> UserResult<UserFromStorage> {
self.check_if_already_exists_in_db(state.clone()).await?;
let db = state.store.as_ref();
let merchant_id = self.get_new_merchant().get_merchant_id();
self.new_merchant
@ -495,6 +560,46 @@ impl TryFrom<NewUser> for storage_user::UserNew {
}
}
impl TryFrom<user_api::SignUpWithMerchantIdRequest> for NewUser {
type Error = error_stack::Report<UserErrors>;
fn try_from(value: user_api::SignUpWithMerchantIdRequest) -> UserResult<Self> {
let email = value.email.clone().try_into()?;
let name = UserName::new(value.name.clone())?;
let password = UserPassword::new(value.password.clone())?;
let user_id = uuid::Uuid::new_v4().to_string();
let new_merchant = NewUserMerchant::try_from(value)?;
Ok(Self {
name,
email,
password,
user_id,
new_merchant,
})
}
}
impl TryFrom<user_api::SignUpRequest> for NewUser {
type Error = error_stack::Report<UserErrors>;
fn try_from(value: user_api::SignUpRequest) -> UserResult<Self> {
let user_id = uuid::Uuid::new_v4().to_string();
let email = value.email.clone().try_into()?;
let name = UserName::try_from(value.email.clone())?;
let password = UserPassword::new(value.password.clone())?;
let new_merchant = NewUserMerchant::try_from(value)?;
Ok(Self {
user_id,
name,
email,
password,
new_merchant,
})
}
}
impl TryFrom<user_api::ConnectAccountRequest> for NewUser {
type Error = error_stack::Report<UserErrors>;
@ -502,7 +607,7 @@ impl TryFrom<user_api::ConnectAccountRequest> for NewUser {
let user_id = uuid::Uuid::new_v4().to_string();
let email = value.email.clone().try_into()?;
let name = UserName::try_from(value.email.clone())?;
let password = UserPassword::new(value.password.clone())?;
let password = UserPassword::new(uuid::Uuid::new_v4().to_string().into())?;
let new_merchant = NewUserMerchant::try_from(value)?;
Ok(Self {
@ -582,41 +687,6 @@ impl UserFromStorage {
self.0.email.clone()
}
pub async fn get_jwt_auth_token(&self, state: AppState, org_id: String) -> UserResult<String> {
let role_id = self.get_role_from_db(state.clone()).await?.role_id;
let merchant_id = state
.store
.find_user_role_by_user_id(self.get_user_id())
.await
.change_context(UserErrors::InternalServerError)?
.merchant_id;
AuthToken::new_token(
self.0.user_id.clone(),
merchant_id,
role_id,
&state.conf,
org_id,
)
.await
}
pub async fn get_jwt_auth_token_with_custom_merchant_id(
&self,
state: AppState,
merchant_id: String,
org_id: String,
) -> UserResult<String> {
let role_id = self.get_role_from_db(state.clone()).await?.role_id;
AuthToken::new_token(
self.0.user_id.clone(),
merchant_id,
role_id,
&state.conf,
org_id,
)
.await
}
pub async fn get_role_from_db(&self, state: AppState) -> UserResult<UserRole> {
state
.store

View File

@ -1,11 +1,13 @@
use diesel_models::enums::UserStatus;
use api_models::user as user_api;
use diesel_models::{enums::UserStatus, user_role::UserRole};
use error_stack::ResultExt;
use masking::Secret;
use crate::{
core::errors::{UserErrors, UserResult},
routes::AppState,
services::authentication::UserFromToken,
types::domain::MerchantAccount,
services::authentication::{AuthToken, UserFromToken},
types::domain::{MerchantAccount, UserFromStorage},
};
pub mod dashboard_metadata;
@ -68,3 +70,52 @@ pub async fn get_merchant_ids_for_user(state: AppState, user_id: &str) -> UserRe
})
.collect())
}
pub async fn generate_jwt_auth_token(
state: AppState,
user: &UserFromStorage,
user_role: &UserRole,
) -> UserResult<Secret<String>> {
let token = AuthToken::new_token(
user.get_user_id().to_string(),
user_role.merchant_id.clone(),
user_role.role_id.clone(),
&state.conf,
user_role.org_id.clone(),
)
.await?;
Ok(Secret::new(token))
}
pub async fn generate_jwt_auth_token_with_custom_merchant_id(
state: AppState,
user: &UserFromStorage,
user_role: &UserRole,
merchant_id: String,
) -> UserResult<Secret<String>> {
let token = AuthToken::new_token(
user.get_user_id().to_string(),
merchant_id,
user_role.role_id.clone(),
&state.conf,
user_role.org_id.to_owned(),
)
.await?;
Ok(Secret::new(token))
}
pub fn get_dashboard_entry_response(
user: UserFromStorage,
user_role: UserRole,
token: Secret<String>,
) -> user_api::DashboardEntryResponse {
user_api::DashboardEntryResponse {
merchant_id: user_role.merchant_id,
token,
name: user.get_name(),
email: user.get_email(),
user_id: user.get_user_id().to_string(),
verification_days_left: None,
user_role: user_role.role_id,
}
}

View File

@ -249,6 +249,12 @@ pub enum Flow {
GsmRuleUpdate,
/// Gsm Rule Delete flow
GsmRuleDelete,
/// User Sign Up
UserSignUp,
/// User Sign Up
UserSignUpWithMerchantId,
/// User Sign In
UserSignIn,
/// User connect account
UserConnectAccount,
/// Upsert Decision Manager Config