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