mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +08:00
feat(user): implement change password for user (#2959)
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
use common_utils::events::{ApiEventMetric, ApiEventsType};
|
||||
|
||||
use crate::user::{ConnectAccountRequest, ConnectAccountResponse};
|
||||
use crate::user::{ChangePasswordRequest, ConnectAccountRequest, ConnectAccountResponse};
|
||||
|
||||
impl ApiEventMetric for ConnectAccountResponse {
|
||||
fn get_api_event_type(&self) -> Option<ApiEventsType> {
|
||||
@ -12,3 +12,5 @@ impl ApiEventMetric for ConnectAccountResponse {
|
||||
}
|
||||
|
||||
impl ApiEventMetric for ConnectAccountRequest {}
|
||||
|
||||
common_utils::impl_misc_api_event_type!(ChangePasswordRequest);
|
||||
|
||||
@ -19,3 +19,9 @@ pub struct ConnectAccountResponse {
|
||||
#[serde(skip_serializing)]
|
||||
pub user_id: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, serde::Serialize)]
|
||||
pub struct ChangePasswordRequest {
|
||||
pub new_password: Secret<String>,
|
||||
pub old_password: Secret<String>,
|
||||
}
|
||||
|
||||
@ -13,6 +13,8 @@ pub enum UserErrors {
|
||||
InvalidCredentials,
|
||||
#[error("UserExists")]
|
||||
UserExists,
|
||||
#[error("InvalidOldPassword")]
|
||||
InvalidOldPassword,
|
||||
#[error("EmailParsingError")]
|
||||
EmailParsingError,
|
||||
#[error("NameParsingError")]
|
||||
@ -27,6 +29,8 @@ pub enum UserErrors {
|
||||
InvalidEmailError,
|
||||
#[error("DuplicateOrganizationId")]
|
||||
DuplicateOrganizationId,
|
||||
#[error("MerchantIdNotFound")]
|
||||
MerchantIdNotFound,
|
||||
}
|
||||
|
||||
impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorResponse> for UserErrors {
|
||||
@ -49,6 +53,12 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
|
||||
"An account already exists with this email",
|
||||
None,
|
||||
)),
|
||||
Self::InvalidOldPassword => AER::BadRequest(ApiError::new(
|
||||
sub_code,
|
||||
6,
|
||||
"Old password incorrect. Please enter the correct password",
|
||||
None,
|
||||
)),
|
||||
Self::EmailParsingError => {
|
||||
AER::BadRequest(ApiError::new(sub_code, 7, "Invalid Email", None))
|
||||
}
|
||||
@ -73,6 +83,9 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
|
||||
"An Organization with the id already exists",
|
||||
None,
|
||||
)),
|
||||
Self::MerchantIdNotFound => {
|
||||
AER::BadRequest(ApiError::new(sub_code, 18, "Invalid Merchant ID", None))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,17 @@
|
||||
use api_models::user as api;
|
||||
use diesel_models::enums::UserStatus;
|
||||
use error_stack::IntoReport;
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use masking::{ExposeInterface, Secret};
|
||||
use router_env::env;
|
||||
|
||||
use super::errors::{UserErrors, UserResponse};
|
||||
use crate::{consts, routes::AppState, services::ApplicationResponse, types::domain};
|
||||
use crate::{
|
||||
consts,
|
||||
db::user::UserInterface,
|
||||
routes::AppState,
|
||||
services::{authentication::UserFromToken, ApplicationResponse},
|
||||
types::domain,
|
||||
};
|
||||
|
||||
pub async fn connect_account(
|
||||
state: AppState,
|
||||
@ -77,3 +83,35 @@ pub async fn connect_account(
|
||||
Err(UserErrors::InternalServerError.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn change_password(
|
||||
state: AppState,
|
||||
request: api::ChangePasswordRequest,
|
||||
user_from_token: UserFromToken,
|
||||
) -> UserResponse<()> {
|
||||
let user: domain::UserFromStorage =
|
||||
UserInterface::find_user_by_id(&*state.store, &user_from_token.user_id)
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)?
|
||||
.into();
|
||||
|
||||
user.compare_password(request.old_password)
|
||||
.change_context(UserErrors::InvalidOldPassword)?;
|
||||
|
||||
let new_password_hash =
|
||||
crate::utils::user::password::generate_password_hash(request.new_password)?;
|
||||
|
||||
let _ = UserInterface::update_user_by_user_id(
|
||||
&*state.store,
|
||||
user.get_user_id(),
|
||||
diesel_models::user::UserUpdate::AccountUpdate {
|
||||
name: None,
|
||||
password: Some(new_password_hash),
|
||||
is_verified: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)?;
|
||||
|
||||
Ok(ApplicationResponse::StatusOk)
|
||||
}
|
||||
|
||||
@ -759,6 +759,7 @@ impl User {
|
||||
.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("/change_password").route(web::post().to(change_password)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -144,7 +144,7 @@ impl From<Flow> for ApiIdentifier {
|
||||
| Flow::GsmRuleUpdate
|
||||
| Flow::GsmRuleDelete => Self::Gsm,
|
||||
|
||||
Flow::UserConnectAccount => Self::User,
|
||||
Flow::UserConnectAccount | Flow::ChangePassword => Self::User,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,3 +29,21 @@ pub async fn user_connect_account(
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn change_password(
|
||||
state: web::Data<AppState>,
|
||||
http_req: HttpRequest,
|
||||
json_payload: web::Json<user_api::ChangePasswordRequest>,
|
||||
) -> HttpResponse {
|
||||
let flow = Flow::ChangePassword;
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state.clone(),
|
||||
&http_req,
|
||||
json_payload.into_inner(),
|
||||
|state, user, req| user::change_password(state, req, user),
|
||||
&auth::DashboardNoPermissionAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
@ -14,6 +14,8 @@ use super::authorization::{self, permissions::Permission};
|
||||
use super::jwt;
|
||||
#[cfg(feature = "olap")]
|
||||
use crate::consts;
|
||||
#[cfg(feature = "olap")]
|
||||
use crate::core::errors::UserResult;
|
||||
use crate::{
|
||||
configs::settings,
|
||||
core::{
|
||||
@ -97,7 +99,7 @@ impl AuthToken {
|
||||
role_id: String,
|
||||
settings: &settings::Settings,
|
||||
org_id: String,
|
||||
) -> errors::UserResult<String> {
|
||||
) -> UserResult<String> {
|
||||
let exp_duration = std::time::Duration::from_secs(consts::JWT_TOKEN_TIME_IN_SECS);
|
||||
let exp = jwt::generate_exp(exp_duration)?.as_secs();
|
||||
let token_payload = Self {
|
||||
@ -111,6 +113,14 @@ impl AuthToken {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UserFromToken {
|
||||
pub user_id: String,
|
||||
pub merchant_id: String,
|
||||
pub role_id: String,
|
||||
pub org_id: String,
|
||||
}
|
||||
|
||||
pub trait AuthInfo {
|
||||
fn get_merchant_id(&self) -> Option<&str>;
|
||||
}
|
||||
@ -421,6 +431,34 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "olap")]
|
||||
#[async_trait]
|
||||
impl<A> AuthenticateAndFetch<UserFromToken, A> for JWTAuth
|
||||
where
|
||||
A: AppStateInfo + Sync,
|
||||
{
|
||||
async fn authenticate_and_fetch(
|
||||
&self,
|
||||
request_headers: &HeaderMap,
|
||||
state: &A,
|
||||
) -> RouterResult<(UserFromToken, AuthenticationType)> {
|
||||
let payload = parse_jwt_payload::<A, AuthToken>(request_headers, state).await?;
|
||||
|
||||
Ok((
|
||||
UserFromToken {
|
||||
user_id: payload.user_id.clone(),
|
||||
merchant_id: payload.merchant_id.clone(),
|
||||
org_id: payload.org_id,
|
||||
role_id: payload.role_id,
|
||||
},
|
||||
AuthenticationType::MerchantJWT {
|
||||
merchant_id: payload.merchant_id,
|
||||
user_id: Some(payload.user_id),
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct JWTAuthMerchantFromRoute {
|
||||
pub merchant_id: String,
|
||||
pub required_permission: Permission,
|
||||
@ -519,6 +557,53 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DashboardNoPermissionAuth;
|
||||
|
||||
#[cfg(feature = "olap")]
|
||||
#[async_trait]
|
||||
impl<A> AuthenticateAndFetch<UserFromToken, A> for DashboardNoPermissionAuth
|
||||
where
|
||||
A: AppStateInfo + Sync,
|
||||
{
|
||||
async fn authenticate_and_fetch(
|
||||
&self,
|
||||
request_headers: &HeaderMap,
|
||||
state: &A,
|
||||
) -> RouterResult<(UserFromToken, AuthenticationType)> {
|
||||
let payload = parse_jwt_payload::<A, AuthToken>(request_headers, state).await?;
|
||||
|
||||
Ok((
|
||||
UserFromToken {
|
||||
user_id: payload.user_id.clone(),
|
||||
merchant_id: payload.merchant_id.clone(),
|
||||
org_id: payload.org_id,
|
||||
role_id: payload.role_id,
|
||||
},
|
||||
AuthenticationType::MerchantJWT {
|
||||
merchant_id: payload.merchant_id,
|
||||
user_id: Some(payload.user_id),
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "olap")]
|
||||
#[async_trait]
|
||||
impl<A> AuthenticateAndFetch<(), A> for DashboardNoPermissionAuth
|
||||
where
|
||||
A: AppStateInfo + Sync,
|
||||
{
|
||||
async fn authenticate_and_fetch(
|
||||
&self,
|
||||
request_headers: &HeaderMap,
|
||||
state: &A,
|
||||
) -> RouterResult<((), AuthenticationType)> {
|
||||
parse_jwt_payload::<A, AuthToken>(request_headers, state).await?;
|
||||
|
||||
Ok(((), AuthenticationType::NoAuth))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ClientSecretFetch {
|
||||
fn get_client_secret(&self) -> Option<&String>;
|
||||
}
|
||||
|
||||
@ -255,6 +255,8 @@ pub enum Flow {
|
||||
DecisionManagerDeleteConfig,
|
||||
/// Retrieve Decision Manager Config
|
||||
DecisionManagerRetrieveConfig,
|
||||
/// Change password flow
|
||||
ChangePassword,
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user