systemprompt_users/repository/user/
operations.rs1use 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}