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