1use serde::{Deserialize, Serialize};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21pub enum UserRole {
22 #[serde(rename = "admin")]
23 Admin,
24 #[serde(rename = "analyst")]
25 Analyst,
26 #[serde(rename = "viewer")]
27 Viewer,
28 #[serde(rename = "api_only")]
29 ApiOnly,
30}
31
32impl UserRole {
33 #[must_use]
34 pub fn as_str(&self) -> &'static str {
35 match self {
36 Self::Admin => "admin",
37 Self::Analyst => "analyst",
38 Self::Viewer => "viewer",
39 Self::ApiOnly => "api_only",
40 }
41 }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(deny_unknown_fields)]
47pub struct User {
48 pub user_id: String,
49 pub username: String,
50 pub email: String,
51 pub role: UserRole,
52 pub created_at: u64,
53 pub last_login: Option<u64>,
54 pub active: bool,
55}
56
57impl User {
58 #[must_use]
60 pub fn new(username: String, email: String, role: UserRole) -> Self {
61 Self {
62 user_id: uuid::Uuid::new_v4().to_string(),
63 username,
64 email,
65 role,
66 created_at: std::time::SystemTime::now()
67 .duration_since(std::time::UNIX_EPOCH)
68 .unwrap_or_default()
69 .as_secs(),
70 last_login: None,
71 active: true,
72 }
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79 use serde_json::json;
80
81 #[test]
82 fn role_names_are_metadata_not_permission_decisions() {
83 assert_eq!(UserRole::Admin.as_str(), "admin");
84 assert_eq!(UserRole::Analyst.as_str(), "analyst");
85 assert_eq!(UserRole::Viewer.as_str(), "viewer");
86 assert_eq!(UserRole::ApiOnly.as_str(), "api_only");
87 }
88
89 #[test]
90 fn user_serialization_contains_no_credential_fields() {
91 let user = User::new(
92 "analyst".to_owned(),
93 "analyst@example.test".to_owned(),
94 UserRole::Analyst,
95 );
96
97 let encoded = serde_json::to_value(&user).unwrap();
98 let object = encoded.as_object().unwrap();
99 for forbidden in ["api_key", "password", "token", "secret"] {
100 assert!(!object.contains_key(forbidden));
101 }
102
103 let debug = format!("{user:?}");
104 assert!(!debug.contains("credential-canary"));
105 }
106
107 #[test]
108 fn legacy_secret_bearing_user_shape_is_rejected() {
109 let encoded = json!({
110 "user_id": "user-1",
111 "username": "analyst",
112 "email": "analyst@example.test",
113 "role": "analyst",
114 "api_key": "credential-canary",
115 "created_at": 1,
116 "last_login": null,
117 "active": true
118 });
119
120 assert!(serde_json::from_value::<User>(encoded).is_err());
121 }
122}