fix: check for duplicate email and send confirmation link while updating email

This commit is contained in:
Aravinth Manivannan 2024-05-19 00:32:03 +05:30
parent 956b94f97b
commit 434436b81f
Signed by: realaravinth
GPG key ID: F8F50389936984FF
4 changed files with 130 additions and 19 deletions

View file

@ -10,10 +10,12 @@ use super::*;
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)]
pub struct UpdateEmailCommand { pub struct UpdateEmailCommand {
new_email: String, new_email: String,
username: String,
} }
impl UpdateEmailCommand { impl UpdateEmailCommand {
pub fn new( pub fn new(
username: String,
new_email: String, new_email: String,
supplied_password: String, supplied_password: String,
actual_password_hash: &str, actual_password_hash: &str,
@ -24,7 +26,10 @@ impl UpdateEmailCommand {
} }
config.email(&new_email)?; config.email(&new_email)?;
Ok(Self { new_email }) Ok(Self {
username,
new_email,
})
} }
} }
@ -41,6 +46,7 @@ mod tests {
let hashed_password = config.password(password).unwrap(); let hashed_password = config.password(password).unwrap();
assert_eq!( assert_eq!(
UpdateEmailCommand::new( UpdateEmailCommand::new(
username.into(),
new_email.clone(), new_email.clone(),
password.into(), password.into(),
&hashed_password, &hashed_password,
@ -53,14 +59,26 @@ mod tests {
// email is not valid email // email is not valid email
assert_eq!( assert_eq!(
UpdateEmailCommand::new(username.into(), password.into(), &hashed_password, &config) UpdateEmailCommand::new(
username.into(),
username.into(),
password.into(),
&hashed_password,
&config
)
.err(), .err(),
Some(IdentityCommandError::BadEmail) Some(IdentityCommandError::BadEmail)
); );
// wrong password // wrong password
assert_eq!( assert_eq!(
UpdateEmailCommand::new(username.into(), username.into(), &hashed_password, &config) UpdateEmailCommand::new(
username.into(),
username.into(),
username.into(),
&hashed_password,
&config
)
.err(), .err(),
Some(IdentityCommandError::WrongPassword) Some(IdentityCommandError::WrongPassword)
); );

View file

@ -7,11 +7,11 @@ use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd)] #[derive(Clone, Debug, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd)]
pub struct EmailUpdatedEvent { pub struct EmailUpdatedEvent {
email: String, new_email: String,
} }
impl EmailUpdatedEvent { impl EmailUpdatedEvent {
pub fn new(email: String) -> Self { pub fn new(new_email: String) -> Self {
Self { email } Self { new_email }
} }
} }

View file

@ -9,10 +9,11 @@ pub mod service;
use super::errors::*; use super::errors::*;
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait UpdateEmailUseCase { pub trait UpdateEmailUseCase: Send + Sync {
async fn update_email( async fn update_email(
&self, &self,
cmd: command::UpdateEmailCommand, cmd: command::UpdateEmailCommand,
//) -> errors::ProcessAuthorizationServiceResult<String>; //) -> errors::ProcessAuthorizationServiceResult<String>;
) -> events::EmailUpdatedEvent; ) -> IdentityResult<events::EmailUpdatedEvent>;
} }
pub type UpdateEmailServiceObj = std::sync::Arc<dyn UpdateEmailUseCase>;

View file

@ -2,25 +2,70 @@
// //
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
use super::*; use derive_builder::Builder;
struct UpdateEmailService; use super::*;
use crate::identity::application::port::output::{
db::{create_verification_secret::*, email_exists::*},
mailer::account_validation_link::*,
};
use crate::utils::random_string::*;
use crate::identity::application::services::register_user::service::SECRET_LEN;
#[derive(Builder)]
pub struct UpdateEmailService {
db_email_exists_adapter: EmailExistsOutDBPortObj,
// TODO: update email must use special PURPOSE
// TODO: User must have email_verified field
db_create_verification_secret_adapter: CreateVerificationSecretOutDBPortObj,
mailer_account_validation_link_adapter: AccountValidationLinkOutMailerPortObj,
random_string_adapter: GenerateRandomStringInterfaceObj,
}
#[async_trait::async_trait] #[async_trait::async_trait]
impl UpdateEmailUseCase for UpdateEmailService { impl UpdateEmailUseCase for UpdateEmailService {
async fn update_email( async fn update_email(
&self, &self,
cmd: command::UpdateEmailCommand, cmd: command::UpdateEmailCommand,
//) -> errors::ProcessAuthorizationServiceResult<String>; ) -> IdentityResult<events::EmailUpdatedEvent> {
) -> events::EmailUpdatedEvent { if self
// TODO: check if email exists in DB .db_email_exists_adapter
events::EmailUpdatedEvent::new(cmd.new_email().into()) .email_exists(cmd.new_email())
.await
.unwrap()
{
return Err(IdentityError::DuplicateEmail);
}
let secret = self.random_string_adapter.get_random(SECRET_LEN);
self.db_create_verification_secret_adapter
.create_verification_secret(
CreateSecretMsgBuilder::default()
.secret(secret.clone())
.username(cmd.username().into())
.build()
.unwrap(),
)
.await
.unwrap();
self.mailer_account_validation_link_adapter
.account_validation_link(cmd.new_email(), cmd.username(), &secret)
.await
.unwrap();
Ok(events::EmailUpdatedEvent::new(cmd.new_email().into()))
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::utils::random_string::tests::*;
use crate::tests::bdd::*;
#[actix_rt::test] #[actix_rt::test]
async fn test_service() { async fn test_service() {
@ -31,6 +76,7 @@ mod tests {
let hashed_password = config.password(password).unwrap(); let hashed_password = config.password(password).unwrap();
let cmd = command::UpdateEmailCommand::new( let cmd = command::UpdateEmailCommand::new(
username.into(),
new_email.clone(), new_email.clone(),
password.into(), password.into(),
&hashed_password, &hashed_password,
@ -38,8 +84,54 @@ mod tests {
) )
.unwrap(); .unwrap();
let s = UpdateEmailService; // happy case
let res = s.update_email(cmd.clone()).await; {
assert_eq!(res.email(), cmd.new_email()); let s = UpdateEmailServiceBuilder::default()
.db_create_verification_secret_adapter(mock_create_verification_secret_db_port(
IS_CALLED_ONLY_ONCE,
))
.db_email_exists_adapter(mock_email_exists_db_port(
IS_CALLED_ONLY_ONCE,
RETURNS_FALSE,
))
.random_string_adapter(mock_generate_random_string(
IS_CALLED_ONLY_ONCE,
RETURNS_RANDOM_STRING.into(),
))
.mailer_account_validation_link_adapter(mock_account_validation_link_db_port(
IS_CALLED_ONLY_ONCE,
))
.build()
.unwrap();
let res = s.update_email(cmd.clone()).await.unwrap();
assert_eq!(res.new_email(), cmd.new_email());
}
// email exists
{
let s = UpdateEmailServiceBuilder::default()
.db_create_verification_secret_adapter(mock_create_verification_secret_db_port(
IS_NEVER_CALLED,
))
.db_email_exists_adapter(mock_email_exists_db_port(
IS_CALLED_ONLY_ONCE,
RETURNS_TRUE,
))
.random_string_adapter(mock_generate_random_string(
IS_NEVER_CALLED,
RETURNS_RANDOM_STRING.into(),
))
.mailer_account_validation_link_adapter(mock_account_validation_link_db_port(
IS_NEVER_CALLED,
))
.build()
.unwrap();
assert_eq!(
s.update_email(cmd.clone()).await.err(),
Some(IdentityError::DuplicateEmail)
);
}
} }
} }