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;
10use crate::models::{User, UserRole, 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::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        let email = normalise_email(email);
35
36        let row = sqlx::query_as!(
37            User,
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
60        Ok(row)
61    }
62
63    pub async fn create_if_absent(
64        &self,
65        name: &str,
66        email: &str,
67        full_name: Option<&str>,
68        display_name: Option<&str>,
69    ) -> Result<Option<User>> {
70        let now = Utc::now();
71        let id = UserId::new(uuid::Uuid::new_v4().to_string());
72        let display_name_val = display_name.or(full_name);
73        let status = UserStatus::Active.as_str();
74        let role = UserRole::User.as_str();
75        let email = normalise_email(email);
76
77        let row = sqlx::query_as!(
78            User,
79            r#"
80            INSERT INTO users (
81                id, name, email, full_name, display_name,
82                status, email_verified, roles, is_bot,
83                created_at, updated_at
84            )
85            VALUES ($1, $2, $3, $4, $5, $6, false, ARRAY[$7]::TEXT[], false, $8, $8)
86            ON CONFLICT DO NOTHING
87            RETURNING id, name, email, full_name, display_name, status, email_verified,
88                      roles, avatar_url, is_bot, is_scanner, created_at, updated_at
89            "#,
90            id.as_str(),
91            name,
92            email,
93            full_name,
94            display_name_val,
95            status,
96            role,
97            now
98        )
99        .fetch_optional(&*self.write_pool)
100        .await?;
101
102        Ok(row)
103    }
104
105    pub async fn create_anonymous(&self, fingerprint: &str) -> Result<User> {
106        let email = normalise_email(&format!("{}@anonymous.local", fingerprint));
107
108        if let Some(existing) = sqlx::query_as!(
109            User,
110            r#"
111            SELECT id, name, email, full_name, display_name, status, email_verified,
112                   roles, avatar_url, is_bot, is_scanner, created_at, updated_at
113            FROM users
114            WHERE email = $1
115            "#,
116            email
117        )
118        .fetch_optional(&*self.pool)
119        .await?
120        {
121            return Ok(existing);
122        }
123
124        let user_id = uuid::Uuid::new_v4();
125        let id = UserId::new(user_id.to_string());
126        let name = format!("anonymous_{}", &user_id.to_string()[..8]);
127        let now = Utc::now();
128        let status = UserStatus::Active.as_str();
129        let role = UserRole::Anonymous.as_str();
130
131        let row = sqlx::query_as!(
132            User,
133            r#"
134            INSERT INTO users (
135                id, name, email, status, email_verified, roles,
136                is_bot, created_at, updated_at
137            )
138            VALUES ($1, $2, $3, $4, false, ARRAY[$5]::TEXT[], false, $6, $6)
139            ON CONFLICT (email) DO UPDATE SET updated_at = $6
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            id.as_str(),
144            name,
145            email,
146            status,
147            role,
148            now
149        )
150        .fetch_one(&*self.write_pool)
151        .await?;
152
153        Ok(row)
154    }
155}