fix: check for duplicate email and send confirmation link while updating email
This commit is contained in:
parent
956b94f97b
commit
434436b81f
4 changed files with 130 additions and 19 deletions
|
@ -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)
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue