Files
novaconium/novaconium/bin/create-admin-user.php
T
code e699027b4b Add email verification for user accounts
Every account created after the first must confirm a 24-hour link
(Lib\Mailer::sendMail(), log-file or MailJet) before AdminAuth::attempt()
allows login, folded into the same generic pass/fail as a disabled account
or wrong password. First user and pre-migration rows are grandfathered;
create-admin-user.php also auto-verifies for lockout recovery.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 19:23:20 +00:00

81 lines
3.1 KiB
PHP

<?php
use Lib\Db;
use Lib\Validate;
require __DIR__ . '/../autoload.php';
// Creates an admin user from the command line (e.g. from a deploy script,
// or to fix a lockout) — the CLI counterpart to /admin/users, and the way
// to create the first user *before* flipping admin_auth_enabled on, which
// avoids the brief open-access setup window /admin/users otherwise relies
// on (see /admin/docs/admin-auth).
//
// php novaconium/bin/create-admin-user.php <username> <email>
//
// The password is read from stdin — typed at the prompt (echo suppressed
// where the terminal supports it), or piped:
//
// echo 'the-password' | php novaconium/bin/create-admin-user.php admin admin@example.com
//
// Deliberately not gated on admin_auth_enabled: creating the row is
// harmless while the gate is off, and doing it first is the safer order.
$username = trim((string) ($argv[1] ?? ''));
$email = Validate::isEmail((string) ($argv[2] ?? ''));
if ($username === '' || strlen($username) > 64 || $email === false) {
fwrite(STDERR, "Usage: php novaconium/bin/create-admin-user.php <username> <email>\n");
exit(1);
}
// Db::connection() runs pending migrations on first touch, so the users
// table exists after this even on a fresh clone.
$exists = (bool) Db::query('SELECT EXISTS(SELECT 1 FROM users WHERE username = ?)', [$username])->fetchColumn();
if ($exists) {
fwrite(STDERR, "A user named '{$username}' already exists.\n");
exit(1);
}
$emailExists = (bool) Db::query('SELECT EXISTS(SELECT 1 FROM users WHERE email = ?)', [$email])->fetchColumn();
if ($emailExists) {
fwrite(STDERR, "A user with the email '{$email}' already exists.\n");
exit(1);
}
$interactive = stream_isatty(STDIN);
if ($interactive) {
fwrite(STDOUT, "Password for '{$username}': ");
// Suppress echo while the password is typed; restore afterwards.
// shell_exec() may be unavailable/no-op on some setups — then the
// password just echoes, same as any basic CLI prompt.
shell_exec('stty -echo 2> /dev/null');
}
$password = rtrim((string) fgets(STDIN), "\r\n");
if ($interactive) {
shell_exec('stty echo 2> /dev/null');
fwrite(STDOUT, "\n");
}
if (strlen($password) < 8) {
fwrite(STDERR, "Use a password of at least 8 characters.\n");
exit(1);
}
// Always role 'admin', as the script name says — /admin/users is the
// place to create registered users; this exists for first-user setup and
// lockout recovery, both of which need an admin. Auto-verified for the
// same reason /admin/users auto-verifies the very first user: an admin
// created this way has nobody else to have vouched for them, and lockout
// recovery in particular can't depend on a mail transport being
// configured (see /admin/docs/admin-auth's email verification section).
$now = gmdate('Y-m-d\TH:i:s\Z');
Db::query(
"INSERT INTO users (username, email, password_hash, role, user_group, is_disabled, created_at, verified_at) VALUES (?, ?, ?, 'admin', '', 0, ?, ?)",
[$username, $email, password_hash($password, PASSWORD_DEFAULT), $now, $now]
);
echo "User '{$username}' created.\n";
echo "If admin auth isn't enabled yet, set 'admin_auth_enabled' => true in App/config.php.\n";