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_anonymous(&self, fingerprint: &str) -> Result<User> {
63 let email = format!("{}@anonymous.local", fingerprint);
64
65 if let Some(existing) = sqlx::query_as!(
68 User,
69 r#"
70 SELECT id, name, email, full_name, display_name, status, email_verified,
71 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
72 FROM users
73 WHERE email = $1
74 "#,
75 email
76 )
77 .fetch_optional(&*self.pool)
78 .await?
79 {
80 return Ok(existing);
81 }
82
83 let user_id = uuid::Uuid::new_v4();
85 let id = UserId::new(user_id.to_string());
86 let name = format!("anonymous_{}", &user_id.to_string()[..8]);
87 let now = Utc::now();
88 let status = UserStatus::Active.as_str();
89 let role = UserRole::Anonymous.as_str();
90
91 let row = sqlx::query_as!(
92 User,
93 r#"
94 INSERT INTO users (
95 id, name, email, status, email_verified, roles,
96 is_bot, created_at, updated_at
97 )
98 VALUES ($1, $2, $3, $4, false, ARRAY[$5]::TEXT[], false, $6, $6)
99 ON CONFLICT (email) DO UPDATE SET updated_at = $6
100 RETURNING id, name, email, full_name, display_name, status, email_verified,
101 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
102 "#,
103 id.as_str(),
104 name,
105 email,
106 status,
107 role,
108 now
109 )
110 .fetch_one(&*self.write_pool)
111 .await?;
112
113 Ok(row)
114 }
115
116 pub async fn update_email(&self, id: &UserId, email: &str) -> Result<User> {
117 let row = sqlx::query_as!(
118 User,
119 r#"
120 UPDATE users
121 SET email = $1, email_verified = false, updated_at = $2
122 WHERE id = $3
123 RETURNING id, name, email, full_name, display_name, status, email_verified,
124 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
125 "#,
126 email,
127 Utc::now(),
128 id.as_str()
129 )
130 .fetch_optional(&*self.write_pool)
131 .await?
132 .ok_or_else(|| UserError::NotFound(id.clone()))?;
133
134 Ok(row)
135 }
136
137 pub async fn update_full_name(&self, id: &UserId, full_name: &str) -> Result<User> {
138 let row = sqlx::query_as!(
139 User,
140 r#"
141 UPDATE users
142 SET full_name = $1, updated_at = $2
143 WHERE id = $3
144 RETURNING id, name, email, full_name, display_name, status, email_verified,
145 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
146 "#,
147 full_name,
148 Utc::now(),
149 id.as_str()
150 )
151 .fetch_optional(&*self.write_pool)
152 .await?
153 .ok_or_else(|| UserError::NotFound(id.clone()))?;
154
155 Ok(row)
156 }
157
158 pub async fn update_status(&self, id: &UserId, status: UserStatus) -> Result<User> {
159 let row = sqlx::query_as!(
160 User,
161 r#"
162 UPDATE users
163 SET status = $1, updated_at = $2
164 WHERE id = $3
165 RETURNING id, name, email, full_name, display_name, status, email_verified,
166 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
167 "#,
168 status.as_str(),
169 Utc::now(),
170 id.as_str()
171 )
172 .fetch_optional(&*self.write_pool)
173 .await?
174 .ok_or_else(|| UserError::NotFound(id.clone()))?;
175
176 Ok(row)
177 }
178
179 pub async fn update_email_verified(&self, id: &UserId, verified: bool) -> Result<User> {
180 let row = sqlx::query_as!(
181 User,
182 r#"
183 UPDATE users
184 SET email_verified = $1, updated_at = $2
185 WHERE id = $3
186 RETURNING id, name, email, full_name, display_name, status, email_verified,
187 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
188 "#,
189 verified,
190 Utc::now(),
191 id.as_str()
192 )
193 .fetch_optional(&*self.write_pool)
194 .await?
195 .ok_or_else(|| UserError::NotFound(id.clone()))?;
196
197 Ok(row)
198 }
199
200 pub async fn update_display_name(&self, id: &UserId, display_name: &str) -> Result<User> {
201 let row = sqlx::query_as!(
202 User,
203 r#"
204 UPDATE users
205 SET display_name = $1, updated_at = $2
206 WHERE id = $3
207 RETURNING id, name, email, full_name, display_name, status, email_verified,
208 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
209 "#,
210 display_name,
211 Utc::now(),
212 id.as_str()
213 )
214 .fetch_optional(&*self.write_pool)
215 .await?
216 .ok_or_else(|| UserError::NotFound(id.clone()))?;
217
218 Ok(row)
219 }
220
221 pub async fn update_all_fields(
222 &self,
223 id: &UserId,
224 params: UpdateUserParams<'_>,
225 ) -> Result<User> {
226 let row = sqlx::query_as!(
227 User,
228 r#"
229 UPDATE users
230 SET email = $1, full_name = $2, display_name = $3, status = $4, updated_at = $5
231 WHERE id = $6
232 RETURNING id, name, email, full_name, display_name, status, email_verified,
233 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
234 "#,
235 params.email,
236 params.full_name,
237 params.display_name,
238 params.status.as_str(),
239 Utc::now(),
240 id.as_str()
241 )
242 .fetch_optional(&*self.write_pool)
243 .await?
244 .ok_or_else(|| UserError::NotFound(id.clone()))?;
245
246 Ok(row)
247 }
248
249 pub async fn assign_roles(&self, id: &UserId, roles: &[String]) -> Result<User> {
250 let row = sqlx::query_as!(
251 User,
252 r#"
253 UPDATE users
254 SET roles = $1, updated_at = $2
255 WHERE id = $3
256 RETURNING id, name, email, full_name, display_name, status, email_verified,
257 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
258 "#,
259 roles,
260 Utc::now(),
261 id.as_str()
262 )
263 .fetch_optional(&*self.write_pool)
264 .await?
265 .ok_or_else(|| UserError::NotFound(id.clone()))?;
266
267 Ok(row)
268 }
269
270 pub async fn delete(&self, id: &UserId) -> Result<()> {
271 let result = sqlx::query!(r#"DELETE FROM users WHERE id = $1"#, id.as_str())
272 .execute(&*self.write_pool)
273 .await?;
274
275 if result.rows_affected() == 0 {
276 return Err(UserError::NotFound(id.clone()));
277 }
278
279 Ok(())
280 }
281
282 pub async fn cleanup_old_anonymous(&self, days: i32) -> Result<u64> {
283 let cutoff = Utc::now() - Duration::days(i64::from(days));
284 let anonymous_role = UserRole::Anonymous.as_str();
285 let result = sqlx::query!(
286 r#"
287 DELETE FROM users u
288 WHERE $1 = ANY(u.roles)
289 AND u.created_at < $2
290 AND NOT EXISTS (
291 SELECT 1
292 FROM user_sessions s
293 WHERE s.user_id = u.id
294 AND s.ended_at IS NULL
295 )
296 "#,
297 anonymous_role,
298 cutoff
299 )
300 .execute(&*self.write_pool)
301 .await?;
302
303 Ok(result.rows_affected())
304 }
305}