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>
194 lines
12 KiB
PHP
194 lines
12 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/Notifier.php';
|
||
require_once __DIR__ . '/../includes/I18n.php';
|
||
|
||
$auth = new Auth();
|
||
$auth->requireAdmin();
|
||
I18n::init();
|
||
|
||
$db = Database::getInstance();
|
||
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
|
||
|
||
$error = '';
|
||
$success = '';
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save'])) {
|
||
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
||
$error = __('error_csrf');
|
||
} else {
|
||
$db->setSetting('enforce_2fa_admins', isset($_POST['enforce_2fa_admins']) ? '1' : '0');
|
||
$db->setSetting('session_driver', ($_POST['session_driver'] ?? 'php') === 'db' ? 'db' : 'php');
|
||
$cm = in_array($_POST['captcha_mode'] ?? 'off', ['off','math','hcaptcha'], true) ? $_POST['captcha_mode'] : 'off';
|
||
$db->setSetting('captcha_mode', $cm);
|
||
$db->setSetting('captcha_site_key', trim($_POST['captcha_site_key'] ?? ''));
|
||
if (!empty($_POST['captcha_secret'])) { $db->setSetting('captcha_secret', trim($_POST['captcha_secret'])); }
|
||
$db->setSetting('sms_enabled', isset($_POST['sms_enabled']) ? '1' : '0');
|
||
$db->setSetting('twilio_sid', trim($_POST['twilio_sid'] ?? ''));
|
||
$db->setSetting('twilio_from', trim($_POST['twilio_from'] ?? ''));
|
||
if (!empty($_POST['twilio_token'])) { $db->setSetting('twilio_token', trim($_POST['twilio_token'])); }
|
||
$db->setSetting('oidc_enabled', isset($_POST['oidc_enabled']) ? '1' : '0');
|
||
$db->setSetting('oidc_name', trim($_POST['oidc_name'] ?? 'SSO'));
|
||
$db->setSetting('oidc_client_id', trim($_POST['oidc_client_id'] ?? ''));
|
||
$db->setSetting('oidc_auth_url', trim($_POST['oidc_auth_url'] ?? ''));
|
||
$db->setSetting('oidc_token_url', trim($_POST['oidc_token_url'] ?? ''));
|
||
$db->setSetting('oidc_userinfo_url', trim($_POST['oidc_userinfo_url'] ?? ''));
|
||
$db->setSetting('oidc_scopes', trim($_POST['oidc_scopes'] ?? 'openid profile email'));
|
||
if (!empty($_POST['oidc_client_secret'])) { $db->setSetting('oidc_client_secret', trim($_POST['oidc_client_secret'])); }
|
||
$db->setSetting('user_daily_voucher_limit', max(0, (int)($_POST['user_daily_voucher_limit'] ?? 0)));
|
||
$db->setSetting('trusted_proxy', trim($_POST['trusted_proxy'] ?? ''));
|
||
$db->setSetting('webhook_enabled', isset($_POST['webhook_enabled']) ? '1' : '0');
|
||
$db->setSetting('webhook_url', trim($_POST['webhook_url'] ?? ''));
|
||
$db->setSetting('cleanup_expired_days', max(0, (int)($_POST['cleanup_expired_days'] ?? 0)));
|
||
$db->setSetting('cleanup_audit_days', max(0, (int)($_POST['cleanup_audit_days'] ?? 0)));
|
||
$db->setSetting('cleanup_login_days', max(0, (int)($_POST['cleanup_login_days'] ?? 30)));
|
||
$auth->writeAuditLog($_SESSION['user_id'], 'settings_update', 'config', null, 'Integration/Wartung gespeichert');
|
||
$success = 'Einstellungen gespeichert.';
|
||
}
|
||
}
|
||
|
||
if (isset($_GET['test_webhook']) && isset($_GET['token']) && $auth->validateCsrfToken($_GET['token'])) {
|
||
Notifier::send('✅ Test-Benachrichtigung vom UniFi Voucher System.', ['type' => 'test']);
|
||
$success = 'Test-Benachrichtigung gesendet (sofern Webhook aktiv & URL gültig).';
|
||
}
|
||
|
||
$enforce2fa = $db->getSetting('enforce_2fa_admins', '0') === '1';
|
||
$sessionDriver = $db->getSetting('session_driver', 'php');
|
||
$captchaMode = $db->getSetting('captcha_mode', 'off');
|
||
$captchaSiteKey = $db->getSetting('captcha_site_key', '');
|
||
$captchaSecretSet = $db->getSetting('captcha_secret', '') !== '';
|
||
$smsEnabled = $db->getSetting('sms_enabled', '0') === '1';
|
||
$twilioSid = $db->getSetting('twilio_sid', '');
|
||
$twilioFrom = $db->getSetting('twilio_from', '');
|
||
$twilioTokenSet = $db->getSetting('twilio_token', '') !== '';
|
||
$oidcEnabled = $db->getSetting('oidc_enabled', '0') === '1';
|
||
$oidcName = $db->getSetting('oidc_name', 'SSO');
|
||
$oidcClientId = $db->getSetting('oidc_client_id', '');
|
||
$oidcAuthUrl = $db->getSetting('oidc_auth_url', '');
|
||
$oidcTokenUrl = $db->getSetting('oidc_token_url', '');
|
||
$oidcUserinfoUrl = $db->getSetting('oidc_userinfo_url', '');
|
||
$oidcScopes = $db->getSetting('oidc_scopes', 'openid profile email');
|
||
$oidcSecretSet = $db->getSetting('oidc_client_secret', '') !== '';
|
||
$dailyLimit = (int)$db->getSetting('user_daily_voucher_limit', 0);
|
||
$trustedProxy = $db->getSetting('trusted_proxy', '');
|
||
$webhookEnabled = $db->getSetting('webhook_enabled', '0') === '1';
|
||
$webhookUrl = $db->getSetting('webhook_url', '');
|
||
$cleanupExpired = (int)$db->getSetting('cleanup_expired_days', 0);
|
||
$cleanupAudit = (int)$db->getSetting('cleanup_audit_days', 0);
|
||
$cleanupLogin = (int)$db->getSetting('cleanup_login_days', 30);
|
||
$lastCleanup = $db->getSetting('last_cleanup', '');
|
||
$csrf = $auth->getCsrfToken();
|
||
$currentPage = 'integrations';
|
||
$adminBase = '';
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="<?= I18n::getLanguage() ?>">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Integration & Wartung – <?= htmlspecialchars($appTitle) ?></title>
|
||
<?php require __DIR__ . '/../includes/admin_nav.php'; ?>
|
||
<div class="page-header">
|
||
<div>
|
||
<h1 class="page-title">Integration & Wartung</h1>
|
||
<p class="page-subtitle">SSO, Webhooks, SMS-Versand und Aufbewahrungsfristen.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<?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; ?>
|
||
|
||
<form method="post">
|
||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
||
|
||
<div class="card">
|
||
<h2>Sicherheitsrichtlinie</h2>
|
||
<p class="muted">Erzwingt Zwei-Faktor-Authentifizierung für alle Administrator-Konten (lokale Accounts). Admins ohne 2FA werden bei der nächsten Aktion zur Einrichtung geleitet.</p>
|
||
<label class="chk"><input type="checkbox" name="enforce_2fa_admins" <?= $enforce2fa ? 'checked' : '' ?>> 2FA für Administratoren verpflichtend</label>
|
||
<label>Tageslimit Voucher pro Nicht-Admin-Benutzer (0 = unbegrenzt)</label>
|
||
<input class="input" type="number" min="0" name="user_daily_voucher_limit" value="<?= $dailyLimit ?>" style="max-width:200px;">
|
||
<label>Session-Speicher</label>
|
||
<select class="input" name="session_driver" style="max-width:340px;">
|
||
<option value="php" <?= $sessionDriver==='php'?'selected':'' ?>>PHP-Standard (Dateien)</option>
|
||
<option value="db" <?= $sessionDriver==='db'?'selected':'' ?>>Datenbank (ermöglicht „überall abmelden")</option>
|
||
</select>
|
||
<label>Captcha im öffentlichen Modus</label>
|
||
<select class="input" name="captcha_mode" style="max-width:340px;">
|
||
<option value="off" <?= $captchaMode==='off'?'selected':'' ?>>Aus</option>
|
||
<option value="math" <?= $captchaMode==='math'?'selected':'' ?>>Rechenaufgabe (ohne externen Dienst)</option>
|
||
<option value="hcaptcha" <?= $captchaMode==='hcaptcha'?'selected':'' ?>>hCaptcha</option>
|
||
</select>
|
||
<div class="row3" style="margin-top:10px;">
|
||
<div><label>hCaptcha Site-Key</label><input class="input" type="text" name="captcha_site_key" value="<?= htmlspecialchars($captchaSiteKey) ?>"></div>
|
||
<div><label>hCaptcha Secret<?= $captchaSecretSet ? ' (gesetzt)' : '' ?></label><input class="input" type="password" name="captcha_secret" placeholder="<?= $captchaSecretSet ? '••••••• (leer = unverändert)' : '' ?>"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Reverse-Proxy</h2>
|
||
<p class="muted">IP-Adressen vertrauenswürdiger Proxies (kommasepariert). Nur dann wird die echte Client-IP aus <code>X-Forwarded-For</code> für Rate-Limit & Audit verwendet.</p>
|
||
<input class="input" type="text" name="trusted_proxy" value="<?= htmlspecialchars($trustedProxy) ?>" placeholder="z.B. 10.0.0.1, 172.18.0.1">
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Webhook-Benachrichtigungen</h2>
|
||
<p class="muted">Slack-, Microsoft-Teams- oder generische JSON-Webhook-URL. Wird bei Voucher-Erstellung ausgelöst.</p>
|
||
<label class="chk"><input type="checkbox" name="webhook_enabled" <?= $webhookEnabled ? 'checked' : '' ?>> Webhook aktiv</label>
|
||
<label>Webhook-URL</label>
|
||
<input class="input" type="url" name="webhook_url" value="<?= htmlspecialchars($webhookUrl) ?>" placeholder="https://hooks.slack.com/services/…">
|
||
<div style="margin-top:12px;">
|
||
<a class="btn btn-secondary" href="?test_webhook=1&token=<?= urlencode($csrf) ?>">Test senden</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>SMS-Versand (Twilio)</h2>
|
||
<p class="muted">Voucher-Codes optional per SMS versenden. Erfordert ein Twilio-Konto.</p>
|
||
<label class="chk"><input type="checkbox" name="sms_enabled" <?= $smsEnabled ? 'checked' : '' ?>> SMS-Versand aktiv</label>
|
||
<div class="row3" style="margin-top:10px;">
|
||
<div><label>Account SID</label><input class="input" type="text" name="twilio_sid" value="<?= htmlspecialchars($twilioSid) ?>"></div>
|
||
<div><label>Auth Token<?= $twilioTokenSet ? ' (gesetzt)' : '' ?></label><input class="input" type="password" name="twilio_token" placeholder="<?= $twilioTokenSet ? '••••••• (leer = unverändert)' : '' ?>"></div>
|
||
<div><label>Absender (From)</label><input class="input" type="text" name="twilio_from" value="<?= htmlspecialchars($twilioFrom) ?>" placeholder="+49…"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Single Sign-On (OpenID Connect)</h2>
|
||
<p class="muted">Generischer OIDC-Provider (z.B. Keycloak, Authentik, Google, Auth0). Redirect-URI: <code><?= htmlspecialchars(((!empty($_SERVER['HTTPS'])&&$_SERVER['HTTPS']!=='off')?'https':'http').'://'.$_SERVER['HTTP_HOST'].rtrim(dirname($_SERVER['SCRIPT_NAME']),'/').'/../oidc_callback.php') ?></code></p>
|
||
<label class="chk"><input type="checkbox" name="oidc_enabled" <?= $oidcEnabled ? 'checked' : '' ?>> OIDC-Login aktiv</label>
|
||
<div class="row3" style="margin-top:10px;">
|
||
<div><label>Button-Text</label><input class="input" type="text" name="oidc_name" value="<?= htmlspecialchars($oidcName) ?>"></div>
|
||
<div><label>Client ID</label><input class="input" type="text" name="oidc_client_id" value="<?= htmlspecialchars($oidcClientId) ?>"></div>
|
||
<div><label>Client Secret<?= $oidcSecretSet ? ' (gesetzt)' : '' ?></label><input class="input" type="password" name="oidc_client_secret" placeholder="<?= $oidcSecretSet ? '••••••• (leer = unverändert)' : '' ?>"></div>
|
||
</div>
|
||
<label>Authorization Endpoint</label><input class="input" type="url" name="oidc_auth_url" value="<?= htmlspecialchars($oidcAuthUrl) ?>" placeholder="https://idp/authorize">
|
||
<label>Token Endpoint</label><input class="input" type="url" name="oidc_token_url" value="<?= htmlspecialchars($oidcTokenUrl) ?>" placeholder="https://idp/token">
|
||
<label>Userinfo Endpoint</label><input class="input" type="url" name="oidc_userinfo_url" value="<?= htmlspecialchars($oidcUserinfoUrl) ?>" placeholder="https://idp/userinfo">
|
||
<label>Scopes</label><input class="input" type="text" name="oidc_scopes" value="<?= htmlspecialchars($oidcScopes) ?>">
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Datenhaltung & Cleanup (DSGVO)</h2>
|
||
<p class="muted">Aufbewahrungsfristen in Tagen (0 = deaktiviert). Ausführung per <code>cron_cleanup.php</code> (täglich empfohlen).
|
||
<?php if ($lastCleanup): ?><br>Letzter Lauf: <?= htmlspecialchars($lastCleanup) ?><?php endif; ?>
|
||
</p>
|
||
<div class="row">
|
||
<div><label>Abgelaufene Voucher</label><input class="input" type="number" min="0" name="cleanup_expired_days" value="<?= $cleanupExpired ?>"></div>
|
||
<div><label>Audit-Log</label><input class="input" type="number" min="0" name="cleanup_audit_days" value="<?= $cleanupAudit ?>"></div>
|
||
<div><label>Login-Versuche</label><input class="input" type="number" min="0" name="cleanup_login_days" value="<?= $cleanupLogin ?>"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="btn btn-primary" type="submit" name="save">Speichern</button>
|
||
</form>
|
||
|
||
</main>
|
||
<script src="../assets/global.js"></script>
|
||
</body>
|
||
</html>
|