Skip to content

Commit

Permalink
feat(user): implement change password for user (#2959)
Browse files Browse the repository at this point in the history
  • Loading branch information
apoorvdixit88 committed Nov 24, 2023
1 parent 107c3b9 commit bfa1645
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 5 deletions.
4 changes: 3 additions & 1 deletion crates/api_models/src/events/user.rs
Original file line number Diff line number Diff line change
@@ -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> {
Expand All @@ -12,3 +12,5 @@ impl ApiEventMetric for ConnectAccountResponse {
}

impl ApiEventMetric for ConnectAccountRequest {}

common_utils::impl_misc_api_event_type!(ChangePasswordRequest);
6 changes: 6 additions & 0 deletions crates/api_models/src/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 13 additions & 0 deletions crates/router/src/core/errors/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pub enum UserErrors {
InvalidCredentials,
#[error("UserExists")]
UserExists,
#[error("InvalidOldPassword")]
InvalidOldPassword,
#[error("EmailParsingError")]
EmailParsingError,
#[error("NameParsingError")]
Expand All @@ -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 {
Expand All @@ -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))
}
Expand All @@ -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))
}
}
}
}
42 changes: 40 additions & 2 deletions crates/router/src/core/user.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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)
}
1 change: 1 addition & 0 deletions crates/router/src/routes/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/router/src/routes/lock_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
}
18 changes: 18 additions & 0 deletions crates/router/src/routes/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
87 changes: 86 additions & 1 deletion crates/router/src/services/authentication.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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 {
Expand All @@ -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>;
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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>;
}
Expand Down
2 changes: 2 additions & 0 deletions crates/router_env/src/logger/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,8 @@ pub enum Flow {
DecisionManagerDeleteConfig,
/// Retrieve Decision Manager Config
DecisionManagerRetrieveConfig,
/// Change password flow
ChangePassword,
}

///
Expand Down

0 comments on commit bfa1645

Please sign in to comment.