From 51872e9a69540eb58e88799748602d74fa077daa Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Sun, 28 Dec 2025 23:53:33 +0100 Subject: [PATCH] ycyc --- public/assets/js/app.js | 15 ++++-- public/page/reset.php | 105 ++++++++++++++++++++++++++++++++++++++++ src/App/Auth.php | 72 +++++++++++++++++++++++++++ 3 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 public/page/reset.php diff --git a/public/assets/js/app.js b/public/assets/js/app.js index de28f4b..33e8047 100644 --- a/public/assets/js/app.js +++ b/public/assets/js/app.js @@ -15,14 +15,23 @@ document.addEventListener('DOMContentLoaded', () => { img.src = `/assets/bilder/${chosenLogo}`; }); - // Header shrink on scroll + // Header shrink on scroll with hysteresis to avoid flicker let ticking = false; + let isShrunk = false; + const SHRINK_ON = 140; + const SHRINK_OFF = 90; const onScroll = () => { if (ticking || !header) return; ticking = true; window.requestAnimationFrame(() => { - const shouldShrink = window.scrollY > 120; - header.classList.toggle('is-scrolled', shouldShrink); + const y = window.scrollY; + 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; }); }; diff --git a/public/page/reset.php b/public/page/reset.php new file mode 100644 index 0000000..ab8aef6 --- /dev/null +++ b/public/page/reset.php @@ -0,0 +1,105 @@ +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(); + } +} +?> +
+
+
+
Passwort zurücksetzen
+

Neues Passwort anfordern

+

Fordere einen Reset-Code an und setze dein Passwort zurück.

+ +
+ + +
+ + +
Fehler:
+ +
+
+ + +
+ + +
+
+

Code eingeben

+
+ +
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ +
+

Zurück zum Login

+
+ + +
+
diff --git a/src/App/Auth.php b/src/App/Auth.php index 1c8fd53..5af7d63 100644 --- a/src/App/Auth.php +++ b/src/App/Auth.php @@ -106,6 +106,78 @@ final class Auth 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 { $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';