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