systemprompt_users/services/user/
mod.rs1mod 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, 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<()> {
215 self.repository.delete(id).await
216 }
217
218 pub async fn cleanup_old_anonymous(&self, days: i32) -> Result<u64> {
219 self.repository.cleanup_old_anonymous(days).await
220 }
221
222 pub async fn count_old_anonymous(&self, days: i32) -> Result<i64> {
223 self.repository.count_old_anonymous(days).await
224 }
225
226 pub async fn count_with_breakdown(&self) -> Result<UserCountBreakdown> {
227 let total = self.repository.count().await?;
228 let by_status_vec = self.repository.count_by_status().await?;
229 let by_role_vec = self.repository.count_by_role().await?;
230
231 let by_status: HashMap<String, i64> = by_status_vec.into_iter().collect();
232 let by_role: HashMap<String, i64> = by_role_vec.into_iter().collect();
233
234 Ok(UserCountBreakdown {
235 total,
236 by_status,
237 by_role,
238 })
239 }
240
241 pub async fn get_stats(&self) -> Result<UserStats> {
242 self.repository.get_stats().await
243 }
244
245 pub async fn list_by_filter(
246 &self,
247 status: Option<&str>,
248 role: Option<&str>,
249 older_than_days: Option<i64>,
250 limit: i64,
251 ) -> Result<Vec<User>> {
252 self.repository
253 .list_by_filter(status, role, older_than_days, limit)
254 .await
255 }
256
257 pub async fn bulk_update_status(&self, user_ids: &[UserId], new_status: &str) -> Result<u64> {
258 self.repository
259 .bulk_update_status(user_ids, new_status)
260 .await
261 }
262
263 pub async fn bulk_delete(&self, user_ids: &[UserId]) -> Result<u64> {
264 self.repository.bulk_delete(user_ids).await
265 }
266
267 pub async fn merge_users(&self, source_id: &UserId, target_id: &UserId) -> Result<MergeResult> {
268 self.repository.merge_users(source_id, target_id).await
269 }
270
271 pub async fn promote_anonymous(
272 &self,
273 source_id: &UserId,
274 target_id: &UserId,
275 ) -> Result<MergeResult> {
276 if source_id == target_id {
277 return Err(UserError::Validation(
278 "cannot promote a user onto itself".to_owned(),
279 ));
280 }
281 let source = self
282 .repository
283 .find_by_id(source_id)
284 .await?
285 .ok_or_else(|| UserError::NotFound(source_id.clone()))?;
286 if !source.has_role(UserRole::Anonymous) {
287 return Err(UserError::Validation(format!(
288 "user {} is not anonymous; use an explicit admin merge instead",
289 source_id
290 )));
291 }
292 self.repository.merge_users(source_id, target_id).await
293 }
294}