v1.3.2 - "Passwort vergessen" Funktion hinzugefügt

This commit is contained in:
Borgal
2025-11-21 11:45:49 +01:00
parent 968bbdec3b
commit 2a96df9381
4 changed files with 275 additions and 45 deletions

123
forgot_password.php Executable file
View File

@@ -0,0 +1,123 @@
<?php
session_start();
include('inc/db.php');
// SMTP-Konfiguration wird über die DB-Einbindung bereits bereitgestellt
// (SMTP_HOST, SMTP_USERNAME, etc. müssen in inc/db.php oder einer config.php definiert sein)
$message = '';
$message_type = '';
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$identifier = trim($_POST['identifier']);
// Benutzer per Username ODER E-Mail finden
$stmt = mysqli_prepare($conn, "SELECT id, username, email FROM users WHERE username = ? OR email = ?");
mysqli_stmt_bind_param($stmt, "ss", $identifier, $identifier);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
$user = mysqli_fetch_assoc($result);
mysqli_stmt_close($stmt);
if ($user && !empty($user['email'])) {
// Sicherer Token (64 Zeichen hex)
$token = bin2hex(random_bytes(32));
$expires_at = date('Y-m-d H:i:s', strtotime('+12 hours'));
// Token in DB speichern
$stmt = mysqli_prepare($conn, "INSERT INTO password_reset_tokens (user_id, token, expires_at) VALUES (?, ?, ?)");
mysqli_stmt_bind_param($stmt, "iss", $user['id'], $token, $expires_at);
mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
// 🔸 PHPMailer wie in deinem Beispiel
try {
require_once __DIR__ . '/vendor/autoload.php';
$mail = new \PHPMailer\PHPMailer\PHPMailer(true);
$mail->CharSet = 'UTF-8';
$mail->isSMTP();
$mail->Host = SMTP_HOST;
$mail->SMTPAuth = true;
$mail->Username = SMTP_USERNAME;
$mail->Password = SMTP_PASSWORD;
$mail->SMTPSecure = SMTP_ENCRYPTION;
$mail->Port = SMTP_PORT;
$mail->setFrom(MAIL_FROM_ADDRESS, MAIL_FROM_NAME);
$reset_link = "https://domili.borgal.de/reset_password.php?token=" . urlencode($token);
// Text-Version (für E-Mail-Clients ohne HTML)
$text_body = "Hallo {$user['username']},\n\n";
$text_body .= "du hast eine Zurücksetzung deines Passworts angefordert.\n";
$text_body .= "Klicke auf den folgenden Link (gültig 12 Stunden):\n";
$text_body .= "$reset_link\n\n";
$text_body .= "Falls du dies nicht angefordert hast, ignoriere diese E-Mail.\n\n";
$text_body .= "\nDein DoMiLi-Admin";
// HTML-Version (mit lesbarer Formatierung)
$html_body = "
<p>Hallo <strong>{$user['username']}</strong>,</p>
<p>du hast eine Zurücksetzung deines Passworts angefordert.</p>
<p>Bitte klicke auf den folgenden Link, um ein neues Passwort festzulegen (gültig für 12&nbsp;Stunden):</p>
<p>
<a href=\"$reset_link\" style=\"color: #0d6efd; text-decoration: underline;\">Passwort zurücksetzen</a>
</p>
<p style=\"margin-top: 16px; color: #555; font-size: 0.95em; line-height: 1.5;\">
Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail ignorieren.
</p>
<p style=\"margin-top: 20px; font-size: 0.9em; color: #777;\">
—<br>
Dein DoMiLi-Admin
</p>
";
$mail->isHTML(true);
$mail->Subject = "DoMiLi: Passwort zurücksetzen";
$mail->Body = $html_body;
$mail->AltBody = $text_body;
$mail->addAddress($user['email'], $user['username']);
$mail->send();
$message = "Ein Link zum Zurücksetzen wurde an deine E-Mail gesendet.";
$message_type = "success";
} catch (Exception $e) {
error_log("PHPMailer Fehler bei Passwort-Zurücksetzung für {$user['email']}: " . $mail->ErrorInfo);
$message = "Fehler beim Senden der E-Mail. Bitte versuche es später erneut.";
$message_type = "danger";
}
} else {
// Vage Antwort Schutz vor Benutzer-Enumeration
$message = "Falls ein Konto mit dieser Angabe existiert, wurde eine E-Mail gesendet.";
$message_type = "info";
}
}
// HTML-Ausgabe
require_once 'inc/public_header.php';
?>
<div class="container d-flex justify-content-center align-items-start py-4 pt-5">
<div class="card bg-light shadow w-100" style="max-width: 400px;">
<div class="card-body">
<h4 class="card-title text-center mb-4 fs-3">Passwort vergessen</h4>
<?php if ($message): ?>
<div class="alert alert-<?= htmlspecialchars($message_type) ?>"><?= htmlspecialchars($message) ?></div>
<?php endif; ?>
<form method="post">
<div class="mb-3">
<label for="identifier" class="form-label">Benutzername oder E-Mail</label>
<input type="text" class="form-control form-control-lg" id="identifier" name="identifier" required autofocus>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg">Link senden</button>
</div>
</form>
<div class="text-center mt-3">
<a href="login.php" class="text-decoration-none">Zurück zum Login</a>
</div>
</div>
</div>
</div>
<?php include('inc/footer.php'); ?>

