use std::io::Write;
use crate::core::Model;
use crate::manage_interactive;
use crate::migrate::MigrateError;
use crate::sql::{Auto, Pool};
use super::user::AdminUser;
pub async fn create_admin_cmd<W: Write + Send>(
pool: &Pool,
args: &[String],
w: &mut W,
) -> Result<(), MigrateError> {
let mut iter = args.iter();
let mut username_arg: Option<String> = None;
let mut password: Option<String> = None;
let mut generate = false;
let mut is_superuser = false;
let mut password_stdin = false;
let mut skip_validation = false;
while let Some(token) = iter.next() {
match token.as_str() {
"--password" => {
let v = iter.next().cloned().ok_or_else(|| {
MigrateError::Validation("--password requires a value".into())
})?;
password = Some(v);
}
"--generate" => generate = true,
"--superuser" => is_superuser = true,
"--password-stdin" => password_stdin = true,
"--skip-validation" => skip_validation = true,
"--help" | "-h" => {
writeln!(
w,
"create-admin <username> [--password <p> | --password-stdin | --generate] \
[--skip-validation] [--superuser]"
)?;
return Ok(());
}
arg if arg.starts_with("--") => {
return Err(MigrateError::Validation(format!(
"create-admin: unknown flag `{arg}`"
)));
}
positional if username_arg.is_none() => {
username_arg = Some(positional.to_owned());
}
other => {
return Err(MigrateError::Validation(format!(
"create-admin: unexpected positional argument `{other}`"
)));
}
}
}
if generate && (password.is_some() || password_stdin) {
return Err(MigrateError::Validation(
"create-admin: --generate and --password are mutually exclusive".into(),
));
}
let username = match username_arg {
Some(u) => u,
None => manage_interactive::ask("Username: ")
.map_err(|e| MigrateError::Validation(format!("prompt failed: {e}")))?
.ok_or_else(|| {
MigrateError::Validation(
"create-admin requires a username positional argument".into(),
)
})?,
};
let (plain, generated) = if generate {
let plain = generate_password(20);
(plain, true)
} else {
use manage_interactive::{NewPasswordError, PasswordInput};
let err = |e: NewPasswordError| match e {
NewPasswordError::Io(e) => MigrateError::Io(e),
e => MigrateError::Validation(format!("create-admin: {e}")),
};
let input = PasswordInput::from_flags(password, password_stdin).map_err(err)?;
let p = manage_interactive::read_new_password(input, skip_validation, "Password: ")
.map_err(err)?;
(p, false)
};
let snapshot = crate::migrate::SchemaSnapshot::from_models_forced(&[AdminUser::SCHEMA]);
crate::migrate::apply_idempotent(pool, &snapshot).await?;
use crate::core::{SelectQuery, SqlValue};
let select = SelectQuery::by_pk(
AdminUser::SCHEMA,
"username",
SqlValue::String(username.clone()),
);
let fields: Vec<&'static crate::core::FieldSchema> = AdminUser::SCHEMA.fields.iter().collect();
let existing = crate::sql::select_one_row_as_json(pool, &select, &fields)
.await
.ok()
.flatten();
if existing.is_some() {
return Err(MigrateError::Validation(format!(
"create-admin: username `{username}` already exists"
)));
}
let hash = crate::passwords::hash_async(&plain)
.await
.map_err(|e| MigrateError::Validation(format!("password hash failed: {e}")))?;
let mut user = AdminUser {
id: Auto::Unset,
username: username.clone(),
password_hash: hash,
#[cfg(feature = "admin-sso")]
email: None,
is_superuser,
active: true,
created_at: chrono::Utc::now(),
sessions_revoked_at: None,
};
user.save_pool(pool)
.await
.map_err(|e| MigrateError::Validation(format!("insert failed: {e}")))?;
let user_id = user.id.get().copied().unwrap_or_default();
writeln!(
w,
"created admin user `{username}` (id={user_id}{super_tag})",
super_tag = if is_superuser { ", superuser" } else { "" }
)?;
if generated {
writeln!(w, "generated password: {plain}")?;
writeln!(w, "(save this — it isn't stored or recoverable)")?;
}
Ok(())
}
fn generate_password(n: usize) -> String {
use base64::Engine;
use rand::RngCore;
let mut buf = vec![0u8; n];
rand::thread_rng().fill_bytes(&mut buf);
let s = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&buf);
s.chars().take(n).collect()
}