Some checks failed
CI / PHP Lint (push) Waiting to run
CI / PHP Lint-1 (push) Waiting to run
CI / Unit Tests & Static Analysis (push) Waiting to run
CI / PHP Lint (pull_request) Has been cancelled
CI / PHP Lint-1 (pull_request) Has been cancelled
CI / Unit Tests & Static Analysis (pull_request) Has been cancelled
Frontend, Login/Installer/Updater und der komplette Admin-Bereich nutzen jetzt ein einziges Stylesheet (assets/global.css) statt pro Seite dupliziertem Inline-CSS. Design-System - Tokens für Flächen, Text, Linien, Marke, Status, Radien, Schatten und Layout-Maße; Dark Mode ausschließlich über Tokens (keine !important- Overrides mehr) - Komponenten: Buttons, Formularfelder, Cards, Tabellen, Badges, Alerts, Tabs, Pagination, Modals, Toasts, Statistik-Kacheln, Empty States - Schrift Inter mit System-Fallback Oberfläche - Admin-Shell neu: durchgehende Sidebar mit Marke, gruppierter Navigation und Benutzerbereich; schlanke Topbar mit Breadcrumb - Dashboard: ruhige KPI-Kacheln mit Icon-Chips, Charts an Theme-Farben gekoppelt - Öffentliche Voucher-Seite: App-Topbar, klare Formularstruktur und Ticket-Darstellung des erstellten Codes inkl. QR-Code - Login/Passwort/2FA: zweispaltiges Auth-Layout bzw. Fokus-Karten - Updater und Wartungsmodus im gleichen Look (Wartungsseite bleibt bewusst eigenständig ohne externe Abhängigkeiten) - Emoji-Icons in der UI durch Font-Awesome-Icons ersetzt Nebenbei behoben - Falscher SRI-Hash blockierte qrcode.min.js – QR-Codes wurden auf der Voucher-Seite und bei der 2FA-Einrichtung nie gerendert - assets/global.css wurde in mehreren Admin-Seiten über einen falschen Pfad eingebunden ($adminBase = '' statt '../') - TinyMCE lädt ohne API-Key jetzt die GPL-Variante von cdnjs – kein "valid API key required"-Banner mehr im Einstellungs-Editor - Tabellen in Cards scrollen horizontal statt zu überlaufen Screenshots in docs/screenshots neu erstellt, README aktualisiert (neuer Abschnitt "Design-System", Version 2.5.0). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
139 lines
5.2 KiB
PHP
139 lines
5.2 KiB
PHP
<?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/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>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||
<link rel="stylesheet" href="assets/global.css">
|
||
<script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script>
|
||
<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($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>
|