Files
aj-portfolio/public/admin/signup.php
2025-12-23 13:18:58 +02:00

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>