#[cfg(feature = "ssr")] use anyhow::anyhow; #[cfg(feature = "ssr")] use argon2::{ password_hash::{rand_core::OsRng, SaltString}, Argon2, PasswordHash, PasswordHasher, PasswordVerifier, }; use derive_more::{derive::Display, From}; #[cfg(feature = "ssr")] use rand::{distributions::Alphanumeric, Rng}; use serde::{ser::SerializeStruct, Deserialize, Serialize}; use thiserror::Error; /// A uniquely identifiable blog of blog blog. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)] pub struct User { id: uuid::Uuid, email: EmailAddress, #[cfg(feature = "ssr")] password: Password, verified: Verified, } impl User { pub fn new( id: uuid::Uuid, email: EmailAddress, #[cfg(feature = "ssr")] password: Password, verified: Verified, ) -> Self { Self { id, email, #[cfg(feature = "ssr")] password, verified, } } pub fn id(&self) -> &uuid::Uuid { &self.id } pub fn email(&self) -> &EmailAddress { &self.email } #[cfg(feature = "ssr")] pub fn password(&self) -> &Password { &self.password } pub fn verified(&self) -> &Verified { &self.verified } } impl Serialize for User { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { let mut state = serializer.serialize_struct("User", 3)?; state.serialize_field("id", &self.id())?; state.serialize_field("email", &self.email())?; state.serialize_field("verified", &self.verified())?; state.end() } } #[cfg(feature = "ssr")] #[derive(Clone, Display, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)] #[serde(from = "String")] pub struct ActivationToken(String); #[cfg(feature = "ssr")] impl From for ActivationToken { fn from(value: String) -> Self { Self(value) } } #[cfg(feature = "ssr")] impl From<&str> for ActivationToken { fn from(value: &str) -> Self { Self(value.to_string()) } } #[cfg(feature = "ssr")] impl ActivationToken { pub fn new() -> Self { let token: String = rand::thread_rng() .sample_iter(&Alphanumeric) .take(11) .map(char::from) .collect(); Self(token) } } #[cfg(feature = "ssr")] #[derive(Clone, Display, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)] #[serde(from = "String")] pub struct PasswordResetToken(String); #[cfg(feature = "ssr")] impl From for PasswordResetToken { fn from(value: String) -> Self { Self(value) } } #[cfg(feature = "ssr")] impl From<&str> for PasswordResetToken { fn from(value: &str) -> Self { Self(value.to_string()) } } #[cfg(feature = "ssr")] impl PasswordResetToken { pub fn new() -> Self { let token: String = rand::thread_rng() .sample_iter(&Alphanumeric) .take(24) .map(char::from) .collect(); Self(token) } } #[cfg(feature = "ssr")] #[derive(Debug, Error)] pub enum ResetPasswordError { #[error("The reset token is no longer valid.")] NotFound, #[error(transparent)] Unknown(#[from] anyhow::Error), // to be extended as new error scenarios are introduced } #[derive( Clone, Copy, Display, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, )] #[serde(from = "bool")] pub struct Verified(bool); impl From for Verified { fn from(value: bool) -> Self { Self::new(value) } } impl Verified { pub fn new(value: bool) -> Self { Self(value) } pub fn is_verified(&self) -> bool { self.0 } } impl From for bool { fn from(value: Verified) -> Self { value.0 } } #[derive(Clone, Display, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(try_from = "String")] /// A valid email address. pub struct EmailAddress(String); impl TryFrom for EmailAddress { type Error = EmailAddressError; fn try_from(value: String) -> Result { Self::new(&value) } } impl TryFrom<&str> for EmailAddress { type Error = EmailAddressError; fn try_from(value: &str) -> Result { Self::new(value) } } #[derive(Clone, Debug, Error)] #[error("{invalid_email} is not a valid email address")] pub struct EmailAddressError { pub invalid_email: String, } impl From for String { fn from(value: EmailAddressError) -> Self { format!("{}", value) } } impl EmailAddress { pub fn new(raw: &str) -> Result { let trimmed = raw.trim(); Self::validate_email_address(trimmed)?; Ok(Self(trimmed.to_string())) } fn validate_email_address(email: &str) -> Result<(), EmailAddressError> { if !email.contains('@') { return Err(EmailAddressError { invalid_email: email.to_string(), }); } if email.contains(' ') { return Err(EmailAddressError { invalid_email: email.to_string(), }); } if email.len() < 3 { // a@b is technically valid, a@ or @b is not return Err(EmailAddressError { invalid_email: email.to_string(), }); } use validator::ValidateEmail; if !email.validate_email() { return Err(EmailAddressError { invalid_email: email.to_string(), }); } Ok(()) } } #[cfg(feature = "ssr")] #[derive(Clone, Display, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)] #[serde(try_from = "String")] /// A valid password. pub struct Password { hashed_password: String, } #[cfg(feature = "ssr")] impl TryFrom for Password { type Error = PasswordError; fn try_from(value: String) -> Result { Self::new(&value) } } #[cfg(feature = "ssr")] impl TryFrom<&str> for Password { type Error = PasswordError; fn try_from(value: &str) -> Result { Self::new(value) } } #[cfg(feature = "ssr")] impl Password { pub fn new(password: &str) -> Result { // password restrictions if password.len() < 6 { return Err(PasswordError::TooShort); } // Do we need to protect people from being stupid? let salt = SaltString::generate(&mut OsRng); let hashed_password = Argon2::default() .hash_password(password.as_bytes(), &salt) .map_err(|e| PasswordError::Unknown(anyhow!(e)))? .to_string(); Ok(Self { hashed_password }) } pub fn new_hashed(hashed_password: &str) -> Self { Self { hashed_password: hashed_password.to_string(), } } pub fn verify(&self, password: &str) -> Result<(), anyhow::Error> { let hash = PasswordHash::new(&self.hashed_password).map_err(|e| anyhow!(e))?; Argon2::default() .verify_password(password.as_bytes(), &hash) .map_err(|e| anyhow!(e)) } } #[cfg(feature = "ssr")] #[derive(Debug, Error)] pub enum PasswordError { #[error("Password must be at least 6 characters long.")] TooShort, #[error(transparent)] Unknown(#[from] anyhow::Error), // to be extended as new error scenarios are introduced } #[cfg(feature = "ssr")] /// The fields required by the domain to create a [User]. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, From)] pub struct CreateUserRequest { email: EmailAddress, password: Password, } #[cfg(feature = "ssr")] impl CreateUserRequest { pub fn new(email: EmailAddress, password: Password) -> Self { Self { email, password } } pub fn email(&self) -> &EmailAddress { &self.email } pub fn password(&self) -> &Password { &self.password } } #[cfg(feature = "ssr")] #[derive(Debug, Error)] pub enum CreateUserError { #[error("User with email {email} already exists")] Duplicate { email: EmailAddress }, #[error(transparent)] Unknown(#[from] anyhow::Error), // to be extended as new error scenarios are introduced } #[cfg(feature = "ssr")] /// The fields required by the domain to create a [User]. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, From)] pub struct UserLoginRequest { email: EmailAddress, password: String, } #[cfg(feature = "ssr")] impl UserLoginRequest { pub fn new(email: EmailAddress, password: String) -> Self { Self { email, password } } pub fn email(&self) -> &EmailAddress { &self.email } pub fn password(&self) -> &str { &self.password } } #[cfg(feature = "ssr")] pub struct UpdateUserRequest { email: Option, password: Option, verified: Option, } #[cfg(feature = "ssr")] impl UpdateUserRequest { pub fn new() -> Self { Self { email: None, password: None, verified: None, } } pub fn email(&self) -> Option<&EmailAddress> { self.email.as_ref() } pub fn set_email(mut self, email: EmailAddress) -> Self { self.email = Some(email); self } pub fn set_password(mut self, password: Password) -> Self { self.password = Some(password); self } pub fn password(&self) -> Option<&Password> { self.password.as_ref() } pub fn set_verified(mut self, verified: Verified) -> Self { self.verified = Some(verified); self } pub fn verified(&self) -> Option<&Verified> { self.verified.as_ref() } } #[cfg(feature = "ssr")] #[derive(Debug, Error)] pub enum UpdateUserError { #[error("User with email {email} already exists")] Duplicate { email: EmailAddress }, #[error(transparent)] Unknown(#[from] anyhow::Error), // to be extended as new error scenarios are introduced } #[cfg(feature = "ssr")] #[derive(Debug, Error)] pub enum DeleteUserError { #[error("User with id {id} not found")] NotFound { id: uuid::Uuid }, #[error(transparent)] Unknown(#[from] anyhow::Error), // to be extended as new error scenarios are introduced } #[cfg(feature = "ssr")] #[derive(Debug, Error)] pub enum ActivateUserError { #[error("Token {token} is invalid")] NotFound { token: ActivationToken }, #[error(transparent)] Unknown(#[from] anyhow::Error), // to be extended as new error scenarios are introduced } #[cfg(feature = "ssr")] #[derive(Debug, Error)] pub enum UserLoginError { #[error("User with email {email} not found")] NotFound { email: EmailAddress }, #[error("User with email {email} not verified")] NotActive { email: EmailAddress }, #[error(transparent)] Unknown(#[from] anyhow::Error), // to be extended as new error scenarios are introduced }