Unifi-Voucher-Tool/reset_password.php
Friederich Loheide 30d0ce3a23 Branding: Farben systemweit einstellbar + Bild-Upload statt nur URLs
Design-Tab (Administration → Einstellungen → Design):
- Akzentfarbe für Hell- und Dark-Mode, Markenverlauf und Eckenradius
- abgeleitete Töne (Hover, weiche Flächen, Fokusring) werden per
  color-mix aus der Grundfarbe berechnet – eine Farbe genügt
- Live-Vorschau mit Button, Badge, Chip, Logo-Kachel und Link
- Ausgabe als schlanker :root-Override über Ui::brandingStyle(), greift
  auf allen Seiten inklusive Login und Installer

Uploads (includes/Upload.php):
- Logo, Favicon, Login-Logo und Login-Hintergrund lassen sich jetzt
  hochladen; das URL-Feld bleibt als Alternative bestehen
- Whitelist nach Endung, 3-MB-Grenze, getimagesize-Prüfung für Raster,
  SVGs werden von Skripten, Event-Handlern und externen Verweisen befreit
- Zufällige Dateinamen in uploads/, dort sperrt eine .htaccess die
  Ausführung von PHP; beim Ersetzen wird die alte Datei gelöscht

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 06:27:30 +00:00

135 lines
4.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/includes/Database.php';
require_once __DIR__ . '/includes/Auth.php';
require_once __DIR__ . '/includes/Ui.php';
require_once __DIR__ . '/includes/I18n.php';
$auth = new Auth();
if ($auth->isLoggedIn()) { header('Location: index.php'); exit; }
I18n::init();
$db = Database::getInstance();
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
$logoUrl = $db->getSetting('logo_url', '');
$token = trim($_GET['token'] ?? '');
$error = '';
$success = '';
$valid = false;
$tokenRow = null;
if (empty($token)) {
$error = __('reset_invalid');
} else {
$tokenRow = $db->fetchOne(
"SELECT prt.*, u.email, u.name FROM password_reset_tokens prt
JOIN users u ON prt.user_id = u.id
WHERE prt.token = ? AND prt.used = 0 AND prt.expires_at > NOW()",
[$token]
);
if (!$tokenRow) {
$error = __('reset_invalid');
} else {
$valid = true;
}
}
if ($valid && $_SERVER['REQUEST_METHOD'] === 'POST') {
$newPw = $_POST['new_password'] ?? '';
$confirm= $_POST['confirm_password'] ?? '';
if (strlen($newPw) < 8) {
$error = __('settings_pw_minlength');
$valid = true; // keep form visible
} elseif ($newPw !== $confirm) {
$error = 'Passwörter stimmen nicht überein';
$valid = true;
} else {
$hash = password_hash($newPw, PASSWORD_DEFAULT);
$db->execute("UPDATE users SET password_hash = ? WHERE id = ?", [$hash, $tokenRow['user_id']]);
$db->execute("UPDATE password_reset_tokens SET used = 1 WHERE token = ?", [$token]);
$db->execute(
"INSERT INTO audit_log (user_id, action, entity_type, entity_id, details, ip_address) VALUES (?, 'password_reset', 'user', ?, 'Passwort erfolgreich geändert', ?)",
[$tokenRow['user_id'], $tokenRow['user_id'], $_SERVER['REMOTE_ADDR'] ?? '']
);
$success = __('reset_done');
$valid = false;
}
}
?>
<!DOCTYPE html>
<html lang="<?= I18n::getLanguage() ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= __('reset_new_pw') ?> <?= htmlspecialchars($appTitle) ?></title>
<?= Ui::head($db) ?>
<style>
.pw-strength { height: 4px; border-radius: var(--r-pill); margin-top: 8px; background: var(--border-color); transition: width .3s, background-color .3s; }
.pw-strength.weak { background: var(--danger); width: 30%; }
.pw-strength.medium { background: var(--warning); width: 65%; }
.pw-strength.strong { background: var(--success); width: 100%; }
</style>
</head>
<body class="app-body focus-page">
<div class="focus-card card">
<?php if ($logoUrl): ?>
<img src="<?= htmlspecialchars(Ui::mediaUrl($logoUrl)) ?>" alt="Logo" class="logo">
<?php else: ?>
<div class="focus-icon" style="margin-bottom:14px;"><i class="fas fa-key"></i></div>
<?php endif; ?>
<h1><?= __('reset_new_pw') ?></h1>
<?php if ($error): ?>
<div class="alert alert-error"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<?php if ($success): ?>
<div class="alert alert-success"><?= htmlspecialchars($success) ?></div>
<?php endif; ?>
<?php if ($valid): ?>
<p class="subtitle">Für <strong><?= htmlspecialchars($tokenRow['email']) ?></strong></p>
<form method="post" action="reset_password.php?token=<?= htmlspecialchars($token) ?>">
<div class="form-group">
<label><?= __('reset_new_pw_label') ?></label>
<input type="password" name="new_password" id="pw" required minlength="8" oninput="checkPw(this.value)">
<div class="pw-strength" id="pwBar"></div>
</div>
<div class="form-group">
<label><?= __('reset_confirm_label') ?></label>
<input type="password" name="confirm_password" required>
</div>
<button type="submit" class="btn btn-primary btn-lg btn-block"><?= __('reset_set_btn') ?></button>
</form>
<?php elseif ($success): ?>
<a href="login.php" class="btn btn-primary btn-lg btn-block"><?= __('btn_login') ?></a>
<?php endif; ?>
<a href="login.php" class="back-link"><?= __('reset_back_login') ?></a>
</div>
<script>
function checkPw(val) {
const bar = document.getElementById('pwBar');
if (!bar) return;
if (val.length >= 12 && /[A-Z]/.test(val) && /[0-9]/.test(val)) {
bar.className = 'pw-strength strong';
} else if (val.length >= 8) {
bar.className = 'pw-strength medium';
} else if (val.length > 0) {
bar.className = 'pw-strength weak';
} else {
bar.className = 'pw-strength';
}
}
</script>
</body>
</html>