Skip to main content

paladin_core/platform/container/
user.rs

1// src/core/platform/container/user.rs
2/*
3User Container
4
5User type built on the core base entity Node type to utilize the versioning system
6through composition. It represents a user entity in the system with all needed values
7including username, email, and password hash.
8
9This follows Domain-Driven Design principles and leverages the existing Node infrastructure.
10*/
11
12use crate::base::entity::node::Node;
13use chrono::Utc;
14use serde::{Deserialize, Serialize};
15use std::hash::{Hash, Hasher};
16use uuid::Uuid;
17
18/// Email value object that encapsulates email validation logic
19#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub struct Email {
21    value: String,
22}
23
24impl Email {
25    /// Creates a new Email value object with validation
26    pub fn new(email: String) -> Result<Self, UserError> {
27        if Self::is_valid(&email) {
28            Ok(Self {
29                value: email.to_lowercase(),
30            })
31        } else {
32            Err(UserError::InvalidEmail(email))
33        }
34    }
35
36    /// Validates email format using a comprehensive regex
37    fn is_valid(email: &str) -> bool {
38        use regex::Regex;
39
40        let email_regex = Regex::new(
41            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])?)*$"
42        ).unwrap();
43
44        email_regex.is_match(email) && email.len() <= 254
45    }
46
47    /// Returns the email value as a string
48    pub fn value(&self) -> &str {
49        &self.value
50    }
51
52    /// Returns the domain part of the email
53    pub fn domain(&self) -> Option<&str> {
54        self.value.split('@').nth(1)
55    }
56
57    /// Returns the local part of the email
58    pub fn local_part(&self) -> Option<&str> {
59        self.value.split('@').next()
60    }
61}
62
63impl std::fmt::Display for Email {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        write!(f, "{}", self.value)
66    }
67}
68
69/// Role assigned to a user, governing access to privileged operations.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
71#[serde(rename_all = "lowercase")]
72pub enum UserRole {
73    /// Administrative user with full access to user-management operations.
74    Admin,
75    /// Standard user with access limited to their own resources.
76    #[default]
77    User,
78}
79
80impl UserRole {
81    /// Returns the canonical lowercase string representation of the role.
82    pub fn as_str(&self) -> &'static str {
83        match self {
84            UserRole::Admin => "admin",
85            UserRole::User => "user",
86        }
87    }
88
89    /// Parses a role from its string representation, defaulting to [`UserRole::User`]
90    /// for unrecognized values to fail safe toward least privilege.
91    pub fn from_str_lossy(value: &str) -> Self {
92        match value.trim().to_lowercase().as_str() {
93            "admin" => UserRole::Admin,
94            _ => UserRole::User,
95        }
96    }
97}
98
99impl std::str::FromStr for UserRole {
100    type Err = UserError;
101
102    fn from_str(value: &str) -> Result<Self, Self::Err> {
103        match value.trim().to_lowercase().as_str() {
104            "admin" => Ok(UserRole::Admin),
105            "user" => Ok(UserRole::User),
106            other => Err(UserError::InvalidRole(other.to_string())),
107        }
108    }
109}
110
111impl std::fmt::Display for UserRole {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        write!(f, "{}", self.as_str())
114    }
115}
116
117/// User data structure that will be wrapped by Node
118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119pub struct UserData {
120    pub username: String,
121    pub email: Email,
122    pub password_hash: String,
123    pub is_active: bool,
124    pub is_verified: bool,
125    #[serde(default)]
126    pub role: UserRole,
127    pub profile: UserProfile,
128}
129
130#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131pub struct UserProfile {
132    pub first_name: Option<String>,
133    pub last_name: Option<String>,
134    pub bio: Option<String>,
135    pub avatar_url: Option<String>,
136    pub timezone: Option<String>,
137    pub locale: Option<String>,
138}
139
140impl Default for UserProfile {
141    fn default() -> Self {
142        Self {
143            first_name: None,
144            last_name: None,
145            bio: None,
146            avatar_url: None,
147            timezone: Some("UTC".to_string()),
148            locale: Some("en-US".to_string()),
149        }
150    }
151}
152
153impl Hash for UserData {
154    fn hash<H: Hasher>(&self, state: &mut H) {
155        self.username.hash(state);
156        self.email.hash(state);
157        self.password_hash.hash(state);
158        self.is_active.hash(state);
159        self.is_verified.hash(state);
160        self.role.hash(state);
161    }
162}
163
164/// User type built on Node for versioning and consistency
165pub type User = Node<UserData>;
166
167impl User {
168    /// Creates a new User with UserData
169    pub fn new_user(
170        username: String,
171        email: Email,
172        password_hash: String,
173        profile: Option<UserProfile>,
174    ) -> Self {
175        let user_data = UserData {
176            username,
177            email,
178            password_hash,
179            is_active: true,
180            is_verified: false,
181            role: UserRole::default(),
182            profile: profile.unwrap_or_default(),
183        };
184
185        let name = Some(format!("User: {}", user_data.username.clone()));
186        Node::new(user_data, name)
187    }
188
189    /// Gets the username
190    pub fn username(&self) -> &str {
191        &self.node.username
192    }
193
194    /// Gets the email
195    pub fn email(&self) -> &Email {
196        &self.node.email
197    }
198
199    /// Gets the password hash
200    pub fn password_hash(&self) -> &str {
201        &self.node.password_hash
202    }
203
204    /// Checks if user is active
205    pub fn is_active(&self) -> bool {
206        self.node.is_active
207    }
208
209    /// Checks if user is verified
210    pub fn is_verified(&self) -> bool {
211        self.node.is_verified
212    }
213
214    /// Gets the user profile
215    pub fn profile(&self) -> &UserProfile {
216        &self.node.profile
217    }
218
219    /// Gets the user's role.
220    pub fn role(&self) -> UserRole {
221        self.node.role
222    }
223
224    /// Sets the user's role and updates the modified timestamp.
225    pub fn set_role(&mut self, role: UserRole) {
226        self.node.role = role;
227        self.modified = Utc::now();
228    }
229
230    /// Updates username
231    pub fn update_username(&mut self, new_username: String) -> Result<(), UserError> {
232        if new_username.trim().is_empty() {
233            return Err(UserError::InvalidUsername(
234                "Username cannot be empty".to_string(),
235            ));
236        }
237        if new_username.len() < 3 {
238            return Err(UserError::InvalidUsername(
239                "Username must be at least 3 characters".to_string(),
240            ));
241        }
242        if new_username.len() > 50 {
243            return Err(UserError::InvalidUsername(
244                "Username cannot exceed 50 characters".to_string(),
245            ));
246        }
247
248        self.node.username = new_username;
249        self.modified = Utc::now(); // Update modified timestamp directly
250        Ok(())
251    }
252
253    /// Updates email
254    pub fn update_email(&mut self, new_email: Email) -> Result<(), UserError> {
255        self.node.email = new_email;
256        self.node.is_verified = false; // Reset verification on email change
257        self.modified = Utc::now(); // Update modified timestamp directly
258        Ok(())
259    }
260
261    /// Updates password hash
262    pub fn update_password_hash(&mut self, new_password_hash: String) {
263        self.node.password_hash = new_password_hash;
264        self.modified = Utc::now(); // Update modified timestamp directly
265    }
266
267    /// Activates the user
268    pub fn activate(&mut self) {
269        self.node.is_active = true;
270        self.modified = Utc::now(); // Update modified timestamp directly
271    }
272
273    /// Deactivates the user
274    pub fn deactivate(&mut self) {
275        self.node.is_active = false;
276        self.modified = Utc::now(); // Update modified timestamp directly
277    }
278
279    /// Verifies the user
280    pub fn verify(&mut self) {
281        self.node.is_verified = true;
282        self.modified = Utc::now(); // Update modified timestamp directly
283    }
284
285    /// Updates the user profile
286    pub fn update_profile(&mut self, profile: UserProfile) {
287        self.node.profile = profile;
288        self.modified = Utc::now(); // Update modified timestamp directly
289    }
290}
291
292/// User-specific error types
293#[derive(Debug, thiserror::Error)]
294pub enum UserError {
295    #[error("Invalid email format: {0}")]
296    InvalidEmail(String),
297    #[error("Invalid username: {0}")]
298    InvalidUsername(String),
299    #[error("Invalid role: {0}")]
300    InvalidRole(String),
301    #[error("User not found with ID: {0}")]
302    UserNotFound(Uuid),
303    #[error("User not found with email: {0}")]
304    UserNotFoundByEmail(String),
305    #[error("Email already exists: {0}")]
306    EmailAlreadyExists(String),
307    #[error("Username already exists: {0}")]
308    UsernameAlreadyExists(String),
309    #[error("Invalid password: {0}")]
310    InvalidPassword(String),
311    #[error("Authentication failed")]
312    AuthenticationFailed,
313    #[error("User is not active")]
314    UserNotActive,
315    #[error("User is not verified")]
316    UserNotVerified,
317    #[error("Repository error: {0}")]
318    RepositoryError(String),
319    #[error("Hash error: {0}")]
320    HashError(String),
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    #[test]
328    fn test_email_validation() {
329        // Valid emails
330        assert!(Email::new("test@example.com".to_string()).is_ok());
331        assert!(Email::new("user.name+tag@domain.co.uk".to_string()).is_ok());
332        assert!(Email::new("123@test.org".to_string()).is_ok());
333
334        // Invalid emails
335        assert!(Email::new("invalid-email".to_string()).is_err());
336        assert!(Email::new("@domain.com".to_string()).is_err());
337        assert!(Email::new("user@".to_string()).is_err());
338        assert!(Email::new("".to_string()).is_err());
339    }
340
341    #[test]
342    fn test_email_methods() {
343        let email = Email::new("Test.User@Example.COM".to_string()).unwrap();
344
345        // Email should be normalized to lowercase
346        assert_eq!(email.value(), "test.user@example.com");
347        assert_eq!(email.domain(), Some("example.com"));
348        assert_eq!(email.local_part(), Some("test.user"));
349        assert_eq!(email.to_string(), "test.user@example.com");
350    }
351
352    #[test]
353    fn test_user_creation() {
354        let email = Email::new("user@example.com".to_string()).unwrap();
355        let user = User::new_user(
356            "testuser".to_string(),
357            email.clone(),
358            "password_hash".to_string(),
359            None,
360        );
361
362        assert_eq!(user.username(), "testuser");
363        assert_eq!(user.email(), &email);
364        assert_eq!(user.password_hash(), "password_hash");
365        assert!(user.is_active());
366        assert!(!user.is_verified());
367    }
368
369    #[test]
370    fn test_user_updates() {
371        let email = Email::new("user@example.com".to_string()).unwrap();
372        let mut user = User::new_user(
373            "testuser".to_string(),
374            email,
375            "password_hash".to_string(),
376            None,
377        );
378
379        let initial_modified = user.modified;
380
381        // Small delay to ensure timestamp difference
382        std::thread::sleep(std::time::Duration::from_millis(1));
383
384        // Test username update
385        assert!(user.update_username("newusername".to_string()).is_ok());
386        assert_eq!(user.username(), "newusername");
387        assert!(user.modified > initial_modified);
388
389        // Test invalid username
390        assert!(user.update_username("a".to_string()).is_err());
391        assert!(user.update_username("".to_string()).is_err());
392
393        // Test email update
394        let new_email = Email::new("new@example.com".to_string()).unwrap();
395        let before_email_update = user.modified;
396        std::thread::sleep(std::time::Duration::from_millis(1));
397
398        assert!(user.update_email(new_email.clone()).is_ok());
399        assert_eq!(user.email(), &new_email);
400        assert!(!user.is_verified()); // Should reset verification
401        assert!(user.modified > before_email_update);
402
403        // Test activation/deactivation
404        let before_deactivate = user.modified;
405        std::thread::sleep(std::time::Duration::from_millis(1));
406
407        user.deactivate();
408        assert!(!user.is_active());
409        assert!(user.modified > before_deactivate);
410
411        let before_activate = user.modified;
412        std::thread::sleep(std::time::Duration::from_millis(1));
413
414        user.activate();
415        assert!(user.is_active());
416        assert!(user.modified > before_activate);
417
418        // Test verification
419        let before_verify = user.modified;
420        std::thread::sleep(std::time::Duration::from_millis(1));
421
422        user.verify();
423        assert!(user.is_verified());
424        assert!(user.modified > before_verify);
425    }
426
427    #[test]
428    fn test_user_profile_update() {
429        let email = Email::new("user@example.com".to_string()).unwrap();
430        let mut user = User::new_user(
431            "testuser".to_string(),
432            email,
433            "password_hash".to_string(),
434            None,
435        );
436
437        let new_profile = UserProfile {
438            first_name: Some("John".to_string()),
439            last_name: Some("Doe".to_string()),
440            bio: Some("Software developer".to_string()),
441            avatar_url: Some("https://example.com/avatar.jpg".to_string()),
442            timezone: Some("America/New_York".to_string()),
443            locale: Some("en-US".to_string()),
444        };
445
446        let before_update = user.modified;
447        std::thread::sleep(std::time::Duration::from_millis(1));
448
449        user.update_profile(new_profile.clone());
450        assert_eq!(user.profile(), &new_profile);
451        assert!(user.modified > before_update);
452    }
453
454    #[test]
455    fn test_username_validation() {
456        let email = Email::new("user@example.com".to_string()).unwrap();
457        let mut user = User::new_user(
458            "testuser".to_string(),
459            email,
460            "password_hash".to_string(),
461            None,
462        );
463
464        // Valid usernames
465        assert!(user.update_username("validuser".to_string()).is_ok());
466        assert!(user.update_username("user_123".to_string()).is_ok());
467        assert!(user.update_username("test-user".to_string()).is_ok());
468
469        // Invalid usernames
470        assert!(user.update_username("".to_string()).is_err());
471        assert!(user.update_username("ab".to_string()).is_err());
472
473        // Username too long
474        let long_username = "a".repeat(51);
475        assert!(user.update_username(long_username).is_err());
476    }
477
478    #[test]
479    fn test_user_versioning() {
480        let email = Email::new("user@example.com".to_string()).unwrap();
481        let user = User::new_user(
482            "testuser".to_string(),
483            email,
484            "password_hash".to_string(),
485            None,
486        );
487
488        // User should have versioning enabled by default
489        assert!(user.is_versioning_enabled());
490
491        // UUID should be generated
492        assert!(!user.uuid.is_nil());
493
494        // Timestamps should be set
495        assert_eq!(user.created, user.modified);
496    }
497
498    #[test]
499    fn test_user_serialization() {
500        let email = Email::new("user@example.com".to_string()).unwrap();
501        let user = User::new_user(
502            "testuser".to_string(),
503            email,
504            "password_hash".to_string(),
505            Some(UserProfile {
506                first_name: Some("Test".to_string()),
507                last_name: Some("User".to_string()),
508                bio: None,
509                avatar_url: None,
510                timezone: Some("UTC".to_string()),
511                locale: Some("en-US".to_string()),
512            }),
513        );
514
515        // Test serialization
516        let serialized = serde_json::to_string(&user).unwrap();
517        assert!(!serialized.is_empty());
518
519        // Test deserialization
520        let deserialized: User = serde_json::from_str(&serialized).unwrap();
521        assert_eq!(user.uuid, deserialized.uuid);
522        assert_eq!(user.node.username, deserialized.node.username);
523        assert_eq!(user.node.email.value(), deserialized.node.email.value());
524    }
525
526    #[test]
527    fn test_user_role_string_round_trip() {
528        use std::str::FromStr;
529
530        assert_eq!(UserRole::Admin.as_str(), "admin");
531        assert_eq!(UserRole::User.as_str(), "user");
532        assert_eq!(UserRole::from_str("admin").unwrap(), UserRole::Admin);
533        assert_eq!(UserRole::from_str(" USER ").unwrap(), UserRole::User);
534        assert!(UserRole::from_str("superuser").is_err());
535
536        // Lossy parsing fails safe to least privilege.
537        assert_eq!(UserRole::from_str_lossy("admin"), UserRole::Admin);
538        assert_eq!(UserRole::from_str_lossy("nonsense"), UserRole::User);
539    }
540
541    #[test]
542    fn test_user_role_default_and_accessors() {
543        let email = Email::new("user@example.com".to_string()).unwrap();
544        let mut user = User::new_user(
545            "testuser".to_string(),
546            email,
547            "password_hash".to_string(),
548            None,
549        );
550
551        // New users default to the least-privileged role.
552        assert_eq!(user.role(), UserRole::User);
553
554        let before = user.modified;
555        std::thread::sleep(std::time::Duration::from_millis(1));
556        user.set_role(UserRole::Admin);
557        assert_eq!(user.role(), UserRole::Admin);
558        assert!(user.modified > before);
559    }
560}