use std::path::PathBuf;
use anyhow::{Context, Result};
use clap::{Args, Subcommand};
use gradatum_acl_auth::{ApiKeyStore, SqliteApiKeyStore};
#[derive(Debug, Subcommand)]
pub enum ApiKeyCmd {
Create(ApiKeyCreateArgs),
List(ApiKeyListArgs),
Revoke(ApiKeyRevokeArgs),
Rotate(ApiKeyRotateArgs),
}
#[derive(Debug, Args)]
pub struct ApiKeyCreateArgs {
#[arg(long)]
pub root: PathBuf,
#[arg(long)]
pub owner: String,
#[arg(long, default_value = "vault_read")]
pub scopes: String,
#[arg(long, default_value = "main")]
pub tenant: String,
#[arg(long)]
pub description: Option<String>,
}
#[derive(Debug, Args)]
pub struct ApiKeyListArgs {
#[arg(long)]
pub root: PathBuf,
#[arg(long)]
pub all: bool,
}
#[derive(Debug, Args)]
pub struct ApiKeyRevokeArgs {
#[arg(long)]
pub root: PathBuf,
#[arg(long)]
pub prefix: String,
}
#[derive(Debug, Args)]
pub struct ApiKeyRotateArgs {
#[arg(long)]
pub root: PathBuf,
#[arg(long)]
pub prefix: String,
}
pub async fn run(cmd: ApiKeyCmd) -> Result<()> {
match cmd {
ApiKeyCmd::Create(args) => run_create(args).await,
ApiKeyCmd::List(args) => run_list(args).await,
ApiKeyCmd::Revoke(args) => run_revoke(args).await,
ApiKeyCmd::Rotate(args) => run_rotate(args).await,
}
}
fn resolve_db_path(root: &std::path::Path) -> PathBuf {
root.join("db/api_keys.sqlite")
}
async fn open_store(root: &std::path::Path) -> Result<SqliteApiKeyStore> {
let db_path = resolve_db_path(root);
SqliteApiKeyStore::init(&db_path)
.await
.with_context(|| format!("ouverture du store api_keys : {}", db_path.display()))
}
async fn run_create(args: ApiKeyCreateArgs) -> Result<()> {
let store = open_store(&args.root).await?;
let scopes: Vec<String> = args
.scopes
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
let material = store
.create(&args.owner, scopes, args.tenant, args.description)
.await
.map_err(|e| anyhow::anyhow!("création API key échouée: {e}"))?;
eprintln!("API key créée (secret affiché UNE SEULE FOIS) :");
eprintln!(" préfixe : {}", material.prefix);
println!("{}", material.secret);
Ok(())
}
async fn run_list(args: ApiKeyListArgs) -> Result<()> {
let store = open_store(&args.root).await?;
let keys = store
.list(args.all)
.await
.map_err(|e| anyhow::anyhow!("listage API keys échoué: {e}"))?;
if keys.is_empty() {
eprintln!("Aucune clé{}.", if args.all { "" } else { " active" });
return Ok(());
}
println!(
"{:<12} {:<24} {:<12} {:<16} état",
"préfixe", "owner", "tenant", "scopes"
);
println!("{}", "-".repeat(80));
for key in &keys {
let etat = if key.is_revoked() {
"révoquée"
} else {
"active"
};
let scopes = key.scopes.join(",");
println!(
"{:<12} {:<24} {:<12} {:<16} {}",
key.prefix, key.owner, key.tenant_id, scopes, etat
);
}
eprintln!("\n{} clé(s) listée(s).", keys.len());
Ok(())
}
async fn run_revoke(args: ApiKeyRevokeArgs) -> Result<()> {
let store = open_store(&args.root).await?;
store
.revoke(&args.prefix)
.await
.map_err(|e| anyhow::anyhow!("révocation échouée pour '{}': {e}", args.prefix))?;
eprintln!("Clé '{}' révoquée avec succès.", args.prefix);
Ok(())
}
async fn run_rotate(args: ApiKeyRotateArgs) -> Result<()> {
let store = open_store(&args.root).await?;
let material = store
.rotate(&args.prefix)
.await
.map_err(|e| anyhow::anyhow!("rotation échouée pour '{}': {e}", args.prefix))?;
eprintln!(
"Rotation réussie (ancien préfixe révoqué : {}).",
args.prefix
);
eprintln!("Nouveau secret (affiché UNE SEULE FOIS) :");
eprintln!(" préfixe : {}", material.prefix);
println!("{}", material.secret);
Ok(())
}