use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use crate::value_objects::{Email, UserId}; /// A user in the system. /// /// Designed to be OIDC-ready: the `subject` field stores the OIDC subject claim. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct User { id: UserId, subject: String, email: Email, password_hash: Option, is_admin: bool, created_at: DateTime, } impl User { /// Create a new OIDC user (no local password). pub fn new(subject: impl Into, email: Email) -> Self { Self { id: UserId::generate(), subject: subject.into(), email, password_hash: None, is_admin: false, created_at: Utc::now(), } } /// Create a new local user with a password hash. pub fn new_local(email: Email, password_hash: impl Into) -> Self { Self { id: UserId::generate(), subject: format!("local|{}", uuid::Uuid::new_v4()), email, password_hash: Some(password_hash.into()), is_admin: false, created_at: Utc::now(), } } /// Hydrate from persistence — no validation, accepts all fields. pub fn from_persistence( id: UserId, subject: String, email: Email, password_hash: Option, is_admin: bool, created_at: DateTime, ) -> Self { Self { id, subject, email, password_hash, is_admin, created_at, } } // -- Getters -- pub fn id(&self) -> UserId { self.id } pub fn subject(&self) -> &str { &self.subject } pub fn email(&self) -> &Email { &self.email } pub fn password_hash(&self) -> Option<&str> { self.password_hash.as_deref() } pub fn is_admin(&self) -> bool { self.is_admin } pub fn created_at(&self) -> DateTime { self.created_at } // -- Mutations -- /// Promote this user to admin. pub fn promote_to_admin(&mut self) { self.is_admin = true; } } #[cfg(test)] mod tests { use super::*; #[test] fn new_generates_id_and_timestamp() { let email = Email::new("test@example.com").unwrap(); let user = User::new("oidc|123", email); assert!(!user.is_admin()); assert!(user.password_hash().is_none()); assert_eq!(user.subject(), "oidc|123"); } #[test] fn new_local_sets_password_and_subject() { let email = Email::new("local@example.com").unwrap(); let user = User::new_local(email, "hashed_pw"); assert!(user.password_hash().is_some()); assert!(user.subject().starts_with("local|")); } #[test] fn from_persistence_round_trip() { let email = Email::new("stored@example.com").unwrap(); let id = UserId::generate(); let now = Utc::now(); let user = User::from_persistence( id, "sub".into(), email.clone(), Some("hash".into()), true, now, ); assert_eq!(user.id(), id); assert_eq!(user.subject(), "sub"); assert!(user.is_admin()); assert_eq!(user.password_hash(), Some("hash")); assert_eq!(user.created_at(), now); } }