Unifi-Voucher-Tool/admin/settings.php
Friederich Loheide 6da46f040a Alle Frontend-Assets lokal ausliefern statt über CDNs
Inter, Font Awesome, Chart.js, qrcodejs und TinyMCE liegen jetzt unter
assets/vendor/ und werden vom eigenen Server ausgeliefert.

Warum:
- Datenschutz: bisher ging bei jedem Seitenaufruf die IP der Nutzer an
  Google Fonts, cdnjs, jsDelivr und Tiny Cloud
- Funktion: UniFi-Installationen stehen oft in abgeschotteten Netzen –
  dort fehlten bisher Schrift, Icons, Diagramme und Editor

Neu: includes/Ui.php
- Ui::head()/Ui::script() binden die Assets ein und hängen einen
  Versionsstempel an (?v=filemtime), damit Browser nach einem Update
  nicht das alte CSS aus dem Cache nehmen
- Ui::themeScript() setzt das Theme aus der gespeicherten Auswahl oder
  – wenn keine vorliegt – aus prefers-color-scheme; global.js folgt
  Systemwechseln live, solange nichts manuell gewählt wurde
- <meta name="color-scheme"> ergänzt, damit Formularelemente passen

Der TinyMCE-API-Key entfällt: der Editor läuft immer lokal (GPL-Variante),
inklusive deutscher Oberfläche, wenn die App auf Deutsch steht.

Neu: tools/demo/build.py – baut aus dem Projekt eine Demo-Instanz ohne
Datenbank (Stubs für Database/Auth, feste Beispieldaten). Damit lassen
sich Screenshots reproduzierbar erzeugen und alle Seiten einmal rendern
(Smoke-Test), ohne eine MySQL-Instanz aufzusetzen.

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

