Initial commit
This commit is contained in:
199
public/admin/signup.php
Normal file
199
public/admin/signup.php
Normal file
@@ -0,0 +1,199 @@
|
||||
<?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>
|
||||
Reference in New Issue
Block a user