1use chrono::{Duration, Utc};
7use systemprompt_identifiers::UserId;
8
9use crate::error::{Result, UserError};
10use crate::models::{User, UserRole, UserStatus};
11use crate::repository::UserRepository;
12
13#[derive(Debug)]
14pub struct UpdateUserParams<'a> {
15 pub email: &'a str,
16 pub full_name: Option<&'a str>,
17 pub display_name: Option<&'a str>,
18 pub status: UserStatus,
19}
20
21impl UserRepository {
22 pub async fn create(
23 &self,
24 name: &str,
25 email: &str,
26 full_name: Option<&str>,
27 display_name: Option<&str>,
28 ) -> Result<User> {
29 let now = Utc::now();
30 let id = UserId::new(uuid::Uuid::new_v4().to_string());
31 let display_name_val = display_name.or(full_name);
32 let status = UserStatus::Active.as_str();
33 let role = UserRole::User.as_str();
34
35 let row = sqlx::query_as!(
36 User,
37 r#"
38 INSERT INTO users (
39 id, name, email, full_name, display_name,
40 status, email_verified, roles, is_bot,
41 created_at, updated_at
42 )
43 VALUES ($1, $2, $3, $4, $5, $6, false, ARRAY[$7]::TEXT[], false, $8, $8)
44 RETURNING id, name, email, full_name, display_name, status, email_verified,
45 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
46 "#,
47 id.as_str(),
48 name,
49 email,
50 full_name,
51 display_name_val,
52 status,
53 role,
54 now
55 )
56 .fetch_one(&*self.write_pool)
57 .await?;
58
59 Ok(row)
60 }
61
62 pub async fn create_if_absent(
72 &self,
73 name: &str,
74 email: &str,
75 full_name: Option<&str>,
76 display_name: Option<&str>,
77 ) -> Result<Option<User>> {
78 let now = Utc::now();
79 let id = UserId::new(uuid::Uuid::new_v4().to_string());
80 let display_name_val = display_name.or(full_name);
81 let status = UserStatus::Active.as_str();
82 let role = UserRole::User.as_str();
83
84 let row = sqlx::query_as!(
85 User,
86 r#"
87 INSERT INTO users (
88 id, name, email, full_name, display_name,
89 status, email_verified, roles, is_bot,
90 created_at, updated_at
91 )
92 VALUES ($1, $2, $3, $4, $5, $6, false, ARRAY[$7]::TEXT[], false, $8, $8)
93 ON CONFLICT DO NOTHING
94 RETURNING id, name, email, full_name, display_name, status, email_verified,
95 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
96 "#,
97 id.as_str(),
98 name,
99 email,
100 full_name,
101 display_name_val,
102 status,
103 role,
104 now
105 )
106 .fetch_optional(&*self.write_pool)
107 .await?;
108
109 Ok(row)
110 }
111
112 pub async fn create_anonymous(&self, fingerprint: &str) -> Result<User> {
113 let email = format!("{}@anonymous.local", fingerprint);
114
115 if let Some(existing) = sqlx::query_as!(
118 User,
119 r#"
120 SELECT id, name, email, full_name, display_name, status, email_verified,
121 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
122 FROM users
123 WHERE email = $1
124 "#,
125 email
126 )
127 .fetch_optional(&*self.pool)
128 .await?
129 {
130 return Ok(existing);
131 }
132
133 let user_id = uuid::Uuid::new_v4();
135 let id = UserId::new(user_id.to_string());
136 let name = format!("anonymous_{}", &user_id.to_string()[..8]);
137 let now = Utc::now();
138 let status = UserStatus::Active.as_str();
139 let role = UserRole::Anonymous.as_str();
140
141 let row = sqlx::query_as!(
142 User,
143 r#"
144 INSERT INTO users (
145 id, name, email, status, email_verified, roles,
146 is_bot, created_at, updated_at
147 )
148 VALUES ($1, $2, $3, $4, false, ARRAY[$5]::TEXT[], false, $6, $6)
149 ON CONFLICT (email) DO UPDATE SET updated_at = $6
150 RETURNING id, name, email, full_name, display_name, status, email_verified,
151 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
152 "#,
153 id.as_str(),
154 name,
155 email,
156 status,
157 role,
158 now
159 )
160 .fetch_one(&*self.write_pool)
161 .await?;
162
163 Ok(row)
164 }
165
166 pub async fn update_email(&self, id: &UserId, email: &str) -> Result<User> {
167 let row = sqlx::query_as!(
168 User,
169 r#"
170 UPDATE users
171 SET email = $1, email_verified = false, updated_at = $2
172 WHERE id = $3
173 RETURNING id, name, email, full_name, display_name, status, email_verified,
174 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
175 "#,
176 email,
177 Utc::now(),
178 id.as_str()
179 )
180 .fetch_optional(&*self.write_pool)
181 .await?
182 .ok_or_else(|| UserError::NotFound(id.clone()))?;
183
184 Ok(row)
185 }
186
187 pub async fn update_full_name(&self, id: &UserId, full_name: &str) -> Result<User> {
188 let row = sqlx::query_as!(
189 User,
190 r#"
191 UPDATE users
192 SET full_name = $1, updated_at = $2
193 WHERE id = $3
194 RETURNING id, name, email, full_name, display_name, status, email_verified,
195 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
196 "#,
197 full_name,
198 Utc::now(),
199 id.as_str()
200 )
201 .fetch_optional(&*self.write_pool)
202 .await?
203 .ok_or_else(|| UserError::NotFound(id.clone()))?;
204
205 Ok(row)
206 }
207
208 pub async fn update_status(&self, id: &UserId, status: UserStatus) -> Result<User> {
209 let row = sqlx::query_as!(
210 User,
211 r#"
212 UPDATE users
213 SET status = $1, updated_at = $2
214 WHERE id = $3
215 RETURNING id, name, email, full_name, display_name, status, email_verified,
216 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
217 "#,
218 status.as_str(),
219 Utc::now(),
220 id.as_str()
221 )
222 .fetch_optional(&*self.write_pool)
223 .await?
224 .ok_or_else(|| UserError::NotFound(id.clone()))?;
225
226 Ok(row)
227 }
228
229 pub async fn update_email_verified(&self, id: &UserId, verified: bool) -> Result<User> {
230 let row = sqlx::query_as!(
231 User,
232 r#"
233 UPDATE users
234 SET email_verified = $1, updated_at = $2
235 WHERE id = $3
236 RETURNING id, name, email, full_name, display_name, status, email_verified,
237 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
238 "#,
239 verified,
240 Utc::now(),
241 id.as_str()
242 )
243 .fetch_optional(&*self.write_pool)
244 .await?
245 .ok_or_else(|| UserError::NotFound(id.clone()))?;
246
247 Ok(row)
248 }
249
250 pub async fn update_display_name(&self, id: &UserId, display_name: &str) -> Result<User> {
251 let row = sqlx::query_as!(
252 User,
253 r#"
254 UPDATE users
255 SET display_name = $1, updated_at = $2
256 WHERE id = $3
257 RETURNING id, name, email, full_name, display_name, status, email_verified,
258 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
259 "#,
260 display_name,
261 Utc::now(),
262 id.as_str()
263 )
264 .fetch_optional(&*self.write_pool)
265 .await?
266 .ok_or_else(|| UserError::NotFound(id.clone()))?;
267
268 Ok(row)
269 }
270
271 pub async fn update_all_fields(
272 &self,
273 id: &UserId,
274 params: UpdateUserParams<'_>,
275 ) -> Result<User> {
276 let row = sqlx::query_as!(
277 User,
278 r#"
279 UPDATE users
280 SET email = $1, full_name = $2, display_name = $3, status = $4, updated_at = $5
281 WHERE id = $6
282 RETURNING id, name, email, full_name, display_name, status, email_verified,
283 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
284 "#,
285 params.email,
286 params.full_name,
287 params.display_name,
288 params.status.as_str(),
289 Utc::now(),
290 id.as_str()
291 )
292 .fetch_optional(&*self.write_pool)
293 .await?
294 .ok_or_else(|| UserError::NotFound(id.clone()))?;
295
296 Ok(row)
297 }
298
299 pub async fn assign_roles(&self, id: &UserId, roles: &[String]) -> Result<User> {
300 let row = sqlx::query_as!(
301 User,
302 r#"
303 UPDATE users
304 SET roles = $1, updated_at = $2
305 WHERE id = $3
306 RETURNING id, name, email, full_name, display_name, status, email_verified,
307 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
308 "#,
309 roles,
310 Utc::now(),
311 id.as_str()
312 )
313 .fetch_optional(&*self.write_pool)
314 .await?
315 .ok_or_else(|| UserError::NotFound(id.clone()))?;
316
317 Ok(row)
318 }
319
320 pub async fn delete(&self, id: &UserId) -> Result<()> {
321 let result = sqlx::query!(r#"DELETE FROM users WHERE id = $1"#, id.as_str())
322 .execute(&*self.write_pool)
323 .await?;
324
325 if result.rows_affected() == 0 {
326 return Err(UserError::NotFound(id.clone()));
327 }
328
329 Ok(())
330 }
331
332 pub async fn cleanup_old_anonymous(&self, days: i32) -> Result<u64> {
333 let cutoff = Utc::now() - Duration::days(i64::from(days));
334 let anonymous_role = UserRole::Anonymous.as_str();
335 let result = sqlx::query!(
336 r#"
337 DELETE FROM users u
338 WHERE $1 = ANY(u.roles)
339 AND u.created_at < $2
340 AND NOT EXISTS (
341 SELECT 1
342 FROM user_sessions s
343 WHERE s.user_id = u.id
344 AND s.ended_at IS NULL
345 )
346 "#,
347 anonymous_role,
348 cutoff
349 )
350 .execute(&*self.write_pool)
351 .await?;
352
353 Ok(result.rows_affected())
354 }
355}