systemprompt_users/repository/user/
declared.rs1use chrono::Utc;
15use sqlx::PgConnection;
16use systemprompt_identifiers::UserId;
17
18use crate::error::{Result, UserError};
19use crate::models::{UserStatus, normalise_email};
20use crate::repository::UserRepository;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct DeclaredUserRow {
24 pub email: String,
25 pub name: String,
26 pub account_kind: String,
27 pub roles: Vec<String>,
28 pub status: String,
29}
30
31#[derive(Debug, Clone, Copy)]
32pub struct DeclaredUserWrite<'a> {
33 pub email: &'a str,
34 pub name: &'a str,
35 pub account_kind: &'a str,
36 pub roles: &'a [String],
37 pub status: &'a str,
38 pub source: &'a str,
39}
40
41impl UserRepository {
42 pub async fn list_declared_on(conn: &mut PgConnection) -> Result<Vec<DeclaredUserRow>> {
43 let rows = sqlx::query_as!(
44 DeclaredUserRow,
45 r#"
46 SELECT email, name, account_kind, roles, status
47 FROM users
48 WHERE declared_source IS NOT NULL
49 ORDER BY email
50 "#,
51 )
52 .fetch_all(conn)
53 .await?;
54 Ok(rows)
55 }
56
57 pub async fn upsert_declared_on(
58 conn: &mut PgConnection,
59 user: &DeclaredUserWrite<'_>,
60 ) -> Result<UserId> {
61 let id = UserId::generate();
62 let email = normalise_email(user.email);
63 let verified = user.account_kind == crate::models::AccountKind::Service.as_str();
64 let written = sqlx::query_scalar!(
65 r#"
66 INSERT INTO users (
67 id, name, email, display_name, status, email_verified, roles, is_bot,
68 account_kind, declared_source, created_at, updated_at
69 )
70 VALUES ($1, $2, $3, $2, $4, $5, $6, false, $7, $8, $9, $9)
71 ON CONFLICT (email) DO UPDATE
72 SET name = EXCLUDED.name,
73 status = EXCLUDED.status,
74 roles = EXCLUDED.roles,
75 declared_source = EXCLUDED.declared_source,
76 updated_at = EXCLUDED.updated_at
77 WHERE users.account_kind = EXCLUDED.account_kind
78 RETURNING id
79 "#,
80 id.as_str(),
81 user.name,
82 email,
83 user.status,
84 verified,
85 user.roles,
86 user.account_kind,
87 user.source,
88 Utc::now()
89 )
90 .fetch_optional(conn)
91 .await?;
92 written
93 .map(UserId::new)
94 .ok_or(UserError::AccountKindMismatch(email))
95 }
96
97 pub async fn release_declared_on(conn: &mut PgConnection, email: &str) -> Result<()> {
98 sqlx::query!(
99 r#"
100 UPDATE users
101 SET declared_source = NULL, status = $2, updated_at = $3
102 WHERE email = $1 AND declared_source IS NOT NULL
103 "#,
104 normalise_email(email),
105 UserStatus::Inactive.as_str(),
106 Utc::now()
107 )
108 .execute(conn)
109 .await?;
110 Ok(())
111 }
112
113 pub async fn find_id_by_email_on(
114 conn: &mut PgConnection,
115 email: &str,
116 ) -> Result<Option<UserId>> {
117 let id = sqlx::query_scalar!(
118 "SELECT id FROM users WHERE email = $1",
119 normalise_email(email)
120 )
121 .fetch_optional(conn)
122 .await?;
123 Ok(id.map(UserId::new))
124 }
125
126 pub async fn emails_by_ids_on(
127 conn: &mut PgConnection,
128 ids: &[UserId],
129 ) -> Result<Vec<(UserId, String)>> {
130 let ids: Vec<String> = ids.iter().map(|id| id.as_str().to_owned()).collect();
131 let rows = sqlx::query!(
132 "SELECT id, email FROM users WHERE id = ANY($1::text[])",
133 &ids
134 )
135 .fetch_all(conn)
136 .await?;
137 Ok(rows
138 .into_iter()
139 .map(|row| (UserId::new(row.id), row.email))
140 .collect())
141 }
142}