mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-01 19:42:27 +08:00
feat(users): implement force set and force change password (#4564)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -347,6 +347,9 @@ active_email_client = "SES" # The currently active email client
|
|||||||
email_role_arn = "" # The amazon resource name ( arn ) of the role which has permission to send emails
|
email_role_arn = "" # The amazon resource name ( arn ) of the role which has permission to send emails
|
||||||
sts_role_session_name = "" # An identifier for the assumed role session, used to uniquely identify a session.
|
sts_role_session_name = "" # An identifier for the assumed role session, used to uniquely identify a session.
|
||||||
|
|
||||||
|
[user]
|
||||||
|
password_validity_in_days = 90 # Number of days after which password should be updated
|
||||||
|
|
||||||
#tokenization configuration which describe token lifetime and payment method for specific connector
|
#tokenization configuration which describe token lifetime and payment method for specific connector
|
||||||
[tokenization]
|
[tokenization]
|
||||||
stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { type = "disable_only", list = "google_pay" } }
|
stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { type = "disable_only", list = "google_pay" } }
|
||||||
|
|||||||
@ -109,6 +109,9 @@ refund_tolerance = 100 # Fake d
|
|||||||
refund_ttl = 172800 # Time to live for dummy connector refund in redis
|
refund_ttl = 172800 # Time to live for dummy connector refund in redis
|
||||||
slack_invite_url = "https://join.slack.com/t/hyperswitch-io/shared_invite/zt-2awm23agh-p_G5xNpziv6yAiedTkkqLg" # Slack invite url for hyperswitch
|
slack_invite_url = "https://join.slack.com/t/hyperswitch-io/shared_invite/zt-2awm23agh-p_G5xNpziv6yAiedTkkqLg" # Slack invite url for hyperswitch
|
||||||
|
|
||||||
|
[user]
|
||||||
|
password_validity_in_days = 90
|
||||||
|
|
||||||
[frm]
|
[frm]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
|
||||||
|
|||||||
@ -116,6 +116,9 @@ refund_tolerance = 100 # Fake d
|
|||||||
refund_ttl = 172800 # Time to live for dummy connector refund in redis
|
refund_ttl = 172800 # Time to live for dummy connector refund in redis
|
||||||
slack_invite_url = "https://join.slack.com/t/hyperswitch-io/shared_invite/zt-2awm23agh-p_G5xNpziv6yAiedTkkqLg" # Slack invite url for hyperswitch
|
slack_invite_url = "https://join.slack.com/t/hyperswitch-io/shared_invite/zt-2awm23agh-p_G5xNpziv6yAiedTkkqLg" # Slack invite url for hyperswitch
|
||||||
|
|
||||||
|
[user]
|
||||||
|
password_validity_in_days = 90
|
||||||
|
|
||||||
[frm]
|
[frm]
|
||||||
enabled = false
|
enabled = false
|
||||||
|
|
||||||
|
|||||||
@ -116,6 +116,9 @@ refund_tolerance = 100 # Fake d
|
|||||||
refund_ttl = 172800 # Time to live for dummy connector refund in redis
|
refund_ttl = 172800 # Time to live for dummy connector refund in redis
|
||||||
slack_invite_url = "https://join.slack.com/t/hyperswitch-io/shared_invite/zt-2awm23agh-p_G5xNpziv6yAiedTkkqLg" # Slack invite url for hyperswitch
|
slack_invite_url = "https://join.slack.com/t/hyperswitch-io/shared_invite/zt-2awm23agh-p_G5xNpziv6yAiedTkkqLg" # Slack invite url for hyperswitch
|
||||||
|
|
||||||
|
[user]
|
||||||
|
password_validity_in_days = 90
|
||||||
|
|
||||||
[frm]
|
[frm]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
|
||||||
|
|||||||
@ -263,6 +263,9 @@ active_email_client = "SES"
|
|||||||
email_role_arn = ""
|
email_role_arn = ""
|
||||||
sts_role_session_name = ""
|
sts_role_session_name = ""
|
||||||
|
|
||||||
|
[user]
|
||||||
|
password_validity_in_days = 90
|
||||||
|
|
||||||
[bank_config.eps]
|
[bank_config.eps]
|
||||||
stripe = { banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria,bankhaus_carl_spangler,bankhaus_schelhammer_und_schattera_ag,bawag_psk_ag,bks_bank_ag,brull_kallmus_bank_ag,btv_vier_lander_bank,capital_bank_grawe_gruppe_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_alpeadriabank_international_ag,hypo_noe_lb_fur_niederosterreich_u_wien,hypo_oberosterreich_salzburg_steiermark,hypo_tirol_bank_ag,hypo_vorarlberg_bank_ag,hypo_bank_burgenland_aktiengesellschaft,marchfelder_bank,oberbank_ag,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" }
|
stripe = { banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria,bankhaus_carl_spangler,bankhaus_schelhammer_und_schattera_ag,bawag_psk_ag,bks_bank_ag,brull_kallmus_bank_ag,btv_vier_lander_bank,capital_bank_grawe_gruppe_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_alpeadriabank_international_ag,hypo_noe_lb_fur_niederosterreich_u_wien,hypo_oberosterreich_salzburg_steiermark,hypo_tirol_bank_ag,hypo_vorarlberg_bank_ag,hypo_bank_burgenland_aktiengesellschaft,marchfelder_bank,oberbank_ag,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" }
|
||||||
adyen = { banks = "bank_austria,bawag_psk_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_tirol_bank_ag,posojilnica_bank_e_gen,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag" }
|
adyen = { banks = "bank_austria,bawag_psk_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_tirol_bank_ag,posojilnica_bank_e_gen,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag" }
|
||||||
|
|||||||
@ -51,6 +51,9 @@ jwt_secret = "secret"
|
|||||||
master_enc_key = "73ad7bbbbc640c845a150f67d058b279849370cd2c1f3c67c4dd6c869213e13a"
|
master_enc_key = "73ad7bbbbc640c845a150f67d058b279849370cd2c1f3c67c4dd6c869213e13a"
|
||||||
recon_admin_api_key = "recon_test_admin"
|
recon_admin_api_key = "recon_test_admin"
|
||||||
|
|
||||||
|
[user]
|
||||||
|
password_validity_in_days = 90
|
||||||
|
|
||||||
[locker]
|
[locker]
|
||||||
host = ""
|
host = ""
|
||||||
host_rs = ""
|
host_rs = ""
|
||||||
|
|||||||
@ -14,8 +14,8 @@ 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,
|
RotatePasswordRequest, SendVerifyEmailRequest, SignInResponse, SignUpRequest,
|
||||||
SwitchMerchantIdRequest, TokenOrPayloadResponse, TokenResponse,
|
SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, TokenOrPayloadResponse, TokenResponse,
|
||||||
UpdateUserAccountDetailsRequest, UserFromEmailRequest, UserMerchantCreate, VerifyEmailRequest,
|
UpdateUserAccountDetailsRequest, UserFromEmailRequest, UserMerchantCreate, VerifyEmailRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -60,6 +60,7 @@ common_utils::impl_misc_api_event_type!(
|
|||||||
ConnectAccountRequest,
|
ConnectAccountRequest,
|
||||||
ForgotPasswordRequest,
|
ForgotPasswordRequest,
|
||||||
ResetPasswordRequest,
|
ResetPasswordRequest,
|
||||||
|
RotatePasswordRequest,
|
||||||
InviteUserRequest,
|
InviteUserRequest,
|
||||||
ReInviteUserRequest,
|
ReInviteUserRequest,
|
||||||
VerifyEmailRequest,
|
VerifyEmailRequest,
|
||||||
|
|||||||
@ -91,6 +91,11 @@ pub struct ResetPasswordRequest {
|
|||||||
pub password: Secret<String>,
|
pub password: Secret<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug, serde::Serialize)]
|
||||||
|
pub struct RotatePasswordRequest {
|
||||||
|
pub password: Secret<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
|
||||||
pub struct InviteUserRequest {
|
pub struct InviteUserRequest {
|
||||||
pub email: pii::Email,
|
pub email: pii::Email,
|
||||||
|
|||||||
@ -2717,6 +2717,7 @@ pub enum TokenPurpose {
|
|||||||
TOTP,
|
TOTP,
|
||||||
VerifyEmail,
|
VerifyEmail,
|
||||||
AcceptInvitationFromEmail,
|
AcceptInvitationFromEmail,
|
||||||
|
ForceSetPassword,
|
||||||
ResetPassword,
|
ResetPassword,
|
||||||
AcceptInvite,
|
AcceptInvite,
|
||||||
UserInfo,
|
UserInfo,
|
||||||
|
|||||||
@ -1191,6 +1191,7 @@ diesel::table! {
|
|||||||
last_modified_at -> Timestamp,
|
last_modified_at -> Timestamp,
|
||||||
#[max_length = 64]
|
#[max_length = 64]
|
||||||
preferred_merchant_id -> Nullable<Varchar>,
|
preferred_merchant_id -> Nullable<Varchar>,
|
||||||
|
last_password_modified_at -> Nullable<Timestamp>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,7 @@ pub struct User {
|
|||||||
pub created_at: PrimitiveDateTime,
|
pub created_at: PrimitiveDateTime,
|
||||||
pub last_modified_at: PrimitiveDateTime,
|
pub last_modified_at: PrimitiveDateTime,
|
||||||
pub preferred_merchant_id: Option<String>,
|
pub preferred_merchant_id: Option<String>,
|
||||||
|
pub last_password_modified_at: Option<PrimitiveDateTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
@ -35,6 +36,7 @@ pub struct UserNew {
|
|||||||
pub created_at: Option<PrimitiveDateTime>,
|
pub created_at: Option<PrimitiveDateTime>,
|
||||||
pub last_modified_at: Option<PrimitiveDateTime>,
|
pub last_modified_at: Option<PrimitiveDateTime>,
|
||||||
pub preferred_merchant_id: Option<String>,
|
pub preferred_merchant_id: Option<String>,
|
||||||
|
pub last_password_modified_at: Option<PrimitiveDateTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)]
|
#[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)]
|
||||||
@ -45,6 +47,7 @@ pub struct UserUpdateInternal {
|
|||||||
is_verified: Option<bool>,
|
is_verified: Option<bool>,
|
||||||
last_modified_at: PrimitiveDateTime,
|
last_modified_at: PrimitiveDateTime,
|
||||||
preferred_merchant_id: Option<String>,
|
preferred_merchant_id: Option<String>,
|
||||||
|
last_password_modified_at: Option<PrimitiveDateTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -52,10 +55,12 @@ pub enum UserUpdate {
|
|||||||
VerifyUser,
|
VerifyUser,
|
||||||
AccountUpdate {
|
AccountUpdate {
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
password: Option<Secret<String>>,
|
|
||||||
is_verified: Option<bool>,
|
is_verified: Option<bool>,
|
||||||
preferred_merchant_id: Option<String>,
|
preferred_merchant_id: Option<String>,
|
||||||
},
|
},
|
||||||
|
PasswordUpdate {
|
||||||
|
password: Option<Secret<String>>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<UserUpdate> for UserUpdateInternal {
|
impl From<UserUpdate> for UserUpdateInternal {
|
||||||
@ -68,18 +73,27 @@ impl From<UserUpdate> for UserUpdateInternal {
|
|||||||
is_verified: Some(true),
|
is_verified: Some(true),
|
||||||
last_modified_at,
|
last_modified_at,
|
||||||
preferred_merchant_id: None,
|
preferred_merchant_id: None,
|
||||||
|
last_password_modified_at: None,
|
||||||
},
|
},
|
||||||
UserUpdate::AccountUpdate {
|
UserUpdate::AccountUpdate {
|
||||||
name,
|
name,
|
||||||
password,
|
|
||||||
is_verified,
|
is_verified,
|
||||||
preferred_merchant_id,
|
preferred_merchant_id,
|
||||||
} => Self {
|
} => Self {
|
||||||
name,
|
name,
|
||||||
password,
|
password: None,
|
||||||
is_verified,
|
is_verified,
|
||||||
last_modified_at,
|
last_modified_at,
|
||||||
preferred_merchant_id,
|
preferred_merchant_id,
|
||||||
|
last_password_modified_at: None,
|
||||||
|
},
|
||||||
|
UserUpdate::PasswordUpdate { password } => Self {
|
||||||
|
name: None,
|
||||||
|
password,
|
||||||
|
is_verified: None,
|
||||||
|
last_modified_at,
|
||||||
|
preferred_merchant_id: None,
|
||||||
|
last_password_modified_at: Some(last_modified_at),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -334,6 +334,7 @@ pub(crate) async fn fetch_raw_secrets(
|
|||||||
dummy_connector: conf.dummy_connector,
|
dummy_connector: conf.dummy_connector,
|
||||||
#[cfg(feature = "email")]
|
#[cfg(feature = "email")]
|
||||||
email: conf.email,
|
email: conf.email,
|
||||||
|
user: conf.user,
|
||||||
mandates: conf.mandates,
|
mandates: conf.mandates,
|
||||||
network_transaction_id_supported_connectors: conf
|
network_transaction_id_supported_connectors: conf
|
||||||
.network_transaction_id_supported_connectors,
|
.network_transaction_id_supported_connectors,
|
||||||
|
|||||||
@ -89,6 +89,7 @@ pub struct Settings<S: SecretState> {
|
|||||||
pub dummy_connector: DummyConnector,
|
pub dummy_connector: DummyConnector,
|
||||||
#[cfg(feature = "email")]
|
#[cfg(feature = "email")]
|
||||||
pub email: EmailSettings,
|
pub email: EmailSettings,
|
||||||
|
pub user: UserSettings,
|
||||||
pub cors: CorsSettings,
|
pub cors: CorsSettings,
|
||||||
pub mandates: Mandates,
|
pub mandates: Mandates,
|
||||||
pub network_transaction_id_supported_connectors: NetworkTransactionIdSupportedConnectors,
|
pub network_transaction_id_supported_connectors: NetworkTransactionIdSupportedConnectors,
|
||||||
@ -390,6 +391,11 @@ pub struct Secrets {
|
|||||||
pub master_enc_key: Secret<String>,
|
pub master_enc_key: Secret<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Deserialize)]
|
||||||
|
pub struct UserSettings {
|
||||||
|
pub password_validity_in_days: u16,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct Locker {
|
pub struct Locker {
|
||||||
|
|||||||
@ -198,7 +198,7 @@ impl UserErrors {
|
|||||||
Self::IpAddressParsingFailed => "Something went wrong",
|
Self::IpAddressParsingFailed => "Something went wrong",
|
||||||
Self::InvalidMetadataRequest => "Invalid Metadata Request",
|
Self::InvalidMetadataRequest => "Invalid Metadata Request",
|
||||||
Self::MerchantIdParsingError => "Invalid Merchant Id",
|
Self::MerchantIdParsingError => "Invalid Merchant Id",
|
||||||
Self::ChangePasswordError => "Old and new password cannot be the same",
|
Self::ChangePasswordError => "Old and new password cannot be same",
|
||||||
Self::InvalidDeleteOperation => "Delete Operation Not Supported",
|
Self::InvalidDeleteOperation => "Delete Operation Not Supported",
|
||||||
Self::MaxInvitationsError => "Maximum invite count per request exceeded",
|
Self::MaxInvitationsError => "Maximum invite count per request exceeded",
|
||||||
Self::RoleNotFound => "Role Not Found",
|
Self::RoleNotFound => "Role Not Found",
|
||||||
|
|||||||
@ -349,11 +349,8 @@ pub async fn change_password(
|
|||||||
.store
|
.store
|
||||||
.update_user_by_user_id(
|
.update_user_by_user_id(
|
||||||
user.get_user_id(),
|
user.get_user_id(),
|
||||||
diesel_models::user::UserUpdate::AccountUpdate {
|
diesel_models::user::UserUpdate::PasswordUpdate {
|
||||||
name: None,
|
|
||||||
password: Some(new_password_hash),
|
password: Some(new_password_hash),
|
||||||
is_verified: None,
|
|
||||||
preferred_merchant_id: None,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@ -419,6 +416,98 @@ pub async fn forgot_password(
|
|||||||
Ok(ApplicationResponse::StatusOk)
|
Ok(ApplicationResponse::StatusOk)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn rotate_password(
|
||||||
|
state: AppState,
|
||||||
|
user_token: auth::UserFromSinglePurposeToken,
|
||||||
|
request: user_api::RotatePasswordRequest,
|
||||||
|
_req_state: ReqState,
|
||||||
|
) -> UserResponse<()> {
|
||||||
|
let user: domain::UserFromStorage = state
|
||||||
|
.store
|
||||||
|
.find_user_by_id(&user_token.user_id)
|
||||||
|
.await
|
||||||
|
.change_context(UserErrors::InternalServerError)?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let password = domain::UserPassword::new(request.password.to_owned())?;
|
||||||
|
let hash_password = utils::user::password::generate_password_hash(password.get_secret())?;
|
||||||
|
|
||||||
|
if user.compare_password(request.password).is_ok() {
|
||||||
|
return Err(UserErrors::ChangePasswordError.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = state
|
||||||
|
.store
|
||||||
|
.update_user_by_user_id(
|
||||||
|
&user_token.user_id,
|
||||||
|
storage_user::UserUpdate::PasswordUpdate {
|
||||||
|
password: Some(hash_password),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.change_context(UserErrors::InternalServerError)?;
|
||||||
|
|
||||||
|
let _ = auth::blacklist::insert_user_in_blacklist(&state, &user.user_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| logger::error!(?e));
|
||||||
|
|
||||||
|
Ok(ApplicationResponse::StatusOk)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "email")]
|
||||||
|
pub async fn reset_password_token_only_flow(
|
||||||
|
state: AppState,
|
||||||
|
user_token: auth::UserFromSinglePurposeToken,
|
||||||
|
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 user_from_db: domain::UserFromStorage = state
|
||||||
|
.store
|
||||||
|
.find_user_by_email(
|
||||||
|
&email_token
|
||||||
|
.get_email()
|
||||||
|
.change_context(UserErrors::InternalServerError)?,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.change_context(UserErrors::InternalServerError)?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
if user_from_db.get_user_id() != user_token.user_id {
|
||||||
|
return Err(UserErrors::LinkInvalid.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let password = domain::UserPassword::new(request.password)?;
|
||||||
|
let hash_password = utils::user::password::generate_password_hash(password.get_secret())?;
|
||||||
|
|
||||||
|
let user = state
|
||||||
|
.store
|
||||||
|
.update_user_by_email(
|
||||||
|
&email_token
|
||||||
|
.get_email()
|
||||||
|
.change_context(UserErrors::InternalServerError)?,
|
||||||
|
storage_user::UserUpdate::PasswordUpdate {
|
||||||
|
password: Some(hash_password),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.change_context(UserErrors::InternalServerError)?;
|
||||||
|
|
||||||
|
let _ = auth::blacklist::insert_email_token_in_blacklist(&state, &token)
|
||||||
|
.await
|
||||||
|
.map_err(|e| logger::error!(?e));
|
||||||
|
let _ = auth::blacklist::insert_user_in_blacklist(&state, &user.user_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| logger::error!(?e));
|
||||||
|
|
||||||
|
Ok(ApplicationResponse::StatusOk)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "email")]
|
#[cfg(feature = "email")]
|
||||||
pub async fn reset_password(
|
pub async fn reset_password(
|
||||||
state: AppState,
|
state: AppState,
|
||||||
@ -432,7 +521,6 @@ pub async fn reset_password(
|
|||||||
auth::blacklist::check_email_token_in_blacklist(&state, &token).await?;
|
auth::blacklist::check_email_token_in_blacklist(&state, &token).await?;
|
||||||
|
|
||||||
let password = domain::UserPassword::new(request.password)?;
|
let password = domain::UserPassword::new(request.password)?;
|
||||||
|
|
||||||
let hash_password = utils::user::password::generate_password_hash(password.get_secret())?;
|
let hash_password = utils::user::password::generate_password_hash(password.get_secret())?;
|
||||||
|
|
||||||
let user = state
|
let user = state
|
||||||
@ -441,11 +529,8 @@ pub async fn reset_password(
|
|||||||
&email_token
|
&email_token
|
||||||
.get_email()
|
.get_email()
|
||||||
.change_context(UserErrors::InternalServerError)?,
|
.change_context(UserErrors::InternalServerError)?,
|
||||||
storage_user::UserUpdate::AccountUpdate {
|
storage_user::UserUpdate::PasswordUpdate {
|
||||||
name: None,
|
|
||||||
password: Some(hash_password),
|
password: Some(hash_password),
|
||||||
is_verified: Some(true),
|
|
||||||
preferred_merchant_id: None,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@ -1449,7 +1534,6 @@ pub async fn update_user_details(
|
|||||||
|
|
||||||
let user_update = storage_user::UserUpdate::AccountUpdate {
|
let user_update = storage_user::UserUpdate::AccountUpdate {
|
||||||
name: name.map(|x| x.get_secret().expose()),
|
name: name.map(|x| x.get_secret().expose()),
|
||||||
password: None,
|
|
||||||
is_verified: None,
|
is_verified: None,
|
||||||
preferred_merchant_id: req.preferred_merchant_id,
|
preferred_merchant_id: req.preferred_merchant_id,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -162,6 +162,7 @@ impl UserInterface for MockDb {
|
|||||||
created_at: user_data.created_at.unwrap_or(time_now),
|
created_at: user_data.created_at.unwrap_or(time_now),
|
||||||
last_modified_at: user_data.created_at.unwrap_or(time_now),
|
last_modified_at: user_data.created_at.unwrap_or(time_now),
|
||||||
preferred_merchant_id: user_data.preferred_merchant_id,
|
preferred_merchant_id: user_data.preferred_merchant_id,
|
||||||
|
last_password_modified_at: user_data.last_password_modified_at,
|
||||||
};
|
};
|
||||||
users.push(user.clone());
|
users.push(user.clone());
|
||||||
Ok(user)
|
Ok(user)
|
||||||
@ -218,18 +219,21 @@ impl UserInterface for MockDb {
|
|||||||
},
|
},
|
||||||
storage::UserUpdate::AccountUpdate {
|
storage::UserUpdate::AccountUpdate {
|
||||||
name,
|
name,
|
||||||
password,
|
|
||||||
is_verified,
|
is_verified,
|
||||||
preferred_merchant_id,
|
preferred_merchant_id,
|
||||||
} => storage::User {
|
} => storage::User {
|
||||||
name: name.clone().map(Secret::new).unwrap_or(user.name.clone()),
|
name: name.clone().map(Secret::new).unwrap_or(user.name.clone()),
|
||||||
password: password.clone().unwrap_or(user.password.clone()),
|
|
||||||
is_verified: is_verified.unwrap_or(user.is_verified),
|
is_verified: is_verified.unwrap_or(user.is_verified),
|
||||||
preferred_merchant_id: preferred_merchant_id
|
preferred_merchant_id: preferred_merchant_id
|
||||||
.clone()
|
.clone()
|
||||||
.or(user.preferred_merchant_id.clone()),
|
.or(user.preferred_merchant_id.clone()),
|
||||||
..user.to_owned()
|
..user.to_owned()
|
||||||
},
|
},
|
||||||
|
storage::UserUpdate::PasswordUpdate { password } => storage::User {
|
||||||
|
password: password.clone().unwrap_or(user.password.clone()),
|
||||||
|
last_password_modified_at: Some(common_utils::date_time::now()),
|
||||||
|
..user.to_owned()
|
||||||
|
},
|
||||||
};
|
};
|
||||||
user.to_owned()
|
user.to_owned()
|
||||||
})
|
})
|
||||||
@ -258,18 +262,21 @@ impl UserInterface for MockDb {
|
|||||||
},
|
},
|
||||||
storage::UserUpdate::AccountUpdate {
|
storage::UserUpdate::AccountUpdate {
|
||||||
name,
|
name,
|
||||||
password,
|
|
||||||
is_verified,
|
is_verified,
|
||||||
preferred_merchant_id,
|
preferred_merchant_id,
|
||||||
} => storage::User {
|
} => storage::User {
|
||||||
name: name.clone().map(Secret::new).unwrap_or(user.name.clone()),
|
name: name.clone().map(Secret::new).unwrap_or(user.name.clone()),
|
||||||
password: password.clone().unwrap_or(user.password.clone()),
|
|
||||||
is_verified: is_verified.unwrap_or(user.is_verified),
|
is_verified: is_verified.unwrap_or(user.is_verified),
|
||||||
preferred_merchant_id: preferred_merchant_id
|
preferred_merchant_id: preferred_merchant_id
|
||||||
.clone()
|
.clone()
|
||||||
.or(user.preferred_merchant_id.clone()),
|
.or(user.preferred_merchant_id.clone()),
|
||||||
..user.to_owned()
|
..user.to_owned()
|
||||||
},
|
},
|
||||||
|
storage::UserUpdate::PasswordUpdate { password } => storage::User {
|
||||||
|
password: password.clone().unwrap_or(user.password.clone()),
|
||||||
|
last_password_modified_at: Some(common_utils::date_time::now()),
|
||||||
|
..user.to_owned()
|
||||||
|
},
|
||||||
};
|
};
|
||||||
user.to_owned()
|
user.to_owned()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1180,6 +1180,7 @@ impl User {
|
|||||||
.service(web::resource("").route(web::get().to(get_user_details)))
|
.service(web::resource("").route(web::get().to(get_user_details)))
|
||||||
.service(web::resource("/v2/signin").route(web::post().to(user_signin)))
|
.service(web::resource("/v2/signin").route(web::post().to(user_signin)))
|
||||||
.service(web::resource("/signout").route(web::post().to(signout)))
|
.service(web::resource("/signout").route(web::post().to(signout)))
|
||||||
|
.service(web::resource("/rotate_password").route(web::post().to(rotate_password)))
|
||||||
.service(web::resource("/change_password").route(web::post().to(change_password)))
|
.service(web::resource("/change_password").route(web::post().to(change_password)))
|
||||||
.service(web::resource("/internal_signup").route(web::post().to(internal_user_signup)))
|
.service(web::resource("/internal_signup").route(web::post().to(internal_user_signup)))
|
||||||
.service(web::resource("/switch_merchant").route(web::post().to(switch_merchant_id)))
|
.service(web::resource("/switch_merchant").route(web::post().to(switch_merchant_id)))
|
||||||
|
|||||||
@ -203,6 +203,7 @@ impl From<Flow> for ApiIdentifier {
|
|||||||
| Flow::ListUsersForMerchantAccount
|
| Flow::ListUsersForMerchantAccount
|
||||||
| Flow::ForgotPassword
|
| Flow::ForgotPassword
|
||||||
| Flow::ResetPassword
|
| Flow::ResetPassword
|
||||||
|
| Flow::RotatePassword
|
||||||
| Flow::InviteMultipleUser
|
| Flow::InviteMultipleUser
|
||||||
| Flow::ReInviteUser
|
| Flow::ReInviteUser
|
||||||
| Flow::UserSignUpWithMerchantId
|
| Flow::UserSignUpWithMerchantId
|
||||||
|
|||||||
@ -5,6 +5,7 @@ use api_models::{
|
|||||||
errors::types::ApiErrorResponse,
|
errors::types::ApiErrorResponse,
|
||||||
user::{self as user_api},
|
user::{self as user_api},
|
||||||
};
|
};
|
||||||
|
use common_enums::TokenPurpose;
|
||||||
use common_utils::errors::ReportSwitchExt;
|
use common_utils::errors::ReportSwitchExt;
|
||||||
use router_env::Flow;
|
use router_env::Flow;
|
||||||
|
|
||||||
@ -358,6 +359,24 @@ pub async fn list_users_for_merchant_account(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn rotate_password(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
req: HttpRequest,
|
||||||
|
payload: web::Json<user_api::RotatePasswordRequest>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let flow = Flow::RotatePassword;
|
||||||
|
Box::pin(api::server_wrap(
|
||||||
|
flow,
|
||||||
|
state.clone(),
|
||||||
|
&req,
|
||||||
|
payload.into_inner(),
|
||||||
|
user_core::rotate_password,
|
||||||
|
&auth::SinglePurposeJWTAuth(TokenPurpose::ForceSetPassword),
|
||||||
|
api_locking::LockAction::NotApplicable,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "email")]
|
#[cfg(feature = "email")]
|
||||||
pub async fn forgot_password(
|
pub async fn forgot_password(
|
||||||
state: web::Data<AppState>,
|
state: web::Data<AppState>,
|
||||||
@ -382,18 +401,35 @@ pub async fn reset_password(
|
|||||||
state: web::Data<AppState>,
|
state: web::Data<AppState>,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
payload: web::Json<user_api::ResetPasswordRequest>,
|
payload: web::Json<user_api::ResetPasswordRequest>,
|
||||||
|
query: web::Query<user_api::TokenOnlyQueryParam>,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
let flow = Flow::ResetPassword;
|
let flow = Flow::ResetPassword;
|
||||||
Box::pin(api::server_wrap(
|
let is_token_only = query.into_inner().token_only;
|
||||||
flow,
|
if let Some(true) = is_token_only {
|
||||||
state.clone(),
|
Box::pin(api::server_wrap(
|
||||||
&req,
|
flow,
|
||||||
payload.into_inner(),
|
state.clone(),
|
||||||
|state, _, payload, _| user_core::reset_password(state, payload),
|
&req,
|
||||||
&auth::NoAuth,
|
payload.into_inner(),
|
||||||
api_locking::LockAction::NotApplicable,
|
|state, user, payload, _| {
|
||||||
))
|
user_core::reset_password_token_only_flow(state, user, payload)
|
||||||
.await
|
},
|
||||||
|
&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(
|
pub async fn invite_multiple_user(
|
||||||
state: web::Data<AppState>,
|
state: web::Data<AppState>,
|
||||||
|
|||||||
@ -806,6 +806,26 @@ impl UserFromStorage {
|
|||||||
Ok(Some(days_left_for_verification.whole_days()))
|
Ok(Some(days_left_for_verification.whole_days()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_password_rotate_required(&self, state: &AppState) -> UserResult<bool> {
|
||||||
|
let last_password_modified_at =
|
||||||
|
if let Some(last_password_modified_at) = self.0.last_password_modified_at {
|
||||||
|
last_password_modified_at.date()
|
||||||
|
} else {
|
||||||
|
return Ok(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
let password_change_duration =
|
||||||
|
time::Duration::days(state.conf.user.password_validity_in_days.into());
|
||||||
|
let last_date_for_password_rotate = last_password_modified_at
|
||||||
|
.checked_add(password_change_duration)
|
||||||
|
.ok_or(UserErrors::InternalServerError)?;
|
||||||
|
|
||||||
|
let today = common_utils::date_time::now().date();
|
||||||
|
let days_left_for_password_rotate = last_date_for_password_rotate - today;
|
||||||
|
|
||||||
|
Ok(days_left_for_password_rotate.whole_days() < 0)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_preferred_merchant_id(&self) -> Option<String> {
|
pub fn get_preferred_merchant_id(&self) -> Option<String> {
|
||||||
self.0.preferred_merchant_id.clone()
|
self.0.preferred_merchant_id.clone()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,8 +44,7 @@ impl SPTFlow {
|
|||||||
Self::AcceptInvitationFromEmail | Self::ResetPassword => Ok(true),
|
Self::AcceptInvitationFromEmail | Self::ResetPassword => Ok(true),
|
||||||
Self::VerifyEmail => Ok(user.0.is_verified),
|
Self::VerifyEmail => Ok(user.0.is_verified),
|
||||||
// Final Checks
|
// Final Checks
|
||||||
// TODO: this should be based on last_password_modified_at as a placeholder using false
|
Self::ForceSetPassword => user.is_password_rotate_required(state),
|
||||||
Self::ForceSetPassword => Ok(false),
|
|
||||||
Self::MerchantSelect => user
|
Self::MerchantSelect => user
|
||||||
.get_roles_from_db(state)
|
.get_roles_from_db(state)
|
||||||
.await
|
.await
|
||||||
@ -159,8 +158,9 @@ const ACCEPT_INVITATION_FROM_EMAIL_FLOW: [UserFlow; 5] = [
|
|||||||
UserFlow::JWTFlow(JWTFlow::UserInfo),
|
UserFlow::JWTFlow(JWTFlow::UserInfo),
|
||||||
];
|
];
|
||||||
|
|
||||||
const RESET_PASSWORD_FLOW: [UserFlow; 2] = [
|
const RESET_PASSWORD_FLOW: [UserFlow; 3] = [
|
||||||
UserFlow::SPTFlow(SPTFlow::TOTP),
|
UserFlow::SPTFlow(SPTFlow::TOTP),
|
||||||
|
UserFlow::SPTFlow(SPTFlow::VerifyEmail),
|
||||||
UserFlow::SPTFlow(SPTFlow::ResetPassword),
|
UserFlow::SPTFlow(SPTFlow::ResetPassword),
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -286,7 +286,8 @@ impl From<SPTFlow> for TokenPurpose {
|
|||||||
SPTFlow::VerifyEmail => Self::VerifyEmail,
|
SPTFlow::VerifyEmail => Self::VerifyEmail,
|
||||||
SPTFlow::AcceptInvitationFromEmail => Self::AcceptInvitationFromEmail,
|
SPTFlow::AcceptInvitationFromEmail => Self::AcceptInvitationFromEmail,
|
||||||
SPTFlow::MerchantSelect => Self::AcceptInvite,
|
SPTFlow::MerchantSelect => Self::AcceptInvite,
|
||||||
SPTFlow::ResetPassword | SPTFlow::ForceSetPassword => Self::ResetPassword,
|
SPTFlow::ResetPassword => Self::ResetPassword,
|
||||||
|
SPTFlow::ForceSetPassword => Self::ForceSetPassword,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -360,6 +360,8 @@ pub enum Flow {
|
|||||||
ForgotPassword,
|
ForgotPassword,
|
||||||
/// Reset password using link
|
/// Reset password using link
|
||||||
ResetPassword,
|
ResetPassword,
|
||||||
|
/// Force set or force change password
|
||||||
|
RotatePassword,
|
||||||
/// Invite multiple users
|
/// Invite multiple users
|
||||||
InviteMultipleUser,
|
InviteMultipleUser,
|
||||||
/// Reinvite user
|
/// Reinvite user
|
||||||
|
|||||||
@ -28,6 +28,9 @@ host = "redis-queue"
|
|||||||
admin_api_key = "test_admin"
|
admin_api_key = "test_admin"
|
||||||
jwt_secret = "secret"
|
jwt_secret = "secret"
|
||||||
|
|
||||||
|
[user]
|
||||||
|
password_validity_in_days = 90
|
||||||
|
|
||||||
[locker]
|
[locker]
|
||||||
host = ""
|
host = ""
|
||||||
host_rs = ""
|
host_rs = ""
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
ALTER TABLE users DROP COLUMN IF EXISTS last_password_modified_at;
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
-- Your SQL goes here
|
||||||
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_password_modified_at TIMESTAMP;
|
||||||
Reference in New Issue
Block a user