mod config; mod db; mod models; use clap::{Parser, Subcommand}; use argon2::{Argon2, PasswordHasher, password_hash::{SaltString, rand_core::OsRng}}; use uuid::Uuid; use rand::Rng; #[derive(Parser)] #[command(name = "gateway-cli", about = "Action Gateway admin CLI")] struct Cli { #[command(subcommand)] command: Commands, } #[derive(Subcommand)] enum Commands { /// Add a user AddUser { username: String, password: String, }, /// List all users ListUsers, /// Remove a user RemoveUser { username: String, }, /// Generate a new API key for the agent GenApiKey { #[arg(default_value = "agent")] label: String, }, /// List API keys ListKeys, } fn main() { let cli = Cli::parse(); let config = config::Config::load(); let conn = db::open(&config.database_path).expect("Failed to open database"); match cli.command { Commands::AddUser { username, password } => { let salt = SaltString::generate(&mut OsRng); let hash = Argon2::default() .hash_password(password.as_bytes(), &salt) .unwrap() .to_string(); conn.execute( "INSERT INTO users (id, username, password_hash) VALUES (?1, ?2, ?3)", [&Uuid::new_v4().to_string(), &username, &hash], ).unwrap(); println!("✅ User '{}' created.", username); } Commands::ListUsers => { let mut stmt = conn.prepare("SELECT username, created_at FROM users ORDER BY created_at").unwrap(); let users: Vec<(String, String)> = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?))).unwrap() .filter_map(|r| r.ok()).collect(); if users.is_empty() { println!("No users."); } else { println!("{:<20} {}", "Username", "Created"); println!("{}", "-".repeat(40)); for (u, c) in users { println!("{:<20} {}", u, c); } } } Commands::RemoveUser { username } => { let n = conn.execute("DELETE FROM users WHERE username = ?1", [&username]).unwrap(); if n > 0 { println!("✅ User '{}' removed.", username); } else { println!("❌ User '{}' not found.", username); } } Commands::GenApiKey { label } => { // Generate random key let key: String = rand::thread_rng() .sample_iter(&rand::distributions::Alphanumeric) .take(48) .map(char::from) .collect(); let salt = SaltString::generate(&mut OsRng); let hash = Argon2::default() .hash_password(key.as_bytes(), &salt) .unwrap() .to_string(); conn.execute( "INSERT INTO api_keys (id, key_hash, label) VALUES (?1, ?2, ?3)", [&Uuid::new_v4().to_string(), &hash, &label], ).unwrap(); println!("✅ API key generated for '{}':", label); println!("\n {}\n", key); println!("Store this key securely — it won't be shown again."); } Commands::ListKeys => { let mut stmt = conn.prepare("SELECT label, created_at FROM api_keys ORDER BY created_at").unwrap(); let keys: Vec<(String, String)> = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?))).unwrap() .filter_map(|r| r.ok()).collect(); if keys.is_empty() { println!("No API keys."); } else { println!("{:<20} {}", "Label", "Created"); println!("{}", "-".repeat(40)); for (l, c) in keys { println!("{:<20} {}", l, c); } } } } }