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