feat(users): Add merchant_id in EmailToken and change user status in reset password (#3473)

This commit is contained in:
Mani Chandra
2024-01-31 13:58:37 +05:30
committed by GitHub
parent 7597f3b692
commit db3d53ff1d
5 changed files with 140 additions and 29 deletions

View File

@ -3,7 +3,7 @@ use diesel::{
associations::HasTable, debug_query, result::Error as DieselError, ExpressionMethods, associations::HasTable, debug_query, result::Error as DieselError, ExpressionMethods,
JoinOnDsl, QueryDsl, JoinOnDsl, QueryDsl,
}; };
use error_stack::{report, IntoReport}; use error_stack::IntoReport;
use router_env::{ use router_env::{
logger, logger,
tracing::{self, instrument}, tracing::{self, instrument},
@ -49,19 +49,37 @@ impl User {
pub async fn update_by_user_id( pub async fn update_by_user_id(
conn: &PgPooledConn, conn: &PgPooledConn,
user_id: &str, user_id: &str,
user: UserUpdate, user_update: UserUpdate,
) -> StorageResult<Self> { ) -> StorageResult<Self> {
generics::generic_update_with_results::<<Self as HasTable>::Table, _, _, _>( generics::generic_update_with_unique_predicate_get_result::<
<Self as HasTable>::Table,
_,
_,
_,
>(
conn, conn,
users_dsl::user_id.eq(user_id.to_owned()), users_dsl::user_id.eq(user_id.to_owned()),
UserUpdateInternal::from(user), UserUpdateInternal::from(user_update),
) )
.await? .await
.first() }
.cloned()
.ok_or_else(|| { pub async fn update_by_user_email(
report!(errors::DatabaseError::NotFound).attach_printable("Error while updating user") conn: &PgPooledConn,
}) user_email: &str,
user_update: UserUpdate,
) -> StorageResult<Self> {
generics::generic_update_with_unique_predicate_get_result::<
<Self as HasTable>::Table,
_,
_,
_,
>(
conn,
users_dsl::email.eq(user_email.to_owned()),
UserUpdateInternal::from(user_update),
)
.await
} }
pub async fn delete_by_user_id(conn: &PgPooledConn, user_id: &str) -> StorageResult<bool> { pub async fn delete_by_user_id(conn: &PgPooledConn, user_id: &str) -> StorageResult<bool> {

View File

@ -1,4 +1,6 @@
use api_models::user::{self as user_api, InviteMultipleUserResponse}; use api_models::user::{self as user_api, InviteMultipleUserResponse};
#[cfg(feature = "email")]
use diesel_models::user_role::UserRoleUpdate;
use diesel_models::{enums::UserStatus, user as storage_user, user_role::UserRoleNew}; use diesel_models::{enums::UserStatus, user as storage_user, user_role::UserRoleNew};
#[cfg(feature = "email")] #[cfg(feature = "email")]
use error_stack::IntoReport; use error_stack::IntoReport;
@ -362,18 +364,10 @@ pub async fn reset_password(
let hash_password = utils::user::password::generate_password_hash(password.get_secret())?; let hash_password = utils::user::password::generate_password_hash(password.get_secret())?;
//TODO: Create Update by email query let user = state
let user_id = state
.store .store
.find_user_by_email(token.get_email()) .update_user_by_email(
.await token.get_email(),
.change_context(UserErrors::InternalServerError)?
.user_id;
state
.store
.update_user_by_user_id(
user_id.as_str(),
storage_user::UserUpdate::AccountUpdate { storage_user::UserUpdate::AccountUpdate {
name: None, name: None,
password: Some(hash_password), password: Some(hash_password),
@ -384,7 +378,20 @@ pub async fn reset_password(
.await .await
.change_context(UserErrors::InternalServerError)?; .change_context(UserErrors::InternalServerError)?;
//TODO: Update User role status for invited user if let Some(inviter_merchant_id) = token.get_merchant_id() {
let update_status_result = state
.store
.update_user_role_by_user_id_merchant_id(
user.user_id.clone().as_str(),
inviter_merchant_id,
UserRoleUpdate::UpdateStatus {
status: UserStatus::Active,
modified_by: user.user_id,
},
)
.await;
logger::info!(?update_status_result);
}
Ok(ApplicationResponse::StatusOk) Ok(ApplicationResponse::StatusOk)
} }
@ -467,7 +474,7 @@ pub async fn invite_user(
.store .store
.insert_user_role(UserRoleNew { .insert_user_role(UserRoleNew {
user_id: new_user.get_user_id().to_owned(), user_id: new_user.get_user_id().to_owned(),
merchant_id: user_from_token.merchant_id, merchant_id: user_from_token.merchant_id.clone(),
role_id: request.role_id, role_id: request.role_id,
org_id: user_from_token.org_id, org_id: user_from_token.org_id,
status: invitation_status, status: invitation_status,
@ -493,6 +500,7 @@ pub async fn invite_user(
user_name: domain::UserName::new(new_user.get_name())?, user_name: domain::UserName::new(new_user.get_name())?,
settings: state.conf.clone(), settings: state.conf.clone(),
subject: "You have been invited to join Hyperswitch Community!", subject: "You have been invited to join Hyperswitch Community!",
merchant_id: user_from_token.merchant_id,
}; };
let send_email_result = state let send_email_result = state
.email_client .email_client
@ -669,6 +677,7 @@ async fn handle_new_user_invitation(
user_name: domain::UserName::new(new_user.get_name())?, user_name: domain::UserName::new(new_user.get_name())?,
settings: state.conf.clone(), settings: state.conf.clone(),
subject: "You have been invited to join Hyperswitch Community!", subject: "You have been invited to join Hyperswitch Community!",
merchant_id: user_from_token.merchant_id.clone(),
}; };
let send_email_result = state let send_email_result = state
.email_client .email_client

View File

@ -1895,6 +1895,16 @@ impl UserInterface for KafkaStore {
.await .await
} }
async fn update_user_by_email(
&self,
user_email: &str,
user: storage::UserUpdate,
) -> CustomResult<storage::User, errors::StorageError> {
self.diesel_store
.update_user_by_email(user_email, user)
.await
}
async fn delete_user_by_user_id( async fn delete_user_by_user_id(
&self, &self,
user_id: &str, user_id: &str,

View File

@ -33,6 +33,12 @@ pub trait UserInterface {
user: storage::UserUpdate, user: storage::UserUpdate,
) -> CustomResult<storage::User, errors::StorageError>; ) -> CustomResult<storage::User, errors::StorageError>;
async fn update_user_by_email(
&self,
user_email: &str,
user: storage::UserUpdate,
) -> CustomResult<storage::User, errors::StorageError>;
async fn delete_user_by_user_id( async fn delete_user_by_user_id(
&self, &self,
user_id: &str, user_id: &str,
@ -92,6 +98,18 @@ impl UserInterface for Store {
.into_report() .into_report()
} }
async fn update_user_by_email(
&self,
user_email: &str,
user: storage::UserUpdate,
) -> CustomResult<storage::User, errors::StorageError> {
let conn = connection::pg_connection_write(self).await?;
storage::User::update_by_user_email(&conn, user_email, user)
.await
.map_err(Into::into)
.into_report()
}
async fn delete_user_by_user_id( async fn delete_user_by_user_id(
&self, &self,
user_id: &str, user_id: &str,
@ -229,6 +247,50 @@ impl UserInterface for MockDb {
) )
} }
async fn update_user_by_email(
&self,
user_email: &str,
update_user: storage::UserUpdate,
) -> CustomResult<storage::User, errors::StorageError> {
let mut users = self.users.lock().await;
let user_email_pii: common_utils::pii::Email = user_email
.to_string()
.try_into()
.map_err(|_| errors::StorageError::MockDbError)?;
users
.iter_mut()
.find(|user| user.email == user_email_pii)
.map(|user| {
*user = match &update_user {
storage::UserUpdate::VerifyUser => storage::User {
is_verified: true,
..user.to_owned()
},
storage::UserUpdate::AccountUpdate {
name,
password,
is_verified,
preferred_merchant_id,
} => storage::User {
name: name.clone().map(Secret::new).unwrap_or(user.name.clone()),
password: password.clone().unwrap_or(user.password.clone()),
is_verified: is_verified.unwrap_or(user.is_verified),
preferred_merchant_id: preferred_merchant_id
.clone()
.or(user.preferred_merchant_id.clone()),
..user.to_owned()
},
};
user.to_owned()
})
.ok_or(
errors::StorageError::ValueNotFound(format!(
"No user available for user_email = {user_email}"
))
.into(),
)
}
async fn delete_user_by_user_id( async fn delete_user_by_user_id(
&self, &self,
user_id: &str, user_id: &str,

View File

@ -92,18 +92,21 @@ Email : {user_email}
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
pub struct EmailToken { pub struct EmailToken {
email: String, email: String,
merchant_id: Option<String>,
exp: u64, exp: u64,
} }
impl EmailToken { impl EmailToken {
pub async fn new_token( pub async fn new_token(
email: domain::UserEmail, email: domain::UserEmail,
merchant_id: Option<String>,
settings: &configs::settings::Settings, settings: &configs::settings::Settings,
) -> CustomResult<String, UserErrors> { ) -> CustomResult<String, UserErrors> {
let expiration_duration = std::time::Duration::from_secs(consts::EMAIL_TOKEN_TIME_IN_SECS); let expiration_duration = std::time::Duration::from_secs(consts::EMAIL_TOKEN_TIME_IN_SECS);
let exp = jwt::generate_exp(expiration_duration)?.as_secs(); let exp = jwt::generate_exp(expiration_duration)?.as_secs();
let token_payload = Self { let token_payload = Self {
email: email.get_secret().expose(), email: email.get_secret().expose(),
merchant_id,
exp, exp,
}; };
jwt::generate_jwt(&token_payload, settings).await jwt::generate_jwt(&token_payload, settings).await
@ -112,6 +115,10 @@ impl EmailToken {
pub fn get_email(&self) -> &str { pub fn get_email(&self) -> &str {
self.email.as_str() self.email.as_str()
} }
pub fn get_merchant_id(&self) -> Option<&str> {
self.merchant_id.as_deref()
}
} }
pub fn get_link_with_token( pub fn get_link_with_token(
@ -132,7 +139,7 @@ pub struct VerifyEmail {
#[async_trait::async_trait] #[async_trait::async_trait]
impl EmailData for VerifyEmail { impl EmailData for VerifyEmail {
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> { async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) let token = EmailToken::new_token(self.recipient_email.clone(), None, &self.settings)
.await .await
.change_context(EmailError::TokenGenerationFailure)?; .change_context(EmailError::TokenGenerationFailure)?;
@ -161,7 +168,7 @@ pub struct ResetPassword {
#[async_trait::async_trait] #[async_trait::async_trait]
impl EmailData for ResetPassword { impl EmailData for ResetPassword {
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> { async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) let token = EmailToken::new_token(self.recipient_email.clone(), None, &self.settings)
.await .await
.change_context(EmailError::TokenGenerationFailure)?; .change_context(EmailError::TokenGenerationFailure)?;
@ -191,7 +198,7 @@ pub struct MagicLink {
#[async_trait::async_trait] #[async_trait::async_trait]
impl EmailData for MagicLink { impl EmailData for MagicLink {
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> { async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) let token = EmailToken::new_token(self.recipient_email.clone(), None, &self.settings)
.await .await
.change_context(EmailError::TokenGenerationFailure)?; .change_context(EmailError::TokenGenerationFailure)?;
@ -216,14 +223,19 @@ pub struct InviteUser {
pub user_name: domain::UserName, pub user_name: domain::UserName,
pub settings: std::sync::Arc<configs::settings::Settings>, pub settings: std::sync::Arc<configs::settings::Settings>,
pub subject: &'static str, pub subject: &'static str,
pub merchant_id: String,
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl EmailData for InviteUser { impl EmailData for InviteUser {
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> { async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) let token = EmailToken::new_token(
.await self.recipient_email.clone(),
.change_context(EmailError::TokenGenerationFailure)?; Some(self.merchant_id.clone()),
&self.settings,
)
.await
.change_context(EmailError::TokenGenerationFailure)?;
let invite_user_link = let invite_user_link =
get_link_with_token(&self.settings.email.base_url, token, "set_password"); get_link_with_token(&self.settings.email.base_url, token, "set_password");