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

This commit is contained in:
Apoorv Dixit
2023-11-24 20:34:27 +05:30
committed by GitHub
parent 107c3b9941
commit bfa1645b84
9 changed files with 170 additions and 5 deletions

View File

@ -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);

View File

@ -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>,
}

View File

@ -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))
}
} }
} }
} }

View File

@ -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)
}

View File

@ -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)))
} }
} }

View File

@ -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,
} }
} }
} }

View File

@ -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
}

View File

@ -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>;
} }

View File

@ -255,6 +255,8 @@ pub enum Flow {
DecisionManagerDeleteConfig, DecisionManagerDeleteConfig,
/// Retrieve Decision Manager Config /// Retrieve Decision Manager Config
DecisionManagerRetrieveConfig, DecisionManagerRetrieveConfig,
/// Change password flow
ChangePassword,
} }
/// ///