ycyc
This commit is contained in:
@@ -15,14 +15,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
img.src = `/assets/bilder/${chosenLogo}`;
|
img.src = `/assets/bilder/${chosenLogo}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Header shrink on scroll
|
// Header shrink on scroll with hysteresis to avoid flicker
|
||||||
let ticking = false;
|
let ticking = false;
|
||||||
|
let isShrunk = false;
|
||||||
|
const SHRINK_ON = 140;
|
||||||
|
const SHRINK_OFF = 90;
|
||||||
const onScroll = () => {
|
const onScroll = () => {
|
||||||
if (ticking || !header) return;
|
if (ticking || !header) return;
|
||||||
ticking = true;
|
ticking = true;
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
const shouldShrink = window.scrollY > 120;
|
const y = window.scrollY;
|
||||||
header.classList.toggle('is-scrolled', shouldShrink);
|
if (!isShrunk && y > SHRINK_ON) {
|
||||||
|
isShrunk = true;
|
||||||
|
header.classList.add('is-scrolled');
|
||||||
|
} else if (isShrunk && y < SHRINK_OFF) {
|
||||||
|
isShrunk = false;
|
||||||
|
header.classList.remove('is-scrolled');
|
||||||
|
}
|
||||||
ticking = false;
|
ticking = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
105
public/page/reset.php
Normal file
105
public/page/reset.php
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$app = app();
|
||||||
|
$flash = $app->flash()->get();
|
||||||
|
$error = '';
|
||||||
|
$info = '';
|
||||||
|
$email = '';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$action = $_POST['action'] ?? 'request';
|
||||||
|
$email = strtolower(trim((string)($_POST['email'] ?? '')));
|
||||||
|
$code = strtoupper(trim((string)($_POST['code'] ?? '')));
|
||||||
|
$password = (string)($_POST['password'] ?? '');
|
||||||
|
$password2 = (string)($_POST['password_confirm'] ?? '');
|
||||||
|
|
||||||
|
$auth = new \App\Auth($app);
|
||||||
|
$mailer = new \App\Mailer($app);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($action === 'request') {
|
||||||
|
$data = $auth->createResetCode($email);
|
||||||
|
$mailer->sendTemplate('password_reset', $email, [
|
||||||
|
'code' => $data['code'],
|
||||||
|
'display_name' => $data['display_name'] ?? $email,
|
||||||
|
]);
|
||||||
|
$info = 'Reset-Code wurde gesendet. Bitte prüfe dein Postfach (und Spam).';
|
||||||
|
} else {
|
||||||
|
if ($password !== $password2) {
|
||||||
|
throw new \RuntimeException('Passwörter stimmen nicht überein.');
|
||||||
|
}
|
||||||
|
if (strlen($password) < 8) {
|
||||||
|
throw new \RuntimeException('Passwort muss mindestens 8 Zeichen haben.');
|
||||||
|
}
|
||||||
|
$userId = $auth->verifyResetCode($email, $code);
|
||||||
|
$auth->resetPassword($userId, $password);
|
||||||
|
$app->flash()->set('success', 'Passwort wurde aktualisiert. Bitte melde dich an.');
|
||||||
|
redirect('/login');
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$error = $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<main class="auth-wrap">
|
||||||
|
<div class="container auth-grid">
|
||||||
|
<section class="card auth-card">
|
||||||
|
<div class="badge">Passwort zurücksetzen</div>
|
||||||
|
<h1 class="mt-1" style="margin: 12px 0;">Neues Passwort anfordern</h1>
|
||||||
|
<p class="muted">Fordere einen Reset-Code an und setze dein Passwort zurück.</p>
|
||||||
|
<?php if ($flash): ?>
|
||||||
|
<div class="toast-bar" style="margin-top: 10px;"><?= htmlspecialchars($flash['message'], ENT_QUOTES) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($info): ?>
|
||||||
|
<div class="toast-bar" style="margin-top: 10px;"><?= htmlspecialchars($info, ENT_QUOTES) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="toast-bar" style="margin-top: 10px; border-color:#f87171; color:#991b1b;">Fehler: <?= htmlspecialchars($error, ENT_QUOTES) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<form class="stack gap-12" style="margin-top: 14px;" method="post" action="/reset">
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="resetEmail">E-Mail</label>
|
||||||
|
<input id="resetEmail" name="email" class="input" type="email" required placeholder="du@example.com" value="<?= htmlspecialchars($email, ENT_QUOTES) ?>">
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="action" value="request">
|
||||||
|
<button class="btn block" type="submit">Reset-Code senden</button>
|
||||||
|
</form>
|
||||||
|
<hr style="margin:18px 0; border:none; border-top:1px solid #e5e7eb;">
|
||||||
|
<h3>Code eingeben</h3>
|
||||||
|
<form class="stack gap-12" style="margin-top: 10px;" method="post" action="/reset">
|
||||||
|
<input type="hidden" name="action" value="reset">
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="resetEmail2">E-Mail</label>
|
||||||
|
<input id="resetEmail2" name="email" class="input" type="email" required placeholder="du@example.com" value="<?= htmlspecialchars($email, ENT_QUOTES) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="resetCode">Reset-Code</label>
|
||||||
|
<input id="resetCode" name="code" class="input" maxlength="6" required placeholder="ABC123">
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="newPassword">Neues Passwort</label>
|
||||||
|
<input id="newPassword" name="password" class="input" type="password" required placeholder="********" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="newPassword2">Neues Passwort bestätigen</label>
|
||||||
|
<input id="newPassword2" name="password_confirm" class="input" type="password" required placeholder="********" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn block" type="submit">Passwort setzen</button>
|
||||||
|
</form>
|
||||||
|
<p class="muted small" style="margin-top: 12px;"><a href="/login">Zurück zum Login</a></p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside class="auth-aside">
|
||||||
|
<img class="auth-logo" src="/assets/bilder/logo_female.png" alt="Papa-Kind-Treff Logo">
|
||||||
|
<h3>Tipps</h3>
|
||||||
|
<p class="auth-meta">Schau im Spam nach, wenn die Mail nicht ankommt. Codes sind 2 Stunden gültig.</p>
|
||||||
|
<div class="stack gap-12" style="margin-top: 12px;">
|
||||||
|
<a class="btn block" href="/register">Neu registrieren</a>
|
||||||
|
<a class="btn ghost block" href="/">Zur Startseite</a>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
@@ -106,6 +106,78 @@ final class Auth
|
|||||||
return $userId;
|
return $userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function createResetCode(string $email): array
|
||||||
|
{
|
||||||
|
$pdo = $this->pdo();
|
||||||
|
$email = strtolower(trim($email));
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT u.id, p.display_name FROM users u LEFT JOIN user_profiles p ON p.user_id = u.id WHERE u.email = :email LIMIT 1');
|
||||||
|
$stmt->execute(['email' => $email]);
|
||||||
|
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||||
|
if (!$row) {
|
||||||
|
throw new \RuntimeException('E-Mail ist nicht registriert.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = (int)$row['id'];
|
||||||
|
$displayName = (string)($row['display_name'] ?? $email);
|
||||||
|
$code = $this->generateCode(6);
|
||||||
|
$hash = hash('sha256', $code);
|
||||||
|
|
||||||
|
$pdo->prepare('DELETE FROM user_tokens WHERE user_id = :uid AND type = :t')->execute(['uid' => $userId, 't' => 'reset']);
|
||||||
|
$stmt = $pdo->prepare('INSERT INTO user_tokens (user_id, type, code, token_hash, expires_at, created_at) VALUES (:uid, :type, :code, :hash, DATE_ADD(NOW(), INTERVAL 2 HOUR), NOW())');
|
||||||
|
$stmt->execute([
|
||||||
|
'uid' => $userId,
|
||||||
|
'type' => 'reset',
|
||||||
|
'code' => $code,
|
||||||
|
'hash' => $hash,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ['user_id' => $userId, 'code' => $code, 'display_name' => $displayName];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verifyResetCode(string $email, string $code): int
|
||||||
|
{
|
||||||
|
$pdo = $this->pdo();
|
||||||
|
$email = strtolower(trim($email));
|
||||||
|
$hash = hash('sha256', $code);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT u.id, t.id AS tid, t.token_hash FROM users u JOIN user_tokens t ON t.user_id = u.id AND t.type = :type WHERE u.email = :email AND (t.used_at IS NULL) AND t.expires_at > NOW() ORDER BY t.expires_at DESC LIMIT 1');
|
||||||
|
$stmt->execute(['type' => 'reset', 'email' => $email]);
|
||||||
|
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||||
|
if (!$row || !hash_equals((string)$row['token_hash'], $hash)) {
|
||||||
|
throw new \RuntimeException('Code ist ungültig oder abgelaufen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = (int)$row['id'];
|
||||||
|
$tid = (int)$row['tid'];
|
||||||
|
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
try {
|
||||||
|
$pdo->prepare('UPDATE user_tokens SET used_at = NOW() WHERE id = :id')->execute(['id' => $tid]);
|
||||||
|
$pdo->commit();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resetPassword(int $userId, string $password): void
|
||||||
|
{
|
||||||
|
$pdo = $this->pdo();
|
||||||
|
if ($password === '' || strlen($password) < 8) {
|
||||||
|
throw new \InvalidArgumentException('Passwort muss mindestens 8 Zeichen haben.');
|
||||||
|
}
|
||||||
|
$hash = password_hash($password, PASSWORD_ARGON2ID);
|
||||||
|
$stmt = $pdo->prepare('UPDATE users SET password_hash = :pw, status = :status, updated_at = NOW() WHERE id = :id');
|
||||||
|
$stmt->execute([
|
||||||
|
'pw' => $hash,
|
||||||
|
'status' => 'active',
|
||||||
|
'id' => $userId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
private function generateCode(int $len = 6): string
|
private function generateCode(int $len = 6): string
|
||||||
{
|
{
|
||||||
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||||
|
|||||||
Reference in New Issue
Block a user