Skip to content

Commit

Permalink
feat(users): Add merchant_id in EmailToken and change user status…
Browse files Browse the repository at this point in the history
… in reset password (#3473)
  • Loading branch information
ThisIsMani authored Jan 31, 2024
1 parent 7597f3b commit db3d53f
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 29 deletions.
38 changes: 28 additions & 10 deletions crates/diesel_models/src/query/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use diesel::{
associations::HasTable, debug_query, result::Error as DieselError, ExpressionMethods,
JoinOnDsl, QueryDsl,
};
use error_stack::{report, IntoReport};
use error_stack::IntoReport;
use router_env::{
logger,
tracing::{self, instrument},
Expand Down Expand Up @@ -49,19 +49,37 @@ impl User {
pub async fn update_by_user_id(
conn: &PgPooledConn,
user_id: &str,
user: UserUpdate,
user_update: UserUpdate,
) -> StorageResult<Self> {
generics::generic_update_with_results::<<Self as HasTable>::Table, _, _, _>(
generics::generic_update_with_unique_predicate_get_result::<
<Self as HasTable>::Table,
_,
_,
_,
>(
conn,
users_dsl::user_id.eq(user_id.to_owned()),
UserUpdateInternal::from(user),
UserUpdateInternal::from(user_update),
)
.await?
.first()
.cloned()
.ok_or_else(|| {
report!(errors::DatabaseError::NotFound).attach_printable("Error while updating user")
})
.await
}

pub async fn update_by_user_email(
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> {
Expand Down
35 changes: 22 additions & 13 deletions crates/router/src/core/user.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
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};
#[cfg(feature = "email")]
use error_stack::IntoReport;
Expand Down Expand Up @@ -362,18 +364,10 @@ pub async fn reset_password(

let hash_password = utils::user::password::generate_password_hash(password.get_secret())?;

//TODO: Create Update by email query
let user_id = state
.store
.find_user_by_email(token.get_email())
.await
.change_context(UserErrors::InternalServerError)?
.user_id;

state
let user = state
.store
.update_user_by_user_id(
user_id.as_str(),
.update_user_by_email(
token.get_email(),
storage_user::UserUpdate::AccountUpdate {
name: None,
password: Some(hash_password),
Expand All @@ -384,7 +378,20 @@ pub async fn reset_password(
.await
.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)
}
Expand Down Expand Up @@ -467,7 +474,7 @@ pub async fn invite_user(
.store
.insert_user_role(UserRoleNew {
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,
org_id: user_from_token.org_id,
status: invitation_status,
Expand All @@ -493,6 +500,7 @@ pub async fn invite_user(
user_name: domain::UserName::new(new_user.get_name())?,
settings: state.conf.clone(),
subject: "You have been invited to join Hyperswitch Community!",
merchant_id: user_from_token.merchant_id,
};
let send_email_result = state
.email_client
Expand Down Expand Up @@ -669,6 +677,7 @@ async fn handle_new_user_invitation(
user_name: domain::UserName::new(new_user.get_name())?,
settings: state.conf.clone(),
subject: "You have been invited to join Hyperswitch Community!",
merchant_id: user_from_token.merchant_id.clone(),
};
let send_email_result = state
.email_client
Expand Down
10 changes: 10 additions & 0 deletions crates/router/src/db/kafka_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1895,6 +1895,16 @@ impl UserInterface for KafkaStore {
.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(
&self,
user_id: &str,
Expand Down
62 changes: 62 additions & 0 deletions crates/router/src/db/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ pub trait UserInterface {
user: storage::UserUpdate,
) -> 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(
&self,
user_id: &str,
Expand Down Expand Up @@ -92,6 +98,18 @@ impl UserInterface for Store {
.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(
&self,
user_id: &str,
Expand Down Expand Up @@ -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(
&self,
user_id: &str,
Expand Down
24 changes: 18 additions & 6 deletions crates/router/src/services/email/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,18 +92,21 @@ Email : {user_email}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct EmailToken {
email: String,
merchant_id: Option<String>,
exp: u64,
}

impl EmailToken {
pub async fn new_token(
email: domain::UserEmail,
merchant_id: Option<String>,
settings: &configs::settings::Settings,
) -> CustomResult<String, UserErrors> {
let expiration_duration = std::time::Duration::from_secs(consts::EMAIL_TOKEN_TIME_IN_SECS);
let exp = jwt::generate_exp(expiration_duration)?.as_secs();
let token_payload = Self {
email: email.get_secret().expose(),
merchant_id,
exp,
};
jwt::generate_jwt(&token_payload, settings).await
Expand All @@ -112,6 +115,10 @@ impl EmailToken {
pub fn get_email(&self) -> &str {
self.email.as_str()
}

pub fn get_merchant_id(&self) -> Option<&str> {
self.merchant_id.as_deref()
}
}

pub fn get_link_with_token(
Expand All @@ -132,7 +139,7 @@ pub struct VerifyEmail {
#[async_trait::async_trait]
impl EmailData for VerifyEmail {
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
.change_context(EmailError::TokenGenerationFailure)?;

Expand Down Expand Up @@ -161,7 +168,7 @@ pub struct ResetPassword {
#[async_trait::async_trait]
impl EmailData for ResetPassword {
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
.change_context(EmailError::TokenGenerationFailure)?;

Expand Down Expand Up @@ -191,7 +198,7 @@ pub struct MagicLink {
#[async_trait::async_trait]
impl EmailData for MagicLink {
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
.change_context(EmailError::TokenGenerationFailure)?;

Expand All @@ -216,14 +223,19 @@ pub struct InviteUser {
pub user_name: domain::UserName,
pub settings: std::sync::Arc<configs::settings::Settings>,
pub subject: &'static str,
pub merchant_id: String,
}

#[async_trait::async_trait]
impl EmailData for InviteUser {
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings)
.await
.change_context(EmailError::TokenGenerationFailure)?;
let token = EmailToken::new_token(
self.recipient_email.clone(),
Some(self.merchant_id.clone()),
&self.settings,
)
.await
.change_context(EmailError::TokenGenerationFailure)?;

let invite_user_link =
get_link_with_token(&self.settings.email.base_url, token, "set_password");
Expand Down

0 comments on commit db3d53f

Please sign in to comment.