Skip to main content

systemprompt_cli/commands/cloud/tenant/
rotate.rs

1use anyhow::{anyhow, bail, Result};
2use dialoguer::theme::ColorfulTheme;
3use dialoguer::Confirm;
4use systemprompt_cloud::{get_cloud_paths, CloudApiClient, CloudPath, TenantStore, TenantType};
5use systemprompt_logging::CliService;
6
7use super::select::{get_credentials, select_tenant};
8use crate::cli_settings::CliConfig;
9use crate::cloud::types::{RotateCredentialsOutput, RotateSyncTokenOutput};
10use crate::shared::CommandResult;
11
12pub async fn rotate_credentials(
13    id: Option<String>,
14    skip_confirm: bool,
15    config: &CliConfig,
16) -> Result<CommandResult<RotateCredentialsOutput>> {
17    let cloud_paths = get_cloud_paths()?;
18    let tenants_path = cloud_paths.resolve(CloudPath::Tenants);
19    let mut store = TenantStore::load_from_path(&tenants_path).unwrap_or_else(|e| {
20        if !config.is_json_output() {
21            CliService::warning(&format!("Failed to load tenant store: {}", e));
22        }
23        TenantStore::default()
24    });
25
26    let tenant_id = if let Some(id) = id {
27        id
28    } else {
29        if store.tenants.is_empty() {
30            bail!("No tenants configured.");
31        }
32        if skip_confirm {
33            bail!("Tenant ID required in non-interactive mode");
34        }
35        select_tenant(&store.tenants)?.id.clone()
36    };
37
38    let tenant = store
39        .tenants
40        .iter()
41        .find(|t| t.id == tenant_id)
42        .ok_or_else(|| anyhow!("Tenant not found: {}", tenant_id))?;
43
44    if tenant.tenant_type != TenantType::Cloud {
45        bail!("Credential rotation is only available for cloud tenants");
46    }
47
48    if !skip_confirm {
49        let confirm = Confirm::with_theme(&ColorfulTheme::default())
50            .with_prompt(format!(
51                "Rotate database credentials for '{}'? This will generate a new password.",
52                tenant.name
53            ))
54            .default(false)
55            .interact()?;
56
57        if !confirm {
58            if !config.is_json_output() {
59                CliService::info("Cancelled");
60            }
61            let output = RotateCredentialsOutput {
62                tenant_id: tenant_id.clone(),
63                status: "cancelled".to_string(),
64                internal_database_url: String::new(),
65                external_database_url: String::new(),
66            };
67            return Ok(CommandResult::card(output).with_title("Rotate Credentials"));
68        }
69    }
70
71    let creds = get_credentials()?;
72    let client = CloudApiClient::new(&creds.api_url, &creds.api_token)?;
73
74    let response = if config.is_json_output() {
75        client.rotate_credentials(&tenant_id).await?
76    } else {
77        let spinner = CliService::spinner("Rotating database credentials...");
78        let resp = client.rotate_credentials(&tenant_id).await?;
79        spinner.finish_and_clear();
80        resp
81    };
82
83    let tenant = store
84        .tenants
85        .iter_mut()
86        .find(|t| t.id == tenant_id)
87        .ok_or_else(|| anyhow!("Tenant not found after rotation"))?;
88
89    tenant.internal_database_url = Some(response.internal_database_url.clone());
90    if tenant.external_db_access {
91        tenant.database_url = Some(response.external_database_url.clone());
92    }
93
94    store.save_to_path(&tenants_path)?;
95
96    let output = RotateCredentialsOutput {
97        tenant_id: tenant_id.clone(),
98        status: response.status.clone(),
99        internal_database_url: response.internal_database_url.clone(),
100        external_database_url: response.external_database_url.clone(),
101    };
102
103    if !config.is_json_output() {
104        CliService::success("Database credentials rotated");
105        CliService::key_value("Status", &response.status);
106
107        CliService::section("New Database Connection");
108        CliService::key_value("Internal URL", &response.internal_database_url);
109        CliService::key_value("External URL", &response.external_database_url);
110    }
111
112    Ok(CommandResult::card(output).with_title("Rotate Credentials"))
113}
114
115pub async fn rotate_sync_token(
116    id: Option<String>,
117    skip_confirm: bool,
118    config: &CliConfig,
119) -> Result<CommandResult<RotateSyncTokenOutput>> {
120    let cloud_paths = get_cloud_paths()?;
121    let tenants_path = cloud_paths.resolve(CloudPath::Tenants);
122    let mut store = TenantStore::load_from_path(&tenants_path).unwrap_or_else(|e| {
123        if !config.is_json_output() {
124            CliService::warning(&format!("Failed to load tenant store: {}", e));
125        }
126        TenantStore::default()
127    });
128
129    let tenant_id = if let Some(id) = id {
130        id
131    } else {
132        if store.tenants.is_empty() {
133            bail!("No tenants configured.");
134        }
135        if skip_confirm {
136            bail!("Tenant ID required in non-interactive mode");
137        }
138        select_tenant(&store.tenants)?.id.clone()
139    };
140
141    let tenant = store
142        .tenants
143        .iter()
144        .find(|t| t.id == tenant_id)
145        .ok_or_else(|| anyhow!("Tenant not found: {}", tenant_id))?;
146
147    if tenant.tenant_type != TenantType::Cloud {
148        bail!("Sync token rotation is only available for cloud tenants");
149    }
150
151    if !skip_confirm {
152        let confirm = Confirm::with_theme(&ColorfulTheme::default())
153            .with_prompt(format!(
154                "Rotate sync token for '{}'? This will generate a new token for file \
155                 synchronization.",
156                tenant.name
157            ))
158            .default(false)
159            .interact()?;
160
161        if !confirm {
162            if !config.is_json_output() {
163                CliService::info("Cancelled");
164            }
165            let output = RotateSyncTokenOutput {
166                tenant_id: tenant_id.clone(),
167                status: "cancelled".to_string(),
168                message: "Cancelled".to_string(),
169            };
170            return Ok(CommandResult::card(output).with_title("Rotate Sync Token"));
171        }
172    }
173
174    let creds = get_credentials()?;
175    let client = CloudApiClient::new(&creds.api_url, &creds.api_token)?;
176
177    let response = if config.is_json_output() {
178        client.rotate_sync_token(&tenant_id).await?
179    } else {
180        let spinner = CliService::spinner("Rotating sync token...");
181        let resp = client.rotate_sync_token(&tenant_id).await?;
182        spinner.finish_and_clear();
183        resp
184    };
185
186    let tenant = store
187        .tenants
188        .iter_mut()
189        .find(|t| t.id == tenant_id)
190        .ok_or_else(|| anyhow!("Tenant not found after rotation"))?;
191
192    tenant.sync_token = Some(response.sync_token);
193
194    store.save_to_path(&tenants_path)?;
195
196    let output = RotateSyncTokenOutput {
197        tenant_id: tenant_id.clone(),
198        status: "success".to_string(),
199        message: "New sync token has been saved locally.".to_string(),
200    };
201
202    if !config.is_json_output() {
203        CliService::success("Sync token rotated");
204        CliService::info("New sync token has been saved locally.");
205    }
206
207    Ok(CommandResult::card(output).with_title("Rotate Sync Token"))
208}