30
inc/public_header.php Executable file
View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars('DoMiLi Farbe der Woche'); ?></title>
<!-- PWA Meta Tags -->
<meta name="theme-color" content="#212529">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-title" content="<?php echo htmlspecialchars('DoMiLi'); ?>">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<!-- Bootstrap -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Material Icons -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet">
<!-- Custom styles -->
<link rel="stylesheet" href="/css/style.css">
<!-- PWA Manifest -->
<link rel="manifest" href="./manifest.json?v=1">
<link rel="apple-touch-icon" href="../img/icon-192.png">
</head>
<body>

View File

@@ -46,25 +46,9 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
$error = "Datenbankfehler.";
}
}
require_once 'inc/public_header.php';
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DoMiLi Login</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Google-->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet">
<!-- Custom styles -->
<link rel="stylesheet" href="css/style.css">
<link rel="manifest" href="manifest.json">
</head>
<body>
<div class="container d-flex justify-content-center align-items-start py-4 pt-5">
<div class="card bg-light shadow w-100" style="max-width: 400px;">
<div class="card-body">
@@ -86,11 +70,11 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg">Einloggen</button>
</div>
<div class="text-center mt-3">
<a href="forgot_password.php" class="text-decoration-none">Passwort vergessen?</a>
</div>
</form>
</div>
</div>
</div>
<?php include('inc/footer.php'); ?>
</body>
</html>

93
reset_password.php Executable file
View File

@@ -0,0 +1,93 @@
<?php
include('inc/db.php');
$token = $_GET['token'] ?? null;
$error = '';
$success = false;
$username = '';
if (!$token) {
die("Ungültiger Zugriff.");
}
// Token prüfen: existiert, nicht abgelaufen, nicht verwendet
$stmt = mysqli_prepare($conn, "
SELECT prt.id, prt.user_id, prt.expires_at, u.username
FROM password_reset_tokens prt
JOIN users u ON prt.user_id = u.id
WHERE prt.token = ? AND prt.used = 0
");
mysqli_stmt_bind_param($stmt, "s", $token);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
$token_data = mysqli_fetch_assoc($result);
mysqli_stmt_close($stmt);
if (!$token_data) {
$error = "Ungültiger oder bereits verwendeter Link.";
} else if (strtotime($token_data['expires_at']) < time()) {
$error = "Der Link ist abgelaufen. Bitte fordere einen neuen Link an.";
} else {
$username = htmlspecialchars($token_data['username']);
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$new_password = $_POST['new_password'] ?? '';
$confirm_password = $_POST['confirm_password'] ?? '';
if (strlen($new_password) < 6) {
$error = "Das Passwort muss mindestens 6 Zeichen lang sein.";
} else if ($new_password !== $confirm_password) {
$error = "Die Passwörter stimmen nicht überein.";
} else {
// Neues Passwort hashen und speichern
$hashed = password_hash($new_password, PASSWORD_DEFAULT);
$stmt = mysqli_prepare($conn, "UPDATE users SET password = ? WHERE id = ?");
mysqli_stmt_bind_param($stmt, "si", $hashed, $token_data['user_id']);
mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
// Token als verwendet markieren
$stmt = mysqli_prepare($conn, "UPDATE password_reset_tokens SET used = 1 WHERE id = ?");
mysqli_stmt_bind_param($stmt, "i", $token_data['id']);
mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
$success = true;
}
}
}
require_once 'inc/public_header.php';
?>
<div class="container d-flex justify-content-center align-items-start py-4 pt-5">
<div class="card bg-light shadow w-100" style="max-width: 400px;">
<div class="card-body">
<h4 class="card-title text-center mb-4 fs-3">Neues Passwort</h4>
<?php if ($error): ?>
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
<?php elseif ($success): ?>
<div class="alert alert-success">
Dein Passwort wurde erfolgreich geändert!<br>
<a href="login.php" class="alert-link">Zum Login</a>
</div>
<?php else: ?>
<p>Neues Passwort für: <strong><?= $username ?></strong></p>
<form method="post">
<div class="mb-3">
<label for="new_password" class="form-label">Neues Passwort</label>
<input type="password" class="form-control" id="new_password" name="new_password" required minlength="6">
</div>
<div class="mb-3">
<label for="confirm_password" class="form-label">Bestätigen</label>
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
</div>
<button type="submit" class="btn btn-primary w-100">Passwort speichern</button>
</form>
<?php endif; ?>
</div>
</div>
</div>
<?php include('inc/footer.php'); ?>