[FÁZE-2][auth] Přidána stránka pro nastavení nového hesla

This commit is contained in:
stepan
2026-03-17 00:08:03 +01:00
parent b866eaafd0
commit abfd08dc6b
+279
View File
@@ -0,0 +1,279 @@
<?php
// ============================================================
// OBNOVA HESLA KROK 2
// ============================================================
// Na tuto stránku přichází uživatel z odkazu v emailu.
// Odkaz obsahuje ID záznamu a plaintext token.
//
// Stránka ověří platnost tokenu a umožní nastavit nové heslo.
// ============================================================
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/db.php';
session_name(SESSION_NAZEV);
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'httponly' => true,
'samesite' => 'Strict',
// 'secure' => true,
]);
session_start();
// Pokud je uživatel přihlášen, přesměrujeme ho
if (isset($_SESSION['uzivatel_id'])) {
header('Location: ' . AUTH_REDIRECT_PO_PRIHLASENI);
exit;
}
// ------------------------------------------------------------
// CSRF TOKEN
// ------------------------------------------------------------
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$csrf_token = $_SESSION['csrf_token'];
// ------------------------------------------------------------
// OVĚŘENÍ ODKAZU (parametry z URL)
// ------------------------------------------------------------
$token_ok = false; // true pokud je token platný
$zaznam_id = 0; // ID záznamu v auth_password_resets
$uzivatel_id = 0; // ID uživatele
$id_z_url = $_GET['id'] ?? '';
$token_z_url = $_GET['token'] ?? '';
if (!empty($id_z_url) && !empty($token_z_url)) {
// Vyhledáme záznam podle ID
$stmt = $pdo->prepare("
SELECT `id`, `uzivatel_id`, `token_hash`, `expiruje`
FROM `" . DB_TABULKA_RESET . "`
WHERE `id` = :id
LIMIT 1
");
$stmt->execute([':id' => (int) $id_z_url]);
$zaznam = $stmt->fetch();
if ($zaznam) {
// Zkontrolujeme expiraci
if (time() < strtotime($zaznam['expiruje'])) {
// Ověříme token (password_verify = konstantní čas,
// ochrana před timing útoky)
if (password_verify($token_z_url, $zaznam['token_hash'])) {
$token_ok = true;
$zaznam_id = $zaznam['id'];
$uzivatel_id = $zaznam['uzivatel_id'];
}
}
}
}
// ------------------------------------------------------------
// ZPRACOVÁNÍ FORMULÁŘE (nastavení nového hesla)
// ------------------------------------------------------------
$chyba = '';
$uspech = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $token_ok) {
// -- Ověření CSRF tokenu ----------------------------------
$csrf_z_formulare = $_POST['csrf_token'] ?? '';
if (!hash_equals($csrf_token, $csrf_z_formulare)) {
$chyba = 'Neplatný požadavek. Zkuste stránku obnovit a zkusit znovu.';
}
if (empty($chyba)) {
$heslo = $_POST['heslo'] ?? '';
$heslo2 = $_POST['heslo2'] ?? '';
// Validace hesla
if (empty($heslo)) {
$chyba = 'Heslo nesmí být prázdné.';
} elseif (mb_strlen($heslo) < HESLO_MIN_DELKA) {
$chyba = 'Heslo musí mít alespoň ' . HESLO_MIN_DELKA . ' znaků.';
} elseif ($heslo !== $heslo2) {
$chyba = 'Hesla se neshodují.';
}
}
if (empty($chyba)) {
try {
// Zahashujeme nové heslo
$heslo_hash = password_hash($heslo, PASSWORD_DEFAULT);
// Aktualizujeme heslo uživatele v DB
$stmt = $pdo->prepare("
UPDATE `" . DB_TABULKA_UZIVATELE . "`
SET `heslo` = :heslo
WHERE `id` = :id
");
$stmt->execute([
':heslo' => $heslo_hash,
':id' => $uzivatel_id,
]);
// Smažeme použitý reset token je jednorázový
$stmt2 = $pdo->prepare("
DELETE FROM `" . DB_TABULKA_RESET . "`
WHERE `id` = :id
");
$stmt2->execute([':id' => $zaznam_id]);
// Smažeme všechny remember me tokeny tohoto uživatele
// po změně hesla jsou všechna zařízení odhlášena
$stmt3 = $pdo->prepare("
DELETE FROM `" . DB_TABULKA_TOKENY . "`
WHERE `uzivatel_id` = :uzivatel_id
");
$stmt3->execute([':uzivatel_id' => $uzivatel_id]);
$uspech = true;
} catch (PDOException $e) {
error_log('Chyba při změně hesla: ' . $e->getMessage());
$chyba = 'Při ukládání hesla došlo k chybě. Zkuste to prosím znovu.';
}
}
}
// ------------------------------------------------------------
// HTML VÝSTUP
// ------------------------------------------------------------
?>
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<title>Nové heslo <?php echo htmlspecialchars(PROJEKT_NAZEV); ?></title>
<?php if (AUTH_CSS !== ''): ?>
<link rel="stylesheet" href="<?php echo htmlspecialchars(AUTH_CSS); ?>">
<?php endif; ?>
</head>
<body>
<h1>Nastavení nového hesla</h1>
<h2><?php echo htmlspecialchars(PROJEKT_NAZEV); ?></h2>
<?php if ($uspech): ?>
<p><strong>Heslo bylo úspěšně změněno.</strong></p>
<p>Všechna zařízení byla odhlášena. Můžeš se nyní
<a href="<?php echo htmlspecialchars(AUTH_LOGIN_URL); ?>">přihlásit</a>
novým heslem.</p>
<?php elseif (!$token_ok): ?>
<!-- Token je neplatný nebo vypršel -->
<p><strong>Tento odkaz pro obnovu hesla je neplatný nebo vypršel.</strong></p>
<p>Požádej o
<a href="<?php echo htmlspecialchars(dirname($_SERVER['PHP_SELF'])); ?>/reset_hesla.php">
nový odkaz pro obnovu hesla</a>.</p>
<?php else: ?>
<!-- Token je platný zobrazíme formulář -->
<?php if (!empty($chyba)): ?>
<p><strong>Chyba: <?php echo htmlspecialchars($chyba); ?></strong></p>
<?php endif; ?>
<p>Zadej nové heslo pro svůj účet.</p>
<form method="POST" action="">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<p>
<label for="heslo">Nové heslo:</label><br>
<input
type="password"
id="heslo"
name="heslo"
required
autocomplete="new-password"
>
<br>
<span id="heslo-sila">Síla hesla: zadej heslo</span>
</p>
<p>
<label for="heslo2">Nové heslo znovu (pro ověření):</label><br>
<input
type="password"
id="heslo2"
name="heslo2"
required
autocomplete="new-password"
>
</p>
<p>
<button type="submit" id="tlacitko-odeslat" disabled>
Nastavit nové heslo
</button>
<span id="tlacitko-duvod"> (čekám na dostatečně silné heslo)</span>
</p>
</form>
<?php endif; ?>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zxcvbn/4.4.2/zxcvbn.js"></script>
<script>
const MIN_SILA = <?php echo (int) HESLO_MIN_SILA; ?>;
const POPISKY_SILY = [
'Velmi slabé',
'Slabé',
'Průměrné',
'Silné',
'Velmi silné'
];
const inputHeslo = document.getElementById('heslo');
const spanSila = document.getElementById('heslo-sila');
const tlacitko = document.getElementById('tlacitko-odeslat');
const spanDuvod = document.getElementById('tlacitko-duvod');
// Prvky existují jen pokud je zobrazen formulář
// (ne při úspěchu nebo neplatném tokenu)
if (inputHeslo) {
inputHeslo.addEventListener('input', function () {
const hodnota = this.value;
if (hodnota.length === 0) {
spanSila.textContent = 'Síla hesla: zadej heslo';
tlacitko.disabled = true;
spanDuvod.textContent = ' (čekám na dostatečně silné heslo)';
return;
}
const vysledek = zxcvbn(hodnota);
const skore = vysledek.score;
spanSila.textContent = 'Síla hesla: ' + POPISKY_SILY[skore] + ' (' + skore + '/4)';
if (skore >= MIN_SILA) {
tlacitko.disabled = false;
spanDuvod.textContent = '';
} else {
tlacitko.disabled = true;
spanDuvod.textContent = ' (heslo je příliš slabé, potřebuji alespoň ' + MIN_SILA + '/4)';
}
});
}
</script>
</body>
</html>