Skip to main content

systemprompt_cli/commands/cloud/sync/admin_user/
sync.rs

1//! Admin-user synchronisation against profile databases.
2//!
3//! Connects to each profile's database and either promotes an existing user or
4//! creates and promotes the cloud user to admin, reporting outcomes as
5//! [`SyncResult`] values.
6//!
7//! Copyright (c) systemprompt.io — Business Source License 1.1.
8//! See <https://systemprompt.io> for licensing details.
9
10use std::sync::Arc;
11use systemprompt_database::Database;
12use systemprompt_logging::CliService;
13use systemprompt_users::{PromoteResult, UserAdminService, UserService};
14
15use super::discovery::{discover_profiles, print_discovery_summary};
16use super::types::{CloudUser, SyncResult};
17
18async fn promote_existing_user(
19    admin_service: &UserAdminService,
20    email: &str,
21    profile_name: &str,
22) -> SyncResult {
23    match admin_service.promote_to_admin(email).await {
24        Ok(PromoteResult::Promoted(_, _)) => SyncResult::Promoted {
25            email: email.to_owned(),
26            profile: profile_name.to_owned(),
27        },
28        Ok(PromoteResult::AlreadyAdmin(_)) => SyncResult::AlreadyAdmin {
29            email: email.to_owned(),
30            profile: profile_name.to_owned(),
31        },
32        Ok(PromoteResult::UserNotFound) => SyncResult::Failed {
33            profile: profile_name.to_owned(),
34            error: "User not found after existence check".to_owned(),
35        },
36        Err(e) => SyncResult::Failed {
37            profile: profile_name.to_owned(),
38            error: format!("Promotion failed: {}", e),
39        },
40    }
41}
42
43async fn create_and_promote_user(
44    user_service: &UserService,
45    admin_service: &UserAdminService,
46    user: &CloudUser,
47    profile_name: &str,
48) -> SyncResult {
49    let username = user.username();
50    let display_name = user.name.as_deref();
51
52    match user_service
53        .create(&username, &user.email, display_name, display_name)
54        .await
55    {
56        Ok(_) => match admin_service.promote_to_admin(&user.email).await {
57            Ok(_) => SyncResult::Created {
58                email: user.email.clone(),
59                profile: profile_name.to_owned(),
60            },
61            Err(e) => SyncResult::Failed {
62                profile: profile_name.to_owned(),
63                error: format!("Created user but promotion failed: {}", e),
64            },
65        },
66        Err(e) => SyncResult::Failed {
67            profile: profile_name.to_owned(),
68            error: format!("User creation failed: {}", e),
69        },
70    }
71}
72
73pub async fn sync_admin_to_database(
74    user: &CloudUser,
75    database_url: &str,
76    profile_name: &str,
77) -> SyncResult {
78    let db = match tokio::time::timeout(
79        std::time::Duration::from_secs(5),
80        Database::new_postgres(database_url),
81    )
82    .await
83    {
84        Ok(Ok(db)) => Arc::new(db),
85        Ok(Err(e)) => {
86            return SyncResult::ConnectionFailed {
87                profile: profile_name.to_owned(),
88                error: e.to_string(),
89            };
90        },
91        Err(e) => {
92            tracing::warn!(profile = %profile_name, error = %e, "Database connection timed out");
93            return SyncResult::ConnectionFailed {
94                profile: profile_name.to_owned(),
95                error: "Connection timed out (5s)".to_owned(),
96            };
97        },
98    };
99
100    let user_service = match UserService::new(&db) {
101        Ok(s) => s,
102        Err(e) => {
103            return SyncResult::Failed {
104                profile: profile_name.to_owned(),
105                error: format!("Failed to create user service: {}", e),
106            };
107        },
108    };
109
110    let admin_service = UserAdminService::new(user_service.clone());
111
112    match user_service.find_by_email(&user.email).await {
113        Ok(Some(_)) => promote_existing_user(&admin_service, &user.email, profile_name).await,
114        Ok(None) => {
115            create_and_promote_user(&user_service, &admin_service, user, profile_name).await
116        },
117        Err(e) => SyncResult::Failed {
118            profile: profile_name.to_owned(),
119            error: format!("Failed to check existing user: {}", e),
120        },
121    }
122}
123
124pub async fn sync_admin_to_all_profiles(user: &CloudUser, verbose: bool) -> Vec<SyncResult> {
125    let discovery = match discover_profiles() {
126        Ok(d) => d,
127        Err(e) => {
128            CliService::warning(&format!("Failed to discover profiles: {}", e));
129            return Vec::new();
130        },
131    };
132
133    print_discovery_summary(&discovery, verbose);
134
135    if discovery.profiles.is_empty() {
136        if discovery.skipped.is_empty() {
137            CliService::info("No profiles found to sync admin user.");
138        } else {
139            CliService::warning(
140                "No profiles available for sync (all skipped due to configuration issues).",
141            );
142        }
143        return Vec::new();
144    }
145
146    let mut results = Vec::new();
147
148    for profile in discovery.profiles {
149        let Some(database_url) = profile.database_url.as_deref() else {
150            results.push(SyncResult::Failed {
151                profile: profile.name.clone(),
152                error: "Missing database_url".to_owned(),
153            });
154            continue;
155        };
156        let result = sync_admin_to_database(user, database_url, &profile.name).await;
157        results.push(result);
158    }
159
160    results
161}
162
163pub fn print_sync_results(results: &[SyncResult]) {
164    for result in results {
165        match result {
166            SyncResult::Created { email, profile } => {
167                CliService::success(&format!(
168                    "Created admin user '{}' in profile '{}'",
169                    email, profile
170                ));
171            },
172            SyncResult::Promoted { email, profile } => {
173                CliService::success(&format!(
174                    "Promoted existing user '{}' to admin in profile '{}'",
175                    email, profile
176                ));
177            },
178            SyncResult::AlreadyAdmin { email, profile } => {
179                CliService::info(&format!(
180                    "User '{}' is already admin in profile '{}'",
181                    email, profile
182                ));
183            },
184            SyncResult::ConnectionFailed { profile, error } => {
185                CliService::warning(&format!(
186                    "Could not connect to profile '{}': {}",
187                    profile, error
188                ));
189            },
190            SyncResult::Failed { profile, error } => {
191                CliService::warning(&format!(
192                    "Failed to sync admin to profile '{}': {}",
193                    profile, error
194                ));
195            },
196        }
197    }
198}