Merge pull request 'feat: verify updated email address' (#23) from verify-updated-email into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #23
This commit is contained in:
commit
6609e52632
9 changed files with 246 additions and 0 deletions
|
@ -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);
|
||||
|
|
|
@ -15,4 +15,5 @@ pub struct UserRegisteredEvent {
|
|||
hashed_password: String,
|
||||
is_verified: bool,
|
||||
is_admin: bool,
|
||||
email_verified: bool,
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<Self> {
|
||||
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(_))
|
||||
));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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<dyn VerifyUpdatedEmailUseCase>;
|
|
@ -0,0 +1,152 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue