From bfa1645b847fb881eb2370d5dbfef6fd0b53725d Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Fri, 24 Nov 2023 20:34:27 +0530 Subject: [PATCH] feat(user): implement change password for user (#2959) --- crates/api_models/src/events/user.rs | 4 +- crates/api_models/src/user.rs | 6 ++ crates/router/src/core/errors/user.rs | 13 +++ crates/router/src/core/user.rs | 42 +++++++++- crates/router/src/routes/app.rs | 1 + crates/router/src/routes/lock_utils.rs | 2 +- crates/router/src/routes/user.rs | 18 ++++ crates/router/src/services/authentication.rs | 87 +++++++++++++++++++- crates/router_env/src/logger/types.rs | 2 + 9 files changed, 170 insertions(+), 5 deletions(-) diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 2a896cc38776..4e9f2f284173 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -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 { @@ -12,3 +12,5 @@ impl ApiEventMetric for ConnectAccountResponse { } impl ApiEventMetric for ConnectAccountRequest {} + +common_utils::impl_misc_api_event_type!(ChangePasswordRequest); diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 91f7702c654e..41ea9cc5193a 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -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, + pub old_password: Secret, +} diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index b4d48365dc84..b86c395b9814 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -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 for UserErrors { @@ -49,6 +53,12 @@ impl common_utils::errors::ErrorSwitch 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 { + AER::BadRequest(ApiError::new(sub_code, 18, "Invalid Merchant ID", None)) + } } } } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 8b4cf45fe5ef..94cd482a2291 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -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) +} diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 96bb47ea4e97..84848e030120 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -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))) } } diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index a9cf7b44a73d..219948bdd4d2 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -144,7 +144,7 @@ impl From for ApiIdentifier { | Flow::GsmRuleUpdate | Flow::GsmRuleDelete => Self::Gsm, - Flow::UserConnectAccount => Self::User, + Flow::UserConnectAccount | Flow::ChangePassword => Self::User, } } } diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 0ff11ce087b5..7d3d183eda76 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -29,3 +29,21 @@ pub async fn user_connect_account( )) .await } + +pub async fn change_password( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> 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 +} diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 876804b7bb93..e24c7cebcb2a 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -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 { + ) -> UserResult { 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 AuthenticateAndFetch 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::(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 AuthenticateAndFetch 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::(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 AuthenticateAndFetch<(), A> for DashboardNoPermissionAuth +where + A: AppStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<((), AuthenticationType)> { + parse_jwt_payload::(request_headers, state).await?; + + Ok(((), AuthenticationType::NoAuth)) + } +} + pub trait ClientSecretFetch { fn get_client_secret(&self) -> Option<&String>; } diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 178f837fce18..7978e98e52c0 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -255,6 +255,8 @@ pub enum Flow { DecisionManagerDeleteConfig, /// Retrieve Decision Manager Config DecisionManagerRetrieveConfig, + /// Change password flow + ChangePassword, } ///