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`], [`UserSessionRow`]), 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};
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
38impl User {
39    pub fn is_active(&self) -> bool {
40        self.status.as_deref() == Some(UserStatus::Active.as_str())
41    }
42
43    pub fn is_admin(&self) -> bool {
44        self.roles.contains(&UserRole::Admin.as_str().to_owned())
45    }
46
47    pub fn has_role(&self, role: UserRole) -> bool {
48        self.roles.contains(&role.as_str().to_owned())
49    }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
53pub struct UserActivity {
54    #[sqlx(try_from = "String")]
55    pub user_id: UserId,
56    pub last_active: Option<DateTime<Utc>>,
57    pub session_count: i64,
58    pub task_count: i64,
59    pub message_count: i64,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
63pub struct UserWithSessions {
64    #[sqlx(try_from = "String")]
65    pub id: UserId,
66    pub name: String,
67    pub email: String,
68    pub full_name: Option<String>,
69    pub status: Option<String>,
70    pub roles: Vec<String>,
71    pub created_at: Option<DateTime<Utc>>,
72    pub active_sessions: i64,
73    pub last_session_at: Option<DateTime<Utc>>,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct UserSession {
78    pub session_id: SessionId,
79    pub user_id: Option<UserId>,
80    pub ip_address: Option<String>,
81    pub user_agent: Option<String>,
82    pub device_type: Option<String>,
83    pub started_at: Option<DateTime<Utc>>,
84    pub last_activity_at: Option<DateTime<Utc>>,
85    pub ended_at: Option<DateTime<Utc>>,
86}
87
88#[derive(Debug, Clone, FromRow)]
89pub struct UserSessionRow {
90    #[sqlx(try_from = "String")]
91    pub session_id: SessionId,
92    pub user_id: Option<UserId>,
93    pub ip_address: Option<String>,
94    pub user_agent: Option<String>,
95    pub device_type: Option<String>,
96    pub started_at: Option<DateTime<Utc>>,
97    pub last_activity_at: Option<DateTime<Utc>>,
98    pub ended_at: Option<DateTime<Utc>>,
99}
100
101impl From<UserSessionRow> for UserSession {
102    fn from(row: UserSessionRow) -> Self {
103        Self {
104            session_id: row.session_id,
105            user_id: row.user_id,
106            ip_address: row.ip_address,
107            user_agent: row.user_agent,
108            device_type: row.device_type,
109            started_at: row.started_at,
110            last_activity_at: row.last_activity_at,
111            ended_at: row.ended_at,
112        }
113    }
114}
115
116#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
117pub struct UserStats {
118    pub total: i64,
119    pub created_24h: i64,
120    pub created_7d: i64,
121    pub created_30d: i64,
122    pub active: i64,
123    pub suspended: i64,
124    pub admins: i64,
125    pub anonymous: i64,
126    pub bots: i64,
127    pub oldest_user: Option<DateTime<Utc>>,
128    pub newest_user: Option<DateTime<Utc>>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct UserCountBreakdown {
133    pub total: i64,
134    pub by_status: std::collections::HashMap<String, i64>,
135    pub by_role: std::collections::HashMap<String, i64>,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct UserExport {
140    pub id: UserId,
141    pub name: String,
142    pub email: String,
143    pub full_name: Option<String>,
144    pub display_name: Option<String>,
145    pub status: Option<String>,
146    pub email_verified: Option<bool>,
147    pub roles: Vec<String>,
148    pub is_bot: bool,
149    pub is_scanner: bool,
150    pub created_at: Option<DateTime<Utc>>,
151    pub updated_at: Option<DateTime<Utc>>,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
155pub struct UserApiKey {
156    #[sqlx(try_from = "String")]
157    pub id: ApiKeyId,
158    #[sqlx(try_from = "String")]
159    pub user_id: UserId,
160    pub name: String,
161    pub key_prefix: String,
162    pub key_hash: String,
163    pub created_at: Option<DateTime<Utc>>,
164    pub last_used_at: Option<DateTime<Utc>>,
165    pub expires_at: Option<DateTime<Utc>>,
166    pub revoked_at: Option<DateTime<Utc>>,
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}