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