Skip to main content

systemprompt_users/repository/user/
updates.rs

1//! User record field updates, role assignment, and anonymous-account cleanup.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use chrono::{Duration, Utc};
7use systemprompt_identifiers::UserId;
8
9use super::operations::UpdateUserParams;
10use crate::error::{Result, UserError};
11use crate::models::{User, UserRole, UserStatus};
12use crate::repository::UserRepository;
13
14impl UserRepository {
15    pub async fn update_email(&self, id: &UserId, email: &str) -> Result<User> {
16        let row = sqlx::query_as!(
17            User,
18            r#"
19            UPDATE users
20            SET email = $1, email_verified = false, updated_at = $2
21            WHERE id = $3
22            RETURNING id, name, email, full_name, display_name, status, email_verified,
23                      roles, avatar_url, is_bot, is_scanner, created_at, updated_at
24            "#,
25            email,
26            Utc::now(),
27            id.as_str()
28        )
29        .fetch_optional(&*self.write_pool)
30        .await?
31        .ok_or_else(|| UserError::NotFound(id.clone()))?;
32
33        Ok(row)
34    }
35
36    pub async fn update_full_name(&self, id: &UserId, full_name: &str) -> Result<User> {
37        let row = sqlx::query_as!(
38            User,
39            r#"
40            UPDATE users
41            SET full_name = $1, updated_at = $2
42            WHERE id = $3
43            RETURNING id, name, email, full_name, display_name, status, email_verified,
44                      roles, avatar_url, is_bot, is_scanner, created_at, updated_at
45            "#,
46            full_name,
47            Utc::now(),
48            id.as_str()
49        )
50        .fetch_optional(&*self.write_pool)
51        .await?
52        .ok_or_else(|| UserError::NotFound(id.clone()))?;
53
54        Ok(row)
55    }
56
57    pub async fn update_status(&self, id: &UserId, status: UserStatus) -> Result<User> {
58        let row = sqlx::query_as!(
59            User,
60            r#"
61            UPDATE users
62            SET status = $1, updated_at = $2
63            WHERE id = $3
64            RETURNING id, name, email, full_name, display_name, status, email_verified,
65                      roles, avatar_url, is_bot, is_scanner, created_at, updated_at
66            "#,
67            status.as_str(),
68            Utc::now(),
69            id.as_str()
70        )
71        .fetch_optional(&*self.write_pool)
72        .await?
73        .ok_or_else(|| UserError::NotFound(id.clone()))?;
74
75        Ok(row)
76    }
77
78    pub async fn update_email_verified(&self, id: &UserId, verified: bool) -> Result<User> {
79        let row = sqlx::query_as!(
80            User,
81            r#"
82            UPDATE users
83            SET email_verified = $1, updated_at = $2
84            WHERE id = $3
85            RETURNING id, name, email, full_name, display_name, status, email_verified,
86                      roles, avatar_url, is_bot, is_scanner, created_at, updated_at
87            "#,
88            verified,
89            Utc::now(),
90            id.as_str()
91        )
92        .fetch_optional(&*self.write_pool)
93        .await?
94        .ok_or_else(|| UserError::NotFound(id.clone()))?;
95
96        Ok(row)
97    }
98
99    pub async fn update_display_name(&self, id: &UserId, display_name: &str) -> Result<User> {
100        let row = sqlx::query_as!(
101            User,
102            r#"
103            UPDATE users
104            SET display_name = $1, updated_at = $2
105            WHERE id = $3
106            RETURNING id, name, email, full_name, display_name, status, email_verified,
107                      roles, avatar_url, is_bot, is_scanner, created_at, updated_at
108            "#,
109            display_name,
110            Utc::now(),
111            id.as_str()
112        )
113        .fetch_optional(&*self.write_pool)
114        .await?
115        .ok_or_else(|| UserError::NotFound(id.clone()))?;
116
117        Ok(row)
118    }
119
120    pub async fn update_all_fields(
121        &self,
122        id: &UserId,
123        params: UpdateUserParams<'_>,
124    ) -> Result<User> {
125        let row = sqlx::query_as!(
126            User,
127            r#"
128            UPDATE users
129            SET email = $1, full_name = $2, display_name = $3, status = $4, updated_at = $5
130            WHERE id = $6
131            RETURNING id, name, email, full_name, display_name, status, email_verified,
132                      roles, avatar_url, is_bot, is_scanner, created_at, updated_at
133            "#,
134            params.email,
135            params.full_name,
136            params.display_name,
137            params.status.as_str(),
138            Utc::now(),
139            id.as_str()
140        )
141        .fetch_optional(&*self.write_pool)
142        .await?
143        .ok_or_else(|| UserError::NotFound(id.clone()))?;
144
145        Ok(row)
146    }
147
148    pub async fn assign_roles(&self, id: &UserId, roles: &[String]) -> Result<User> {
149        let row = sqlx::query_as!(
150            User,
151            r#"
152            UPDATE users
153            SET roles = $1, updated_at = $2
154            WHERE id = $3
155            RETURNING id, name, email, full_name, display_name, status, email_verified,
156                      roles, avatar_url, is_bot, is_scanner, created_at, updated_at
157            "#,
158            roles,
159            Utc::now(),
160            id.as_str()
161        )
162        .fetch_optional(&*self.write_pool)
163        .await?
164        .ok_or_else(|| UserError::NotFound(id.clone()))?;
165
166        Ok(row)
167    }
168
169    pub async fn delete(&self, id: &UserId) -> Result<()> {
170        let result = sqlx::query!(r#"DELETE FROM users WHERE id = $1"#, id.as_str())
171            .execute(&*self.write_pool)
172            .await?;
173
174        if result.rows_affected() == 0 {
175            return Err(UserError::NotFound(id.clone()));
176        }
177
178        Ok(())
179    }
180
181    pub async fn cleanup_old_anonymous(&self, days: i32) -> Result<u64> {
182        let cutoff = Utc::now() - Duration::days(i64::from(days));
183        let anonymous_role = UserRole::Anonymous.as_str();
184        let result = sqlx::query!(
185            r#"
186            DELETE FROM users u
187            WHERE $1 = ANY(u.roles)
188              AND u.created_at < $2
189              AND NOT EXISTS (
190                  SELECT 1
191                  FROM user_sessions s
192                  WHERE s.user_id = u.id
193                    AND s.ended_at IS NULL
194              )
195            "#,
196            anonymous_role,
197            cutoff
198        )
199        .execute(&*self.write_pool)
200        .await?;
201
202        Ok(result.rows_affected())
203    }
204
205    pub async fn count_old_anonymous(&self, days: i32) -> Result<i64> {
206        let cutoff = Utc::now() - Duration::days(i64::from(days));
207        let anonymous_role = UserRole::Anonymous.as_str();
208        let count = sqlx::query_scalar!(
209            r#"
210            SELECT COUNT(*) as "count!"
211            FROM users u
212            WHERE $1 = ANY(u.roles)
213              AND u.created_at < $2
214              AND NOT EXISTS (
215                  SELECT 1
216                  FROM user_sessions s
217                  WHERE s.user_id = u.id
218                    AND s.ended_at IS NULL
219              )
220            "#,
221            anonymous_role,
222            cutoff
223        )
224        .fetch_one(&*self.write_pool)
225        .await?;
226
227        Ok(count)
228    }
229}