use crate::auth;
use crate::cli::{Cli, UserCommands};
use crate::client::ApiClient;
use crate::error::{CliError, Result};
use crate::output::OutputFormat;
use colored::Colorize;
use dialoguer::Confirm;
use serde::{Deserialize, Serialize};
pub async fn execute(command: &UserCommands, cli: &Cli) -> Result<()> {
let client = auth::get_api_client()?;
let output_format = cli.output.as_ref().map(|s| OutputFormat::from_str(s)).unwrap_or(OutputFormat::Table);
UserCommand::execute(&client, command, output_format).await
}
pub struct UserCommand;
impl UserCommand {
pub async fn execute(
client: &ApiClient,
command: &UserCommands,
format: OutputFormat,
) -> Result<()> {
match command {
UserCommands::Create {
app_id,
token,
label,
storage_quota,
max_connections,
qps_limit,
} => Self::create(client, app_id, token.clone(), label.clone(), *storage_quota, *max_connections, *qps_limit, format).await,
UserCommands::List {
app_id,
status,
page,
page_size,
} => Self::list(client, app_id, status.clone(), *page, *page_size, format).await,
UserCommands::Show { app_id, user_uid } => {
Self::show(client, app_id, user_uid, format).await
}
UserCommands::Update {
app_id,
user_uid,
label,
storage_quota,
max_connections,
qps_limit,
} => Self::update(client, app_id, user_uid, label.clone(), storage_quota.clone(), max_connections.clone(), qps_limit.clone(), format).await,
UserCommands::Enable { app_id, user_uid } => {
Self::enable(client, app_id, user_uid).await
}
UserCommands::Disable { app_id, user_uid } => {
Self::disable(client, app_id, user_uid).await
}
UserCommands::Delete {
app_id,
user_uid,
force,
} => Self::delete(client, app_id, user_uid, *force).await,
UserCommands::ResetToken { app_id, user_uid } => {
Self::reset_token(client, app_id, user_uid, format).await
}
UserCommands::Stats { app_id, user_uid } => {
Self::stats(client, app_id, user_uid, format).await
}
}
}
async fn create(
client: &ApiClient,
app_id: &str,
token: Option<String>,
label: Option<String>,
storage_quota: Option<u32>,
max_connections: Option<u32>,
qps_limit: Option<u32>,
format: OutputFormat,
) -> Result<()> {
let mut body = serde_json::json!({});
if let Some(t) = token {
body["token"] = serde_json::json!(t);
}
if let Some(l) = label {
body["label"] = serde_json::json!(l);
}
if let Some(sq) = storage_quota {
body["storage_quota_mb"] = serde_json::json!(sq);
}
if let Some(mc) = max_connections {
body["max_connections"] = serde_json::json!(mc);
}
if let Some(qps) = qps_limit {
body["qps_limit"] = serde_json::json!(qps);
}
let response: CreateUserResponse = client
.post(&format!("/api/v1/apps/{}/users", app_id), &body)
.await?;
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&response)?);
}
OutputFormat::Yaml => {
println!("{}", serde_yaml::to_string(&response)?);
}
OutputFormat::Table => {
println!("{}", "✓ User created successfully".green().bold());
println!(" {}: {}", "User UID".bold(), response.user_uid);
println!(" {}: {}", "Token".bold(), response.token.yellow());
println!(" {}: {}", "Label".bold(), response.label.as_deref().unwrap_or("(none)"));
println!(" {}: {}", "Status".bold(), response.status);
println!(" {}: {}", "Created At".bold(), response.created_at);
println!();
println!("{}", "⚠️ IMPORTANT: Save this token - it won't be shown again!".yellow().bold());
}
}
Ok(())
}
async fn list(
client: &ApiClient,
app_id: &str,
status: Option<String>,
page: u32,
page_size: u32,
format: OutputFormat,
) -> Result<()> {
let mut query = vec![
("page", page.to_string()),
("page_size", page_size.to_string()),
];
if let Some(s) = status {
query.push(("status", s));
}
let response: ListUsersResponse = client
.get(&format!("/api/v1/apps/{}/users", app_id), &query)
.await?;
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&response)?);
}
OutputFormat::Yaml => {
println!("{}", serde_yaml::to_string(&response)?);
}
OutputFormat::Table => {
if response.items.is_empty() {
println!("{}", "No users found".yellow());
return Ok(());
}
let mut table = comfy_table::Table::new();
table.set_header(vec![
"USER UID",
"LABEL",
"STATUS",
"LAST ACCESSED",
"CREATED AT",
]);
for user in &response.items {
table.add_row(vec![
user.user_uid.clone(),
user.label.clone().unwrap_or_else(|| "(none)".to_string()),
user.status.clone(),
user.last_accessed_at
.clone()
.unwrap_or_else(|| "Never".to_string()),
user.created_at.clone(),
]);
}
println!("{}", table);
println!(
"\nShowing {} of {} users (page {}/{})",
response.items.len(),
response.total,
page,
(response.total + page_size - 1) / page_size
);
}
}
Ok(())
}
async fn show(
client: &ApiClient,
app_id: &str,
user_uid: &str,
format: OutputFormat,
) -> Result<()> {
let response: UserDetail = client
.get(&format!("/api/v1/apps/{}/users/{}", app_id, user_uid), &[])
.await?;
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&response)?);
}
OutputFormat::Yaml => {
println!("{}", serde_yaml::to_string(&response)?);
}
OutputFormat::Table => {
println!("{}", format!("User: {}", response.user_uid).bold());
println!();
println!("{}", "Basic Information:".bold());
println!(" Label: {}", response.label.as_deref().unwrap_or("(none)"));
println!(" Status: {}", response.status);
println!();
println!("{}", "Quotas:".bold());
println!(" Storage Quota: {} MB (effective: {} MB)",
response.storage_quota_mb.map(|v| v.to_string()).unwrap_or_else(|| "inherited".to_string()),
response.effective_quotas.storage_quota_mb
);
println!(" Max Connections: {} (effective: {})",
response.max_connections.map(|v| v.to_string()).unwrap_or_else(|| "inherited".to_string()),
response.effective_quotas.max_connections
);
println!(" QPS Limit: {} (effective: {})",
response.qps_limit.map(|v| v.to_string()).unwrap_or_else(|| "inherited".to_string()),
response.effective_quotas.qps_limit
);
println!();
println!("{}", "Usage:".bold());
println!(" Storage Used: {:.2} MB", response.usage.storage_used_mb);
println!(" Active Connections: {}", response.usage.active_connections);
println!(" Databases: {}", response.usage.databases.join(", "));
println!();
println!("{}", "Timestamps:".bold());
println!(" Last Accessed: {}", response.last_accessed_at.as_deref().unwrap_or("Never"));
println!(" Created: {}", response.created_at);
println!(" Updated: {}", response.updated_at);
}
}
Ok(())
}
async fn update(
client: &ApiClient,
app_id: &str,
user_uid: &str,
label: Option<String>,
storage_quota: Option<String>,
max_connections: Option<String>,
qps_limit: Option<String>,
format: OutputFormat,
) -> Result<()> {
let mut body = serde_json::json!({});
if let Some(l) = label {
body["label"] = serde_json::json!(l);
}
if let Some(sq) = storage_quota {
if sq == "null" {
body["storage_quota_mb"] = serde_json::Value::Null;
} else {
let val: u32 = sq.parse()
.map_err(|_| CliError::InvalidInput("storage_quota must be a number or 'null'".to_string()))?;
body["storage_quota_mb"] = serde_json::json!(val);
}
}
if let Some(mc) = max_connections {
if mc == "null" {
body["max_connections"] = serde_json::Value::Null;
} else {
let val: u32 = mc.parse()
.map_err(|_| CliError::InvalidInput("max_connections must be a number or 'null'".to_string()))?;
body["max_connections"] = serde_json::json!(val);
}
}
if let Some(qps) = qps_limit {
if qps == "null" {
body["qps_limit"] = serde_json::Value::Null;
} else {
let val: u32 = qps.parse()
.map_err(|_| CliError::InvalidInput("qps_limit must be a number or 'null'".to_string()))?;
body["qps_limit"] = serde_json::json!(val);
}
}
let response: UpdateUserResponse = client
.patch(&format!("/api/v1/apps/{}/users/{}", app_id, user_uid), &body)
.await?;
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&response)?);
}
OutputFormat::Yaml => {
println!("{}", serde_yaml::to_string(&response)?);
}
OutputFormat::Table => {
println!("{}", "✓ User updated successfully".green().bold());
println!(" {}: {}", "User UID".bold(), response.user_uid);
println!(" {}: {}", "Updated Fields".bold(), response.updated_fields.join(", "));
println!(" {}: {}", "Updated At".bold(), response.updated_at);
}
}
Ok(())
}
async fn enable(client: &ApiClient, app_id: &str, user_uid: &str) -> Result<()> {
let body = serde_json::json!({ "status": "active" });
let _: UpdateUserResponse = client
.patch(&format!("/api/v1/apps/{}/users/{}", app_id, user_uid), &body)
.await?;
println!("{}", format!("✓ User {} enabled successfully", user_uid).green().bold());
Ok(())
}
async fn disable(client: &ApiClient, app_id: &str, user_uid: &str) -> Result<()> {
let body = serde_json::json!({ "status": "disabled" });
let _: UpdateUserResponse = client
.patch(&format!("/api/v1/apps/{}/users/{}", app_id, user_uid), &body)
.await?;
println!("{}", format!("✓ User {} disabled successfully", user_uid).green().bold());
Ok(())
}
async fn delete(
client: &ApiClient,
app_id: &str,
user_uid: &str,
force: bool,
) -> Result<()> {
if !force {
let user: UserDetail = client
.get(&format!("/api/v1/apps/{}/users/{}", app_id, user_uid), &[])
.await?;
println!("{}", "⚠️ WARNING: This will permanently delete the user and all their data.".yellow().bold());
println!(" App ID: {}", app_id);
println!(" User UID: {}", user_uid);
println!(" Label: {}", user.label.as_deref().unwrap_or("(none)"));
println!(" Storage Used: {:.2} MB", user.usage.storage_used_mb);
println!();
let confirmed = Confirm::new()
.with_prompt("Are you sure?")
.default(false)
.interact()
.map_err(|e| CliError::InvalidInput(format!("Failed to read input: {}", e)))?;
if !confirmed {
println!("{}", "Cancelled".yellow());
return Ok(());
}
}
let response: DeleteUserResponse = client
.delete(&format!("/api/v1/apps/{}/users/{}", app_id, user_uid))
.await?;
println!("{}", "✓ User deleted successfully".green().bold());
println!(" {}: {}", "User UID".bold(), response.user_uid);
println!(" {}: {}", "Workspace Cleanup".bold(), response.workspace_cleanup);
Ok(())
}
async fn reset_token(
client: &ApiClient,
app_id: &str,
user_uid: &str,
format: OutputFormat,
) -> Result<()> {
println!("{}", "⚠️ WARNING: This will invalidate the current token immediately.".yellow().bold());
println!(" All active connections using the old token will be disconnected.");
println!();
let confirmed = Confirm::new()
.with_prompt("Are you sure you want to reset the token?")
.default(false)
.interact()
.map_err(|e| CliError::InvalidInput(format!("Failed to read input: {}", e)))?;
if !confirmed {
println!("{}", "Cancelled".yellow());
return Ok(());
}
let body = serde_json::json!({});
let response: ResetTokenResponse = client
.patch(&format!("/api/v1/apps/{}/users/{}", app_id, user_uid), &body)
.await?;
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&response)?);
}
OutputFormat::Yaml => {
println!("{}", serde_yaml::to_string(&response)?);
}
OutputFormat::Table => {
println!("{}", "✓ Token reset successfully".green().bold());
println!(" {}: {}", "User UID".bold(), response.user_uid);
if let Some(token) = &response.token {
println!(" {}: {}", "New Token".bold(), token.yellow());
println!();
println!("{}", "⚠️ IMPORTANT: Save this token - it won't be shown again!".yellow().bold());
}
println!(" {}: {}", "Updated At".bold(), response.updated_at);
}
}
Ok(())
}
async fn stats(
client: &ApiClient,
app_id: &str,
user_uid: &str,
format: OutputFormat,
) -> Result<()> {
let response: UserDetail = client
.get(&format!("/api/v1/apps/{}/users/{}", app_id, user_uid), &[])
.await?;
match format {
OutputFormat::Json => {
let stats = serde_json::json!({
"user_uid": response.user_uid,
"label": response.label,
"status": response.status,
"quotas": response.effective_quotas,
"usage": response.usage,
});
println!("{}", serde_json::to_string_pretty(&stats)?);
}
OutputFormat::Yaml => {
let stats = serde_json::json!({
"user_uid": response.user_uid,
"label": response.label,
"status": response.status,
"quotas": response.effective_quotas,
"usage": response.usage,
});
println!("{}", serde_yaml::to_string(&stats)?);
}
OutputFormat::Table => {
println!("{}", format!("User Statistics: {} ({})", response.user_uid, response.label.as_deref().unwrap_or("no label")).bold());
println!("{}: {}", "Status".bold(), response.status);
println!();
println!("{}", "Storage:".bold());
let storage_pct = (response.usage.storage_used_mb / response.effective_quotas.storage_quota_mb as f64) * 100.0;
println!(" Used: {:.2} MB / {} MB ({:.1}%)",
response.usage.storage_used_mb,
response.effective_quotas.storage_quota_mb,
storage_pct
);
let bar_width = 40;
let filled = ((storage_pct / 100.0) * bar_width as f64) as usize;
let bar = format!("[{}{}]",
"█".repeat(filled),
"░".repeat(bar_width - filled)
);
println!(" {}", if storage_pct > 90.0 { bar.red() } else if storage_pct > 75.0 { bar.yellow() } else { bar.green() });
println!();
println!("{}", "Connections:".bold());
println!(" Active: {} / {}",
response.usage.active_connections,
response.effective_quotas.max_connections
);
println!();
println!("{}", "Rate Limit:".bold());
println!(" QPS Limit: {}", response.effective_quotas.qps_limit);
println!();
println!("{}", "Databases:".bold());
if response.usage.databases.is_empty() {
println!(" (none created yet)");
} else {
for db in &response.usage.databases {
println!(" • {}", db);
}
}
println!();
println!("{}", "Activity:".bold());
println!(" Last Accessed: {}", response.last_accessed_at.as_deref().unwrap_or("Never"));
}
}
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize)]
struct CreateUserResponse {
user_uid: String,
token: String,
label: Option<String>,
status: String,
created_at: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct ListUsersResponse {
items: Vec<UserListItem>,
total: u32,
page: u32,
page_size: u32,
}
#[derive(Debug, Serialize, Deserialize)]
struct UserListItem {
user_uid: String,
label: Option<String>,
status: String,
last_accessed_at: Option<String>,
created_at: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct UserDetail {
user_uid: String,
label: Option<String>,
status: String,
storage_quota_mb: Option<u32>,
max_connections: Option<u32>,
qps_limit: Option<u32>,
effective_quotas: EffectiveQuotas,
usage: Usage,
last_accessed_at: Option<String>,
created_at: String,
updated_at: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct EffectiveQuotas {
storage_quota_mb: u32,
max_connections: u32,
qps_limit: u32,
}
#[derive(Debug, Serialize, Deserialize)]
struct Usage {
storage_used_mb: f64,
active_connections: u32,
databases: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct UpdateUserResponse {
user_uid: String,
updated_fields: Vec<String>,
updated_at: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct DeleteUserResponse {
user_uid: String,
deleted: bool,
workspace_cleanup: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct ResetTokenResponse {
user_uid: String,
token: Option<String>,
updated_at: String,
}