From 90729bd401d59c5781f58f0db00e48405cf6ec33 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 12 Jul 2024 17:49:36 +0530 Subject: [PATCH 1/2] fix: set email_verified to false by default --- .../application/services/register_user/events.rs | 1 + .../application/services/register_user/service.rs | 1 + .../application/services/set_user_admin/command.rs | 2 ++ .../application/services/set_user_admin/service.rs | 1 + src/identity/domain/aggregate.rs | 10 ++++++++++ 5 files changed, 15 insertions(+) diff --git a/src/identity/application/services/register_user/events.rs b/src/identity/application/services/register_user/events.rs index 9bc3280..43fd097 100644 --- a/src/identity/application/services/register_user/events.rs +++ b/src/identity/application/services/register_user/events.rs @@ -15,4 +15,5 @@ pub struct UserRegisteredEvent { hashed_password: String, is_verified: bool, is_admin: bool, + email_verified: bool, } diff --git a/src/identity/application/services/register_user/service.rs b/src/identity/application/services/register_user/service.rs index a858e9e..593ed8b 100644 --- a/src/identity/application/services/register_user/service.rs +++ b/src/identity/application/services/register_user/service.rs @@ -71,6 +71,7 @@ impl RegisterUserUseCase for RegisterUserService { .email(cmd.email().into()) .hashed_password(cmd.hashed_password().into()) .is_verified(false) + .email_verified(false) .is_admin(false) // TODO: if UID == 0; set true .build() .unwrap()) diff --git a/src/identity/application/services/set_user_admin/command.rs b/src/identity/application/services/set_user_admin/command.rs index 5aaf715..174df06 100644 --- a/src/identity/application/services/set_user_admin/command.rs +++ b/src/identity/application/services/set_user_admin/command.rs @@ -39,6 +39,7 @@ mod tests { .email(username.into()) .hashed_password(username.into()) .is_verified(true) + .email_verified(false) .is_admin(true) .deleted(false) .build() @@ -54,6 +55,7 @@ mod tests { .hashed_password(username.into()) .is_verified(true) .is_admin(false) + .email_verified(false) .deleted(false) .build() .unwrap(), diff --git a/src/identity/application/services/set_user_admin/service.rs b/src/identity/application/services/set_user_admin/service.rs index fdee643..3417b12 100644 --- a/src/identity/application/services/set_user_admin/service.rs +++ b/src/identity/application/services/set_user_admin/service.rs @@ -33,6 +33,7 @@ mod tests { .email(username.into()) .hashed_password(username.into()) .is_verified(true) + .email_verified(false) .is_admin(true) .deleted(false) .build() diff --git a/src/identity/domain/aggregate.rs b/src/identity/domain/aggregate.rs index 39f6396..2cd3067 100644 --- a/src/identity/domain/aggregate.rs +++ b/src/identity/domain/aggregate.rs @@ -15,6 +15,7 @@ pub struct User { hashed_password: String, is_verified: bool, is_admin: bool, + email_verified: bool, deleted: bool, } @@ -26,6 +27,7 @@ impl Default for User { hashed_password: "".to_string(), is_verified: false, is_admin: false, + email_verified: false, deleted: false, } } @@ -51,6 +53,11 @@ impl User { self } + pub fn set_email_verified(&mut self, email_verified: bool) -> &mut Self { + self.email_verified = email_verified; + self + } + pub fn set_deleted(&mut self, deleted: bool) -> &mut Self { self.deleted = deleted; self @@ -75,5 +82,8 @@ mod tests { assert!(!u.deleted()); assert!(u.set_deleted(true).deleted()); + + assert!(!u.email_verified()); + assert!(u.set_email_verified(true).deleted()); } } -- 2.39.5 From 879dc952958eabb15d9bd413ef8637b1fdd871b3 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 12 Jul 2024 17:49:52 +0530 Subject: [PATCH 2/2] feat: service to verify email --- src/identity/application/aggregate.rs | 3 + .../services/verify_updated_email/command.rs | 58 +++++++ .../services/verify_updated_email/mod.rs | 18 +++ .../services/verify_updated_email/service.rs | 152 ++++++++++++++++++ 4 files changed, 231 insertions(+) create mode 100644 src/identity/application/services/verify_updated_email/command.rs create mode 100644 src/identity/application/services/verify_updated_email/mod.rs create mode 100644 src/identity/application/services/verify_updated_email/service.rs diff --git a/src/identity/application/aggregate.rs b/src/identity/application/aggregate.rs index 412ef14..f89f75c 100644 --- a/src/identity/application/aggregate.rs +++ b/src/identity/application/aggregate.rs @@ -77,6 +77,7 @@ impl Aggregate for User { .email(e.email().into()) .hashed_password(e.hashed_password().into()) .is_admin(e.is_admin().to_owned()) + .email_verified(e.email_verified().to_owned()) .is_verified(e.is_verified().to_owned()) .deleted(false) .build() @@ -89,9 +90,11 @@ impl Aggregate for User { UserEvent::PasswordUpdated(_) => (), UserEvent::EmailUpdated(e) => { self.set_email(e.new_email().into()); + self.set_email_verified(false); } UserEvent::UserVerified => { self.set_is_verified(true); + self.set_email_verified(true); } UserEvent::UserPromotedToAdmin(_) => { self.set_is_admin(true); diff --git a/src/identity/application/services/verify_updated_email/command.rs b/src/identity/application/services/verify_updated_email/command.rs new file mode 100644 index 0000000..cf7149f --- /dev/null +++ b/src/identity/application/services/verify_updated_email/command.rs @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use super::*; +use derive_getters::Getters; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] +pub struct VerifyUpdatedEmailCommand { + username: String, + email: String, +} + +impl VerifyUpdatedEmailCommand { + pub fn new( + username: String, + email: String, + config: &argon2_creds::Config, + ) -> IdentityCommandResult { + let username = config.username(&username)?; + config.email(&email)?; + + Ok(Self { username, email }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cmd() { + let config = argon2_creds::Config::default(); + VerifyUpdatedEmailCommand::new( + "realaravinth".into(), + "realaravinth@example.com".into(), + &config, + ) + .unwrap(); + + assert_eq!( + VerifyUpdatedEmailCommand::new("realaravinth".into(), "username".into(), &config,) + .err(), + Some(IdentityCommandError::BadEmail) + ); + + assert!(matches!( + VerifyUpdatedEmailCommand::new( + "username".into(), + "username@example.com".into(), + &config, + ) + .err(), + Some(IdentityCommandError::BadUsername(_)) + )); + } +} diff --git a/src/identity/application/services/verify_updated_email/mod.rs b/src/identity/application/services/verify_updated_email/mod.rs new file mode 100644 index 0000000..c07cf61 --- /dev/null +++ b/src/identity/application/services/verify_updated_email/mod.rs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +pub mod command; +pub mod service; + +use super::errors::*; + +#[async_trait::async_trait] +pub trait VerifyUpdatedEmailUseCase: Send + Sync { + async fn send_verification_email( + &self, + cmd: command::VerifyUpdatedEmailCommand, + ) -> IdentityResult<()>; +} + +pub type VerifyUpdatedEmailServiceObj = std::sync::Arc; diff --git a/src/identity/application/services/verify_updated_email/service.rs b/src/identity/application/services/verify_updated_email/service.rs new file mode 100644 index 0000000..2476cad --- /dev/null +++ b/src/identity/application/services/verify_updated_email/service.rs @@ -0,0 +1,152 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use derive_builder::Builder; + +use super::*; +use crate::identity::application::port::output::{ + db::{email_exists::*, get_verification_secret::*, username_exists::*}, + mailer::account_validation_link::*, +}; + +#[derive(Builder)] +pub struct VerifyUpdatedEmailService { + db_email_exists_adapter: EmailExistsOutDBPortObj, + db_username_exists_adapter: UsernameExistsOutDBPortObj, + db_get_verification_secret_adapter: GetVerificationSecretOutDBPortObj, + mailer_account_validation_link_adapter: AccountValidationLinkOutMailerPortObj, +} + +#[async_trait::async_trait] +impl VerifyUpdatedEmailUseCase for VerifyUpdatedEmailService { + async fn resend_verification_email( + &self, + cmd: command::VerifyUpdatedEmailCommand, + ) -> IdentityResult<()> { + if self + .db_username_exists_adapter + .username_exists(cmd.username()) + .await + .unwrap() + { + return Err(IdentityError::DuplicateUsername); + } + + if self + .db_email_exists_adapter + .email_exists(cmd.email()) + .await + .unwrap() + { + return Err(IdentityError::DuplicateEmail); + } + + let secret = self + .db_get_verification_secret_adapter + .get_verification_secret(cmd.username()) + .await + .unwrap(); + + self.mailer_account_validation_link_adapter + .account_validation_link(cmd.email(), cmd.username(), &secret) + .await + .unwrap(); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::tests::bdd::*; + use crate::utils::random_string::tests::*; + + #[actix_rt::test] + async fn test_service() { + let username = "realaravinth"; + let email = format!("{username}@example.com"); + let secret = "asdfasdf"; + let config = argon2_creds::Config::default(); + let cmd = + command::VerifyUpdatedEmailCommand::new(username.into(), email.clone(), &config) + .unwrap(); + + // happy case + { + let s = VerifyUpdatedEmailServiceBuilder::default() + .db_username_exists_adapter(mock_username_exists_db_port( + IS_CALLED_ONLY_ONCE, + RETURNS_FALSE, + )) + .db_get_verification_secret_adapter(mock_get_verification_secret_db_port( + IS_CALLED_ONLY_ONCE, + secret.into(), + )) + .db_email_exists_adapter(mock_email_exists_db_port( + IS_CALLED_ONLY_ONCE, + RETURNS_FALSE, + )) + .mailer_account_validation_link_adapter(mock_account_validation_link_db_port( + IS_CALLED_ONLY_ONCE, + )) + .build() + .unwrap(); + + s.resend_verification_email(cmd.clone()).await.unwrap(); + } + + // username exists + { + let s = VerifyUpdatedEmailServiceBuilder::default() + .db_username_exists_adapter(mock_username_exists_db_port( + IS_CALLED_ONLY_ONCE, + RETURNS_TRUE, + )) + .db_email_exists_adapter(mock_email_exists_db_port(IS_NEVER_CALLED, RETURNS_FALSE)) + .db_get_verification_secret_adapter(mock_get_verification_secret_db_port( + IS_NEVER_CALLED, + secret.into(), + )) + .mailer_account_validation_link_adapter(mock_account_validation_link_db_port( + IS_NEVER_CALLED, + )) + .build() + .unwrap(); + + assert_eq!( + s.resend_verification_email(cmd.clone()).await.err(), + Some(IdentityError::DuplicateUsername) + ); + } + + // email exists + { + let s = VerifyUpdatedEmailServiceBuilder::default() + .db_username_exists_adapter(mock_username_exists_db_port( + IS_CALLED_ONLY_ONCE, + RETURNS_FALSE, + )) + .db_get_verification_secret_adapter(mock_get_verification_secret_db_port( + IS_NEVER_CALLED, + secret.into(), + )) + .db_email_exists_adapter(mock_email_exists_db_port( + IS_CALLED_ONLY_ONCE, + RETURNS_TRUE, + )) + .mailer_account_validation_link_adapter(mock_account_validation_link_db_port( + IS_NEVER_CALLED, + )) + .build() + .unwrap(); + + assert_eq!( + s.resend_verification_email(cmd.clone()).await.err(), + Some(IdentityError::DuplicateEmail) + ); + } + } +} -- 2.39.5