1use crate::auth;
2use crate::cli::{Cli, UserCommands};
3use crate::client::ApiClient;
4use crate::error::{CliError, Result};
5use crate::output::OutputFormat;
6use colored::Colorize;
7use dialoguer::Confirm;
8use serde::{Deserialize, Serialize};
9
10pub async fn execute(command: &UserCommands, cli: &Cli) -> Result<()> {
11 let client = auth::get_api_client()?;
12 let output_format = cli.output.as_ref().map(|s| OutputFormat::from_str(s)).unwrap_or(OutputFormat::Table);
13 UserCommand::execute(&client, command, output_format).await
14}
15
16pub struct UserCommand;
17
18impl UserCommand {
19 pub async fn execute(
20 client: &ApiClient,
21 command: &UserCommands,
22 format: OutputFormat,
23 ) -> Result<()> {
24 match command {
25 UserCommands::Create {
26 app_id,
27 token,
28 label,
29 storage_quota,
30 max_connections,
31 qps_limit,
32 } => Self::create(client, app_id, token.clone(), label.clone(), *storage_quota, *max_connections, *qps_limit, format).await,
33
34 UserCommands::List {
35 app_id,
36 status,
37 page,
38 page_size,
39 } => Self::list(client, app_id, status.clone(), *page, *page_size, format).await,
40
41 UserCommands::Show { app_id, user_uid } => {
42 Self::show(client, app_id, user_uid, format).await
43 }
44
45 UserCommands::Update {
46 app_id,
47 user_uid,
48 label,
49 storage_quota,
50 max_connections,
51 qps_limit,
52 } => Self::update(client, app_id, user_uid, label.clone(), storage_quota.clone(), max_connections.clone(), qps_limit.clone(), format).await,
53
54 UserCommands::Enable { app_id, user_uid } => {
55 Self::enable(client, app_id, user_uid).await
56 }
57
58 UserCommands::Disable { app_id, user_uid } => {
59 Self::disable(client, app_id, user_uid).await
60 }
61
62 UserCommands::Delete {
63 app_id,
64 user_uid,
65 force,
66 } => Self::delete(client, app_id, user_uid, *force).await,
67
68 UserCommands::ResetToken { app_id, user_uid } => {
69 Self::reset_token(client, app_id, user_uid, format).await
70 }
71
72 UserCommands::Stats { app_id, user_uid } => {
73 Self::stats(client, app_id, user_uid, format).await
74 }
75 }
76 }
77
78 async fn create(
79 client: &ApiClient,
80 app_id: &str,
81 token: Option<String>,
82 label: Option<String>,
83 storage_quota: Option<u32>,
84 max_connections: Option<u32>,
85 qps_limit: Option<u32>,
86 format: OutputFormat,
87 ) -> Result<()> {
88 let mut body = serde_json::json!({});
89
90 if let Some(t) = token {
91 body["token"] = serde_json::json!(t);
92 }
93 if let Some(l) = label {
94 body["label"] = serde_json::json!(l);
95 }
96 if let Some(sq) = storage_quota {
97 body["storage_quota_mb"] = serde_json::json!(sq);
98 }
99 if let Some(mc) = max_connections {
100 body["max_connections"] = serde_json::json!(mc);
101 }
102 if let Some(qps) = qps_limit {
103 body["qps_limit"] = serde_json::json!(qps);
104 }
105
106 let response: CreateUserResponse = client
107 .post(&format!("/api/v1/apps/{}/users", app_id), &body)
108 .await?;
109
110 match format {
111 OutputFormat::Json => {
112 println!("{}", serde_json::to_string_pretty(&response)?);
113 }
114 OutputFormat::Yaml => {
115 println!("{}", serde_yaml::to_string(&response)?);
116 }
117 OutputFormat::Table => {
118 println!("{}", "✓ User created successfully".green().bold());
119 println!(" {}: {}", "User UID".bold(), response.user_uid);
120 println!(" {}: {}", "Token".bold(), response.token.yellow());
121 println!(" {}: {}", "Label".bold(), response.label.as_deref().unwrap_or("(none)"));
122 println!(" {}: {}", "Status".bold(), response.status);
123 println!(" {}: {}", "Created At".bold(), response.created_at);
124 println!();
125 println!("{}", "⚠️ IMPORTANT: Save this token - it won't be shown again!".yellow().bold());
126 }
127 }
128
129 Ok(())
130 }
131
132 async fn list(
133 client: &ApiClient,
134 app_id: &str,
135 status: Option<String>,
136 page: u32,
137 page_size: u32,
138 format: OutputFormat,
139 ) -> Result<()> {
140 let mut query = vec![
141 ("page", page.to_string()),
142 ("page_size", page_size.to_string()),
143 ];
144
145 if let Some(s) = status {
146 query.push(("status", s));
147 }
148
149 let response: ListUsersResponse = client
150 .get(&format!("/api/v1/apps/{}/users", app_id), &query)
151 .await?;
152
153 match format {
154 OutputFormat::Json => {
155 println!("{}", serde_json::to_string_pretty(&response)?);
156 }
157 OutputFormat::Yaml => {
158 println!("{}", serde_yaml::to_string(&response)?);
159 }
160 OutputFormat::Table => {
161 if response.items.is_empty() {
162 println!("{}", "No users found".yellow());
163 return Ok(());
164 }
165
166 let mut table = comfy_table::Table::new();
167 table.set_header(vec![
168 "USER UID",
169 "LABEL",
170 "STATUS",
171 "LAST ACCESSED",
172 "CREATED AT",
173 ]);
174
175 for user in &response.items {
176 table.add_row(vec![
177 user.user_uid.clone(),
178 user.label.clone().unwrap_or_else(|| "(none)".to_string()),
179 user.status.clone(),
180 user.last_accessed_at
181 .clone()
182 .unwrap_or_else(|| "Never".to_string()),
183 user.created_at.clone(),
184 ]);
185 }
186
187 println!("{}", table);
188 println!(
189 "\nShowing {} of {} users (page {}/{})",
190 response.items.len(),
191 response.total,
192 page,
193 (response.total + page_size - 1) / page_size
194 );
195 }
196 }
197
198 Ok(())
199 }
200
201 async fn show(
202 client: &ApiClient,
203 app_id: &str,
204 user_uid: &str,
205 format: OutputFormat,
206 ) -> Result<()> {
207 let response: UserDetail = client
208 .get(&format!("/api/v1/apps/{}/users/{}", app_id, user_uid), &[])
209 .await?;
210
211 match format {
212 OutputFormat::Json => {
213 println!("{}", serde_json::to_string_pretty(&response)?);
214 }
215 OutputFormat::Yaml => {
216 println!("{}", serde_yaml::to_string(&response)?);
217 }
218 OutputFormat::Table => {
219 println!("{}", format!("User: {}", response.user_uid).bold());
220 println!();
221
222 println!("{}", "Basic Information:".bold());
223 println!(" Label: {}", response.label.as_deref().unwrap_or("(none)"));
224 println!(" Status: {}", response.status);
225 println!();
226
227 println!("{}", "Quotas:".bold());
228 println!(" Storage Quota: {} MB (effective: {} MB)",
229 response.storage_quota_mb.map(|v| v.to_string()).unwrap_or_else(|| "inherited".to_string()),
230 response.effective_quotas.storage_quota_mb
231 );
232 println!(" Max Connections: {} (effective: {})",
233 response.max_connections.map(|v| v.to_string()).unwrap_or_else(|| "inherited".to_string()),
234 response.effective_quotas.max_connections
235 );
236 println!(" QPS Limit: {} (effective: {})",
237 response.qps_limit.map(|v| v.to_string()).unwrap_or_else(|| "inherited".to_string()),
238 response.effective_quotas.qps_limit
239 );
240 println!();
241
242 println!("{}", "Usage:".bold());
243 println!(" Storage Used: {:.2} MB", response.usage.storage_used_mb);
244 println!(" Active Connections: {}", response.usage.active_connections);
245 println!(" Databases: {}", response.usage.databases.join(", "));
246 println!();
247
248 println!("{}", "Timestamps:".bold());
249 println!(" Last Accessed: {}", response.last_accessed_at.as_deref().unwrap_or("Never"));
250 println!(" Created: {}", response.created_at);
251 println!(" Updated: {}", response.updated_at);
252 }
253 }
254
255 Ok(())
256 }
257
258 async fn update(
259 client: &ApiClient,
260 app_id: &str,
261 user_uid: &str,
262 label: Option<String>,
263 storage_quota: Option<String>,
264 max_connections: Option<String>,
265 qps_limit: Option<String>,
266 format: OutputFormat,
267 ) -> Result<()> {
268 let mut body = serde_json::json!({});
269
270 if let Some(l) = label {
271 body["label"] = serde_json::json!(l);
272 }
273
274 if let Some(sq) = storage_quota {
276 if sq == "null" {
277 body["storage_quota_mb"] = serde_json::Value::Null;
278 } else {
279 let val: u32 = sq.parse()
280 .map_err(|_| CliError::InvalidInput("storage_quota must be a number or 'null'".to_string()))?;
281 body["storage_quota_mb"] = serde_json::json!(val);
282 }
283 }
284
285 if let Some(mc) = max_connections {
286 if mc == "null" {
287 body["max_connections"] = serde_json::Value::Null;
288 } else {
289 let val: u32 = mc.parse()
290 .map_err(|_| CliError::InvalidInput("max_connections must be a number or 'null'".to_string()))?;
291 body["max_connections"] = serde_json::json!(val);
292 }
293 }
294
295 if let Some(qps) = qps_limit {
296 if qps == "null" {
297 body["qps_limit"] = serde_json::Value::Null;
298 } else {
299 let val: u32 = qps.parse()
300 .map_err(|_| CliError::InvalidInput("qps_limit must be a number or 'null'".to_string()))?;
301 body["qps_limit"] = serde_json::json!(val);
302 }
303 }
304
305 let response: UpdateUserResponse = client
306 .patch(&format!("/api/v1/apps/{}/users/{}", app_id, user_uid), &body)
307 .await?;
308
309 match format {
310 OutputFormat::Json => {
311 println!("{}", serde_json::to_string_pretty(&response)?);
312 }
313 OutputFormat::Yaml => {
314 println!("{}", serde_yaml::to_string(&response)?);
315 }
316 OutputFormat::Table => {
317 println!("{}", "✓ User updated successfully".green().bold());
318 println!(" {}: {}", "User UID".bold(), response.user_uid);
319 println!(" {}: {}", "Updated Fields".bold(), response.updated_fields.join(", "));
320 println!(" {}: {}", "Updated At".bold(), response.updated_at);
321 }
322 }
323
324 Ok(())
325 }
326
327 async fn enable(client: &ApiClient, app_id: &str, user_uid: &str) -> Result<()> {
328 let body = serde_json::json!({ "status": "active" });
329 let _: UpdateUserResponse = client
330 .patch(&format!("/api/v1/apps/{}/users/{}", app_id, user_uid), &body)
331 .await?;
332
333 println!("{}", format!("✓ User {} enabled successfully", user_uid).green().bold());
334 Ok(())
335 }
336
337 async fn disable(client: &ApiClient, app_id: &str, user_uid: &str) -> Result<()> {
338 let body = serde_json::json!({ "status": "disabled" });
339 let _: UpdateUserResponse = client
340 .patch(&format!("/api/v1/apps/{}/users/{}", app_id, user_uid), &body)
341 .await?;
342
343 println!("{}", format!("✓ User {} disabled successfully", user_uid).green().bold());
344 Ok(())
345 }
346
347 async fn delete(
348 client: &ApiClient,
349 app_id: &str,
350 user_uid: &str,
351 force: bool,
352 ) -> Result<()> {
353 if !force {
354 let user: UserDetail = client
356 .get(&format!("/api/v1/apps/{}/users/{}", app_id, user_uid), &[])
357 .await?;
358
359 println!("{}", "⚠️ WARNING: This will permanently delete the user and all their data.".yellow().bold());
360 println!(" App ID: {}", app_id);
361 println!(" User UID: {}", user_uid);
362 println!(" Label: {}", user.label.as_deref().unwrap_or("(none)"));
363 println!(" Storage Used: {:.2} MB", user.usage.storage_used_mb);
364 println!();
365
366 let confirmed = Confirm::new()
367 .with_prompt("Are you sure?")
368 .default(false)
369 .interact()
370 .map_err(|e| CliError::InvalidInput(format!("Failed to read input: {}", e)))?;
371
372 if !confirmed {
373 println!("{}", "Cancelled".yellow());
374 return Ok(());
375 }
376 }
377
378 let response: DeleteUserResponse = client
379 .delete(&format!("/api/v1/apps/{}/users/{}", app_id, user_uid))
380 .await?;
381
382 println!("{}", "✓ User deleted successfully".green().bold());
383 println!(" {}: {}", "User UID".bold(), response.user_uid);
384 println!(" {}: {}", "Workspace Cleanup".bold(), response.workspace_cleanup);
385
386 Ok(())
387 }
388
389 async fn reset_token(
390 client: &ApiClient,
391 app_id: &str,
392 user_uid: &str,
393 format: OutputFormat,
394 ) -> Result<()> {
395 println!("{}", "⚠️ WARNING: This will invalidate the current token immediately.".yellow().bold());
397 println!(" All active connections using the old token will be disconnected.");
398 println!();
399
400 let confirmed = Confirm::new()
401 .with_prompt("Are you sure you want to reset the token?")
402 .default(false)
403 .interact()
404 .map_err(|e| CliError::InvalidInput(format!("Failed to read input: {}", e)))?;
405
406 if !confirmed {
407 println!("{}", "Cancelled".yellow());
408 return Ok(());
409 }
410
411 let body = serde_json::json!({});
413 let response: ResetTokenResponse = client
414 .patch(&format!("/api/v1/apps/{}/users/{}", app_id, user_uid), &body)
415 .await?;
416
417 match format {
418 OutputFormat::Json => {
419 println!("{}", serde_json::to_string_pretty(&response)?);
420 }
421 OutputFormat::Yaml => {
422 println!("{}", serde_yaml::to_string(&response)?);
423 }
424 OutputFormat::Table => {
425 println!("{}", "✓ Token reset successfully".green().bold());
426 println!(" {}: {}", "User UID".bold(), response.user_uid);
427 if let Some(token) = &response.token {
428 println!(" {}: {}", "New Token".bold(), token.yellow());
429 println!();
430 println!("{}", "⚠️ IMPORTANT: Save this token - it won't be shown again!".yellow().bold());
431 }
432 println!(" {}: {}", "Updated At".bold(), response.updated_at);
433 }
434 }
435
436 Ok(())
437 }
438
439 async fn stats(
440 client: &ApiClient,
441 app_id: &str,
442 user_uid: &str,
443 format: OutputFormat,
444 ) -> Result<()> {
445 let response: UserDetail = client
447 .get(&format!("/api/v1/apps/{}/users/{}", app_id, user_uid), &[])
448 .await?;
449
450 match format {
451 OutputFormat::Json => {
452 let stats = serde_json::json!({
453 "user_uid": response.user_uid,
454 "label": response.label,
455 "status": response.status,
456 "quotas": response.effective_quotas,
457 "usage": response.usage,
458 });
459 println!("{}", serde_json::to_string_pretty(&stats)?);
460 }
461 OutputFormat::Yaml => {
462 let stats = serde_json::json!({
463 "user_uid": response.user_uid,
464 "label": response.label,
465 "status": response.status,
466 "quotas": response.effective_quotas,
467 "usage": response.usage,
468 });
469 println!("{}", serde_yaml::to_string(&stats)?);
470 }
471 OutputFormat::Table => {
472 println!("{}", format!("User Statistics: {} ({})", response.user_uid, response.label.as_deref().unwrap_or("no label")).bold());
473 println!("{}: {}", "Status".bold(), response.status);
474 println!();
475
476 println!("{}", "Storage:".bold());
477 let storage_pct = (response.usage.storage_used_mb / response.effective_quotas.storage_quota_mb as f64) * 100.0;
478 println!(" Used: {:.2} MB / {} MB ({:.1}%)",
479 response.usage.storage_used_mb,
480 response.effective_quotas.storage_quota_mb,
481 storage_pct
482 );
483
484 let bar_width = 40;
486 let filled = ((storage_pct / 100.0) * bar_width as f64) as usize;
487 let bar = format!("[{}{}]",
488 "█".repeat(filled),
489 "░".repeat(bar_width - filled)
490 );
491 println!(" {}", if storage_pct > 90.0 { bar.red() } else if storage_pct > 75.0 { bar.yellow() } else { bar.green() });
492 println!();
493
494 println!("{}", "Connections:".bold());
495 println!(" Active: {} / {}",
496 response.usage.active_connections,
497 response.effective_quotas.max_connections
498 );
499 println!();
500
501 println!("{}", "Rate Limit:".bold());
502 println!(" QPS Limit: {}", response.effective_quotas.qps_limit);
503 println!();
504
505 println!("{}", "Databases:".bold());
506 if response.usage.databases.is_empty() {
507 println!(" (none created yet)");
508 } else {
509 for db in &response.usage.databases {
510 println!(" • {}", db);
511 }
512 }
513 println!();
514
515 println!("{}", "Activity:".bold());
516 println!(" Last Accessed: {}", response.last_accessed_at.as_deref().unwrap_or("Never"));
517 }
518 }
519
520 Ok(())
521 }
522}
523
524#[derive(Debug, Serialize, Deserialize)]
526struct CreateUserResponse {
527 user_uid: String,
528 token: String,
529 label: Option<String>,
530 status: String,
531 created_at: String,
532}
533
534#[derive(Debug, Serialize, Deserialize)]
535struct ListUsersResponse {
536 items: Vec<UserListItem>,
537 total: u32,
538 page: u32,
539 page_size: u32,
540}
541
542#[derive(Debug, Serialize, Deserialize)]
543struct UserListItem {
544 user_uid: String,
545 label: Option<String>,
546 status: String,
547 last_accessed_at: Option<String>,
548 created_at: String,
549}
550
551#[derive(Debug, Serialize, Deserialize)]
552struct UserDetail {
553 user_uid: String,
554 label: Option<String>,
555 status: String,
556 storage_quota_mb: Option<u32>,
557 max_connections: Option<u32>,
558 qps_limit: Option<u32>,
559 effective_quotas: EffectiveQuotas,
560 usage: Usage,
561 last_accessed_at: Option<String>,
562 created_at: String,
563 updated_at: String,
564}
565
566#[derive(Debug, Serialize, Deserialize)]
567struct EffectiveQuotas {
568 storage_quota_mb: u32,
569 max_connections: u32,
570 qps_limit: u32,
571}
572
573#[derive(Debug, Serialize, Deserialize)]
574struct Usage {
575 storage_used_mb: f64,
576 active_connections: u32,
577 databases: Vec<String>,
578}
579
580#[derive(Debug, Serialize, Deserialize)]
581struct UpdateUserResponse {
582 user_uid: String,
583 updated_fields: Vec<String>,
584 updated_at: String,
585}
586
587#[derive(Debug, Serialize, Deserialize)]
588struct DeleteUserResponse {
589 user_uid: String,
590 deleted: bool,
591 workspace_cleanup: String,
592}
593
594#[derive(Debug, Serialize, Deserialize)]
595struct ResetTokenResponse {
596 user_uid: String,
597 token: Option<String>,
598 updated_at: String,
599}