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, FromRow)]
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, Copy, Serialize, Deserialize)]
94pub struct UserStats {
95 pub total: i64,
96 pub created_24h: i64,
97 pub created_7d: i64,
98 pub created_30d: i64,
99 pub active: i64,
100 pub suspended: i64,
101 pub admins: i64,
102 pub anonymous: i64,
103 pub bots: i64,
104 pub oldest_user: Option<DateTime<Utc>>,
105 pub newest_user: Option<DateTime<Utc>>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct UserCountBreakdown {
110 pub total: i64,
111 pub by_status: std::collections::HashMap<String, i64>,
112 pub by_role: std::collections::HashMap<String, i64>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct UserExport {
117 pub id: UserId,
118 pub name: String,
119 pub email: String,
120 pub full_name: Option<String>,
121 pub display_name: Option<String>,
122 pub status: Option<String>,
123 pub email_verified: Option<bool>,
124 pub roles: Vec<String>,
125 pub is_bot: bool,
126 pub is_scanner: bool,
127 pub created_at: Option<DateTime<Utc>>,
128 pub updated_at: Option<DateTime<Utc>>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
132pub struct UserApiKey {
133 #[sqlx(try_from = "String")]
134 pub id: ApiKeyId,
135 #[sqlx(try_from = "String")]
136 pub user_id: UserId,
137 pub name: String,
138 pub key_prefix: String,
139 pub key_hash: String,
140 pub created_at: Option<DateTime<Utc>>,
141 pub last_used_at: Option<DateTime<Utc>>,
142 pub expires_at: Option<DateTime<Utc>>,
143 pub revoked_at: Option<DateTime<Utc>>,
144}
145
146impl UserApiKey {
147 pub fn is_active(&self, now: DateTime<Utc>) -> bool {
148 if self.revoked_at.is_some() {
149 return false;
150 }
151 if let Some(expires_at) = self.expires_at
152 && now >= expires_at
153 {
154 return false;
155 }
156 true
157 }
158}
159
160#[derive(Debug, Clone)]
161pub struct NewApiKey {
162 pub record: UserApiKey,
163 pub secret: String,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
167pub struct UserDeviceCert {
168 #[sqlx(try_from = "String")]
169 pub id: DeviceCertId,
170 #[sqlx(try_from = "String")]
171 pub user_id: UserId,
172 pub fingerprint: String,
173 pub label: String,
174 pub enrolled_at: Option<DateTime<Utc>>,
175 pub revoked_at: Option<DateTime<Utc>>,
176}
177
178impl UserDeviceCert {
179 pub const fn is_active(&self) -> bool {
180 self.revoked_at.is_none()
181 }
182}
183
184impl From<User> for UserExport {
185 fn from(user: User) -> Self {
186 Self {
187 id: user.id,
188 name: user.name,
189 email: user.email,
190 full_name: user.full_name,
191 display_name: user.display_name,
192 status: user.status,
193 email_verified: user.email_verified,
194 roles: user.roles,
195 is_bot: user.is_bot,
196 is_scanner: user.is_scanner,
197 created_at: user.created_at,
198 updated_at: user.updated_at,
199 }
200 }
201}