use anyhow::{Context, Result};
use clap::{Args, Subcommand};
use crate::cmd::login::read_password;
#[derive(Args, Debug)]
pub struct AccountArgs {
#[command(subcommand)]
pub sub: AccountSub,
}
#[derive(Subcommand, Debug)]
pub enum AccountSub {
List,
Create {
username: String,
#[arg(long)]
role: String,
#[arg(long)]
password_stdin: bool,
},
SetRole {
username: String,
role: String,
},
ResetPassword {
username: String,
#[arg(long)]
password_stdin: bool,
},
Disable { username: String },
Enable { username: String },
Delete { username: String },
}
pub async fn execute(backend_url: &str, args: AccountArgs) -> Result<()> {
let base = backend_url.trim_end_matches('/');
match args.sub {
AccountSub::List => list(base).await,
AccountSub::Create {
username,
role,
password_stdin,
} => {
let password = read_password(&format!("Password for {username}: "), password_stdin)?;
patch_or_post(
base,
Method::Post,
"/api/accounts",
serde_json::json!({ "username": username, "password": password, "role": role }),
&format!("created {username} ({role})"),
)
.await
}
AccountSub::SetRole { username, role } => {
patch_or_post(
base,
Method::Patch,
&format!("/api/accounts/{username}"),
serde_json::json!({ "role": role }),
&format!("{username} role -> {role}"),
)
.await
}
AccountSub::ResetPassword {
username,
password_stdin,
} => {
let password =
read_password(&format!("New password for {username}: "), password_stdin)?;
patch_or_post(
base,
Method::Patch,
&format!("/api/accounts/{username}"),
serde_json::json!({ "password": password }),
&format!("{username} password reset (must change on next login)"),
)
.await
}
AccountSub::Disable { username } => {
patch_or_post(
base,
Method::Patch,
&format!("/api/accounts/{username}"),
serde_json::json!({ "disabled": true }),
&format!("{username} disabled"),
)
.await
}
AccountSub::Enable { username } => {
patch_or_post(
base,
Method::Patch,
&format!("/api/accounts/{username}"),
serde_json::json!({ "disabled": false }),
&format!("{username} enabled"),
)
.await
}
AccountSub::Delete { username } => delete(base, &username).await,
}
}
enum Method {
Post,
Patch,
}
async fn list(base: &str) -> Result<()> {
let url = format!("{base}/api/accounts");
let resp = crate::http_client::authed_client()?
.get(&url)
.send()
.await
.with_context(|| format!("GET {url}"))?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
anyhow::bail!("list failed: {status} — {body}");
}
let payload: serde_json::Value = resp.json().await?;
println!("{}", serde_json::to_string_pretty(&payload)?);
Ok(())
}
async fn patch_or_post(
base: &str,
method: Method,
path: &str,
body: serde_json::Value,
ok_msg: &str,
) -> Result<()> {
let url = format!("{base}{path}");
let client = crate::http_client::authed_client()?;
let req = match method {
Method::Post => client.post(&url),
Method::Patch => client.patch(&url),
};
let resp = req
.json(&body)
.send()
.await
.with_context(|| format!("request to {url}"))?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
anyhow::bail!("request failed: {status} — {body}");
}
println!("{ok_msg}");
Ok(())
}
async fn delete(base: &str, username: &str) -> Result<()> {
let url = format!("{base}/api/accounts/{username}");
let resp = crate::http_client::authed_client()?
.delete(&url)
.send()
.await
.with_context(|| format!("DELETE {url}"))?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
anyhow::bail!("delete failed: {status} — {body}");
}
println!("deleted {username}");
Ok(())
}