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