Skip to main content

systemprompt_cli/commands/cloud/auth/
login.rs

1//! Interactive OAuth login against systemprompt.io Cloud.
2//!
3//! Runs the provider-selection prompt and browser OAuth flow, then persists
4//! the returned credentials and tenant list to the local cloud config paths.
5//!
6//! Copyright (c) systemprompt.io — Business Source License 1.1.
7//! See <https://systemprompt.io> for licensing details.
8
9use 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}