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