Initial commit: AzerothCore registration site
PHP registration page for AzerothCore with SRP6 salt/verifier generation, Cloudflare Turnstile + honeypot + rate limiting anti-spam, and a WoW-themed landing page with client setup instructions.
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 1022 KiB |
@@ -0,0 +1,433 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--gold-bright: #f0d78c;
|
||||
--gold: #c9a227;
|
||||
--gold-dim: #7a5c14;
|
||||
--ink: #0a0805;
|
||||
--panel: #14100a;
|
||||
--panel-light: #1d1710;
|
||||
--parchment: #e8dcc0;
|
||||
--parchment-dim: #a99a7c;
|
||||
--blood: #b23a2f;
|
||||
--moss: #6b8f3f;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px 16px;
|
||||
font-family: "EB Garamond", Georgia, "Times New Roman", serif;
|
||||
color: var(--parchment);
|
||||
background: url("background.jpg") center center / cover no-repeat fixed, #05040a;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* dark overlay + vignette over the background artwork, so the gold/parchment
|
||||
UI stays readable regardless of how bright the underlying image is */
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background:
|
||||
radial-gradient(ellipse at 50% 45%, rgba(5, 4, 8, 0.35) 0%, rgba(4, 3, 6, 0.72) 60%, rgba(3, 2, 4, 0.9) 100%),
|
||||
repeating-linear-gradient(0deg, rgba(255,255,255,0.012) 0px, rgba(255,255,255,0.012) 1px, transparent 1px, transparent 3px);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.frame {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
max-width: 460px;
|
||||
padding: 3px;
|
||||
background: linear-gradient(135deg, var(--gold-dim), var(--gold-bright) 15%, var(--gold-dim) 30%, #4a3a10 50%, var(--gold-bright) 70%, var(--gold-dim) 85%, var(--gold-bright));
|
||||
border-radius: 6px;
|
||||
box-shadow:
|
||||
0 0 0 1px #000,
|
||||
0 20px 60px rgba(0, 0, 0, 0.7),
|
||||
0 0 40px rgba(201, 162, 39, 0.08);
|
||||
}
|
||||
|
||||
.frame-wide {
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
background:
|
||||
linear-gradient(180deg, var(--panel-light) 0%, var(--panel) 12%, var(--panel) 88%, var(--panel-light) 100%);
|
||||
border: 1px solid #000;
|
||||
border-radius: 4px;
|
||||
padding: 40px 36px 34px;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(201, 162, 39, 0.35),
|
||||
inset 0 2px 12px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
/* ornamental corner brackets, drawn in CSS to echo carved-stone UI frames */
|
||||
.card::before,
|
||||
.card::after,
|
||||
.frame::before,
|
||||
.frame::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 2px solid var(--gold-bright);
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.frame::before { top: -3px; left: -3px; border-right: none; border-bottom: none; }
|
||||
.frame::after { top: -3px; right: -3px; border-left: none; border-bottom: none; }
|
||||
.card::before { bottom: -3px; left: -3px; border-right: none; border-top: none; }
|
||||
.card::after { bottom: -3px; right: -3px; border-left: none; border-top: none; }
|
||||
|
||||
.crest {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
margin: 0 auto 10px;
|
||||
background: radial-gradient(circle at 35% 30%, var(--gold-bright), var(--gold) 55%, var(--gold-dim) 100%);
|
||||
clip-path: polygon(50% 0%, 90% 20%, 100% 60%, 50% 100%, 0% 60%, 10% 20%);
|
||||
box-shadow: 0 0 18px rgba(201, 162, 39, 0.45), inset 0 0 0 2px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 6px;
|
||||
text-align: center;
|
||||
font-family: "Cinzel Decorative", "Cinzel", Georgia, serif;
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.3;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--gold-bright);
|
||||
background: linear-gradient(180deg, #fff3cf 0%, var(--gold-bright) 30%, var(--gold) 65%, #8a6a1c 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-shadow: none;
|
||||
filter: drop-shadow(0 2px 1px rgba(0, 0, 0, 0.9));
|
||||
}
|
||||
|
||||
h1 .realm-name {
|
||||
font-size: 1.05rem;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
|
||||
.divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin: 14px 0 26px;
|
||||
color: var(--gold-dim);
|
||||
}
|
||||
.divider::before,
|
||||
.divider::after {
|
||||
content: "";
|
||||
height: 1px;
|
||||
flex: 1;
|
||||
background: linear-gradient(90deg, transparent, var(--gold) 50%, transparent);
|
||||
}
|
||||
.divider span {
|
||||
color: var(--gold);
|
||||
font-size: 0.95rem;
|
||||
transform: rotate(45deg);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin: 18px 0 6px;
|
||||
font-family: "Cinzel", Georgia, serif;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 11px 13px;
|
||||
background: #0d0a06;
|
||||
border: 1px solid #3a2f1a;
|
||||
border-radius: 2px;
|
||||
color: var(--parchment);
|
||||
font-family: "EB Garamond", Georgia, serif;
|
||||
font-size: 1.05rem;
|
||||
box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.6);
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: #5a5040;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: var(--gold);
|
||||
box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.6), 0 0 0 2px rgba(201, 162, 39, 0.25), 0 0 14px rgba(201, 162, 39, 0.25);
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 5px 2px 0;
|
||||
font-size: 0.78rem;
|
||||
font-style: italic;
|
||||
color: var(--parchment-dim);
|
||||
}
|
||||
|
||||
button[type="submit"] {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 30px;
|
||||
padding: 13px;
|
||||
background: linear-gradient(180deg, #3a2f18 0%, #241a0d 55%, #1a1209 100%);
|
||||
border: 1px solid var(--gold-dim);
|
||||
border-radius: 2px;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.08),
|
||||
inset 0 0 0 1px rgba(0, 0, 0, 0.6),
|
||||
0 4px 10px rgba(0, 0, 0, 0.5);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.05s ease;
|
||||
}
|
||||
|
||||
button[type="submit"] span {
|
||||
font-family: "Cinzel", Georgia, serif;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--gold-bright);
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
button[type="submit"]:hover {
|
||||
border-color: var(--gold-bright);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
||||
inset 0 0 0 1px rgba(0, 0, 0, 0.6),
|
||||
0 0 18px rgba(201, 162, 39, 0.35);
|
||||
}
|
||||
|
||||
button[type="submit"]:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.cf-turnstile {
|
||||
margin-top: 22px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 12px 16px;
|
||||
border-radius: 2px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 0.92rem;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: linear-gradient(180deg, rgba(107, 143, 63, 0.18), rgba(107, 143, 63, 0.06));
|
||||
border: 1px solid rgba(150, 190, 90, 0.5);
|
||||
box-shadow: inset 0 0 12px rgba(107, 143, 63, 0.15);
|
||||
color: #c9e8a0;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: linear-gradient(180deg, rgba(178, 58, 47, 0.2), rgba(178, 58, 47, 0.07));
|
||||
border: 1px solid rgba(210, 90, 75, 0.55);
|
||||
box-shadow: inset 0 0 12px rgba(178, 58, 47, 0.18);
|
||||
color: #f0b2a8;
|
||||
}
|
||||
|
||||
.alert-error ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.alert-error li::before {
|
||||
content: "☠ ";
|
||||
}
|
||||
|
||||
/* Honeypot: skryté vizuálně, ale přítomné v DOM pro boty */
|
||||
.hp-field {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
top: -9999px;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
margin-bottom: 18px;
|
||||
font-family: "Cinzel", Georgia, serif;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--parchment-dim);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
.back-link:hover {
|
||||
color: var(--gold-bright);
|
||||
}
|
||||
|
||||
/* --- Landing page: steps list --- */
|
||||
|
||||
.steps-section h2 {
|
||||
margin: 0 0 22px;
|
||||
text-align: center;
|
||||
font-family: "Cinzel", Georgia, serif;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
.steps {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
counter-reset: step;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 26px;
|
||||
}
|
||||
|
||||
.steps li {
|
||||
counter-increment: step;
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.steps li::before {
|
||||
content: counter(step);
|
||||
flex: none;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: "Cinzel", Georgia, serif;
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
color: var(--ink);
|
||||
background: radial-gradient(circle at 35% 30%, var(--gold-bright), var(--gold) 60%, var(--gold-dim) 100%);
|
||||
clip-path: polygon(50% 0%, 90% 20%, 100% 60%, 50% 100%, 0% 60%, 10% 20%);
|
||||
box-shadow: 0 0 10px rgba(201, 162, 39, 0.35);
|
||||
}
|
||||
|
||||
.step-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.step-body h3 {
|
||||
margin: 0 0 6px;
|
||||
font-family: "Cinzel", Georgia, serif;
|
||||
font-size: 0.98rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
color: var(--gold-bright);
|
||||
}
|
||||
|
||||
.step-body p {
|
||||
margin: 0 0 8px;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
color: var(--parchment);
|
||||
}
|
||||
|
||||
.step-body .hint {
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
|
||||
.path,
|
||||
.realmlist-code {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 4px 0 10px;
|
||||
padding: 9px 12px;
|
||||
background: #0d0a06;
|
||||
border: 1px solid #3a2f1a;
|
||||
border-radius: 2px;
|
||||
box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.6);
|
||||
font-family: "Courier New", Consolas, monospace;
|
||||
font-size: 0.85rem;
|
||||
word-break: break-all;
|
||||
color: var(--parchment-dim);
|
||||
}
|
||||
|
||||
.realmlist-code {
|
||||
color: var(--gold-bright);
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
display: inline-block;
|
||||
margin-top: 4px;
|
||||
padding: 9px 20px;
|
||||
background: linear-gradient(180deg, #3a2f18 0%, #241a0d 55%, #1a1209 100%);
|
||||
border: 1px solid var(--gold-dim);
|
||||
border-radius: 2px;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.08),
|
||||
inset 0 0 0 1px rgba(0, 0, 0, 0.6);
|
||||
font-family: "Cinzel", Georgia, serif;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
color: var(--gold-bright);
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.9);
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.btn-link:hover {
|
||||
border-color: var(--gold-bright);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
||||
inset 0 0 0 1px rgba(0, 0, 0, 0.6),
|
||||
0 0 18px rgba(201, 162, 39, 0.35);
|
||||
}
|
||||
|
||||
.btn-link.btn-primary {
|
||||
background: linear-gradient(180deg, #4a3a10 0%, #2c2109 55%, #1a1209 100%);
|
||||
border-color: var(--gold);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.card {
|
||||
padding: 32px 22px 26px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
.steps li {
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$config = require __DIR__ . '/../includes/bootstrap.php';
|
||||
|
||||
$siteName = htmlspecialchars($config['site']['name'], ENT_QUOTES);
|
||||
$realmlist = htmlspecialchars($config['site']['realmlist'], ENT_QUOTES);
|
||||
$clientVersion = htmlspecialchars($config['site']['client_version'], ENT_QUOTES);
|
||||
$clientUrl = $config['site']['client_download_url'];
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="cs">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= $siteName ?></title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;600;700&family=Cinzel+Decorative:wght@700&family=EB+Garamond:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame frame-wide">
|
||||
<main class="card">
|
||||
<div class="crest" aria-hidden="true"></div>
|
||||
<h1>AzerothCore<br><span class="realm-name">by Minkey</span></h1>
|
||||
<div class="divider"><span>⚔</span></div>
|
||||
|
||||
<section class="steps-section">
|
||||
<h2>Jak začít hrát</h2>
|
||||
|
||||
<ol class="steps">
|
||||
<li>
|
||||
<div class="step-body">
|
||||
<h3>Stáhni si herního klienta</h3>
|
||||
<p>Server běží na verzi <strong><?= $clientVersion ?></strong>.</p>
|
||||
<?php if ($clientUrl !== ''): ?>
|
||||
<a class="btn-link" href="<?= htmlspecialchars($clientUrl, ENT_QUOTES) ?>">Stáhnout klienta</a>
|
||||
<?php else: ?>
|
||||
<a href="https://download.therawow.com/TheraWoW-Client.zip" style="text-decoration:none"><p class="hint">Odkaz na stažení</p></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="step-body">
|
||||
<h3>Uprav realmlist</h3>
|
||||
<p>Otevři soubor v instalační složce klienta:</p>
|
||||
<code class="path">World of Warcraft\Data\<lokalizace>\realmlist.wtf</code>
|
||||
<p>Smaž jeho obsah a nahraď tímto řádkem:</p>
|
||||
<code class="realmlist-code">set realmlist <?= $realmlist ?></code>
|
||||
<p class="hint">Soubor ulož a nech ho tak — klient se pak připojí na můj server místo oficiálního Blizzardu.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="step-body">
|
||||
<h3>Vytvoř si účet</h3>
|
||||
<p>Zaregistruj se a přihlas se rovnou do hry.</p>
|
||||
<a class="btn-link btn-primary" href="registrace.php">Vytvořit účet</a>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="step-body">
|
||||
<h3>Spusť Wow.exe a hraj</h3>
|
||||
<p>Přihlas se svým novým účtem a vyraž do Azerothu.</p>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$config = require __DIR__ . '/../includes/bootstrap.php';
|
||||
|
||||
function fail(array $errors, array $old = []): never
|
||||
{
|
||||
$_SESSION['flash_errors'] = $errors;
|
||||
$_SESSION['flash_old'] = $old;
|
||||
header('Location: registrace.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: registrace.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$old = [
|
||||
'username' => trim((string) ($_POST['username'] ?? '')),
|
||||
'email' => trim((string) ($_POST['email'] ?? '')),
|
||||
];
|
||||
|
||||
if (!csrf_verify($_POST['csrf_token'] ?? null)) {
|
||||
fail(['Neplatný nebo vypršelý formulář, zkus to prosím znovu.']);
|
||||
}
|
||||
|
||||
// Honeypot: boti pole často vyplní. Předstíráme úspěch, aby se bot
|
||||
// nedozvěděl, že byl odhalen, ale žádná data se nezapíšou.
|
||||
if (honeypot_triggered()) {
|
||||
$_SESSION['flash_success'] = 'Účet byl úspěšně vytvořen. Nyní se můžeš přihlásit do hry.';
|
||||
header('Location: registrace.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$ip = client_ip();
|
||||
$rateLimiter = new RateLimiter(__DIR__ . '/../data/ratelimit.sqlite', ...array_values($config['rate_limit']));
|
||||
|
||||
if ($rateLimiter->tooManyAttempts($ip)) {
|
||||
fail(['Příliš mnoho pokusů o registraci z tvé IP adresy. Zkus to prosím později.'], $old);
|
||||
}
|
||||
|
||||
$username = $old['username'];
|
||||
$email = $old['email'];
|
||||
$password = (string) ($_POST['password'] ?? '');
|
||||
$passwordConfirm = (string) ($_POST['password_confirm'] ?? '');
|
||||
|
||||
$errors = [];
|
||||
$errors = array_merge($errors, validate_username($username, $config['rules']));
|
||||
$errors = array_merge($errors, validate_email($email));
|
||||
$errors = array_merge($errors, validate_password($password, $config['rules']));
|
||||
|
||||
if ($password !== $passwordConfirm) {
|
||||
$errors[] = 'Zadaná hesla se neshodují.';
|
||||
}
|
||||
|
||||
$turnstileSecret = $config['turnstile']['secret_key'];
|
||||
if ($turnstileSecret !== '') {
|
||||
$turnstileToken = (string) ($_POST['cf-turnstile-response'] ?? '');
|
||||
if (!Turnstile::verify($turnstileToken, $turnstileSecret, $ip)) {
|
||||
$errors[] = 'Ověření „nejsem robot“ se nezdařilo, zkus to prosím znovu.';
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
// Neúspěšný pokus se počítá do rate limitu, aby útočník nemohl
|
||||
// zkoušet donekonečna jen proto, že validace selhala.
|
||||
$rateLimiter->recordAttempt($ip);
|
||||
fail($errors, $old);
|
||||
}
|
||||
|
||||
$pdo = Database::connection();
|
||||
|
||||
$stmt = $pdo->prepare('SELECT id FROM account WHERE username = :username');
|
||||
$stmt->execute([':username' => strtoupper($username)]);
|
||||
if ($stmt->fetch() !== false) {
|
||||
$rateLimiter->recordAttempt($ip);
|
||||
fail(['Toto uživatelské jméno už je obsazené.'], $old);
|
||||
}
|
||||
|
||||
$srp = Srp6::makeRegistrationData($username, $password);
|
||||
|
||||
try {
|
||||
$insert = $pdo->prepare(
|
||||
'INSERT INTO account (username, salt, verifier, email, reg_mail, joindate, last_ip)
|
||||
VALUES (:username, :salt, :verifier, :email, :reg_mail, NOW(), :last_ip)'
|
||||
);
|
||||
$insert->execute([
|
||||
':username' => strtoupper($username),
|
||||
':salt' => $srp['salt'],
|
||||
':verifier' => $srp['verifier'],
|
||||
':email' => $email,
|
||||
':reg_mail' => $email,
|
||||
':last_ip' => $ip,
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
$rateLimiter->recordAttempt($ip);
|
||||
if ($e->getCode() === '23000') {
|
||||
fail(['Toto uživatelské jméno už je obsazené.'], $old);
|
||||
}
|
||||
error_log('Registration insert failed: ' . $e->getMessage());
|
||||
fail(['Registraci se nepodařilo dokončit, zkus to prosím později.'], $old);
|
||||
}
|
||||
|
||||
$rateLimiter->recordAttempt($ip);
|
||||
|
||||
$_SESSION['flash_success'] = 'Účet byl úspěšně vytvořen. Nyní se můžeš přihlásit do hry.';
|
||||
unset($_SESSION['csrf_token']);
|
||||
header('Location: registrace.php');
|
||||
exit;
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$config = require __DIR__ . '/../includes/bootstrap.php';
|
||||
|
||||
$errors = $_SESSION['flash_errors'] ?? [];
|
||||
$old = $_SESSION['flash_old'] ?? [];
|
||||
$success = $_SESSION['flash_success'] ?? null;
|
||||
unset($_SESSION['flash_errors'], $_SESSION['flash_old'], $_SESSION['flash_success']);
|
||||
|
||||
$siteKey = htmlspecialchars($config['turnstile']['site_key'], ENT_QUOTES);
|
||||
$token = csrf_token();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="cs">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Registrace účtu — WoW server</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;600;700&family=Cinzel+Decorative:wght@700&family=EB+Garamond:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
<?php if ($siteKey !== ''): ?>
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||
<?php endif; ?>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<main class="card">
|
||||
<a class="back-link" href="index.php">← Zpět na úvod</a>
|
||||
<div class="crest" aria-hidden="true"></div>
|
||||
<h1>Registrace účtu</h1>
|
||||
<div class="divider"><span>⚔</span></div>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success">
|
||||
<?= htmlspecialchars($success, ENT_QUOTES) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="alert alert-error">
|
||||
<ul>
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<li><?= htmlspecialchars($error, ENT_QUOTES) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!$success): ?>
|
||||
<form method="post" action="register.php" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($token, ENT_QUOTES) ?>">
|
||||
|
||||
<!-- Honeypot: skryté před lidmi přes CSS, boti pole často vyplní -->
|
||||
<div class="hp-field" aria-hidden="true">
|
||||
<label for="website">Nechte prázdné</label>
|
||||
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<label for="username">Uživatelské jméno</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
minlength="<?= (int) $config['rules']['username_min'] ?>"
|
||||
maxlength="<?= (int) $config['rules']['username_max'] ?>"
|
||||
value="<?= htmlspecialchars($old['username'] ?? '', ENT_QUOTES) ?>"
|
||||
required
|
||||
>
|
||||
<p class="hint">Pouze písmena A-Z a číslice, <?= (int) $config['rules']['username_min'] ?>–<?= (int) $config['rules']['username_max'] ?> znaků.</p>
|
||||
|
||||
<label for="email">E-mail</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value="<?= htmlspecialchars($old['email'] ?? '', ENT_QUOTES) ?>"
|
||||
required
|
||||
>
|
||||
|
||||
<label for="password">Heslo</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
minlength="<?= (int) $config['rules']['password_min'] ?>"
|
||||
maxlength="<?= (int) $config['rules']['password_max'] ?>"
|
||||
required
|
||||
>
|
||||
|
||||
<label for="password_confirm">Heslo znovu</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password_confirm"
|
||||
name="password_confirm"
|
||||
minlength="<?= (int) $config['rules']['password_min'] ?>"
|
||||
maxlength="<?= (int) $config['rules']['password_max'] ?>"
|
||||
required
|
||||
>
|
||||
|
||||
<?php if ($siteKey !== ''): ?>
|
||||
<div class="cf-turnstile" data-sitekey="<?= $siteKey ?>"></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<button type="submit"><span>Zaregistrovat se</span></button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user