Skip to main content

systemprompt_users/repository/user/
operations.rs

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.write_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.write_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.write_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.write_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.write_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.write_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.write_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.write_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.write_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.write_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.write_pool)
276        .await?;
277
278        Ok(result.rows_affected())
279    }
280}