use adminx::prelude::*;
use clap::{Parser, Subcommand};
const USERS_TABLE: &str = "adminx_users";
#[derive(Parser)]
#[command(name = "adminx", version, about = "adminx admin-panel CLI")]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
CreateAdmin {
#[arg(short, long, env = "EMAIL")]
email: String,
#[arg(short, long, env = "PASSWORD")]
password: String,
#[arg(short, long, env = "ROLE", default_value = "admin")]
role: String,
},
Seed {
#[arg(short, long)]
file: Option<String>,
},
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
match Cli::parse().command {
Command::CreateAdmin {
email,
password,
role,
} => create_admin_cmd(&email, &password, &role).await,
Command::Seed { file } => seed_cmd(file).await,
}
}
async fn seed_cmd(file: Option<String>) -> anyhow::Result<()> {
use std::io::Read;
let raw = match &file {
Some(path) => std::fs::read_to_string(path)
.map_err(|e| anyhow::anyhow!("reading {path}: {e}"))?,
None => {
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf)?;
buf
}
};
let statements: Vec<String> = raw
.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with("--") && !l.starts_with('#'))
.map(str::to_owned)
.collect();
if statements.is_empty() {
anyhow::bail!("no statements to seed (empty input)");
}
let refs: Vec<&str> = statements.iter().map(String::as_str).collect();
if let Ok(url) = std::env::var("DATABASE_URL") {
let n = adminx::seaorm::seed(&url, &refs)
.await
.map_err(|e| anyhow::anyhow!("{e:?}"))?;
println!("✓ seeded {} statement(s) — {n} row(s) affected", refs.len());
} else if let Some(uri) = env_any(&["MONGO_URL", "MONGODB_URL"]) {
let db = env_any(&["MONGO_DB", "ADMINX_DB_NAME"]).unwrap_or_else(|| "adminx".into());
let n = adminx::mongo::seed(&uri, &db, &refs)
.await
.map_err(|e| anyhow::anyhow!("{e:?}"))?;
println!("✓ seeded {} statement(s) — {n} document(s) affected", refs.len());
} else {
anyhow::bail!(
"no storage configured: set DATABASE_URL (SQL) or MONGO_URL (MongoDB)"
);
}
Ok(())
}
async fn create_admin_cmd(email: &str, password: &str, role: &str) -> anyhow::Result<()> {
init_storage().await?;
configure_auth(AuthConfig {
jwt_secret: "cli-seed".into(),
token_ttl_secs: 86_400,
admin_table: USERS_TABLE.into(),
secure_cookie: false,
});
let existing = storage()
.find_one_by(USERS_TABLE, "email", email)
.await
.map_err(CoreError::from)?;
if existing.is_some() {
println!("user {email} already exists — nothing to do");
return Ok(());
}
create_admin(email, password, role).await?;
println!("✓ created {role} {email}");
Ok(())
}
async fn init_storage() -> anyhow::Result<()> {
if let Ok(url) = std::env::var("DATABASE_URL") {
let store = adminx::seaorm::connect(&url).await?;
store.execute_sql(users_ddl(&url)).await?;
set_storage(Box::new(store));
Ok(())
} else if let Some(uri) = env_any(&["MONGO_URL", "MONGODB_URL"]) {
let db = env_any(&["MONGO_DB", "ADMINX_DB_NAME"]).unwrap_or_else(|| "adminx".into());
adminx::mongo::init(&uri, &db).await?;
Ok(())
} else {
anyhow::bail!(
"no storage configured: set DATABASE_URL (Postgres/MySQL/SQLite) \
or MONGO_URL (MongoDB)"
)
}
}
fn env_any(keys: &[&str]) -> Option<String> {
keys.iter().find_map(|k| std::env::var(k).ok())
}
fn users_ddl(url: &str) -> &'static str {
if url.starts_with("sqlite") {
"CREATE TABLE IF NOT EXISTS adminx_users (\
id INTEGER PRIMARY KEY AUTOINCREMENT, \
email TEXT NOT NULL UNIQUE, \
encrypted_password TEXT NOT NULL, \
role TEXT NOT NULL DEFAULT 'admin', \
mfa_enabled BOOLEAN NOT NULL DEFAULT 0, \
mfa_secret TEXT, \
mfa_backup_codes TEXT)"
} else if url.starts_with("mysql") {
"CREATE TABLE IF NOT EXISTS adminx_users (\
id INT AUTO_INCREMENT PRIMARY KEY, \
email VARCHAR(255) NOT NULL UNIQUE, \
encrypted_password TEXT NOT NULL, \
role VARCHAR(64) NOT NULL DEFAULT 'admin', \
mfa_enabled BOOLEAN NOT NULL DEFAULT false, \
mfa_secret TEXT, \
mfa_backup_codes TEXT)"
} else {
"CREATE TABLE IF NOT EXISTS adminx_users (\
id SERIAL PRIMARY KEY, \
email TEXT NOT NULL UNIQUE, \
encrypted_password TEXT NOT NULL, \
role TEXT NOT NULL DEFAULT 'admin', \
mfa_enabled BOOLEAN NOT NULL DEFAULT false, \
mfa_secret TEXT, \
mfa_backup_codes TEXT)"
}
}