systemprompt_users/models/
mod.rs1use chrono::{DateTime, Utc};
14use serde::{Deserialize, Serialize};
15use sqlx::FromRow;
16use systemprompt_identifiers::{ApiKeyId, DeviceCertId, SessionId, UserId};
17
18pub use systemprompt_models::auth::{UserRole, UserStatus};
19
20#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
21pub struct User {
22 #[sqlx(try_from = "String")]
23 pub id: UserId,
24 pub name: String,
25 pub email: String,
26 pub full_name: Option<String>,
27 pub display_name: Option<String>,
28 pub status: Option<String>,
29 pub email_verified: Option<bool>,
30 pub roles: Vec<String>,
31 pub avatar_url: Option<String>,
32 pub is_bot: bool,
33 pub is_scanner: bool,
34 pub created_at: Option<DateTime<Utc>>,
35 pub updated_at: Option<DateTime<Utc>>,
36}
37
38#[must_use]
39pub fn normalise_email(email: &str) -> String {
40 email.trim().to_lowercase()
41}
42
43impl User {
44 pub fn is_active(&self) -> bool {
45 self.status.as_deref() == Some(UserStatus::Active.as_str())
46 }
47
48 pub fn is_admin(&self) -> bool {
49 self.roles.contains(&UserRole::Admin.as_str().to_owned())
50 }
51
52 pub fn has_role(&self, role: UserRole) -> bool {
53 self.roles.contains(&role.as_str().to_owned())
54 }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
58pub struct UserActivity {
59 #[sqlx(try_from = "String")]
60 pub user_id: UserId,
61 pub last_active: Option<DateTime<Utc>>,
62 pub session_count: i64,
63 pub task_count: i64,
64 pub message_count: i64,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
68pub struct UserWithSessions {
69 #[sqlx(try_from = "String")]
70 pub id: UserId,
71 pub name: String,
72 pub email: String,
73 pub full_name: Option<String>,
74 pub status: Option<String>,
75 pub roles: Vec<String>,
76 pub created_at: Option<DateTime<Utc>>,
77 pub active_sessions: i64,
78 pub last_session_at: Option<DateTime<Utc>>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct UserSession {
83 pub session_id: SessionId,
84 pub user_id: Option<UserId>,
85 pub ip_address: Option<String>,
86 pub user_agent: Option<String>,
87 pub device_type: Option<String>,
88 pub started_at: Option<DateTime<Utc>>,
89 pub last_activity_at: Option<DateTime<Utc>>,
90 pub ended_at: Option<DateTime<Utc>>,
91}
92
93#[derive(Debug, Clone, FromRow)]
94pub struct UserSessionRow {
95 #[sqlx(try_from = "String")]
96 pub session_id: SessionId,
97 pub user_id: Option<UserId>,
98 pub ip_address: Option<String>,
99 pub user_agent: Option<String>,
100 pub device_type: Option<String>,
101 pub started_at: Option<DateTime<Utc>>,
102 pub last_activity_at: Option<DateTime<Utc>>,
103 pub ended_at: Option<DateTime<Utc>>,
104}
105
106impl From<UserSessionRow> for UserSession {
107 fn from(row: UserSessionRow) -> Self {
108 Self {
109 session_id: row.session_id,
110 user_id: row.user_id,
111 ip_address: row.ip_address,
112 user_agent: row.user_agent,
113 device_type: row.device_type,
114 started_at: row.started_at,
115 last_activity_at: row.last_activity_at,
116 ended_at: row.ended_at,
117 }
118 }
119}
120
121#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
122pub struct UserStats {
123 pub total: i64,
124 pub created_24h: i64,
125 pub created_7d: i64,
126 pub created_30d: i64,
127 pub active: i64,
128 pub suspended: i64,
129 pub admins: i64,
130 pub anonymous: i64,
131 pub bots: i64,
132 pub oldest_user: Option<DateTime<Utc>>,
133 pub newest_user: Option<DateTime<Utc>>,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct UserCountBreakdown {
138 pub total: i64,
139 pub by_status: std::collections::HashMap<String, i64>,
140 pub by_role: std::collections::HashMap<String, i64>,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct UserExport {
145 pub id: UserId,
146 pub name: String,
147 pub email: String,
148 pub full_name: Option<String>,
149 pub display_name: Option<String>,
150 pub status: Option<String>,
151 pub email_verified: Option<bool>,
152 pub roles: Vec<String>,
153 pub is_bot: bool,
154 pub is_scanner: bool,
155 pub created_at: Option<DateTime<Utc>>,
156 pub updated_at: Option<DateTime<Utc>>,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
160pub struct UserApiKey {
161 #[sqlx(try_from = "String")]
162 pub id: ApiKeyId,
163 #[sqlx(try_from = "String")]
164 pub user_id: UserId,
165 pub name: String,
166 pub key_prefix: String,
167 pub key_hash: String,
168 pub created_at: Option<DateTime<Utc>>,
169 pub last_used_at: Option<DateTime<Utc>>,
170 pub expires_at: Option<DateTime<Utc>>,
171 pub revoked_at: Option<DateTime<Utc>>,
172}
173
174impl UserApiKey {
175 pub fn is_active(&self, now: DateTime<Utc>) -> bool {
176 if self.revoked_at.is_some() {
177 return false;
178 }
179 if let Some(expires_at) = self.expires_at
180 && now >= expires_at
181 {
182 return false;
183 }
184 true
185 }
186}
187
188#[derive(Debug, Clone)]
189pub struct NewApiKey {
190 pub record: UserApiKey,
191 pub secret: String,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
195pub struct UserDeviceCert {
196 #[sqlx(try_from = "String")]
197 pub id: DeviceCertId,
198 #[sqlx(try_from = "String")]
199 pub user_id: UserId,
200 pub fingerprint: String,
201 pub label: String,
202 pub enrolled_at: Option<DateTime<Utc>>,
203 pub revoked_at: Option<DateTime<Utc>>,
204}
205
206impl UserDeviceCert {
207 pub const fn is_active(&self) -> bool {
208 self.revoked_at.is_none()
209 }
210}
211
212impl From<User> for UserExport {
213 fn from(user: User) -> Self {
214 Self {
215 id: user.id,
216 name: user.name,
217 email: user.email,
218 full_name: user.full_name,
219 display_name: user.display_name,
220 status: user.status,
221 email_verified: user.email_verified,
222 roles: user.roles,
223 is_bot: user.is_bot,
224 is_scanner: user.is_scanner,
225 created_at: user.created_at,
226 updated_at: user.updated_at,
227 }
228 }
229}