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