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}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41
42    #[test]
43    fn user_serde_excludes_password_hash_on_output() {
44        let user = User {
45            id: Uuid::now_v7(),
46            email: "test@example.com".to_string(),
47            username: "testuser".to_string(),
48            password_hash: "secret_hash_should_not_appear".to_string(),
49            is_admin: false,
50            created_at: Utc::now(),
51            updated_at: Utc::now(),
52        };
53
54        let _json = serde_json::to_string(&user).expect("serialize");
55        // Verify the password hash is not in the JSON output
56        assert!(!_json.contains("secret_hash_should_not_appear"));
57        assert!(_json.contains("test@example.com"));
58        assert!(_json.contains("testuser"));
59    }
60
61    #[test]
62    fn user_serde_with_explicit_password_hash() {
63        let now = Utc::now();
64        let user = User {
65            id: Uuid::now_v7(),
66            email: "alice@example.com".to_string(),
67            username: "alice".to_string(),
68            password_hash: "argon2_hash".to_string(),
69            is_admin: true,
70            created_at: now,
71            updated_at: now,
72        };
73
74        // When serialized, password_hash is skipped
75        let _json = serde_json::to_string(&user).expect("serialize");
76
77        // But the struct itself preserves the hash internally
78        assert_eq!(user.password_hash, "argon2_hash");
79        assert_eq!(user.email, "alice@example.com");
80        assert_eq!(user.username, "alice");
81        assert!(user.is_admin);
82    }
83
84    #[test]
85    fn newuser_basic_creation() {
86        let new_user = NewUser {
87            email: "bob@example.com".to_string(),
88            username: "bob".to_string(),
89            password_hash: "hash123".to_string(),
90        };
91
92        assert_eq!(new_user.email, "bob@example.com");
93        assert_eq!(new_user.username, "bob");
94        assert_eq!(new_user.password_hash, "hash123");
95    }
96}