200 lines
7.5 KiB
PHP
200 lines
7.5 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
require __DIR__ . '/../../includes/bootstrap.php';
|
|
|
|
// already logged in? go dashboard
|
|
if (admin_is_logged_in()) {
|
|
header('Location: ' . url_path('/public/admin/dashboard.php'));
|
|
exit;
|
|
}
|
|
|
|
// allow registration only if there are no admin users yet (first-time setup)
|
|
$cnt = (int)pdo()->query("SELECT COUNT(*) FROM admin_users")->fetchColumn();
|
|
if ($cnt > 0) {
|
|
header('Location: ' . url_path('/public/admin/login.php'));
|
|
exit;
|
|
}
|
|
|
|
$err = '';
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$csrf = (string)($_POST['csrf'] ?? '');
|
|
$user = trim((string)($_POST['username'] ?? ''));
|
|
$pass = (string)($_POST['password'] ?? '');
|
|
$pass2 = (string)($_POST['password2'] ?? '');
|
|
|
|
if (!csrf_check($csrf)) {
|
|
$err = 'Invalid session. Refresh and try again.';
|
|
} elseif (!preg_match('/^[a-zA-Z0-9_]{3,32}$/', $user)) {
|
|
$err = 'Username: 3-32 chars (letters, numbers, underscore).';
|
|
} elseif (strlen($pass) < 10) {
|
|
$err = 'Password must be at least 10 characters.';
|
|
} elseif ($pass !== $pass2) {
|
|
$err = 'Passwords do not match.';
|
|
} else {
|
|
// even though cnt==0, still check just in case (race condition)
|
|
$st = pdo()->prepare("SELECT id FROM admin_users WHERE username=? LIMIT 1");
|
|
$st->execute([$user]);
|
|
if ($st->fetch()) {
|
|
$err = 'Username already exists.';
|
|
} else {
|
|
$algo = defined('PASSWORD_ARGON2ID') ? PASSWORD_ARGON2ID : PASSWORD_DEFAULT;
|
|
$hash = password_hash($pass, $algo, [
|
|
'memory_cost' => 1 << 17, // 128MB
|
|
'time_cost' => 4,
|
|
'threads' => 2,
|
|
]);
|
|
|
|
$ins = pdo()->prepare("INSERT INTO admin_users (username, pass_hash) VALUES (?, ?)");
|
|
$ins->execute([$user, $hash]);
|
|
|
|
$id = (int)pdo()->lastInsertId();
|
|
admin_login(['id' => $id, 'username' => $user]);
|
|
|
|
header('Location: ' . url_path('/public/admin/dashboard.php'));
|
|
exit;
|
|
}
|
|
}
|
|
}
|
|
?>
|
|
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Setup Admin</title>
|
|
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="<?= htmlspecialchars(url_path('/public/css/app.css')) ?>">
|
|
<link href="https://cdn.jsdelivr.net/npm/remixicon@4.6.0/fonts/remixicon.css" rel="stylesheet">
|
|
</head>
|
|
|
|
<body class="min-vh-100 d-flex align-items-center text-white">
|
|
|
|
<div class="bg-aurora"></div>
|
|
<div class="bg-grid"></div>
|
|
|
|
<div class="container" style="max-width: 720px;">
|
|
<div class="card card-glass p-4 p-md-5">
|
|
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap mb-3">
|
|
<div class="d-flex align-items-center gap-3">
|
|
<div class="brand-badge">GM</div>
|
|
<div>
|
|
<div class="pill d-inline-flex align-items-center gap-2">
|
|
<span class="dot"></span> First-time setup
|
|
</div>
|
|
<h1 class="h3 mb-0 mt-2 hero-gradient">Create Admin Account</h1>
|
|
<div class="text-white-50 small">This page auto-disables after the first admin is created.</div>
|
|
</div>
|
|
</div>
|
|
<span class="tag"><i class="ri-lock-2-line"></i> Secure</span>
|
|
</div>
|
|
|
|
<?php if ($err): ?>
|
|
<div class="alert alert-danger border-0" style="background: rgba(239,68,68,.15); color:#fff;">
|
|
<i class="ri-error-warning-line"></i> <?= htmlspecialchars($err) ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<form method="post" autocomplete="off" class="mt-3">
|
|
<input type="hidden" name="csrf" value="<?= htmlspecialchars(csrf_token()) ?>">
|
|
|
|
<div class="row g-3">
|
|
<div class="col-12">
|
|
<label class="form-label text-white-50">Username</label>
|
|
<div class="position-relative">
|
|
<input class="field" name="username" maxlength="32" required
|
|
placeholder="georgi_admin"
|
|
value="<?= htmlspecialchars((string)($_POST['username'] ?? '')) ?>">
|
|
<span class="position-absolute top-50 translate-middle-y end-0 me-3 text-white-50">
|
|
<i class="ri-user-3-line"></i>
|
|
</span>
|
|
</div>
|
|
<div class="form-text text-white-50">Allowed: letters, numbers, underscore. 3-32 chars.</div>
|
|
</div>
|
|
|
|
<div class="col-12 col-md-6">
|
|
<label class="form-label text-white-50">Password</label>
|
|
<div class="position-relative">
|
|
<input id="pw" class="field" type="password" name="password" required placeholder="Min 10 characters">
|
|
<button type="button" class="btn btn-sm position-absolute top-50 translate-middle-y end-0 me-2"
|
|
id="togglePw"
|
|
style="background: rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.10); color:#fff; border-radius: 12px;">
|
|
<i class="ri-eye-line"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="mt-2">
|
|
<div class="bar"><div class="bar-fill" id="pwBar"></div></div>
|
|
<div class="small text-white-50 mt-1" id="pwHint">Use 10+ chars, mix letters + numbers.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12 col-md-6">
|
|
<label class="form-label text-white-50">Confirm password</label>
|
|
<div class="position-relative">
|
|
<input id="pw2" class="field" type="password" name="password2" required placeholder="Repeat password">
|
|
<span class="position-absolute top-50 translate-middle-y end-0 me-3 text-white-50" id="matchIcon">
|
|
<i class="ri-shield-check-line"></i>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12 mt-2">
|
|
<button class="btn w-100 py-3"
|
|
style="background: linear-gradient(135deg, rgba(139,92,246,.95), rgba(217,70,239,.85)); border:0; color:#0b0712; font-weight:800; border-radius: 16px;">
|
|
<i class="ri-sparkling-2-line"></i> Create Admin
|
|
</button>
|
|
</div>
|
|
|
|
<div class="col-12 text-center">
|
|
<div class="text-white-50 small">
|
|
After setup, you will be redirected to the dashboard.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
<script>
|
|
$(function(){
|
|
// toggle password
|
|
$('#togglePw').on('click', function(){
|
|
const $pw = $('#pw');
|
|
const isPwd = $pw.attr('type') === 'password';
|
|
$pw.attr('type', isPwd ? 'text' : 'password');
|
|
$(this).find('i').attr('class', isPwd ? 'ri-eye-off-line' : 'ri-eye-line');
|
|
});
|
|
|
|
function scorePassword(p){
|
|
let s = 0;
|
|
if (!p) return 0;
|
|
if (p.length >= 10) s += 35;
|
|
if (p.length >= 14) s += 15;
|
|
if (/[a-z]/.test(p)) s += 15;
|
|
if (/[A-Z]/.test(p)) s += 10;
|
|
if (/[0-9]/.test(p)) s += 15;
|
|
if (/[^a-zA-Z0-9]/.test(p)) s += 10;
|
|
return Math.min(100, s);
|
|
}
|
|
|
|
function updatePwUI(){
|
|
const p = $('#pw').val() || '';
|
|
const s = scorePassword(p);
|
|
$('#pwBar').css('width', s + '%');
|
|
|
|
const p2 = $('#pw2').val() || '';
|
|
const match = p.length > 0 && p === p2;
|
|
$('#matchIcon i').attr('class', match ? 'ri-checkbox-circle-line' : 'ri-shield-check-line');
|
|
$('#matchIcon').toggleClass('text-success', match).toggleClass('text-white-50', !match);
|
|
}
|
|
|
|
$('#pw, #pw2').on('input', updatePwUI);
|
|
updatePwUI();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|