Skip to main content

systemprompt_cli/commands/cloud/auth/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, UserRepository, 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_repository = match UserRepository::new(&db) {
101        Ok(repository) => Arc::new(repository),
102        Err(e) => {
103            return SyncResult::Failed {
104                profile: profile_name.to_owned(),
105                error: format!("Failed to create user repository: {}", e),
106            };
107        },
108    };
109    let user_service = UserService::new(user_repository);
110
111    let admin_service = UserAdminService::new(user_service.clone());
112
113    match user_service.find_by_email(&user.email).await {
114        Ok(Some(_)) => promote_existing_user(&admin_service, &user.email, profile_name).await,
115        Ok(None) => {
116            create_and_promote_user(&user_service, &admin_service, user, profile_name).await
117        },
118        Err(e) => SyncResult::Failed {
119            profile: profile_name.to_owned(),
120            error: format!("Failed to check existing user: {}", e),
121        },
122    }
123}
124
125pub async fn sync_admin_to_all_profiles(user: &CloudUser, verbose: bool) -> Vec<SyncResult> {
126    let discovery = match discover_profiles() {
127        Ok(d) => d,
128        Err(e) => {
129            CliService::warning(&format!("Failed to discover profiles: {}", e));
130            return Vec::new();
131        },
132    };
133
134    print_discovery_summary(&discovery, verbose);
135
136    if discovery.profiles.is_empty() {
137        if discovery.skipped.is_empty() {
138            CliService::info("No profiles found to sync admin user.");
139        } else {
140            CliService::warning(
141                "No profiles available for sync (all skipped due to configuration issues).",
142            );
143        }
144        return Vec::new();
145    }
146
147    let mut results = Vec::new();
148
149    for profile in discovery.profiles {
150        let Some(database_url) = profile.database_url.as_deref() else {
151            results.push(SyncResult::Failed {
152                profile: profile.name.clone(),
153                error: "Missing database_url".to_owned(),
154            });
155            continue;
156        };
157        let result = sync_admin_to_database(user, database_url, &profile.name).await;
158        results.push(result);
159    }
160
161    results
162}
163
164pub fn print_sync_results(results: &[SyncResult]) {
165    for result in results {
166        match result {
167            SyncResult::Created { email, profile } => {
168                CliService::success(&format!(
169                    "Created admin user '{}' in profile '{}'",
170                    email, profile
171                ));
172            },
173            SyncResult::Promoted { email, profile } => {
174                CliService::success(&format!(
175                    "Promoted existing user '{}' to admin in profile '{}'",
176                    email, profile
177                ));
178            },
179            SyncResult::AlreadyAdmin { email, profile } => {
180                CliService::info(&format!(
181                    "User '{}' is already admin in profile '{}'",
182                    email, profile
183                ));
184            },
185            SyncResult::ConnectionFailed { profile, error } => {
186                CliService::warning(&format!(
187                    "Could not connect to profile '{}': {}",
188                    profile, error
189                ));
190            },
191            SyncResult::Failed { profile, error } => {
192                CliService::warning(&format!(
193                    "Failed to sync admin to profile '{}': {}",
194                    profile, error
195                ));
196            },
197        }
198    }
199}