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:
Apoorv Dixit
2024-05-07 19:41:24 +05:30
committed by GitHub
parent d974e6e7c2
commit 59e79ff205
25 changed files with 240 additions and 34 deletions

View File

@ -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" } }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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" }

View File

@ -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 = ""

View File

@ -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,

View File

@ -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,

View File

@ -2717,6 +2717,7 @@ pub enum TokenPurpose {
TOTP, TOTP,
VerifyEmail, VerifyEmail,
AcceptInvitationFromEmail, AcceptInvitationFromEmail,
ForceSetPassword,
ResetPassword, ResetPassword,
AcceptInvite, AcceptInvite,
UserInfo, UserInfo,

View File

@ -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>,
} }
} }

View File

@ -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),
}, },
} }
} }

View File

@ -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,

View File

@ -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 {

View File

@ -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",

View File

@ -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,
}; };

View File

@ -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()
}) })

View File

@ -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)))

View File

@ -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

View File

@ -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>,

View File

@ -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()
} }

View File

@ -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,
} }
} }
} }

View File

@ -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

View File

@ -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 = ""

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
ALTER TABLE users DROP COLUMN IF EXISTS last_password_modified_at;

View File

@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_password_modified_at TIMESTAMP;