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