systemprompt_cli/commands/cloud/auth/
login.rs1use anyhow::{Result, anyhow};
11use systemprompt_cloud::{
12 CloudApiClient, CloudCredentials, CloudPath, OAuthTemplates, TenantInfo, TenantStore,
13 UserMeResponse, get_cloud_paths, run_oauth_flow,
14};
15use systemprompt_logging::CliService;
16use systemprompt_models::modules::ApiPaths;
17
18use crate::cli_settings::CliConfig;
19use crate::cloud::templates::{AUTH_ERROR_HTML, AUTH_SUCCESS_HTML};
20use crate::cloud::types::{
21 LoginCustomerInfo, LoginOutput, LoginTenantInfo, LoginUserInfo, TenantPlanInfo,
22};
23use crate::cloud::{Environment, OAuthProvider};
24use crate::interactive::Prompter;
25use crate::shared::CommandOutput;
26
27pub(super) async fn execute(
28 environment: Environment,
29 prompter: &dyn Prompter,
30 config: &CliConfig,
31) -> Result<CommandOutput> {
32 if !config.is_interactive() {
33 return Err(anyhow!(
34 "OAuth login requires interactive mode.\n\nAlternatives:\n- Set \
35 SYSTEMPROMPT_CLOUD_TOKEN environment variable"
36 ));
37 }
38
39 let api_url = environment.api_url();
40
41 CliService::section("systemprompt.io Cloud Login");
42 CliService::info(&format!("Environment: {:?}", environment));
43
44 let cloud_paths = get_cloud_paths();
45
46 if cloud_paths.exists(CloudPath::Credentials) {
47 let creds_path = cloud_paths.resolve(CloudPath::Credentials);
48 let existing = CloudCredentials::load_from_path(&creds_path)?;
49 CliService::warning(&format!("Already logged in as: {}", existing.user_email));
50 CliService::info("Re-authenticating...");
51 }
52
53 let providers = [OAuthProvider::Github, OAuthProvider::Google];
54 let provider_names: Vec<&str> = providers.iter().map(OAuthProvider::display_name).collect();
55
56 let provider_options: Vec<String> = provider_names.iter().map(|s| (*s).to_owned()).collect();
57 let selection = prompter.select("Select authentication provider", &provider_options)?;
58
59 let provider = providers[selection];
60
61 let templates = OAuthTemplates {
62 success_html: AUTH_SUCCESS_HTML,
63 error_html: AUTH_ERROR_HTML,
64 };
65 let token = run_oauth_flow(api_url, provider, templates).await?;
66
67 complete_login(api_url, token, config).await
68}
69
70pub async fn complete_login(
71 api_url: &str,
72 token: String,
73 config: &CliConfig,
74) -> Result<CommandOutput> {
75 let cloud_paths = get_cloud_paths();
76
77 let spinner = CliService::spinner("Verifying token...");
78 let client = CloudApiClient::new(api_url, &token)?;
79 let response = client.get_user().await?;
80 spinner.finish_and_clear();
81
82 let creds = CloudCredentials::new(
83 systemprompt_identifiers::CloudAuthToken::new(token),
84 api_url.to_owned(),
85 systemprompt_identifiers::Email::new(response.user.email.clone()),
86 );
87
88 let save_path = cloud_paths.resolve(CloudPath::Credentials);
89 creds.save_to_path(&save_path)?;
90 CliService::key_value("Credentials saved to", &save_path.display().to_string());
91
92 let tenant_store = TenantStore::from_tenant_infos(&response.tenants);
93 let tenants_path = cloud_paths.resolve(CloudPath::Tenants);
94 tenant_store.save_to_path(&tenants_path)?;
95 CliService::key_value("Tenants synced to", &tenants_path.display().to_string());
96
97 CliService::success("Logged in successfully");
98
99 let activity_user_id = systemprompt_identifiers::UserId::new(response.user.id.clone());
100 if let Err(e) = client
101 .report_activity(ApiPaths::ACTIVITY_EVENT_LOGIN, &activity_user_id)
102 .await
103 {
104 tracing::debug!(error = %e, "Failed to report login activity");
105 }
106
107 CliService::section("Syncing Admin User to Profiles");
108 if let Some(cloud_user) = crate::cloud::sync::admin_user::CloudUser::from_credentials()? {
109 let verbose = config.should_show_verbose();
110 let results =
111 crate::cloud::sync::admin_user::sync_admin_to_all_profiles(&cloud_user, verbose).await;
112 crate::cloud::sync::admin_user::print_sync_results(&results);
113 } else {
114 CliService::warning("Could not load cloud user for admin sync");
115 }
116
117 print_login_result(&response);
118
119 let output = build_login_output(&response, &save_path, &tenants_path);
120
121 Ok(CommandOutput::card_value("Cloud Login", &output).with_skip_render())
122}
123
124fn build_login_output(
125 response: &UserMeResponse,
126 credentials_path: &std::path::Path,
127 tenants_path: &std::path::Path,
128) -> LoginOutput {
129 let user = LoginUserInfo {
130 id: response.user.id.as_str().to_owned(),
131 email: response.user.email.clone(),
132 name: response.user.name.clone(),
133 };
134
135 let customer = response
136 .customer
137 .as_ref()
138 .map(|c| LoginCustomerInfo { id: c.id.clone() });
139
140 let tenants: Vec<LoginTenantInfo> = response
141 .tenants
142 .iter()
143 .map(|t| LoginTenantInfo {
144 id: t.id.clone(),
145 name: t.name.clone(),
146 subscription_status: t.subscription_status.map(|s| format!("{s:?}")),
147 plan: t.plan.as_ref().map(|p| TenantPlanInfo {
148 name: p.name.clone(),
149 memory_mb: p.memory_mb,
150 volume_gb: p.volume_gb,
151 }),
152 region: t.region.clone(),
153 hostname: t.hostname.clone(),
154 })
155 .collect();
156
157 LoginOutput {
158 user,
159 customer,
160 tenants,
161 credentials_path: credentials_path.display().to_string(),
162 tenants_path: tenants_path.display().to_string(),
163 }
164}
165
166fn print_login_result(response: &UserMeResponse) {
167 CliService::section("User");
168 CliService::key_value("Email", &response.user.email);
169 if let Some(name) = &response.user.name {
170 CliService::key_value("Name", name);
171 }
172 CliService::key_value("ID", response.user.id.as_str());
173
174 if let Some(customer) = &response.customer {
175 CliService::section("Customer");
176 CliService::key_value("ID", &customer.id);
177 }
178
179 print_tenants(&response.tenants);
180}
181
182fn print_tenants(tenants: &[TenantInfo]) {
183 if tenants.is_empty() {
184 CliService::info("No cloud tenants found.");
185 CliService::info(
186 "Run 'systemprompt cloud tenant create' (or 'just tenant') to create a local tenant.",
187 );
188 return;
189 }
190
191 CliService::section("Available Tenants");
192 for tenant in tenants {
193 let status_str = tenant
194 .subscription_status
195 .map_or_else(|| "Unknown".to_owned(), |s| format!("{s:?}"));
196 CliService::key_value(&tenant.name, &status_str);
197 if let Some(plan) = &tenant.plan {
198 CliService::info(&format!(
199 " Plan: {} ({}MB RAM, {}GB storage)",
200 plan.name, plan.memory_mb, plan.volume_gb
201 ));
202 }
203 if let Some(region) = &tenant.region {
204 CliService::info(&format!(" Region: {region}"));
205 }
206 if let Some(hostname) = &tenant.hostname {
207 CliService::info(&format!(" URL: https://{hostname}"));
208 }
209 }
210 CliService::info("");
211 CliService::info(
212 "Run 'systemprompt cloud tenant create' (or 'just tenant') to add a local tenant,",
213 );
214 CliService::info("then 'systemprompt cloud profile create <name>' to create a profile.");
215}