Skip to main content

ironflow_store/entities/
user.rs

1//! User entity for IAM.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7/// A registered user.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct User {
10    /// Unique user ID (UUID v7).
11    pub id: Uuid,
12    /// Email address (unique).
13    pub email: String,
14    /// Display username (unique).
15    pub username: String,
16    /// Argon2id password hash.
17    #[serde(skip_serializing)]
18    pub password_hash: String,
19    /// Whether the user has admin privileges.
20    pub is_admin: bool,
21    /// When the user was created.
22    pub created_at: DateTime<Utc>,
23    /// When the user was last updated.
24    pub updated_at: DateTime<Utc>,
25}
26
27/// Parameters for creating a new user.
28#[derive(Debug, Clone)]
29pub struct NewUser {
30    /// Email address.
31    pub email: String,
32    /// Display username.
33    pub username: String,
34    /// Pre-hashed password (Argon2id).
35    pub password_hash: String,
36    /// Explicit admin flag. When `Some(true)`, the user is created as admin.
37    /// When `None`, the store decides (first user = admin, others = member).
38    pub is_admin: Option<bool>,
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44
45    #[test]
46    fn user_serde_excludes_password_hash_on_output() {
47        let user = User {
48            id: Uuid::now_v7(),
49            email: "test@example.com".to_string(),
50            username: "testuser".to_string(),
51            password_hash: "secret_hash_should_not_appear".to_string(),
52            is_admin: false,
53            created_at: Utc::now(),
54            updated_at: Utc::now(),
55        };
56
57        let _json = serde_json::to_string(&user).expect("serialize");
58        // Verify the password hash is not in the JSON output
59        assert!(!_json.contains("secret_hash_should_not_appear"));
60        assert!(_json.contains("test@example.com"));
61        assert!(_json.contains("testuser"));
62    }
63
64    #[test]
65    fn user_serde_with_explicit_password_hash() {
66        let now = Utc::now();
67        let user = User {
68            id: Uuid::now_v7(),
69            email: "alice@example.com".to_string(),
70            username: "alice".to_string(),
71            password_hash: "argon2_hash".to_string(),
72            is_admin: true,
73            created_at: now,
74            updated_at: now,
75        };
76
77        // When serialized, password_hash is skipped
78        let _json = serde_json::to_string(&user).expect("serialize");
79
80        // But the struct itself preserves the hash internally
81        assert_eq!(user.password_hash, "argon2_hash");
82        assert_eq!(user.email, "alice@example.com");
83        assert_eq!(user.username, "alice");
84        assert!(user.is_admin);
85    }
86
87    #[test]
88    fn newuser_basic_creation() {
89        let new_user = NewUser {
90            email: "bob@example.com".to_string(),
91            username: "bob".to_string(),
92            password_hash: "hash123".to_string(),
93            is_admin: None,
94        };
95
96        assert_eq!(new_user.email, "bob@example.com");
97        assert_eq!(new_user.username, "bob");
98        assert_eq!(new_user.password_hash, "hash123");
99        assert_eq!(new_user.is_admin, None);
100    }
101}