From 2170f03cf23de756ac87e715a3cd4c91efed0c3c Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Sat, 18 May 2024 19:10:47 +0530 Subject: [PATCH] feat: identity: resend verification email service --- src/identity/application/services/events.rs | 2 + src/identity/application/services/mod.rs | 12 +- .../resend_verification_email/command.rs | 58 +++++++ .../services/resend_verification_email/mod.rs | 12 ++ .../resend_verification_email/service.rs | 152 ++++++++++++++++++ 5 files changed, 231 insertions(+), 5 deletions(-) create mode 100644 src/identity/application/services/resend_verification_email/command.rs create mode 100644 src/identity/application/services/resend_verification_email/mod.rs create mode 100644 src/identity/application/services/resend_verification_email/service.rs diff --git a/src/identity/application/services/events.rs b/src/identity/application/services/events.rs index 163c88e..c3fc5cd 100644 --- a/src/identity/application/services/events.rs +++ b/src/identity/application/services/events.rs @@ -19,6 +19,7 @@ pub enum UserEvent { PasswordUpdated(PasswordUpdatedEvent), EmailUpdated(EmailUpdatedEvent), UserVerified, + VerificationEmailResent, UserPromotedToAdmin(UserPromotedToAdminEvent), } @@ -38,6 +39,7 @@ impl DomainEvent for UserEvent { UserEvent::EmailUpdated { .. } => "UserUpdatedAccountEmail", UserEvent::UserVerified => "UserIsVerified", UserEvent::UserPromotedToAdmin { .. } => "UserPromotedToAdmin", + UserEvent::VerificationEmailResent => "VerficationEmailResent", }; e.to_string() diff --git a/src/identity/application/services/mod.rs b/src/identity/application/services/mod.rs index 9159e3b..299104e 100644 --- a/src/identity/application/services/mod.rs +++ b/src/identity/application/services/mod.rs @@ -5,20 +5,21 @@ use serde::{Deserialize, Serialize}; mod delete_user; -mod login; -mod register_user; -mod update_email; -mod update_password; -//mod resend_verification_email pub mod errors; pub mod events; +mod login; mod mark_user_verified; +mod register_user; +mod resend_verification_email; mod set_user_admin; +mod update_email; +mod update_password; use delete_user::command::*; use login::command::*; use mark_user_verified::command::*; use register_user::command::*; +use resend_verification_email::command::*; use set_user_admin::command::*; use update_email::command::*; use update_password::command::*; @@ -32,4 +33,5 @@ pub enum UserCommand { UpdateEmail(UpdateEmailCommand), MarkUserVerified(MarkUserVerifiedCommand), SetAdmin(SetAdminCommand), + ResendVerificationEmail(ResendVerificationEmailCommand), } diff --git a/src/identity/application/services/resend_verification_email/command.rs b/src/identity/application/services/resend_verification_email/command.rs new file mode 100644 index 0000000..82d91a1 --- /dev/null +++ b/src/identity/application/services/resend_verification_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 ResendVerificationEmailCommand { + username: String, + email: String, +} + +impl ResendVerificationEmailCommand { + 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(); + ResendVerificationEmailCommand::new( + "realaravinth".into(), + "realaravinth@example.com".into(), + &config, + ) + .unwrap(); + + assert_eq!( + ResendVerificationEmailCommand::new("realaravinth".into(), "username".into(), &config,) + .err(), + Some(IdentityCommandError::BadEmail) + ); + + assert!(matches!( + ResendVerificationEmailCommand::new( + "username".into(), + "username@example.com".into(), + &config, + ) + .err(), + Some(IdentityCommandError::BadUsername(_)) + )); + } +} diff --git a/src/identity/application/services/resend_verification_email/mod.rs b/src/identity/application/services/resend_verification_email/mod.rs new file mode 100644 index 0000000..c70ff74 --- /dev/null +++ b/src/identity/application/services/resend_verification_email/mod.rs @@ -0,0 +1,12 @@ +pub mod command; +pub mod service; + +use super::errors::*; + +#[async_trait::async_trait] +pub trait ResendVerificationEmailUseCase: Send + Sync { + async fn resend_verification_email( + &self, + cmd: command::ResendVerificationEmailCommand, + ) -> IdentityResult<()>; +} diff --git a/src/identity/application/services/resend_verification_email/service.rs b/src/identity/application/services/resend_verification_email/service.rs new file mode 100644 index 0000000..57541d9 --- /dev/null +++ b/src/identity/application/services/resend_verification_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 ResendVerificationEmailService { + 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 ResendVerificationEmailUseCase for ResendVerificationEmailService { + async fn resend_verification_email( + &self, + cmd: command::ResendVerificationEmailCommand, + ) -> 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::ResendVerificationEmailCommand::new(username.into(), email.clone(), &config) + .unwrap(); + + // happy case + { + let s = ResendVerificationEmailServiceBuilder::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 = ResendVerificationEmailServiceBuilder::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 = ResendVerificationEmailServiceBuilder::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) + ); + } + } +}