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