use crate::base::entity::node::Node;
use chrono::Utc;
use serde::{Deserialize, Serialize};
use std::hash::{Hash, Hasher};
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Email {
value: String,
}
impl Email {
pub fn new(email: String) -> Result<Self, UserError> {
if Self::is_valid(&email) {
Ok(Self {
value: email.to_lowercase(),
})
} else {
Err(UserError::InvalidEmail(email))
}
}
fn is_valid(email: &str) -> bool {
use regex::Regex;
let email_regex = Regex::new(
r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
).unwrap();
email_regex.is_match(email) && email.len() <= 254
}
pub fn value(&self) -> &str {
&self.value
}
pub fn domain(&self) -> Option<&str> {
self.value.split('@').nth(1)
}
pub fn local_part(&self) -> Option<&str> {
self.value.split('@').next()
}
}
impl std::fmt::Display for Email {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum UserRole {
Admin,
#[default]
User,
}
impl UserRole {
pub fn as_str(&self) -> &'static str {
match self {
UserRole::Admin => "admin",
UserRole::User => "user",
}
}
pub fn from_str_lossy(value: &str) -> Self {
match value.trim().to_lowercase().as_str() {
"admin" => UserRole::Admin,
_ => UserRole::User,
}
}
}
impl std::str::FromStr for UserRole {
type Err = UserError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim().to_lowercase().as_str() {
"admin" => Ok(UserRole::Admin),
"user" => Ok(UserRole::User),
other => Err(UserError::InvalidRole(other.to_string())),
}
}
}
impl std::fmt::Display for UserRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UserData {
pub username: String,
pub email: Email,
pub password_hash: String,
pub is_active: bool,
pub is_verified: bool,
#[serde(default)]
pub role: UserRole,
pub profile: UserProfile,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UserProfile {
pub first_name: Option<String>,
pub last_name: Option<String>,
pub bio: Option<String>,
pub avatar_url: Option<String>,
pub timezone: Option<String>,
pub locale: Option<String>,
}
impl Default for UserProfile {
fn default() -> Self {
Self {
first_name: None,
last_name: None,
bio: None,
avatar_url: None,
timezone: Some("UTC".to_string()),
locale: Some("en-US".to_string()),
}
}
}
impl Hash for UserData {
fn hash<H: Hasher>(&self, state: &mut H) {
self.username.hash(state);
self.email.hash(state);
self.password_hash.hash(state);
self.is_active.hash(state);
self.is_verified.hash(state);
self.role.hash(state);
}
}
pub type User = Node<UserData>;
impl User {
pub fn new_user(
username: String,
email: Email,
password_hash: String,
profile: Option<UserProfile>,
) -> Self {
let user_data = UserData {
username,
email,
password_hash,
is_active: true,
is_verified: false,
role: UserRole::default(),
profile: profile.unwrap_or_default(),
};
let name = Some(format!("User: {}", user_data.username.clone()));
Node::new(user_data, name)
}
pub fn username(&self) -> &str {
&self.node.username
}
pub fn email(&self) -> &Email {
&self.node.email
}
pub fn password_hash(&self) -> &str {
&self.node.password_hash
}
pub fn is_active(&self) -> bool {
self.node.is_active
}
pub fn is_verified(&self) -> bool {
self.node.is_verified
}
pub fn profile(&self) -> &UserProfile {
&self.node.profile
}
pub fn role(&self) -> UserRole {
self.node.role
}
pub fn set_role(&mut self, role: UserRole) {
self.node.role = role;
self.modified = Utc::now();
}
pub fn update_username(&mut self, new_username: String) -> Result<(), UserError> {
if new_username.trim().is_empty() {
return Err(UserError::InvalidUsername(
"Username cannot be empty".to_string(),
));
}
if new_username.len() < 3 {
return Err(UserError::InvalidUsername(
"Username must be at least 3 characters".to_string(),
));
}
if new_username.len() > 50 {
return Err(UserError::InvalidUsername(
"Username cannot exceed 50 characters".to_string(),
));
}
self.node.username = new_username;
self.modified = Utc::now(); Ok(())
}
pub fn update_email(&mut self, new_email: Email) -> Result<(), UserError> {
self.node.email = new_email;
self.node.is_verified = false; self.modified = Utc::now(); Ok(())
}
pub fn update_password_hash(&mut self, new_password_hash: String) {
self.node.password_hash = new_password_hash;
self.modified = Utc::now(); }
pub fn activate(&mut self) {
self.node.is_active = true;
self.modified = Utc::now(); }
pub fn deactivate(&mut self) {
self.node.is_active = false;
self.modified = Utc::now(); }
pub fn verify(&mut self) {
self.node.is_verified = true;
self.modified = Utc::now(); }
pub fn update_profile(&mut self, profile: UserProfile) {
self.node.profile = profile;
self.modified = Utc::now(); }
}
#[derive(Debug, thiserror::Error)]
pub enum UserError {
#[error("Invalid email format: {0}")]
InvalidEmail(String),
#[error("Invalid username: {0}")]
InvalidUsername(String),
#[error("Invalid role: {0}")]
InvalidRole(String),
#[error("User not found with ID: {0}")]
UserNotFound(Uuid),
#[error("User not found with email: {0}")]
UserNotFoundByEmail(String),
#[error("Email already exists: {0}")]
EmailAlreadyExists(String),
#[error("Username already exists: {0}")]
UsernameAlreadyExists(String),
#[error("Invalid password: {0}")]
InvalidPassword(String),
#[error("Authentication failed")]
AuthenticationFailed,
#[error("User is not active")]
UserNotActive,
#[error("User is not verified")]
UserNotVerified,
#[error("Repository error: {0}")]
RepositoryError(String),
#[error("Hash error: {0}")]
HashError(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_email_validation() {
assert!(Email::new("test@example.com".to_string()).is_ok());
assert!(Email::new("user.name+tag@domain.co.uk".to_string()).is_ok());
assert!(Email::new("123@test.org".to_string()).is_ok());
assert!(Email::new("invalid-email".to_string()).is_err());
assert!(Email::new("@domain.com".to_string()).is_err());
assert!(Email::new("user@".to_string()).is_err());
assert!(Email::new("".to_string()).is_err());
}
#[test]
fn test_email_methods() {
let email = Email::new("Test.User@Example.COM".to_string()).unwrap();
assert_eq!(email.value(), "test.user@example.com");
assert_eq!(email.domain(), Some("example.com"));
assert_eq!(email.local_part(), Some("test.user"));
assert_eq!(email.to_string(), "test.user@example.com");
}
#[test]
fn test_user_creation() {
let email = Email::new("user@example.com".to_string()).unwrap();
let user = User::new_user(
"testuser".to_string(),
email.clone(),
"password_hash".to_string(),
None,
);
assert_eq!(user.username(), "testuser");
assert_eq!(user.email(), &email);
assert_eq!(user.password_hash(), "password_hash");
assert!(user.is_active());
assert!(!user.is_verified());
}
#[test]
fn test_user_updates() {
let email = Email::new("user@example.com".to_string()).unwrap();
let mut user = User::new_user(
"testuser".to_string(),
email,
"password_hash".to_string(),
None,
);
let initial_modified = user.modified;
std::thread::sleep(std::time::Duration::from_millis(1));
assert!(user.update_username("newusername".to_string()).is_ok());
assert_eq!(user.username(), "newusername");
assert!(user.modified > initial_modified);
assert!(user.update_username("a".to_string()).is_err());
assert!(user.update_username("".to_string()).is_err());
let new_email = Email::new("new@example.com".to_string()).unwrap();
let before_email_update = user.modified;
std::thread::sleep(std::time::Duration::from_millis(1));
assert!(user.update_email(new_email.clone()).is_ok());
assert_eq!(user.email(), &new_email);
assert!(!user.is_verified()); assert!(user.modified > before_email_update);
let before_deactivate = user.modified;
std::thread::sleep(std::time::Duration::from_millis(1));
user.deactivate();
assert!(!user.is_active());
assert!(user.modified > before_deactivate);
let before_activate = user.modified;
std::thread::sleep(std::time::Duration::from_millis(1));
user.activate();
assert!(user.is_active());
assert!(user.modified > before_activate);
let before_verify = user.modified;
std::thread::sleep(std::time::Duration::from_millis(1));
user.verify();
assert!(user.is_verified());
assert!(user.modified > before_verify);
}
#[test]
fn test_user_profile_update() {
let email = Email::new("user@example.com".to_string()).unwrap();
let mut user = User::new_user(
"testuser".to_string(),
email,
"password_hash".to_string(),
None,
);
let new_profile = UserProfile {
first_name: Some("John".to_string()),
last_name: Some("Doe".to_string()),
bio: Some("Software developer".to_string()),
avatar_url: Some("https://example.com/avatar.jpg".to_string()),
timezone: Some("America/New_York".to_string()),
locale: Some("en-US".to_string()),
};
let before_update = user.modified;
std::thread::sleep(std::time::Duration::from_millis(1));
user.update_profile(new_profile.clone());
assert_eq!(user.profile(), &new_profile);
assert!(user.modified > before_update);
}
#[test]
fn test_username_validation() {
let email = Email::new("user@example.com".to_string()).unwrap();
let mut user = User::new_user(
"testuser".to_string(),
email,
"password_hash".to_string(),
None,
);
assert!(user.update_username("validuser".to_string()).is_ok());
assert!(user.update_username("user_123".to_string()).is_ok());
assert!(user.update_username("test-user".to_string()).is_ok());
assert!(user.update_username("".to_string()).is_err());
assert!(user.update_username("ab".to_string()).is_err());
let long_username = "a".repeat(51);
assert!(user.update_username(long_username).is_err());
}
#[test]
fn test_user_versioning() {
let email = Email::new("user@example.com".to_string()).unwrap();
let user = User::new_user(
"testuser".to_string(),
email,
"password_hash".to_string(),
None,
);
assert!(user.is_versioning_enabled());
assert!(!user.uuid.is_nil());
assert_eq!(user.created, user.modified);
}
#[test]
fn test_user_serialization() {
let email = Email::new("user@example.com".to_string()).unwrap();
let user = User::new_user(
"testuser".to_string(),
email,
"password_hash".to_string(),
Some(UserProfile {
first_name: Some("Test".to_string()),
last_name: Some("User".to_string()),
bio: None,
avatar_url: None,
timezone: Some("UTC".to_string()),
locale: Some("en-US".to_string()),
}),
);
let serialized = serde_json::to_string(&user).unwrap();
assert!(!serialized.is_empty());
let deserialized: User = serde_json::from_str(&serialized).unwrap();
assert_eq!(user.uuid, deserialized.uuid);
assert_eq!(user.node.username, deserialized.node.username);
assert_eq!(user.node.email.value(), deserialized.node.email.value());
}
#[test]
fn test_user_role_string_round_trip() {
use std::str::FromStr;
assert_eq!(UserRole::Admin.as_str(), "admin");
assert_eq!(UserRole::User.as_str(), "user");
assert_eq!(UserRole::from_str("admin").unwrap(), UserRole::Admin);
assert_eq!(UserRole::from_str(" USER ").unwrap(), UserRole::User);
assert!(UserRole::from_str("superuser").is_err());
assert_eq!(UserRole::from_str_lossy("admin"), UserRole::Admin);
assert_eq!(UserRole::from_str_lossy("nonsense"), UserRole::User);
}
#[test]
fn test_user_role_default_and_accessors() {
let email = Email::new("user@example.com".to_string()).unwrap();
let mut user = User::new_user(
"testuser".to_string(),
email,
"password_hash".to_string(),
None,
);
assert_eq!(user.role(), UserRole::User);
let before = user.modified;
std::thread::sleep(std::time::Duration::from_millis(1));
user.set_role(UserRole::Admin);
assert_eq!(user.role(), UserRole::Admin);
assert!(user.modified > before);
}
}