Skip to main content

systemprompt_users/repository/user/
operations.rs

1//! User row creation.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use chrono::Utc;
7use systemprompt_identifiers::UserId;
8
9use crate::error::{Result, UserError};
10use crate::models::{User, UserRole, UserRow, UserStatus, normalise_email};
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::generate();
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        let email = normalise_email(email);
35
36        let row = sqlx::query_as!(
37            UserRow,
38            r#"
39            INSERT INTO users (
40                id, name, email, full_name, display_name,
41                status, email_verified, roles, is_bot,
42                created_at, updated_at
43            )
44            VALUES ($1, $2, $3, $4, $5, $6, false, ARRAY[$7]::TEXT[], false, $8, $8)
45            RETURNING id, name, email, full_name, display_name, status, email_verified,
46                      roles, avatar_url, is_bot, is_scanner, created_at, updated_at
47            "#,
48            id.as_str(),
49            name,
50            email,
51            full_name,
52            display_name_val,
53            status,
54            role,
55            now
56        )
57        .fetch_one(&*self.write_pool)
58        .await
59        .map_err(UserError::from)
60        .and_then(User::try_from)?;
61
62        Ok(row)
63    }
64
65    pub async fn create_if_absent(
66        &self,
67        name: &str,
68        email: &str,
69        full_name: Option<&str>,
70        display_name: Option<&str>,
71    ) -> Result<Option<User>> {
72        let now = Utc::now();
73        let id = UserId::generate();
74        let display_name_val = display_name.or(full_name);
75        let status = UserStatus::Active.as_str();
76        let role = UserRole::User.as_str();
77        let email = normalise_email(email);
78
79        let row = sqlx::query_as!(
80            UserRow,
81            r#"
82            INSERT INTO users (
83                id, name, email, full_name, display_name,
84                status, email_verified, roles, is_bot,
85                created_at, updated_at
86            )
87            VALUES ($1, $2, $3, $4, $5, $6, false, ARRAY[$7]::TEXT[], false, $8, $8)
88            ON CONFLICT DO NOTHING
89            RETURNING id, name, email, full_name, display_name, status, email_verified,
90                      roles, avatar_url, is_bot, is_scanner, created_at, updated_at
91            "#,
92            id.as_str(),
93            name,
94            email,
95            full_name,
96            display_name_val,
97            status,
98            role,
99            now
100        )
101        .fetch_optional(&*self.write_pool)
102        .await?
103        .map(User::try_from)
104        .transpose()?;
105
106        Ok(row)
107    }
108
109    pub async fn create_anonymous(&self, fingerprint: &str) -> Result<User> {
110        let email = normalise_email(&format!("{}@anonymous.local", fingerprint));
111
112        if let Some(existing) = sqlx::query_as!(
113            UserRow,
114            r#"
115            SELECT id, name, email, full_name, display_name, status, email_verified,
116                   roles, avatar_url, is_bot, is_scanner, created_at, updated_at
117            FROM users
118            WHERE email = $1
119            "#,
120            email
121        )
122        .fetch_optional(&*self.pool)
123        .await?
124        .map(User::try_from)
125        .transpose()?
126        {
127            return Ok(existing);
128        }
129
130        let id = UserId::generate();
131        let name = format!("anonymous_{}", &id.as_str()[..8]);
132        let now = Utc::now();
133        let status = UserStatus::Active.as_str();
134        let role = UserRole::Anonymous.as_str();
135
136        let row = sqlx::query_as!(
137            UserRow,
138            r#"
139            INSERT INTO users (
140                id, name, email, status, email_verified, roles,
141                is_bot, created_at, updated_at
142            )
143            VALUES ($1, $2, $3, $4, false, ARRAY[$5]::TEXT[], false, $6, $6)
144            ON CONFLICT (email) DO UPDATE SET updated_at = $6
145            RETURNING id, name, email, full_name, display_name, status, email_verified,
146                      roles, avatar_url, is_bot, is_scanner, created_at, updated_at
147            "#,
148            id.as_str(),
149            name,
150            email,
151            status,
152            role,
153            now
154        )
155        .fetch_one(&*self.write_pool)
156        .await
157        .map_err(UserError::from)
158        .and_then(User::try_from)?;
159
160        Ok(row)
161    }
162}