Skip to main content

systemprompt_users/services/user/
mod.rs

1//! User account service.
2//!
3//! [`UserService`] is the primary entry point for the users domain, delegating
4//! to [`UserRepository`] for lookups, listing and search, session management,
5//! account creation (including anonymous and federated identities), field
6//! updates, bulk operations, statistics, and account merging.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11mod provider;
12
13use std::collections::HashMap;
14use std::sync::Arc;
15use systemprompt_identifiers::{SessionId, UserId};
16
17use crate::error::{Result, UserError};
18use crate::models::{
19    User, UserActivity, UserCountBreakdown, UserRole, UserSession, UserStats, UserStatus,
20    UserWithSessions,
21};
22use crate::repository::{MergeResult, PurgeCount, UpdateUserParams, UserRepository};
23
24#[derive(Debug, Clone)]
25pub struct UserService {
26    repository: Arc<UserRepository>,
27}
28
29impl UserService {
30    pub const fn new(repository: Arc<UserRepository>) -> Self {
31        Self { repository }
32    }
33
34    pub async fn find_by_id(&self, id: &UserId) -> Result<Option<User>> {
35        self.repository.find_by_id(id).await
36    }
37
38    pub async fn find_by_email(&self, email: &str) -> Result<Option<User>> {
39        self.repository.find_by_email(email).await
40    }
41
42    pub async fn find_by_name(&self, name: &str) -> Result<Option<User>> {
43        self.repository.find_by_name(name).await
44    }
45
46    pub async fn find_by_role(&self, role: UserRole) -> Result<Vec<User>> {
47        self.repository.find_by_role(role).await
48    }
49
50    pub async fn find_first_user(&self) -> Result<Option<User>> {
51        self.repository.find_first_user().await
52    }
53
54    pub async fn find_first_admin(&self) -> Result<Option<User>> {
55        self.repository.find_first_admin().await
56    }
57
58    pub async fn find_authenticated_user(&self, user_id: &UserId) -> Result<Option<User>> {
59        self.repository.find_authenticated_user(user_id).await
60    }
61
62    pub async fn find_with_sessions(&self, user_id: &UserId) -> Result<Option<UserWithSessions>> {
63        self.repository.find_with_sessions(user_id).await
64    }
65
66    pub async fn get_activity(&self, user_id: &UserId) -> Result<UserActivity> {
67        self.repository.get_activity(user_id).await
68    }
69
70    pub async fn list(&self, limit: i64, offset: i64) -> Result<Vec<User>> {
71        self.repository.list(limit, offset).await
72    }
73
74    pub async fn list_including_anonymous(&self, limit: i64, offset: i64) -> Result<Vec<User>> {
75        self.repository
76            .list_including_anonymous(limit, offset)
77            .await
78    }
79
80    pub async fn list_all(&self) -> Result<Vec<User>> {
81        self.repository.list_all().await
82    }
83
84    pub async fn search(&self, query: &str, limit: i64) -> Result<Vec<User>> {
85        self.repository.search(query, limit).await
86    }
87
88    pub async fn search_including_anonymous(&self, query: &str, limit: i64) -> Result<Vec<User>> {
89        self.repository
90            .search_including_anonymous(query, limit)
91            .await
92    }
93
94    pub async fn count(&self) -> Result<i64> {
95        self.repository.count().await
96    }
97
98    pub async fn count_including_anonymous(&self) -> Result<i64> {
99        self.repository.count_including_anonymous().await
100    }
101
102    pub async fn is_temporary_anonymous(&self, id: &UserId) -> Result<bool> {
103        self.repository.is_temporary_anonymous(id).await
104    }
105
106    pub async fn list_non_anonymous_with_sessions(
107        &self,
108        limit: i64,
109    ) -> Result<Vec<UserWithSessions>> {
110        self.repository
111            .list_non_anonymous_with_sessions(limit)
112            .await
113    }
114
115    pub async fn list_sessions(&self, user_id: &UserId) -> Result<Vec<UserSession>> {
116        self.repository.list_sessions(user_id).await
117    }
118
119    pub async fn list_active_sessions(&self, user_id: &UserId) -> Result<Vec<UserSession>> {
120        self.repository.list_active_sessions(user_id).await
121    }
122
123    pub async fn list_recent_sessions(
124        &self,
125        user_id: &UserId,
126        limit: i64,
127    ) -> Result<Vec<UserSession>> {
128        self.repository.list_recent_sessions(user_id, limit).await
129    }
130
131    pub async fn session_exists(&self, session_id: &SessionId) -> Result<bool> {
132        self.repository.session_exists(session_id).await
133    }
134
135    pub async fn end_session(&self, session_id: &SessionId) -> Result<bool> {
136        self.repository.end_session(session_id).await
137    }
138
139    pub async fn end_all_sessions(&self, user_id: &UserId) -> Result<u64> {
140        self.repository.end_all_sessions(user_id).await
141    }
142
143    pub async fn create(
144        &self,
145        name: &str,
146        email: &str,
147        full_name: Option<&str>,
148        display_name: Option<&str>,
149    ) -> Result<User> {
150        self.repository
151            .create(name, email, full_name, display_name)
152            .await
153    }
154
155    pub async fn create_if_absent(
156        &self,
157        name: &str,
158        email: &str,
159        full_name: Option<&str>,
160        display_name: Option<&str>,
161    ) -> Result<Option<User>> {
162        self.repository
163            .create_if_absent(name, email, full_name, display_name)
164            .await
165    }
166
167    pub async fn create_anonymous(&self, fingerprint: &str) -> Result<User> {
168        self.repository.create_anonymous(fingerprint).await
169    }
170
171    pub async fn find_or_create_federated(
172        &self,
173        issuer: &str,
174        external_sub: &str,
175        claims: &systemprompt_traits::FederatedIdentityClaims,
176    ) -> Result<User> {
177        self.repository
178            .find_or_create_federated(issuer, external_sub, claims)
179            .await
180    }
181
182    pub async fn update_email(&self, id: &UserId, email: &str) -> Result<User> {
183        self.repository.update_email(id, email).await
184    }
185
186    pub async fn update_full_name(&self, id: &UserId, full_name: &str) -> Result<User> {
187        self.repository.update_full_name(id, full_name).await
188    }
189
190    pub async fn update_status(&self, id: &UserId, status: UserStatus) -> Result<User> {
191        self.repository.update_status(id, status).await
192    }
193
194    pub async fn update_email_verified(&self, id: &UserId, verified: bool) -> Result<User> {
195        self.repository.update_email_verified(id, verified).await
196    }
197
198    pub async fn update_display_name(&self, id: &UserId, display_name: &str) -> Result<User> {
199        self.repository.update_display_name(id, display_name).await
200    }
201
202    pub async fn update_all_fields(
203        &self,
204        id: &UserId,
205        params: UpdateUserParams<'_>,
206    ) -> Result<User> {
207        self.repository.update_all_fields(id, params).await
208    }
209
210    pub async fn assign_roles(&self, id: &UserId, roles: &[String]) -> Result<User> {
211        self.repository.assign_roles(id, roles).await
212    }
213
214    pub async fn delete(&self, id: &UserId) -> Result<Vec<PurgeCount>> {
215        self.repository.delete(id).await
216    }
217
218    pub async fn purge_preview(&self, id: &UserId) -> Result<Vec<PurgeCount>> {
219        self.repository.purge_preview(id).await
220    }
221
222    pub async fn cleanup_old_anonymous(&self, days: i32) -> Result<u64> {
223        self.repository.cleanup_old_anonymous(days).await
224    }
225
226    pub async fn count_old_anonymous(&self, days: i32) -> Result<i64> {
227        self.repository.count_old_anonymous(days).await
228    }
229
230    pub async fn count_with_breakdown(&self) -> Result<UserCountBreakdown> {
231        let total = self.repository.count().await?;
232        let by_status_vec = self.repository.count_by_status().await?;
233        let by_role_vec = self.repository.count_by_role().await?;
234
235        let by_status: HashMap<String, i64> = by_status_vec.into_iter().collect();
236        let by_role: HashMap<String, i64> = by_role_vec.into_iter().collect();
237
238        Ok(UserCountBreakdown {
239            total,
240            by_status,
241            by_role,
242        })
243    }
244
245    pub async fn get_stats(&self) -> Result<UserStats> {
246        self.repository.get_stats().await
247    }
248
249    pub async fn list_by_filter(
250        &self,
251        status: Option<&str>,
252        role: Option<&str>,
253        older_than_days: Option<i64>,
254        limit: i64,
255    ) -> Result<Vec<User>> {
256        self.repository
257            .list_by_filter(status, role, older_than_days, limit)
258            .await
259    }
260
261    pub async fn bulk_update_status(&self, user_ids: &[UserId], new_status: &str) -> Result<u64> {
262        self.repository
263            .bulk_update_status(user_ids, new_status)
264            .await
265    }
266
267    pub async fn bulk_delete(&self, user_ids: &[UserId]) -> Result<u64> {
268        self.repository.bulk_delete(user_ids).await
269    }
270
271    pub async fn merge_users(&self, source_id: &UserId, target_id: &UserId) -> Result<MergeResult> {
272        self.repository.merge_users(source_id, target_id).await
273    }
274
275    pub async fn promote_anonymous(
276        &self,
277        source_id: &UserId,
278        target_id: &UserId,
279    ) -> Result<MergeResult> {
280        if source_id == target_id {
281            return Err(UserError::Validation(
282                "cannot promote a user onto itself".to_owned(),
283            ));
284        }
285        let source = self
286            .repository
287            .find_by_id(source_id)
288            .await?
289            .ok_or_else(|| UserError::NotFound(source_id.clone()))?;
290        if !source.has_role(UserRole::Anonymous) {
291            return Err(UserError::Validation(format!(
292                "user {} is not anonymous; use an explicit admin merge instead",
293                source_id
294            )));
295        }
296        self.repository.merge_users(source_id, target_id).await
297    }
298}