use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum Role { Admin, User, } impl Role { pub fn as_str(self) -> &'static str { match self { Role::Admin => "admin", Role::User => "user", } } pub fn parse(s: &str) -> Option { match s { "admin" => Some(Role::Admin), "user" => Some(Role::User), _ => None, } } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct User { pub id: Uuid, pub email: String, pub display_name: String, pub password_hash: String, pub role: Role, pub is_active: bool, pub created_at: DateTime, } impl User { pub fn is_admin(&self) -> bool { self.role == Role::Admin } } /// Input for creating a user; the password is still in plain text here. #[derive(Clone, Debug)] pub struct NewUser { pub email: String, pub display_name: String, pub password: String, pub role: Role, } /// Partial update; `None` keeps the current value. #[derive(Clone, Debug, Default)] pub struct UserUpdate { pub display_name: Option, pub role: Option, pub is_active: Option, } pub const MIN_PASSWORD_LEN: usize = 12; pub fn validate_email(email: &str) -> Result<(), crate::DomainError> { let ok = email.contains('@') && !email.starts_with('@') && !email.ends_with('@') && !email.contains(' '); ok.then_some(()) .ok_or_else(|| crate::DomainError::Validation("invalid email".into())) } pub fn validate_password(password: &str) -> Result<(), crate::DomainError> { (password.chars().count() >= MIN_PASSWORD_LEN) .then_some(()) .ok_or_else(|| { crate::DomainError::Validation(format!( "password must have at least {MIN_PASSWORD_LEN} characters" )) }) }