e699027b4b
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>
81 lines
3.1 KiB
PHP
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";
|