662 lines
42 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/Mailer.php';
require_once __DIR__ . '/../includes/I18n.php';
require_once __DIR__ . '/../includes/Ui.php';
$auth = new Auth();
$auth->requireAdmin();
I18n::init();
$db = Database::getInstance();
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
// AJAX: SMTP-Test-E-Mail senden
if (isset($_POST['ajax_smtp_test'])) {
header('Content-Type: application/json');
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
echo json_encode(['success' => false, 'message' => __('error_csrf')]);
exit;
}
$to = trim($_POST['test_email'] ?? '');
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
echo json_encode(['success' => false, 'message' => __('error_email_invalid')]);
exit;
}
$mailer = new Mailer();
$ok = $mailer->sendTestEmail($to);
echo json_encode([
'success' => $ok,
'message' => $ok ? "Test-E-Mail wurde an {$to} gesendet." : 'Versand fehlgeschlagen. Prüfen Sie die SMTP-Einstellungen.'
]);
exit;
}
$error = '';
$success = '';
// Einstellungen speichern
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_settings'])) {
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
$error = __('error_csrf');
} else {
try {
$settings = [];
$formType = $_POST['form_type'] ?? '';
if ($formType === 'general') {
$settings['app_title'] = trim($_POST['app_title'] ?? '');
$settings['logo_url'] = trim($_POST['logo_url'] ?? '');
$settings['favicon_url'] = trim($_POST['favicon_url'] ?? '');
$settings['instruction_header'] = trim($_POST['instruction_header'] ?? '');
$settings['instruction_text'] = $_POST['instruction_text'] ?? '';
$settings['public_access'] = isset($_POST['public_access']) ? '1' : '0';
}
if ($formType === 'login') {
$settings['login_panel_enabled'] = isset($_POST['login_panel_enabled']) ? '1' : '0';
$settings['login_brand_name'] = trim($_POST['login_brand_name'] ?? '');
$settings['login_logo_url'] = trim($_POST['login_logo_url'] ?? '');
$settings['login_claim_title'] = trim($_POST['login_claim_title'] ?? '');
$settings['login_claim_text'] = trim($_POST['login_claim_text'] ?? '');
$settings['login_features'] = trim($_POST['login_features'] ?? '');
$settings['login_footer'] = trim($_POST['login_footer'] ?? '');
$settings['login_bg_image'] = trim($_POST['login_bg_image'] ?? '');
$settings['login_bg_from'] = trim($_POST['login_bg_from'] ?? '');
$settings['login_bg_to'] = trim($_POST['login_bg_to'] ?? '');
$overlay = (int)($_POST['login_bg_overlay'] ?? 40);
$settings['login_bg_overlay'] = (string)max(0, min(90, $overlay));
}
if ($formType === 'defaults') {
$expMin = (int)($_POST['default_expire_minutes'] ?? 480);
$defDev = (int)($_POST['default_max_uses'] ?? 1);
$maxDev = (int)($_POST['max_uses_limit'] ?? 10);
if ($expMin < 1) $expMin = 480;
if ($defDev < 1) $defDev = 1;
if ($maxDev < 1) $maxDev = 10;
$settings['default_expire_minutes'] = (string)$expMin;
$settings['default_max_uses'] = (string)$defDev;
$settings['max_uses_limit'] = (string)$maxDev;
}
if ($formType === 'm365') {
$settings['m365_client_id'] = trim($_POST['m365_client_id'] ?? '');
$settings['m365_client_secret'] = trim($_POST['m365_client_secret'] ?? '');
$settings['m365_tenant_id'] = trim($_POST['m365_tenant_id'] ?? '');
}
if ($formType === 'smtp') {
$settings['smtp_enabled'] = isset($_POST['smtp_enabled']) ? '1' : '0';
$settings['smtp_host'] = trim($_POST['smtp_host'] ?? '');
$settings['smtp_port'] = trim($_POST['smtp_port'] ?? '587');
$settings['smtp_username'] = trim($_POST['smtp_username'] ?? '');
if (!empty($_POST['smtp_password'])) {
$settings['smtp_password'] = trim($_POST['smtp_password']);
}
$settings['smtp_encryption'] = trim($_POST['smtp_encryption'] ?? 'tls');
$settings['smtp_from_email'] = trim($_POST['smtp_from_email'] ?? '');
$settings['smtp_from_name'] = trim($_POST['smtp_from_name'] ?? '');
}
if ($formType === 'templates') {
$settings['email_voucher_subject'] = trim($_POST['email_voucher_subject'] ?? '');
$settings['email_voucher_body'] = $_POST['email_voucher_body'] ?? '';
$settings['email_user_notification_subject'] = trim($_POST['email_user_notification_subject'] ?? '');
$settings['email_user_notification_body'] = $_POST['email_user_notification_body'] ?? '';
$settings['system_url'] = trim($_POST['system_url'] ?? '');
}
if ($formType === 'system') {
$settings['print_template'] = $_POST['print_template'] ?? '';
}
foreach ($settings as $key => $value) {
$db->setSetting($key, $value);
}
$success = __('settings_saved');
} catch (Exception $e) {
$error = 'Fehler: ' . $e->getMessage();
}
}
}
// Cron-Token generieren/löschen
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['generate_cron_token'])) {
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
$error = __('error_csrf');
} else {
$db->setSetting('cron_token', bin2hex(random_bytes(32)));
$success = 'Neuer Cron-Token wurde generiert!';
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_cron_token'])) {
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
$error = __('error_csrf');
} else {
$db->setSetting('cron_token', '');
$success = 'Cron-Token wurde gelöscht!';
}
}
// Passwort ändern
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['change_password'])) {
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
$error = __('error_csrf');
} else {
try {
$user = $auth->getCurrentUser();
if (!password_verify($_POST['current_password'], $user['password_hash'])) {
throw new Exception('Aktuelles Passwort ist falsch');
}
if (strlen($_POST['new_password']) < 8) {
throw new Exception(__('settings_pw_minlength'));
}
if ($_POST['new_password'] !== $_POST['confirm_password']) {
throw new Exception('Passwörter stimmen nicht überein');
}
$db->query("UPDATE users SET password_hash = ? WHERE id = ?",
[password_hash($_POST['new_password'], PASSWORD_DEFAULT), $user['id']]);
$success = __('settings_pw_changed');
} catch (Exception $e) {
$error = $e->getMessage();
}
}
}
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'];
$scriptPath = dirname($_SERVER['SCRIPT_NAME'], 2);
$scriptPath = $scriptPath === '/' ? '' : $scriptPath;
$autoDetectedUrl = $protocol . '://' . $host . $scriptPath;
$cs = [
'app_title' => $db->getSetting('app_title', 'UniFi Voucher System'),
'logo_url' => $db->getSetting('logo_url', ''),
'favicon_url' => $db->getSetting('favicon_url', ''),
'instruction_header' => $db->getSetting('instruction_header', 'So verwenden Sie Ihren Code'),
'instruction_text' => $db->getSetting('instruction_text', ''),
'public_access' => $db->getSetting('public_access', '0'),
'default_expire_minutes' => $db->getSetting('default_expire_minutes', '480'),
'default_max_uses' => $db->getSetting('default_max_uses', '1'),
'max_uses_limit' => $db->getSetting('max_uses_limit', '10'),
'm365_client_id' => $db->getSetting('m365_client_id', ''),
'm365_client_secret' => $db->getSetting('m365_client_secret', ''),
'm365_tenant_id' => $db->getSetting('m365_tenant_id', ''),
'smtp_enabled' => $db->getSetting('smtp_enabled', '0'),
'smtp_host' => $db->getSetting('smtp_host', ''),
'smtp_port' => $db->getSetting('smtp_port', '587'),
'smtp_username' => $db->getSetting('smtp_username', ''),
'smtp_password' => $db->getSetting('smtp_password', ''),
'smtp_encryption' => $db->getSetting('smtp_encryption', 'tls'),
'smtp_from_email' => $db->getSetting('smtp_from_email', ''),
'smtp_from_name' => $db->getSetting('smtp_from_name', ''),
'system_url' => $db->getSetting('system_url', $autoDetectedUrl),
'email_voucher_subject' => $db->getSetting('email_voucher_subject', '{APP_TITLE} - Ihr WLAN-Zugang'),
'email_voucher_body' => $db->getSetting('email_voucher_body', "Hallo,\n\nIhr Code: {VOUCHER_CODE}\n\nGültigkeit: 8h\nGeräte: {MAX_USES}\nSite: {SITE_NAME}"),
'email_user_notification_subject' => $db->getSetting('email_user_notification_subject', '{APP_TITLE} - Berechtigungen geändert'),
'email_user_notification_body' => $db->getSetting('email_user_notification_body', "Hallo {USER_NAME},\n\n{CHANGES}"),
'print_template' => $db->getSetting('print_template', '<div style="text-align:center;padding:40px"><h1>{APP_TITLE}</h1><h2>WLAN Code</h2><div style="font-size:48px;font-weight:bold;margin:30px 0;font-family:monospace">{VOUCHER_CODE}</div><p><strong>Gültig bis:</strong> {EXPIRY_DATE} {EXPIRY_TIME}</p><p><strong>Site:</strong> {SITE_NAME}</p><p><strong>Geräte:</strong> {MAX_USES}</p><hr style="margin:30px 0"><div>{INSTRUCTIONS}</div></div>'),
'login_panel_enabled' => $db->getSetting('login_panel_enabled', '1'),
'login_brand_name' => $db->getSetting('login_brand_name', ''),
'login_logo_url' => $db->getSetting('login_logo_url', ''),
'login_claim_title' => $db->getSetting('login_claim_title', ''),
'login_claim_text' => $db->getSetting('login_claim_text', ''),
'login_features' => $db->getSetting('login_features', ''),
'login_footer' => $db->getSetting('login_footer', ''),
'login_bg_image' => $db->getSetting('login_bg_image', ''),
'login_bg_from' => $db->getSetting('login_bg_from', '#3b2f8f'),
'login_bg_to' => $db->getSetting('login_bg_to', '#6d5ce7'),
'login_bg_overlay' => $db->getSetting('login_bg_overlay', '40'),
'cron_token' => $db->getSetting('cron_token', ''),
'last_cron_sync' => $db->getSetting('last_cron_sync', ''),
];
$currentPage = 'settings';
$adminBase = '';
?>
<!DOCTYPE html>
<html lang="<?= I18n::getLanguage() ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= __('settings_title') ?> - <?= htmlspecialchars($appTitle) ?></title>
<!-- TinyMCE wird lokal ausgeliefert (GPL-Variante) keine externen Aufrufe. -->
<?= Ui::script('assets/vendor/tinymce/tinymce.min.js', '../') ?>
<script>window.TINYMCE_BASE_URL = '../assets/vendor/tinymce';</script>
<?php include __DIR__ . '/../includes/admin_nav.php'; ?>
<div class="page-header">
<div>
<h1 class="page-title"><?= __('settings_title') ?></h1>
<p class="page-subtitle"><?= __('settings_subtitle') ?></p>
</div>
</div>
<?php if ($error): ?>
<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i><span><?= htmlspecialchars($error) ?></span></div>
<?php endif; ?>
<?php if ($success): ?>
<div class="alert alert-success"><i class="fas fa-check-circle"></i><span><?= htmlspecialchars($success) ?></span></div>
<?php endif; ?>
<div class="tab-container">
<div class="tab-navigation" id="tabNav">
<button class="tab-button active" data-tab="general"><i class="fas fa-sliders-h"></i> <?= __('settings_tab_general') ?></button>
<button class="tab-button" data-tab="defaults"><i class="fas fa-sliders-h"></i> <?= __('settings_tab_defaults') ?></button>
<button class="tab-button" data-tab="login"><i class="fas fa-right-to-bracket"></i> <?= __('settings_tab_login') ?></button>
<button class="tab-button" data-tab="cron"><i class="fas fa-clock"></i> <?= __('settings_tab_cron') ?></button>
<button class="tab-button" data-tab="m365"><i class="fab fa-microsoft"></i> <?= __('settings_tab_m365') ?></button>
<button class="tab-button" data-tab="smtp"><i class="fas fa-envelope"></i> <?= __('settings_tab_smtp') ?></button>
<button class="tab-button" data-tab="templates_email"><i class="fas fa-file-alt"></i> <?= __('settings_tab_templates_email') ?></button>
<button class="tab-button" data-tab="system"><i class="fas fa-cogs"></i> <?= __('settings_tab_system') ?></button>
<button class="tab-button" data-tab="password"><i class="fas fa-key"></i> <?= __('settings_tab_password') ?></button>
</div>
<!-- Allgemein -->
<div id="tab-general" class="tab-content active">
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fas fa-sliders-h"></i> <?= __('settings_tab_general') ?></h2>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
<input type="hidden" name="form_type" value="general">
<div class="form-group"><label><?= __('settings_app_title') ?></label><input type="text" name="app_title" value="<?= htmlspecialchars($cs['app_title']) ?>" required></div>
<div class="form-grid">
<div class="form-group"><label><?= __('settings_logo_url') ?></label><input type="url" name="logo_url" value="<?= htmlspecialchars($cs['logo_url']) ?>" placeholder="https://example.com/logo.png"></div>
<div class="form-group"><label><?= __('settings_favicon_url') ?></label><input type="url" name="favicon_url" value="<?= htmlspecialchars($cs['favicon_url']) ?>" placeholder="https://example.com/favicon.ico"><div class="help-text"><?= __('settings_favicon_hint') ?></div></div>
</div>
<hr class="section-divider">
<div class="form-group"><label><?= __('settings_instr_header') ?></label><input type="text" name="instruction_header" value="<?= htmlspecialchars($cs['instruction_header']) ?>"></div>
<div class="form-group"><label><?= __('settings_instr_text') ?></label><textarea name="instruction_text" class="tinymce-editor"><?= htmlspecialchars($cs['instruction_text']) ?></textarea></div>
<div class="checkbox-group" style="margin-bottom: 20px;"><input type="checkbox" name="public_access" id="public_access" <?= $cs['public_access'] == '1' ? 'checked' : '' ?>><label for="public_access" style="margin:0;"><?= __('settings_public_access') ?></label></div>
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
</form>
</div>
<!-- Voucher-Standards -->
<div id="tab-defaults" class="tab-content">
<h2 style="margin-bottom: 8px; color: var(--text-primary);"><i class="fas fa-sliders-h"></i> <?= __('settings_tab_defaults') ?></h2>
<p style="color: var(--text-muted); font-size: 14px; margin-bottom: 24px;">Diese Werte werden als Vorgabe im Voucher-Formular verwendet.</p>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
<input type="hidden" name="form_type" value="defaults">
<div class="form-grid">
<div class="form-group">
<label><?= __('settings_default_expire') ?></label>
<input type="number" name="default_expire_minutes" value="<?= (int)$cs['default_expire_minutes'] ?>" min="1" max="525600">
<div class="help-text"><?= __('settings_default_expire_hint') ?></div>
</div>
<div class="form-group">
<label><?= __('settings_default_devices') ?></label>
<input type="number" name="default_max_uses" value="<?= (int)$cs['default_max_uses'] ?>" min="1" max="100">
<div class="help-text"><?= __('settings_default_devices_hint') ?></div>
</div>
<div class="form-group">
<label><?= __('settings_max_devices') ?></label>
<input type="number" name="max_uses_limit" value="<?= (int)$cs['max_uses_limit'] ?>" min="1" max="100">
<div class="help-text"><?= __('settings_max_devices_hint') ?></div>
</div>
</div>
<div class="info-box" style="margin-top: 10px;">
<h4><i class="fas fa-info-circle"></i> Gültigkeits-Referenz</h4>
<p>
60 Min = 1 Stunde &nbsp;|&nbsp; 480 Min = 8 Stunden &nbsp;|&nbsp;
1440 Min = 1 Tag &nbsp;|&nbsp; 10080 Min = 1 Woche &nbsp;|&nbsp;
43200 Min = 30 Tage
</p>
</div>
<button type="submit" name="save_settings" class="btn btn-primary" style="margin-top: 16px;"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
</form>
</div>
<!-- Login-Seite -->
<div id="tab-login" class="tab-content">
<h2 style="margin-bottom: 8px; color: var(--text-primary);"><i class="fas fa-right-to-bracket"></i> <?= __('settings_tab_login') ?></h2>
<p style="color: var(--text-muted); font-size: 14px; margin-bottom: 24px;"><?= __('settings_login_intro') ?></p>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
<input type="hidden" name="form_type" value="login">
<div class="checkbox-group" style="margin-bottom: 20px;">
<input type="checkbox" name="login_panel_enabled" id="login_panel_enabled" <?= $cs['login_panel_enabled'] === '1' ? 'checked' : '' ?>>
<label for="login_panel_enabled" style="margin:0;"><?= __('settings_login_panel') ?></label>
</div>
<div class="form-grid">
<div class="form-group">
<label><?= __('settings_login_brand') ?></label>
<input type="text" name="login_brand_name" value="<?= htmlspecialchars($cs['login_brand_name']) ?>" placeholder="<?= htmlspecialchars($cs['app_title']) ?>">
<div class="help-text"><?= __('settings_login_brand_hint') ?></div>
</div>
<div class="form-group">
<label><?= __('settings_login_logo') ?></label>
<input type="url" name="login_logo_url" value="<?= htmlspecialchars($cs['login_logo_url']) ?>" placeholder="https://example.com/logo.svg">
<div class="help-text"><?= __('settings_login_logo_hint') ?></div>
</div>
</div>
<hr class="section-divider">
<div class="form-group">
<label><?= __('settings_login_claim_title') ?></label>
<input type="text" name="login_claim_title" value="<?= htmlspecialchars($cs['login_claim_title']) ?>" placeholder="<?= htmlspecialchars(__('auth_claim_title')) ?>">
</div>
<div class="form-group">
<label><?= __('settings_login_claim_text') ?></label>
<textarea name="login_claim_text" rows="3" placeholder="<?= htmlspecialchars(__('auth_claim_text')) ?>"><?= htmlspecialchars($cs['login_claim_text']) ?></textarea>
</div>
<div class="form-group">
<label><?= __('settings_login_features') ?></label>
<textarea name="login_features" rows="4" placeholder="<?= htmlspecialchars(__('auth_feature_1') . "\n" . __('auth_feature_2') . "\n" . __('auth_feature_3')) ?>"><?= htmlspecialchars($cs['login_features']) ?></textarea>
<div class="help-text"><?= __('settings_login_features_hint') ?></div>
</div>
<div class="form-group">
<label><?= __('settings_login_footer') ?></label>
<input type="text" name="login_footer" value="<?= htmlspecialchars($cs['login_footer']) ?>" placeholder="© <?= date('Y') ?> <?= htmlspecialchars($cs['app_title']) ?>">
</div>
<hr class="section-divider">
<div class="form-group">
<label><?= __('settings_login_bg_image') ?></label>
<input type="url" name="login_bg_image" value="<?= htmlspecialchars($cs['login_bg_image']) ?>" placeholder="https://example.com/empfang.jpg">
<div class="help-text"><?= __('settings_login_bg_image_hint') ?></div>
</div>
<div class="form-grid">
<div class="form-group">
<label><?= __('settings_login_bg_from') ?></label>
<div class="color-field">
<input type="color" class="color-swatch" data-target="login_bg_from"
value="<?= preg_match('/^#[0-9a-fA-F]{6}$/', $cs['login_bg_from']) ? htmlspecialchars($cs['login_bg_from']) : '#3b2f8f' ?>">
<input type="text" id="login_bg_from" name="login_bg_from" value="<?= htmlspecialchars($cs['login_bg_from']) ?>" placeholder="#3b2f8f">
</div>
</div>
<div class="form-group">
<label><?= __('settings_login_bg_to') ?></label>
<div class="color-field">
<input type="color" class="color-swatch" data-target="login_bg_to"
value="<?= preg_match('/^#[0-9a-fA-F]{6}$/', $cs['login_bg_to']) ? htmlspecialchars($cs['login_bg_to']) : '#6d5ce7' ?>">
<input type="text" id="login_bg_to" name="login_bg_to" value="<?= htmlspecialchars($cs['login_bg_to']) ?>" placeholder="#6d5ce7">
</div>
</div>
<div class="form-group">
<label><?= __('settings_login_overlay') ?></label>
<input type="number" name="login_bg_overlay" min="0" max="90" value="<?= (int)$cs['login_bg_overlay'] ?>">
<div class="help-text"><?= __('settings_login_overlay_hint') ?></div>
</div>
</div>
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:16px;">
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
<a href="../login.php?preview=1" target="_blank" rel="noopener" class="btn btn-secondary">
<i class="fas fa-arrow-up-right-from-square"></i> <?= __('settings_login_preview') ?>
</a>
</div>
</form>
</div>
<!-- Cron-Sync -->
<div id="tab-cron" class="tab-content">
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fas fa-clock"></i> <?= __('settings_tab_cron') ?></h2>
<div class="info-box">
<h4><i class="fas fa-info-circle"></i> Was macht der Cron-Job?</h4>
<p>Der Cron-Job synchronisiert automatisch alle Voucher von Ihren UniFi Controllern in die lokale Datenbank.</p>
</div>
<hr class="section-divider">
<?php if (empty($cs['cron_token'])): ?>
<p style="color: var(--text-muted); margin-bottom: 15px;"><i class="fas fa-exclamation-triangle" style="color: var(--warning);"></i> Kein Token konfiguriert.</p>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
<button type="submit" name="generate_cron_token" class="btn btn-primary"><i class="fas fa-key"></i> Token generieren</button>
</form>
<?php else: ?>
<div class="token-box" style="margin-bottom: 15px;"><?= htmlspecialchars($cs['cron_token']) ?></div>
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px;">
<button onclick="copyToClipboard('<?= htmlspecialchars($cs['cron_token']) ?>')" class="btn btn-secondary"><i class="fas fa-copy"></i> Kopieren</button>
<form method="post" style="display:inline;"><input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>"><button type="submit" name="generate_cron_token" class="btn btn-secondary"><i class="fas fa-sync"></i> Neu generieren</button></form>
<form method="post" style="display:inline;" onsubmit="return confirm('Token wirklich löschen?');"><input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>"><button type="submit" name="delete_cron_token" class="btn btn-secondary" style="color: var(--danger);"><i class="fas fa-trash"></i> Löschen</button></form>
</div>
<?php endif; ?>
<?php
$cronUrl = $protocol . '://' . $_SERVER['HTTP_HOST'] . $scriptPath . '/cron_sync.php?token=' . ($cs['cron_token'] ?: 'DEIN_TOKEN');
?>
<div class="form-group">
<label>Cron-URL</label>
<div style="display:flex;gap:10px;">
<input type="text" id="cronUrl" value="<?= htmlspecialchars($cronUrl) ?>" readonly>
<button onclick="copyToClipboard(document.getElementById('cronUrl').value)" class="btn btn-secondary"><i class="fas fa-copy"></i></button>
</div>
</div>
<div class="placeholder-info" style="background: var(--bg-hover); border-color: var(--border-color);">
<h4 style="color: var(--text-primary);">Crontab-Eintrag (alle 30 Min)</h4>
<div style="background: #1e1e1e; color: #d4d4d4; padding: 12px; border-radius: 6px; font-family: monospace; font-size: 13px; margin-top: 8px; overflow-x: auto;">
*/30 * * * * curl -s "<?= htmlspecialchars($cronUrl) ?>" &gt; /dev/null 2&gt;&amp;1
</div>
</div>
<?php if ($cs['cron_token']): ?>
<div style="margin-top: 20px;">
<button onclick="testCronJob()" class="btn btn-primary" id="testCronBtn"><i class="fas fa-play"></i> Jetzt ausführen</button>
<span id="testCronResult" style="margin-left: 15px;"></span>
</div>
<?php endif; ?>
<p style="margin-top:20px; color: var(--text-muted); font-size: 13px;">Letzte Synchronisation: <strong><?= $cs['last_cron_sync'] ? date('d.m.Y H:i:s', strtotime($cs['last_cron_sync'])) : 'Noch nie' ?></strong></p>
</div>
<!-- M365 -->
<div id="tab-m365" class="tab-content">
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fab fa-microsoft"></i> Microsoft 365</h2>
<div class="info-box">
<h4><i class="fas fa-info-circle"></i> Azure AD App</h4>
<p>Redirect URI: <strong><?= $protocol . '://' . $host . $scriptPath ?>/m365_callback.php</strong></p>
</div>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
<input type="hidden" name="form_type" value="m365">
<div class="form-group"><label>Client ID</label><input type="text" name="m365_client_id" value="<?= htmlspecialchars($cs['m365_client_id']) ?>"></div>
<div class="form-group"><label>Client Secret</label><input type="password" name="m365_client_secret" value="<?= htmlspecialchars($cs['m365_client_secret']) ?>"></div>
<div class="form-group"><label>Tenant ID</label><input type="text" name="m365_tenant_id" value="<?= htmlspecialchars($cs['m365_tenant_id']) ?>"></div>
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
</form>
</div>
<!-- SMTP -->
<div id="tab-smtp" class="tab-content">
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fas fa-envelope"></i> SMTP</h2>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
<input type="hidden" name="form_type" value="smtp">
<div class="checkbox-group" style="margin-bottom: 20px;"><input type="checkbox" name="smtp_enabled" id="smtp_enabled" <?= $cs['smtp_enabled'] == '1' ? 'checked' : '' ?>><label for="smtp_enabled" style="margin:0;">SMTP aktivieren</label></div>
<div class="form-grid">
<div class="form-group"><label>Host</label><input type="text" name="smtp_host" value="<?= htmlspecialchars($cs['smtp_host']) ?>"></div>
<div class="form-group"><label>Port</label><input type="number" name="smtp_port" value="<?= htmlspecialchars($cs['smtp_port']) ?>"></div>
</div>
<div class="form-group"><label>Verschlüsselung</label><select name="smtp_encryption"><option value="tls" <?= $cs['smtp_encryption']==='tls'?'selected':'' ?>>TLS</option><option value="ssl" <?= $cs['smtp_encryption']==='ssl'?'selected':'' ?>>SSL</option><option value="none" <?= $cs['smtp_encryption']==='none'?'selected':'' ?>>Keine</option></select></div>
<div class="form-grid">
<div class="form-group"><label>Benutzername</label><input type="text" name="smtp_username" value="<?= htmlspecialchars($cs['smtp_username']) ?>"></div>
<div class="form-group"><label>Passwort</label><input type="password" name="smtp_password" placeholder="Leer = nicht ändern"></div>
</div>
<div class="form-grid">
<div class="form-group"><label>Absender E-Mail</label><input type="email" name="smtp_from_email" value="<?= htmlspecialchars($cs['smtp_from_email']) ?>"></div>
<div class="form-group"><label>Absender Name</label><input type="text" name="smtp_from_name" value="<?= htmlspecialchars($cs['smtp_from_name']) ?>"></div>
</div>
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
</form>
<hr class="section-divider">
<h3 style="margin-bottom:15px; color: var(--text-primary);">SMTP testen</h3>
<div style="display:flex;gap:10px;align-items:flex-end;">
<div style="flex:1;"><label>Test-E-Mail senden an</label><input type="email" id="smtpTestEmail" placeholder="empfaenger@example.com" style="margin-top:6px;"></div>
<button onclick="testSmtp()" class="btn btn-secondary" id="smtpTestBtn"><i class="fas fa-paper-plane"></i> <?= __('btn_test') ?></button>
</div>
<span id="smtpTestResult" style="display:block;margin-top:10px;font-size:13px;"></span>
</div>
<!-- E-Mail Templates -->
<div id="tab-templates_email" class="tab-content">
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fas fa-file-alt"></i> E-Mail Templates</h2>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
<input type="hidden" name="form_type" value="templates">
<div class="form-group"><label>System-URL</label><input type="url" name="system_url" value="<?= htmlspecialchars($cs['system_url']) ?>"><div class="help-text">Auto: <code><?= $autoDetectedUrl ?></code></div></div>
<hr class="section-divider">
<h3 style="margin-bottom:15px; color: var(--text-primary);">Voucher E-Mail</h3>
<div class="placeholder-info"><h4>Platzhalter:</h4><div class="placeholder-list"><code>{VOUCHER_CODE}</code><code>{SITE_NAME}</code><code>{MAX_USES}</code><code>{APP_TITLE}</code><code>{INSTRUCTIONS}</code></div></div>
<div class="form-group"><label>Betreff</label><input type="text" name="email_voucher_subject" value="<?= htmlspecialchars($cs['email_voucher_subject']) ?>"></div>
<div class="form-group"><label>E-Mail Text</label><textarea name="email_voucher_body" class="tinymce-editor"><?= htmlspecialchars($cs['email_voucher_body']) ?></textarea></div>
<hr class="section-divider">
<h3 style="margin-bottom:15px; color: var(--text-primary);">Benutzer-Benachrichtigung</h3>
<div class="placeholder-info"><h4>Platzhalter:</h4><div class="placeholder-list"><code>{USER_NAME}</code><code>{CHANGES}</code><code>{APP_TITLE}</code><code>{SYSTEM_URL}</code></div></div>
<div class="form-group"><label>Betreff</label><input type="text" name="email_user_notification_subject" value="<?= htmlspecialchars($cs['email_user_notification_subject']) ?>"></div>
<div class="form-group"><label>E-Mail Text</label><textarea name="email_user_notification_body" class="tinymce-editor"><?= htmlspecialchars($cs['email_user_notification_body']) ?></textarea></div>
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
</form>
</div>
<!-- System -->
<div id="tab-system" class="tab-content">
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fas fa-cogs"></i> System & Erweitert</h2>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
<input type="hidden" name="form_type" value="system">
<div class="info-box">
<h4><i class="fas fa-info-circle"></i> WYSIWYG-Editor</h4>
<p>Der Editor (TinyMCE, GPL-Variante) wird lokal aus <code>assets/vendor/</code> geladen es werden keine externen Dienste aufgerufen.</p>
</div>
<hr class="section-divider">
<h3 style="margin-bottom:15px; color: var(--text-primary);">Druck-Template</h3>
<div class="placeholder-info"><h4>Platzhalter:</h4><div class="placeholder-list"><code>{VOUCHER_CODE}</code><code>{EXPIRY_DATE}</code><code>{EXPIRY_TIME}</code><code>{SITE_NAME}</code><code>{MAX_USES}</code><code>{APP_TITLE}</code><code>{INSTRUCTIONS}</code></div></div>
<div class="form-group"><label>HTML Template für Voucher-Druck</label><textarea name="print_template" class="tinymce-editor" style="min-height:250px;"><?= htmlspecialchars($cs['print_template']) ?></textarea></div>
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
</form>
<hr class="section-divider">
<h3 style="margin-bottom:15px; color: var(--text-primary);">System-Information</h3>
<table style="width:100%; font-size:14px;">
<tr><td style="padding:8px 0;color:var(--text-muted);">PHP Version:</td><td><strong><?= phpversion() ?></strong></td></tr>
<tr><td style="padding:8px 0;color:var(--text-muted);">Datenbank:</td><td><strong><?= DB_NAME ?></strong></td></tr>
<tr><td style="padding:8px 0;color:var(--text-muted);">Version:</td><td><strong>2.1.0</strong></td></tr>
</table>
</div>
<!-- Passwort -->
<div id="tab-password" class="tab-content">
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fas fa-key"></i> <?= __('settings_tab_password') ?></h2>
<form method="post" style="max-width:500px;">
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
<div class="form-group"><label><?= __('settings_pw_current') ?></label><input type="password" name="current_password" required></div>
<div class="form-group"><label><?= __('settings_pw_new') ?></label><input type="password" name="new_password" required minlength="8"><div class="help-text"><?= __('settings_pw_minlength') ?></div></div>
<div class="form-group"><label><?= __('settings_pw_confirm') ?></label><input type="password" name="confirm_password" required></div>
<button type="submit" name="change_password" class="btn btn-primary"><i class="fas fa-lock"></i> <?= __('settings_tab_password') ?></button>
</form>
</div>
</div>
</main>
<script src="../assets/global.js"></script>
<script>
// Tab switching
document.querySelectorAll('.tab-button').forEach(btn => {
btn.addEventListener('click', function() {
const tab = this.dataset.tab;
document.querySelectorAll('.tab-button').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
this.classList.add('active');
document.getElementById('tab-' + tab).classList.add('active');
location.hash = tab;
});
});
// Farbwähler und Hex-Feld synchron halten
document.querySelectorAll('.color-swatch').forEach(swatch => {
const field = document.getElementById(swatch.dataset.target);
if (!field) return;
swatch.addEventListener('input', () => { field.value = swatch.value; });
field.addEventListener('input', () => {
if (/^#[0-9a-fA-F]{6}$/.test(field.value.trim())) swatch.value = field.value.trim();
});
});
// Restore tab from hash
window.addEventListener('DOMContentLoaded', function() {
const hash = location.hash.substring(1);
if (hash) {
const btn = document.querySelector(`[data-tab="${hash}"]`);
if (btn) btn.click();
}
initTinyMCE();
});
async function testSmtp() {
const email = document.getElementById('smtpTestEmail').value.trim();
const btn = document.getElementById('smtpTestBtn');
const result = document.getElementById('smtpTestResult');
if (!email) { result.textContent = 'Bitte E-Mail eingeben.'; return; }
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
const fd = new FormData();
fd.append('ajax_smtp_test', '1');
fd.append('csrf_token', '<?= $auth->getCsrfToken() ?>');
fd.append('test_email', email);
const res = await fetch('settings.php', { method: 'POST', body: fd });
const data = await res.json();
result.textContent = data.message;
result.style.color = data.success ? 'var(--success)' : 'var(--danger)';
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-paper-plane"></i> Testen';
}
async function testCronJob() {
const btn = document.getElementById('testCronBtn');
const result = document.getElementById('testCronResult');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Läuft...';
try {
const res = await fetch('../cron_sync.php?token=<?= htmlspecialchars($cs['cron_token']) ?>');
const data = await res.json();
result.innerHTML = data.success
? `<span style="color:var(--success)"><i class="fas fa-check-circle"></i> ${data.message}</span>`
: `<span style="color:var(--danger)"><i class="fas fa-times-circle"></i> ${data.message}</span>`;
if (data.success) showToast('success', 'Cron ausgeführt', data.message);
} catch (e) {
result.innerHTML = `<span style="color:var(--danger)">Fehler: ${e.message}</span>`;
}
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-play"></i> Jetzt ausführen';
}
function initTinyMCE() {
if (typeof tinymce === 'undefined') return;
const dark = document.documentElement.getAttribute('data-theme') === 'dark';
const config = {
selector: '.tinymce-editor',
height: 350,
menubar: false,
plugins: ['advlist','autolink','lists','link','searchreplace','visualblocks','code','fullscreen','table','help','wordcount'],
toolbar: 'undo redo | blocks | bold italic forecolor | alignleft aligncenter alignright | bullist numlist | removeformat | help',
content_style: "body { font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; line-height: 1.6; }",
skin: dark ? 'oxide-dark' : 'oxide',
content_css: dark ? 'dark' : 'default',
branding: false, promotion: false
};
if (window.TINYMCE_BASE_URL) {
tinymce.baseURL = window.TINYMCE_BASE_URL;
config.base_url = window.TINYMCE_BASE_URL;
config.suffix = '.min';
if (document.documentElement.lang === 'de') {
config.language = 'de';
config.language_url = window.TINYMCE_BASE_URL + '/langs/de.js';
}
}
tinymce.init(config);
}
</script>
</body>
</html>