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