Skip to main content

systemprompt_users/models/
mod.rs

1//! Data types for the users domain.
2//!
3//! Defines the persisted [`User`] record and its projections
4//! ([`UserActivity`], [`UserWithSessions`], [`UserStats`],
5//! [`UserCountBreakdown`], [`UserExport`]), session rows
6//! ([`UserSession`]), and the credential records
7//! [`UserApiKey`] / [`NewApiKey`] and [`UserDeviceCert`]. Role and status
8//! enums are re-exported from `systemprompt_models::auth`.
9//!
10//! Copyright (c) systemprompt.io — Business Source License 1.1.
11//! See <https://systemprompt.io> for licensing details.
12
13use 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/// The limits an API key carries: an optional model allowlist, and a spend
136/// budget and request ceiling counted over `request_window_seconds`.
137#[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}