systemprompt_users/repository/user/
service_account.rs1use chrono::Utc;
15use systemprompt_identifiers::UserId;
16
17use crate::error::{Result, UserError};
18use crate::models::{AccountKind, User, UserRole, UserRow, UserStatus, normalise_email};
19use crate::repository::UserRepository;
20
21#[derive(Debug, Clone)]
22pub struct NewServiceAccount {
23 pub name: String,
24 pub email: String,
25 pub created_by: UserId,
26}
27
28#[derive(Debug, Clone)]
29pub struct EnsuredServiceAccount {
30 pub user: User,
31 pub created: bool,
32}
33
34impl UserRepository {
35 pub async fn ensure_service_account(
36 &self,
37 account: NewServiceAccount,
38 ) -> Result<EnsuredServiceAccount> {
39 let id = UserId::generate();
40 let email = normalise_email(&account.email);
41 let inserted = sqlx::query_as!(
42 UserRow,
43 r#"
44 INSERT INTO users (
45 id, name, email, display_name, status, email_verified, roles, is_bot,
46 account_kind, created_at, updated_at
47 )
48 VALUES ($1, $2, $3, $2, $4, true, ARRAY[$5]::TEXT[], false, $6, $7, $7)
49 ON CONFLICT (email) DO NOTHING
50 RETURNING id, name, email, full_name, display_name, status, email_verified,
51 roles, avatar_url, is_bot, is_scanner, created_at, updated_at, account_kind
52 "#,
53 id.as_str(),
54 account.name,
55 email,
56 UserStatus::Active.as_str(),
57 UserRole::User.as_str(),
58 AccountKind::Service.as_str(),
59 Utc::now()
60 )
61 .fetch_optional(&*self.write_pool)
62 .await?;
63 if let Some(row) = inserted {
64 let user = User::try_from(row)?;
65 tracing::info!(
66 user_id = %user.id,
67 created_by = %account.created_by,
68 "service account created"
69 );
70 return Ok(EnsuredServiceAccount {
71 user,
72 created: true,
73 });
74 }
75 let existing = sqlx::query_as!(
76 UserRow,
77 r#"
78 SELECT id, name, email, full_name, display_name, status, email_verified,
79 roles, avatar_url, is_bot, is_scanner, created_at, updated_at, account_kind
80 FROM users
81 WHERE email = $1
82 "#,
83 email
84 )
85 .fetch_one(&*self.write_pool)
86 .await
87 .map_err(UserError::from)
88 .and_then(User::try_from)?;
89 if !existing.is_service_account() || existing.status == UserStatus::Deleted {
90 return Err(UserError::EmailAlreadyExists(email));
91 }
92 Ok(EnsuredServiceAccount {
93 user: existing,
94 created: false,
95 })
96 }
97
98 pub async fn list_service_accounts(&self) -> Result<Vec<User>> {
99 let rows = sqlx::query_as!(
100 UserRow,
101 r#"
102 SELECT id, name, email, full_name, display_name, status, email_verified,
103 roles, avatar_url, is_bot, is_scanner, created_at, updated_at, account_kind
104 FROM users
105 WHERE account_kind = $1 AND status != $2
106 ORDER BY created_at
107 "#,
108 AccountKind::Service.as_str(),
109 UserStatus::Deleted.as_str()
110 )
111 .fetch_all(&*self.pool)
112 .await?;
113 rows.into_iter().map(User::try_from).collect()
114 }
115}