systemprompt_cli/commands/cloud/sync/admin_user/
sync.rs1use 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}