mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 04:04:43 +08:00
feat(users): Add merchant_id in EmailToken and change user status in reset password (#3473)
This commit is contained in:
@ -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> {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user