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