Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(user): implement change password for user #2959

Merged
merged 11 commits into from
Nov 24, 2023
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
Loading