mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 18:17:13 +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 common_utils::events::{ApiEventMetric, ApiEventsType}; | ||||||
|  |  | ||||||
| use crate::user::{ConnectAccountRequest, ConnectAccountResponse}; | use crate::user::{ChangePasswordRequest, ConnectAccountRequest, ConnectAccountResponse}; | ||||||
|  |  | ||||||
| impl ApiEventMetric for ConnectAccountResponse { | impl ApiEventMetric for ConnectAccountResponse { | ||||||
|     fn get_api_event_type(&self) -> Option<ApiEventsType> { |     fn get_api_event_type(&self) -> Option<ApiEventsType> { | ||||||
| @ -12,3 +12,5 @@ impl ApiEventMetric for ConnectAccountResponse { | |||||||
| } | } | ||||||
|  |  | ||||||
| impl ApiEventMetric for ConnectAccountRequest {} | impl ApiEventMetric for ConnectAccountRequest {} | ||||||
|  |  | ||||||
|  | common_utils::impl_misc_api_event_type!(ChangePasswordRequest); | ||||||
|  | |||||||
| @ -19,3 +19,9 @@ pub struct ConnectAccountResponse { | |||||||
|     #[serde(skip_serializing)] |     #[serde(skip_serializing)] | ||||||
|     pub user_id: String, |     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, |     InvalidCredentials, | ||||||
|     #[error("UserExists")] |     #[error("UserExists")] | ||||||
|     UserExists, |     UserExists, | ||||||
|  |     #[error("InvalidOldPassword")] | ||||||
|  |     InvalidOldPassword, | ||||||
|     #[error("EmailParsingError")] |     #[error("EmailParsingError")] | ||||||
|     EmailParsingError, |     EmailParsingError, | ||||||
|     #[error("NameParsingError")] |     #[error("NameParsingError")] | ||||||
| @ -27,6 +29,8 @@ pub enum UserErrors { | |||||||
|     InvalidEmailError, |     InvalidEmailError, | ||||||
|     #[error("DuplicateOrganizationId")] |     #[error("DuplicateOrganizationId")] | ||||||
|     DuplicateOrganizationId, |     DuplicateOrganizationId, | ||||||
|  |     #[error("MerchantIdNotFound")] | ||||||
|  |     MerchantIdNotFound, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorResponse> for UserErrors { | 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", |                 "An account already exists with this email", | ||||||
|                 None, |                 None, | ||||||
|             )), |             )), | ||||||
|  |             Self::InvalidOldPassword => AER::BadRequest(ApiError::new( | ||||||
|  |                 sub_code, | ||||||
|  |                 6, | ||||||
|  |                 "Old password incorrect. Please enter the correct password", | ||||||
|  |                 None, | ||||||
|  |             )), | ||||||
|             Self::EmailParsingError => { |             Self::EmailParsingError => { | ||||||
|                 AER::BadRequest(ApiError::new(sub_code, 7, "Invalid Email", None)) |                 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", |                 "An Organization with the id already exists", | ||||||
|                 None, |                 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 api_models::user as api; | ||||||
| use diesel_models::enums::UserStatus; | use diesel_models::enums::UserStatus; | ||||||
| use error_stack::IntoReport; | use error_stack::{IntoReport, ResultExt}; | ||||||
| use masking::{ExposeInterface, Secret}; | use masking::{ExposeInterface, Secret}; | ||||||
| use router_env::env; | use router_env::env; | ||||||
|  |  | ||||||
| use super::errors::{UserErrors, UserResponse}; | 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( | pub async fn connect_account( | ||||||
|     state: AppState, |     state: AppState, | ||||||
| @ -77,3 +83,35 @@ pub async fn connect_account( | |||||||
|         Err(UserErrors::InternalServerError.into()) |         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("/signup").route(web::post().to(user_connect_account))) | ||||||
|             .service(web::resource("/v2/signin").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("/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::GsmRuleUpdate | ||||||
|             | Flow::GsmRuleDelete => Self::Gsm, |             | 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 |     .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; | use super::jwt; | ||||||
| #[cfg(feature = "olap")] | #[cfg(feature = "olap")] | ||||||
| use crate::consts; | use crate::consts; | ||||||
|  | #[cfg(feature = "olap")] | ||||||
|  | use crate::core::errors::UserResult; | ||||||
| use crate::{ | use crate::{ | ||||||
|     configs::settings, |     configs::settings, | ||||||
|     core::{ |     core::{ | ||||||
| @ -97,7 +99,7 @@ impl AuthToken { | |||||||
|         role_id: String, |         role_id: String, | ||||||
|         settings: &settings::Settings, |         settings: &settings::Settings, | ||||||
|         org_id: String, |         org_id: String, | ||||||
|     ) -> errors::UserResult<String> { |     ) -> UserResult<String> { | ||||||
|         let exp_duration = std::time::Duration::from_secs(consts::JWT_TOKEN_TIME_IN_SECS); |         let exp_duration = std::time::Duration::from_secs(consts::JWT_TOKEN_TIME_IN_SECS); | ||||||
|         let exp = jwt::generate_exp(exp_duration)?.as_secs(); |         let exp = jwt::generate_exp(exp_duration)?.as_secs(); | ||||||
|         let token_payload = Self { |         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 { | pub trait AuthInfo { | ||||||
|     fn get_merchant_id(&self) -> Option<&str>; |     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 struct JWTAuthMerchantFromRoute { | ||||||
|     pub merchant_id: String, |     pub merchant_id: String, | ||||||
|     pub required_permission: Permission, |     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 { | pub trait ClientSecretFetch { | ||||||
|     fn get_client_secret(&self) -> Option<&String>; |     fn get_client_secret(&self) -> Option<&String>; | ||||||
| } | } | ||||||
|  | |||||||
| @ -255,6 +255,8 @@ pub enum Flow { | |||||||
|     DecisionManagerDeleteConfig, |     DecisionManagerDeleteConfig, | ||||||
|     /// Retrieve Decision Manager Config |     /// Retrieve Decision Manager Config | ||||||
|     DecisionManagerRetrieveConfig, |     DecisionManagerRetrieveConfig, | ||||||
|  |     /// Change password flow | ||||||
|  |     ChangePassword, | ||||||
| } | } | ||||||
|  |  | ||||||
| /// | /// | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Apoorv Dixit
					Apoorv Dixit