systemprompt_cli/commands/cloud/tenant/
rotate.rs1use 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}