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