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 and statistics.
7//!
8//! Account merging spans every domain that keys rows on a user. The service
9//! runs each injected
10//! [`OwnerReassignment`](systemprompt_traits::OwnerReassignment)
11//! — one per owning crate, each in its own transaction and re-runnable — and
12//! only then the users-owned step that moves sessions, records the merge and
13//! deletes the source. A failed reassignment stops the merge with the source
14//! still present, so a rerun completes it. A service built without
15//! reassignments refuses to merge rather than delete a user whose rows it
16//! cannot move.
17//!
18//! Copyright (c) systemprompt.io — Business Source License 1.1.
19//! See <https://systemprompt.io> for licensing details.
20
21mod archive;
22mod bulk;
23mod merge;
24mod provider;
25
26use std::fmt;
27use std::sync::Arc;
28use systemprompt_identifiers::{SessionId, UserId};
29use systemprompt_traits::DynOwnerReassignment;
30
31use crate::error::Result;
32use crate::models::{User, UserActivity, UserRole, UserSession, UserStatus, UserWithSessions};
33use crate::repository::{PurgeCount, UpdateUserParams, UserRepository};
34
35#[derive(Clone)]
36pub struct UserService {
37    pub(super) repository: Arc<UserRepository>,
38    pub(super) owner_reassignments: Arc<[DynOwnerReassignment]>,
39}
40
41impl fmt::Debug for UserService {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        let domains: Vec<&str> = self
44            .owner_reassignments
45            .iter()
46            .map(|reassignment| reassignment.domain())
47            .collect();
48        f.debug_struct("UserService")
49            .field("repository", &self.repository)
50            .field("owner_reassignments", &domains)
51            .finish()
52    }
53}
54
55impl UserService {
56    pub fn new(repository: Arc<UserRepository>) -> Self {
57        Self {
58            repository,
59            owner_reassignments: Arc::from([]),
60        }
61    }
62
63    #[must_use]
64    pub fn with_owner_reassignments(mut self, reassignments: Vec<DynOwnerReassignment>) -> Self {
65        self.owner_reassignments = Arc::from(reassignments);
66        self
67    }
68
69    pub async fn find_by_id(&self, id: &UserId) -> Result<Option<User>> {
70        self.repository.find_by_id(id).await
71    }
72
73    pub async fn find_by_email(&self, email: &str) -> Result<Option<User>> {
74        self.repository.find_by_email(email).await
75    }
76
77    pub async fn find_by_name(&self, name: &str) -> Result<Option<User>> {
78        self.repository.find_by_name(name).await
79    }
80
81    pub async fn list_by_role(&self, role: UserRole) -> Result<Vec<User>> {
82        self.repository.list_by_role(role).await
83    }
84
85    pub async fn find_first_user(&self) -> Result<Option<User>> {
86        self.repository.find_first_user().await
87    }
88
89    pub async fn find_first_admin(&self) -> Result<Option<User>> {
90        self.repository.find_first_admin().await
91    }
92
93    pub async fn find_authenticated_user(&self, user_id: &UserId) -> Result<Option<User>> {
94        self.repository.find_authenticated_user(user_id).await
95    }
96
97    pub async fn find_with_sessions(&self, user_id: &UserId) -> Result<Option<UserWithSessions>> {
98        self.repository.find_with_sessions(user_id).await
99    }
100
101    pub async fn get_activity(&self, user_id: &UserId) -> Result<UserActivity> {
102        self.repository.get_activity(user_id).await
103    }
104
105    pub async fn list(&self, limit: i64, offset: i64) -> Result<Vec<User>> {
106        self.repository.list(limit, offset).await
107    }
108
109    pub async fn list_including_anonymous(&self, limit: i64, offset: i64) -> Result<Vec<User>> {
110        self.repository
111            .list_including_anonymous(limit, offset)
112            .await
113    }
114
115    pub async fn list_all(&self) -> Result<Vec<User>> {
116        self.repository.list_all().await
117    }
118
119    pub async fn search(&self, query: &str, limit: i64) -> Result<Vec<User>> {
120        self.repository.search(query, limit).await
121    }
122
123    pub async fn search_including_anonymous(&self, query: &str, limit: i64) -> Result<Vec<User>> {
124        self.repository
125            .search_including_anonymous(query, limit)
126            .await
127    }
128
129    pub async fn count(&self) -> Result<i64> {
130        self.repository.count().await
131    }
132
133    pub async fn count_including_anonymous(&self) -> Result<i64> {
134        self.repository.count_including_anonymous().await
135    }
136
137    pub async fn is_temporary_anonymous(&self, id: &UserId) -> Result<bool> {
138        self.repository.is_temporary_anonymous(id).await
139    }
140
141    pub async fn list_non_anonymous_with_sessions(
142        &self,
143        limit: i64,
144    ) -> Result<Vec<UserWithSessions>> {
145        self.repository
146            .list_non_anonymous_with_sessions(limit)
147            .await
148    }
149
150    pub async fn list_sessions(&self, user_id: &UserId) -> Result<Vec<UserSession>> {
151        self.repository.list_sessions(user_id).await
152    }
153
154    pub async fn list_active_sessions(&self, user_id: &UserId) -> Result<Vec<UserSession>> {
155        self.repository.list_active_sessions(user_id).await
156    }
157
158    pub async fn list_recent_sessions(
159        &self,
160        user_id: &UserId,
161        limit: i64,
162    ) -> Result<Vec<UserSession>> {
163        self.repository.list_recent_sessions(user_id, limit).await
164    }
165
166    pub async fn session_exists(&self, session_id: &SessionId) -> Result<bool> {
167        self.repository.session_exists(session_id).await
168    }
169
170    pub async fn end_session(&self, session_id: &SessionId) -> Result<bool> {
171        self.repository.end_session(session_id).await
172    }
173
174    pub async fn end_all_sessions(&self, user_id: &UserId) -> Result<u64> {
175        self.repository.end_all_sessions(user_id).await
176    }
177
178    pub async fn create(
179        &self,
180        name: &str,
181        email: &str,
182        full_name: Option<&str>,
183        display_name: Option<&str>,
184    ) -> Result<User> {
185        self.repository
186            .create(name, email, full_name, display_name)
187            .await
188    }
189
190    pub async fn create_if_absent(
191        &self,
192        name: &str,
193        email: &str,
194        full_name: Option<&str>,
195        display_name: Option<&str>,
196    ) -> Result<Option<User>> {
197        self.repository
198            .create_if_absent(name, email, full_name, display_name)
199            .await
200    }
201
202    pub async fn create_anonymous(&self, fingerprint: &str) -> Result<User> {
203        self.repository.create_anonymous(fingerprint).await
204    }
205
206    pub async fn find_or_create_federated(
207        &self,
208        issuer: &str,
209        external_sub: &str,
210        claims: &systemprompt_traits::FederatedIdentityClaims,
211    ) -> Result<User> {
212        self.repository
213            .find_or_create_federated(issuer, external_sub, claims)
214            .await
215    }
216
217    pub async fn update_email(&self, id: &UserId, email: &str) -> Result<User> {
218        self.repository.update_email(id, email).await
219    }
220
221    pub async fn update_full_name(&self, id: &UserId, full_name: &str) -> Result<User> {
222        self.repository.update_full_name(id, full_name).await
223    }
224
225    pub async fn update_status(&self, id: &UserId, status: UserStatus) -> Result<User> {
226        self.repository.update_status(id, status).await
227    }
228
229    pub async fn update_email_verified(&self, id: &UserId, verified: bool) -> Result<User> {
230        self.repository.update_email_verified(id, verified).await
231    }
232
233    pub async fn update_display_name(&self, id: &UserId, display_name: &str) -> Result<User> {
234        self.repository.update_display_name(id, display_name).await
235    }
236
237    pub async fn update_all_fields(
238        &self,
239        id: &UserId,
240        params: UpdateUserParams<'_>,
241    ) -> Result<User> {
242        self.repository.update_all_fields(id, params).await
243    }
244
245    pub async fn assign_roles(&self, id: &UserId, roles: &[String]) -> Result<User> {
246        self.repository.assign_roles(id, roles).await
247    }
248
249    pub async fn delete(&self, id: &UserId) -> Result<Vec<PurgeCount>> {
250        self.repository.delete(id).await
251    }
252
253    pub async fn purge_preview(&self, id: &UserId) -> Result<Vec<PurgeCount>> {
254        self.repository.purge_preview(id).await
255    }
256
257    pub async fn cleanup_old_anonymous(&self, days: i32) -> Result<u64> {
258        self.repository.cleanup_old_anonymous(days).await
259    }
260
261    pub async fn count_old_anonymous(&self, days: i32) -> Result<i64> {
262        self.repository.count_old_anonymous(days).await
263    }
264}