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