Unifi-Voucher-Tool/admin/security.php
Friederich Loheide 850a04d628 Englische Übersetzungen für die restlichen Admin-Seiten
API-Schlüssel, Integration & Wartung, Voucher-Import, Backup & Restore,
Reporting, Sicherheit (2FA) und Audit-Log waren fest auf Deutsch
verdrahtet, obwohl in der Kopfzeile ein DE/EN-Umschalter sitzt. Diese
Seiten laufen jetzt komplett über lang/de.php bzw. lang/en.php.

- rund 200 neue Sprachschlüssel, beide Dateien deckungsgleich
- Aktionsnamen im Audit-Log werden übersetzt statt fest ausgegeben
- Bestätigungsdialoge und Statusmeldungen in JavaScript ebenfalls
- admin/security.php initialisiert jetzt I18n und setzt <html lang>
  passend zur Auswahl

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

181 lines
7.2 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/I18n.php';
require_once __DIR__ . '/../includes/Ui.php';
I18n::init();
$auth = new Auth();
$auth->requireLogin();
$db = Database::getInstance();
$user = $auth->getCurrentUser();
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
$error = '';
$success = '';
$backupCodes = []; // nur direkt nach Erzeugung gefüllt
$hasPassword = !empty($user['password_hash']);
$totpEnabled = !empty($user['totp_enabled']);
$setupRequired = isset($_GET['setup_required']);
// 2FA aktivieren (Code bestaetigen)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['enable_totp'])) {
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
$error = __('sec_token_invalid');
} else {
$secret = $_SESSION['totp_setup_secret'] ?? '';
$code = trim($_POST['code'] ?? '');
if ($secret === '') {
$error = __('sec_setup_expired');
} elseif (!Totp::verify($secret, $code)) {
$error = __('sec_code_invalid');
} else {
$backupCodes = $auth->enableTotp($user['id'], $secret);
unset($_SESSION['totp_setup_secret']);
$totpEnabled = true;
$user = $auth->getCurrentUser();
$success = __('sec_enabled');
}
}
}
// Überall abmelden (andere Sessions beenden)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['logout_others'])) {
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
$error = __('sec_token_invalid');
} else {
$auth->logoutOtherSessions();
$success = __('sec_sessions_closed');
}
}
// Recovery-Codes neu erzeugen
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['regen_codes'])) {
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
$error = __('sec_token_invalid');
} elseif (!empty($user['totp_enabled'])) {
$backupCodes = $auth->regenerateBackupCodes($user['id']);
$user = $auth->getCurrentUser();
$success = __('sec_codes_new');
}
}
// 2FA deaktivieren
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['disable_totp'])) {
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
$error = __('sec_token_invalid');
} else {
$auth->disableTotp($user['id']);
$totpEnabled = false;
$success = __('sec_disabled');
}
}
// Für die Setup-Ansicht ein Secret erzeugen (in Session halten bis bestätigt)
$setupSecret = '';
$otpUri = '';
if (!$totpEnabled && $hasPassword) {
$setupSecret = $_SESSION['totp_setup_secret'] ?? Totp::generateSecret();
$_SESSION['totp_setup_secret'] = $setupSecret;
$otpUri = Totp::provisioningUri($setupSecret, $user['email'], $appTitle);
}
$csrf = $auth->getCsrfToken();
$dbSessions = $db->getSetting('session_driver', 'php') === 'db';
$activeSessions = $dbSessions ? $auth->activeSessionCount() : 0;
?>
<!DOCTYPE html>
<html lang="<?= I18n::getLanguage() ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= __('sec_title') ?> <?= htmlspecialchars($appTitle) ?></title>
<?php if (!$totpEnabled && $hasPassword): ?>
<?= Ui::script('assets/vendor/qrcodejs/qrcode.min.js', '../') ?>
<?php endif; ?>
<?= Ui::head($db, '../') ?>
</head>
<body class="app-body focus-page">
<div class="focus-card card">
<div class="focus-head">
<span class="focus-icon"><i class="fas fa-shield-halved"></i></span>
<div>
<h1><?= __('sec_title') ?></h1>
<p class="sub"><?= __('sec_account') ?> <?= htmlspecialchars($user['email']) ?></p>
</div>
</div>
<?php if ($setupRequired && !$totpEnabled): ?>
<div class="alert alert-error"><?= __('sec_required_hint') ?></div>
<?php endif; ?>
<?php if ($error): ?><div class="alert alert-error"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<?php if ($success): ?><div class="alert alert-ok"><?= htmlspecialchars($success) ?></div><?php endif; ?>
<?php if (!empty($backupCodes)): ?>
<div class="codes-box">
<strong><i class="fas fa-key"></i> <?= __('sec_recovery_codes') ?></strong>
<p><?= __('sec_recovery_hint') ?></p>
<div class="codes">
<?php foreach ($backupCodes as $c): ?><span><?= htmlspecialchars($c) ?></span><?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php if (!$hasPassword): ?>
<div class="status off"><i class="fas fa-circle-minus"></i> <?= __('sec_unavailable') ?></div>
<p class="sub"><?= __('sec_m365_hint') ?></p>
<?php elseif ($totpEnabled): ?>
<div class="status on"><i class="fas fa-circle-check"></i> <?= __('sec_active') ?></div>
<p class="sub"><?= __('sec_active_hint') ?><br>
<?= __('sec_codes_left') ?> <strong><?= (int)$auth->backupCodesRemaining($user) ?></strong></p>
<form method="post" style="margin-bottom:10px;">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
<button type="submit" name="regen_codes" class="btn btn-secondary btn-lg btn-block"><?= __('sec_regen_codes') ?></button>
</form>
<form method="post" onsubmit="return confirm('<?= __('sec_disable_confirm') ?>');">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
<button type="submit" name="disable_totp" class="btn btn-danger btn-lg btn-block"><?= __('sec_disable') ?></button>
</form>
<?php else: ?>
<div class="status off"><i class="fas fa-circle-minus"></i> <?= __('sec_inactive') ?></div>
<ol>
<li><?= __('sec_step_1') ?></li>
<li><?= __('sec_step_2') ?></li>
<li><?= __('sec_step_3') ?></li>
</ol>
<div class="qr"><div id="qrcode"></div></div>
<div class="secret"><?= htmlspecialchars($setupSecret) ?></div>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
<label for="code"><?= __('sec_code_label') ?></label>
<input type="text" id="code" name="code" class="code-input" inputmode="numeric" pattern="[0-9]*" maxlength="6" autocomplete="one-time-code" required placeholder="123456">
<button type="submit" name="enable_totp" class="btn btn-primary btn-lg btn-block" style="margin-top:14px;"><?= __('sec_enable') ?></button>
</form>
<script>
new QRCode(document.getElementById('qrcode'), {
text: <?= json_encode($otpUri) ?>, width: 168, height: 168,
colorDark: '#101625', colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M
});
</script>
<?php endif; ?>
<?php if ($dbSessions): ?>
<hr>
<p class="sub"><?= __('sec_sessions') ?> <strong><?= (int)$activeSessions ?></strong></p>
<form method="post" onsubmit="return confirm('<?= __('sec_logout_others_confirm') ?>');">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
<button type="submit" name="logout_others" class="btn btn-secondary btn-lg btn-block"><?= __('sec_logout_others') ?></button>
</form>
<?php endif; ?>
<div class="auth-links"><a class="back-link" href="../index.php"><i class="fas fa-arrow-left"></i> <?= __('nav_back') ?></a></div>
</div>
</body>
</html>