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>
This commit is contained in:
parent
30d0ce3a23
commit
850a04d628
16 changed files with 580 additions and 196 deletions
|
|
@ -37,7 +37,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_key'])) {
|
||||||
);
|
);
|
||||||
$auth->writeAuditLog($_SESSION['user_id'], 'api_key_create', 'api_key', null, "API-Key '$name' erstellt");
|
$auth->writeAuditLog($_SESSION['user_id'], 'api_key_create', 'api_key', null, "API-Key '$name' erstellt");
|
||||||
$newKey = $k['plain'];
|
$newKey = $k['plain'];
|
||||||
$success = 'API-Schlüssel erstellt. Bitte JETZT kopieren – er wird nur einmal angezeigt!';
|
$success = __('api_created_once');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -46,14 +46,14 @@ if (isset($_GET['toggle']) && isset($_GET['token']) && $auth->validateCsrfToken(
|
||||||
$row = $db->fetchOne("SELECT is_active FROM api_keys WHERE id = ?", [(int)$_GET['toggle']]);
|
$row = $db->fetchOne("SELECT is_active FROM api_keys WHERE id = ?", [(int)$_GET['toggle']]);
|
||||||
if ($row) {
|
if ($row) {
|
||||||
$db->query("UPDATE api_keys SET is_active = ? WHERE id = ?", [$row['is_active'] ? 0 : 1, (int)$_GET['toggle']]);
|
$db->query("UPDATE api_keys SET is_active = ? WHERE id = ?", [$row['is_active'] ? 0 : 1, (int)$_GET['toggle']]);
|
||||||
$success = 'Status aktualisiert.';
|
$success = __('api_status_updated');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($_GET['delete']) && isset($_GET['token']) && $auth->validateCsrfToken($_GET['token'])) {
|
if (isset($_GET['delete']) && isset($_GET['token']) && $auth->validateCsrfToken($_GET['token'])) {
|
||||||
$db->query("DELETE FROM api_keys WHERE id = ?", [(int)$_GET['delete']]);
|
$db->query("DELETE FROM api_keys WHERE id = ?", [(int)$_GET['delete']]);
|
||||||
$auth->writeAuditLog($_SESSION['user_id'], 'api_key_delete', 'api_key', (int)$_GET['delete'], 'API-Key gelöscht');
|
$auth->writeAuditLog($_SESSION['user_id'], 'api_key_delete', 'api_key', (int)$_GET['delete'], 'API-Key gelöscht');
|
||||||
$success = 'API-Schlüssel gelöscht.';
|
$success = __('api_deleted');
|
||||||
}
|
}
|
||||||
|
|
||||||
$keys = $db->fetchAll("SELECT k.*, u.name AS creator FROM api_keys k LEFT JOIN users u ON k.created_by = u.id ORDER BY k.created_at DESC");
|
$keys = $db->fetchAll("SELECT k.*, u.name AS creator FROM api_keys k LEFT JOIN users u ON k.created_by = u.id ORDER BY k.created_at DESC");
|
||||||
|
|
@ -66,12 +66,12 @@ $adminBase = '';
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>API-Schlüssel – <?= htmlspecialchars($appTitle) ?></title>
|
<title><?= __('api_title') ?> – <?= htmlspecialchars($appTitle) ?></title>
|
||||||
<?php require __DIR__ . '/../includes/admin_nav.php'; ?>
|
<?php require __DIR__ . '/../includes/admin_nav.php'; ?>
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="page-title">API-Schlüssel</h1>
|
<h1 class="page-title"><?= __('api_title') ?></h1>
|
||||||
<p class="page-subtitle">Zugänge für externe Systeme – mit Scope und Rate-Limit.</p>
|
<p class="page-subtitle"><?= __('api_subtitle') ?></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -80,55 +80,55 @@ $adminBase = '';
|
||||||
|
|
||||||
<?php if ($newKey): ?>
|
<?php if ($newKey): ?>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Neuer Schlüssel</h2>
|
<h2><?= __('api_new_key') ?></h2>
|
||||||
<p class="muted">Kopieren Sie ihn jetzt – aus Sicherheitsgründen wird er nicht erneut angezeigt.</p>
|
<p class="muted"><?= __('api_new_key_hint') ?></p>
|
||||||
<div class="keybox"><?= htmlspecialchars($newKey) ?></div>
|
<div class="keybox"><?= htmlspecialchars($newKey) ?></div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Neuen API-Schlüssel erstellen</h2>
|
<h2><?= __('api_create_title') ?></h2>
|
||||||
<form method="post" style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;">
|
<form method="post" style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;">
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
||||||
<div style="flex:2;min-width:200px;">
|
<div style="flex:2;min-width:200px;">
|
||||||
<label class="muted" style="display:block;margin-bottom:6px;">Bezeichnung</label>
|
<label class="muted" style="display:block;margin-bottom:6px;"><?= __('api_label_name') ?></label>
|
||||||
<input class="input" type="text" name="name" placeholder="z.B. Buchungssystem, Terminal Foyer" required>
|
<input class="input" type="text" name="name" placeholder="<?= __('api_name_placeholder') ?>" required>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex:1;min-width:130px;">
|
<div style="flex:1;min-width:130px;">
|
||||||
<label class="muted" style="display:block;margin-bottom:6px;">Berechtigung</label>
|
<label class="muted" style="display:block;margin-bottom:6px;"><?= __('api_label_scope') ?></label>
|
||||||
<select class="input" name="scope">
|
<select class="input" name="scope">
|
||||||
<option value="write">Lesen + Erstellen</option>
|
<option value="write"><?= __('api_scope_write') ?></option>
|
||||||
<option value="read">Nur Lesen</option>
|
<option value="read"><?= __('api_scope_read') ?></option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex:1;min-width:120px;">
|
<div style="flex:1;min-width:120px;">
|
||||||
<label class="muted" style="display:block;margin-bottom:6px;">Limit (Anfr./min)</label>
|
<label class="muted" style="display:block;margin-bottom:6px;"><?= __('api_label_limit') ?></label>
|
||||||
<input class="input" type="number" name="rate_limit" min="0" value="0" title="0 = unbegrenzt">
|
<input class="input" type="number" name="rate_limit" min="0" value="0" title="<?= __('api_limit_title') ?>">
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" type="submit" name="create_key">Erstellen</button>
|
<button class="btn btn-primary" type="submit" name="create_key"><?= __('btn_create') ?></button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Vorhandene Schlüssel</h2>
|
<h2><?= __('api_existing') ?></h2>
|
||||||
<?php if (empty($keys)): ?>
|
<?php if (empty($keys)): ?>
|
||||||
<p class="muted">Noch keine API-Schlüssel angelegt.</p>
|
<p class="muted"><?= __('api_none') ?></p>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table>
|
<table>
|
||||||
<tr><th>Name</th><th>Präfix</th><th>Scope</th><th>Limit</th><th>Status</th><th>Zuletzt genutzt</th><th>Erstellt von</th><th></th></tr>
|
<tr><th><?= __('label_name') ?></th><th><?= __('api_col_prefix') ?></th><th><?= __('api_col_scope') ?></th><th><?= __('api_col_limit') ?></th><th><?= __('label_status') ?></th><th><?= __('api_col_last_used') ?></th><th><?= __('api_col_created_by') ?></th><th></th></tr>
|
||||||
<?php foreach ($keys as $k): ?>
|
<?php foreach ($keys as $k): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?= htmlspecialchars($k['name']) ?></td>
|
<td><?= htmlspecialchars($k['name']) ?></td>
|
||||||
<td><code>uvt_<?= htmlspecialchars($k['key_prefix']) ?>…</code></td>
|
<td><code>uvt_<?= htmlspecialchars($k['key_prefix']) ?>…</code></td>
|
||||||
<td><?= ($k['scope'] ?? 'write') === 'read' ? 'nur Lesen' : 'Lesen+Erstellen' ?></td>
|
<td><?= ($k['scope'] ?? 'write') === 'read' ? __('api_scope_read_short') : __('api_scope_write_short') ?></td>
|
||||||
<td><?= (int)($k['rate_limit'] ?? 0) === 0 ? '∞' : (int)$k['rate_limit'] . '/min' ?></td>
|
<td><?= (int)($k['rate_limit'] ?? 0) === 0 ? '∞' : (int)$k['rate_limit'] . '/min' ?></td>
|
||||||
<td><span class="badge <?= $k['is_active'] ? 'b-on' : 'b-off' ?>"><?= $k['is_active'] ? 'aktiv' : 'gesperrt' ?></span></td>
|
<td><span class="badge <?= $k['is_active'] ? 'b-on' : 'b-off' ?>"><?= $k['is_active'] ? __('api_state_active') : __('api_state_blocked') ?></span></td>
|
||||||
<td class="muted"><?= $k['last_used_at'] ? date('d.m.Y H:i', strtotime($k['last_used_at'])) : '–' ?></td>
|
<td class="muted"><?= $k['last_used_at'] ? date('d.m.Y H:i', strtotime($k['last_used_at'])) : '–' ?></td>
|
||||||
<td class="muted"><?= htmlspecialchars($k['creator'] ?? '–') ?></td>
|
<td class="muted"><?= htmlspecialchars($k['creator'] ?? '–') ?></td>
|
||||||
<td style="text-align:right;white-space:nowrap;">
|
<td style="text-align:right;white-space:nowrap;">
|
||||||
<a class="a-link" href="?toggle=<?= (int)$k['id'] ?>&token=<?= urlencode($csrf) ?>"><?= $k['is_active'] ? 'Sperren' : 'Aktivieren' ?></a>
|
<a class="a-link" href="?toggle=<?= (int)$k['id'] ?>&token=<?= urlencode($csrf) ?>"><?= $k['is_active'] ? __('api_action_block') : __('api_action_unblock') ?></a>
|
||||||
<a class="a-link" style="color:var(--danger);" href="?delete=<?= (int)$k['id'] ?>&token=<?= urlencode($csrf) ?>" onclick="return confirm('Schlüssel löschen?');">Löschen</a>
|
<a class="a-link" style="color:var(--danger);" href="?delete=<?= (int)$k['id'] ?>&token=<?= urlencode($csrf) ?>" onclick="return confirm('<?= __('api_delete_confirm') ?>');"><?= __('btn_delete') ?></a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
@ -138,8 +138,8 @@ $adminBase = '';
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Verwendung</h2>
|
<h2><?= __('api_usage') ?></h2>
|
||||||
<p class="muted" style="margin-bottom:10px;">Authentifizierung per Header <code>Authorization: Bearer <key></code> oder <code>X-API-Key: <key></code>.</p>
|
<p class="muted" style="margin-bottom:10px;"><?= __('api_usage_hint') ?> <code>Authorization: Bearer <key></code> oder <code>X-API-Key: <key></code>.</p>
|
||||||
<pre class="keybox" style="color:#cdd3e0;white-space:pre-wrap;"># Voucher erstellen
|
<pre class="keybox" style="color:#cdd3e0;white-space:pre-wrap;"># Voucher erstellen
|
||||||
curl -X POST https://IHRE-DOMAIN/api/vouchers.php \
|
curl -X POST https://IHRE-DOMAIN/api/vouchers.php \
|
||||||
-H "Authorization: Bearer uvt_…" \
|
-H "Authorization: Bearer uvt_…" \
|
||||||
|
|
@ -148,7 +148,7 @@ curl -X POST https://IHRE-DOMAIN/api/vouchers.php \
|
||||||
|
|
||||||
# Sites auflisten
|
# Sites auflisten
|
||||||
curl https://IHRE-DOMAIN/api/sites.php -H "X-API-Key: uvt_…"</pre>
|
curl https://IHRE-DOMAIN/api/sites.php -H "X-API-Key: uvt_…"</pre>
|
||||||
<p class="muted" style="margin-top:12px;">OpenAPI-Spezifikation (Import in Postman/Swagger): <a href="../api/openapi.php" target="_blank">/api/openapi.php</a></p>
|
<p class="muted" style="margin-top:12px;"><?= __('api_openapi') ?> <a href="../api/openapi.php" target="_blank">/api/openapi.php</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -45,23 +45,14 @@ $users = $db->fetchAll("SELECT id, name FROM users WHERE is_active = 1 ORDER BY
|
||||||
$currentPage = 'audit_log';
|
$currentPage = 'audit_log';
|
||||||
$adminBase = '';
|
$adminBase = '';
|
||||||
|
|
||||||
$actionLabels = [
|
// Aktionsnamen uebersetzt anzeigen; unbekannte Aktionen bleiben technisch.
|
||||||
'voucher_created' => 'Voucher erstellt',
|
$actionLabels = [];
|
||||||
'voucher_bulk' => 'Bulk Voucher',
|
foreach (['voucher_created', 'voucher_bulk', 'user_login', 'user_logout', 'user_created',
|
||||||
'user_login' => 'Login',
|
'user_updated', 'user_deleted', 'site_added', 'site_updated', 'site_deleted',
|
||||||
'user_logout' => 'Logout',
|
'settings_saved', 'password_reset', 'template_created', 'template_updated',
|
||||||
'user_created' => 'Benutzer erstellt',
|
'template_deleted'] as $action) {
|
||||||
'user_updated' => 'Benutzer geändert',
|
$actionLabels[$action] = __('audit_action_' . $action);
|
||||||
'user_deleted' => 'Benutzer gelöscht',
|
}
|
||||||
'site_added' => 'Site hinzugefügt',
|
|
||||||
'site_updated' => 'Site geändert',
|
|
||||||
'site_deleted' => 'Site gelöscht',
|
|
||||||
'settings_saved' => 'Einstellungen gespeichert',
|
|
||||||
'password_reset' => 'Passwort-Reset',
|
|
||||||
'template_created' => 'Profil erstellt',
|
|
||||||
'template_updated' => 'Profil geändert',
|
|
||||||
'template_deleted' => 'Profil gelöscht',
|
|
||||||
];
|
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="<?= I18n::getLanguage() ?>">
|
<html lang="<?= I18n::getLanguage() ?>">
|
||||||
|
|
@ -97,7 +88,7 @@ $actionLabels = [
|
||||||
<div>
|
<div>
|
||||||
<label style="display:block;font-size:12px;color:var(--text-muted);margin-bottom:5px;"><?= __('audit_user') ?></label>
|
<label style="display:block;font-size:12px;color:var(--text-muted);margin-bottom:5px;"><?= __('audit_user') ?></label>
|
||||||
<select name="user_id">
|
<select name="user_id">
|
||||||
<option value="">Alle Benutzer</option>
|
<option value=""><?= __('audit_all_users') ?></option>
|
||||||
<?php foreach ($users as $u): ?>
|
<?php foreach ($users as $u): ?>
|
||||||
<option value="<?= $u['id'] ?>" <?= (string)$filterUser === (string)$u['id'] ? 'selected' : '' ?>>
|
<option value="<?= $u['id'] ?>" <?= (string)$filterUser === (string)$u['id'] ? 'selected' : '' ?>>
|
||||||
<?= htmlspecialchars($u['name']) ?>
|
<?= htmlspecialchars($u['name']) ?>
|
||||||
|
|
@ -173,7 +164,7 @@ $actionLabels = [
|
||||||
|
|
||||||
<?php if ($pages > 1): ?>
|
<?php if ($pages > 1): ?>
|
||||||
<div class="pagination">
|
<div class="pagination">
|
||||||
<span class="page-info">Seite <?= $page ?> von <?= $pages ?> (<?= $total ?> Einträge)</span>
|
<span class="page-info"><?= str_replace(['{page}', '{pages}', '{total}'], [(string)$page, (string)$pages, number_format((int)$total, 0, ',', '.')], __('audit_page_info')) ?></span>
|
||||||
<div class="page-btns">
|
<div class="page-btns">
|
||||||
<?php
|
<?php
|
||||||
$baseUrl = '?' . http_build_query(array_filter(['action' => $filterAction, 'user_id' => $filterUser]));
|
$baseUrl = '?' . http_build_query(array_filter(['action' => $filterAction, 'user_id' => $filterUser]));
|
||||||
|
|
|
||||||
|
|
@ -43,12 +43,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['import'])) {
|
||||||
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
||||||
$error = __('error_csrf');
|
$error = __('error_csrf');
|
||||||
} elseif (empty($_FILES['backup']['tmp_name'])) {
|
} elseif (empty($_FILES['backup']['tmp_name'])) {
|
||||||
$error = 'Bitte eine Backup-Datei auswählen.';
|
$error = __('backup_choose_file');
|
||||||
} else {
|
} else {
|
||||||
$raw = file_get_contents($_FILES['backup']['tmp_name']);
|
$raw = file_get_contents($_FILES['backup']['tmp_name']);
|
||||||
$data = json_decode($raw, true);
|
$data = json_decode($raw, true);
|
||||||
if (!is_array($data) || ($data['meta']['app'] ?? '') !== 'unifi-voucher-tool') {
|
if (!is_array($data) || ($data['meta']['app'] ?? '') !== 'unifi-voucher-tool') {
|
||||||
$error = 'Ungültige oder fremde Backup-Datei.';
|
$error = __('backup_invalid_file');
|
||||||
} else {
|
} else {
|
||||||
$importSites = isset($_POST['import_sites']);
|
$importSites = isset($_POST['import_sites']);
|
||||||
$importTemplates = isset($_POST['import_templates']);
|
$importTemplates = isset($_POST['import_templates']);
|
||||||
|
|
@ -95,7 +95,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['import'])) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$auth->writeAuditLog($_SESSION['user_id'], 'config_import', 'config', null, 'Konfiguration importiert');
|
$auth->writeAuditLog($_SESSION['user_id'], 'config_import', 'config', null, 'Konfiguration importiert');
|
||||||
$success = "Import abgeschlossen: {$counts['settings']} Einstellungen, {$counts['sites']} Sites, {$counts['templates']} Profile.";
|
$success = str_replace(['{settings}', '{sites}', '{templates}'],
|
||||||
|
[(string)$counts['settings'], (string)$counts['sites'], (string)$counts['templates']],
|
||||||
|
__('backup_imported'));
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$error = 'Import-Fehler: ' . $e->getMessage();
|
$error = 'Import-Fehler: ' . $e->getMessage();
|
||||||
}
|
}
|
||||||
|
|
@ -112,12 +114,12 @@ $adminBase = '';
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Backup & Restore – <?= htmlspecialchars($appTitle) ?></title>
|
<title><?= __('backup_title') ?> – <?= htmlspecialchars($appTitle) ?></title>
|
||||||
<?php require __DIR__ . '/../includes/admin_nav.php'; ?>
|
<?php require __DIR__ . '/../includes/admin_nav.php'; ?>
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="page-title">Backup & Restore</h1>
|
<h1 class="page-title"><?= __('backup_title') ?></h1>
|
||||||
<p class="page-subtitle">Konfiguration und Daten sichern oder wiederherstellen.</p>
|
<p class="page-subtitle"><?= __('backup_subtitle') ?></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -125,21 +127,21 @@ $adminBase = '';
|
||||||
<?php if ($success): ?><div class="alert alert-ok"><?= htmlspecialchars($success) ?></div><?php endif; ?>
|
<?php if ($success): ?><div class="alert alert-ok"><?= htmlspecialchars($success) ?></div><?php endif; ?>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Export</h2>
|
<h2><?= __('backup_export') ?></h2>
|
||||||
<p class="muted">Lädt Einstellungen, Sites und Voucher-Profile als JSON. Site-Passwörter bleiben mit dem <code>APP_KEY</code> dieser Installation verschlüsselt – ein Restore auf einer Installation mit anderem APP_KEY kann sie nicht entschlüsseln.</p>
|
<p class="muted"><?= __('backup_export_hint') ?> <code>APP_KEY</code> <?= __('backup_export_hint2') ?></p>
|
||||||
<a class="btn btn-primary" href="?export=1&token=<?= urlencode($csrf) ?>">Konfiguration exportieren</a>
|
<a class="btn btn-primary" href="?export=1&token=<?= urlencode($csrf) ?>"><?= __('backup_export_btn') ?></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Import / Restore</h2>
|
<h2><?= __('backup_import') ?></h2>
|
||||||
<p class="muted">Vorhandene Sites werden anhand von Name + Site-ID aktualisiert, neue hinzugefügt. Profile werden nur angelegt, wenn der Name noch nicht existiert. Der Cron-Token wird nie überschrieben.</p>
|
<p class="muted"><?= __('backup_import_hint') ?></p>
|
||||||
<form method="post" enctype="multipart/form-data">
|
<form method="post" enctype="multipart/form-data">
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
||||||
<input type="file" name="backup" accept="application/json,.json" required><br>
|
<input type="file" name="backup" accept="application/json,.json" required><br>
|
||||||
<label class="chk"><input type="checkbox" name="import_settings" checked> Einstellungen</label>
|
<label class="chk"><input type="checkbox" name="import_settings" checked> <?= __('backup_opt_settings') ?></label>
|
||||||
<label class="chk"><input type="checkbox" name="import_sites" checked> Sites</label>
|
<label class="chk"><input type="checkbox" name="import_sites" checked> <?= __('backup_opt_sites') ?></label>
|
||||||
<label class="chk"><input type="checkbox" name="import_templates" checked> Voucher-Profile</label>
|
<label class="chk"><input type="checkbox" name="import_templates" checked> <?= __('backup_opt_templates') ?></label>
|
||||||
<button class="btn btn-primary" type="submit" name="import" style="margin-top:12px;" onclick="return confirm('Import jetzt durchführen?');">Importieren</button>
|
<button class="btn btn-primary" type="submit" name="import" style="margin-top:12px;" onclick="return confirm('<?= __('backup_import_confirm') ?>');"><?= __('import_submit') ?></button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['do_import'])) {
|
||||||
Notifier::voucherCreated($created, $site['name'], $_SESSION['user_name'] ?? null);
|
Notifier::voucherCreated($created, $site['name'], $_SESSION['user_name'] ?? null);
|
||||||
$auth->writeAuditLog($_SESSION['user_id'], 'voucher_import', 'site', $siteId, "$created Voucher importiert");
|
$auth->writeAuditLog($_SESSION['user_id'], 'voucher_import', 'site', $siteId, "$created Voucher importiert");
|
||||||
}
|
}
|
||||||
$success = "$created Voucher erstellt.";
|
$success = str_replace('{count}', (string)$created, __('import_created'));
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$error = $e->getMessage();
|
$error = $e->getMessage();
|
||||||
}
|
}
|
||||||
|
|
@ -95,8 +95,8 @@ $adminBase = '';
|
||||||
<?php require __DIR__ . '/../includes/admin_nav.php'; ?>
|
<?php require __DIR__ . '/../includes/admin_nav.php'; ?>
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="page-title">Voucher-Import</h1>
|
<h1 class="page-title"><?= __('import_title') ?></h1>
|
||||||
<p class="page-subtitle">Mehrere Vouchers auf einmal aus einer CSV-Liste erstellen.</p>
|
<p class="page-subtitle"><?= __('import_subtitle') ?></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -104,27 +104,27 @@ $adminBase = '';
|
||||||
<?php if ($success): ?><div class="alert alert-ok"><?= htmlspecialchars($success) ?></div><?php endif; ?>
|
<?php if ($success): ?><div class="alert alert-ok"><?= htmlspecialchars($success) ?></div><?php endif; ?>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Mehrere Voucher erstellen</h2>
|
<h2><?= __('import_card_title') ?></h2>
|
||||||
<p class="muted">Eine Zeile pro Voucher: <code>Name,MaxGeräte,Minuten</code> – MaxGeräte und Minuten sind optional (Standardwerte greifen). Max. 200 Zeilen. Beispiel:<br>
|
<p class="muted"><?= __('import_format_hint') ?> <code>Name,MaxGeräte,Minuten</code> <?= __('import_format_hint2') ?><br>
|
||||||
<code>Gast Müller,1,480</code> · <code>Konferenzraum A,5,240</code> · <code>Tagespass</code></p>
|
<code>Gast Müller,1,480</code> · <code>Konferenzraum A,5,240</code> · <code>Tagespass</code></p>
|
||||||
<form method="post" enctype="multipart/form-data">
|
<form method="post" enctype="multipart/form-data">
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
||||||
<label>Standort</label>
|
<label><?= __('import_site') ?></label>
|
||||||
<select class="input" name="site_id" required>
|
<select class="input" name="site_id" required>
|
||||||
<?php foreach ($sites as $s): ?><option value="<?= (int)$s['id'] ?>"><?= htmlspecialchars($s['name']) ?></option><?php endforeach; ?>
|
<?php foreach ($sites as $s): ?><option value="<?= (int)$s['id'] ?>"><?= htmlspecialchars($s['name']) ?></option><?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
<label>CSV-Datei (optional)</label>
|
<label><?= __('import_file') ?></label>
|
||||||
<input class="input" type="file" name="csv" accept=".csv,text/csv">
|
<input class="input" type="file" name="csv" accept=".csv,text/csv">
|
||||||
<label>… oder direkt einfügen</label>
|
<label><?= __('import_paste') ?></label>
|
||||||
<textarea name="csv_text" placeholder="Gast Müller,1,480 Konferenzraum A,5,240"></textarea>
|
<textarea name="csv_text" placeholder="Gast Müller,1,480 Konferenzraum A,5,240"></textarea>
|
||||||
<button class="btn" type="submit" name="do_import" style="margin-top:14px;" onclick="return confirm('Import jetzt starten?');">Importieren</button>
|
<button class="btn" type="submit" name="do_import" style="margin-top:14px;" onclick="return confirm('<?= __('import_confirm') ?>');"><?= __('import_submit') ?></button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if (!empty($results)): ?>
|
<?php if (!empty($results)): ?>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Ergebnis</h2>
|
<h2><?= __('import_result') ?></h2>
|
||||||
<table><tr><th>Name</th><th>Code / Fehler</th><th>Status</th></tr>
|
<table><tr><th><?= __('label_name') ?></th><th><?= __('import_col_code') ?></th><th><?= __('label_status') ?></th></tr>
|
||||||
<?php foreach ($results as $r): ?>
|
<?php foreach ($results as $r): ?>
|
||||||
<tr><td><?= htmlspecialchars($r['name']) ?></td><td><code><?= htmlspecialchars($r['code']) ?></code></td><td><?= $r['ok'] ? '✅' : '❌' ?></td></tr>
|
<tr><td><?= htmlspecialchars($r['name']) ?></td><td><code><?= htmlspecialchars($r['code']) ?></code></td><td><?= $r['ok'] ? '✅' : '❌' ?></td></tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
|
||||||
|
|
@ -49,13 +49,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save'])) {
|
||||||
$db->setSetting('cleanup_audit_days', max(0, (int)($_POST['cleanup_audit_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)));
|
$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');
|
$auth->writeAuditLog($_SESSION['user_id'], 'settings_update', 'config', null, 'Integration/Wartung gespeichert');
|
||||||
$success = 'Einstellungen gespeichert.';
|
$success = __('settings_saved');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($_GET['test_webhook']) && isset($_GET['token']) && $auth->validateCsrfToken($_GET['token'])) {
|
if (isset($_GET['test_webhook']) && isset($_GET['token']) && $auth->validateCsrfToken($_GET['token'])) {
|
||||||
Notifier::send('✅ Test-Benachrichtigung vom UniFi Voucher System.', ['type' => 'test']);
|
Notifier::send('✅ Test-Benachrichtigung vom UniFi Voucher System.', ['type' => 'test']);
|
||||||
$success = 'Test-Benachrichtigung gesendet (sofern Webhook aktiv & URL gültig).';
|
$success = __('int_webhook_test_sent');
|
||||||
}
|
}
|
||||||
|
|
||||||
$enforce2fa = $db->getSetting('enforce_2fa_admins', '0') === '1';
|
$enforce2fa = $db->getSetting('enforce_2fa_admins', '0') === '1';
|
||||||
|
|
@ -92,12 +92,12 @@ $adminBase = '';
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Integration & Wartung – <?= htmlspecialchars($appTitle) ?></title>
|
<title><?= __('int_title') ?> – <?= htmlspecialchars($appTitle) ?></title>
|
||||||
<?php require __DIR__ . '/../includes/admin_nav.php'; ?>
|
<?php require __DIR__ . '/../includes/admin_nav.php'; ?>
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="page-title">Integration & Wartung</h1>
|
<h1 class="page-title"><?= __('int_title') ?></h1>
|
||||||
<p class="page-subtitle">SSO, Webhooks, SMS-Versand und Aufbewahrungsfristen.</p>
|
<p class="page-subtitle"><?= __('int_subtitle') ?></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -108,64 +108,64 @@ $adminBase = '';
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Sicherheitsrichtlinie</h2>
|
<h2><?= __('int_security') ?></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>
|
<p class="muted"><?= __('int_security_hint') ?></p>
|
||||||
<label class="chk"><input type="checkbox" name="enforce_2fa_admins" <?= $enforce2fa ? 'checked' : '' ?>> 2FA für Administratoren verpflichtend</label>
|
<label class="chk"><input type="checkbox" name="enforce_2fa_admins" <?= $enforce2fa ? 'checked' : '' ?>> <?= __('int_enforce_2fa') ?></label>
|
||||||
<label>Tageslimit Voucher pro Nicht-Admin-Benutzer (0 = unbegrenzt)</label>
|
<label><?= __('int_daily_limit') ?></label>
|
||||||
<input class="input" type="number" min="0" name="user_daily_voucher_limit" value="<?= $dailyLimit ?>" style="max-width:200px;">
|
<input class="input" type="number" min="0" name="user_daily_voucher_limit" value="<?= $dailyLimit ?>" style="max-width:200px;">
|
||||||
<label>Session-Speicher</label>
|
<label><?= __('int_session_driver') ?></label>
|
||||||
<select class="input" name="session_driver" style="max-width:340px;">
|
<select class="input" name="session_driver" style="max-width:340px;">
|
||||||
<option value="php" <?= $sessionDriver==='php'?'selected':'' ?>>PHP-Standard (Dateien)</option>
|
<option value="php" <?= $sessionDriver==='php'?'selected':'' ?>><?= __('int_session_php') ?></option>
|
||||||
<option value="db" <?= $sessionDriver==='db'?'selected':'' ?>>Datenbank (ermöglicht „überall abmelden")</option>
|
<option value="db" <?= $sessionDriver==='db'?'selected':'' ?>><?= __('int_session_db') ?></option>
|
||||||
</select>
|
</select>
|
||||||
<label>Captcha im öffentlichen Modus</label>
|
<label><?= __('int_captcha') ?></label>
|
||||||
<select class="input" name="captcha_mode" style="max-width:340px;">
|
<select class="input" name="captcha_mode" style="max-width:340px;">
|
||||||
<option value="off" <?= $captchaMode==='off'?'selected':'' ?>>Aus</option>
|
<option value="off" <?= $captchaMode==='off'?'selected':'' ?>><?= __('int_captcha_off') ?></option>
|
||||||
<option value="math" <?= $captchaMode==='math'?'selected':'' ?>>Rechenaufgabe (ohne externen Dienst)</option>
|
<option value="math" <?= $captchaMode==='math'?'selected':'' ?>><?= __('int_captcha_math') ?></option>
|
||||||
<option value="hcaptcha" <?= $captchaMode==='hcaptcha'?'selected':'' ?>>hCaptcha</option>
|
<option value="hcaptcha" <?= $captchaMode==='hcaptcha'?'selected':'' ?>>hCaptcha</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="row3" style="margin-top:10px;">
|
<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 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><label>hCaptcha Secret<?= $captchaSecretSet ? __('int_secret_set') : '' ?></label><input class="input" type="password" name="captcha_secret" placeholder="<?= $captchaSecretSet ? __('int_secret_placeholder') : '' ?>"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Reverse-Proxy</h2>
|
<h2><?= __('int_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>
|
<p class="muted"><?= __('int_proxy_hint') ?> <code>X-Forwarded-For</code> <?= __('int_proxy_hint2') ?></p>
|
||||||
<input class="input" type="text" name="trusted_proxy" value="<?= htmlspecialchars($trustedProxy) ?>" placeholder="z.B. 10.0.0.1, 172.18.0.1">
|
<input class="input" type="text" name="trusted_proxy" value="<?= htmlspecialchars($trustedProxy) ?>" placeholder="z.B. 10.0.0.1, 172.18.0.1">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Webhook-Benachrichtigungen</h2>
|
<h2><?= __('int_webhook') ?></h2>
|
||||||
<p class="muted">Slack-, Microsoft-Teams- oder generische JSON-Webhook-URL. Wird bei Voucher-Erstellung ausgelöst.</p>
|
<p class="muted"><?= __('int_webhook_hint') ?></p>
|
||||||
<label class="chk"><input type="checkbox" name="webhook_enabled" <?= $webhookEnabled ? 'checked' : '' ?>> Webhook aktiv</label>
|
<label class="chk"><input type="checkbox" name="webhook_enabled" <?= $webhookEnabled ? 'checked' : '' ?>> <?= __('int_webhook_active') ?></label>
|
||||||
<label>Webhook-URL</label>
|
<label><?= __('int_webhook_url') ?></label>
|
||||||
<input class="input" type="url" name="webhook_url" value="<?= htmlspecialchars($webhookUrl) ?>" placeholder="https://hooks.slack.com/services/…">
|
<input class="input" type="url" name="webhook_url" value="<?= htmlspecialchars($webhookUrl) ?>" placeholder="https://hooks.slack.com/services/…">
|
||||||
<div style="margin-top:12px;">
|
<div style="margin-top:12px;">
|
||||||
<a class="btn btn-secondary" href="?test_webhook=1&token=<?= urlencode($csrf) ?>">Test senden</a>
|
<a class="btn btn-secondary" href="?test_webhook=1&token=<?= urlencode($csrf) ?>"><?= __('int_webhook_test') ?></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>SMS-Versand (Twilio)</h2>
|
<h2><?= __('int_sms') ?></h2>
|
||||||
<p class="muted">Voucher-Codes optional per SMS versenden. Erfordert ein Twilio-Konto.</p>
|
<p class="muted"><?= __('int_sms_hint') ?></p>
|
||||||
<label class="chk"><input type="checkbox" name="sms_enabled" <?= $smsEnabled ? 'checked' : '' ?>> SMS-Versand aktiv</label>
|
<label class="chk"><input type="checkbox" name="sms_enabled" <?= $smsEnabled ? 'checked' : '' ?>> <?= __('int_sms_active') ?></label>
|
||||||
<div class="row3" style="margin-top:10px;">
|
<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>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>Auth Token<?= $twilioTokenSet ? __('int_secret_set') : '' ?></label><input class="input" type="password" name="twilio_token" placeholder="<?= $twilioTokenSet ? __('int_secret_placeholder') : '' ?>"></div>
|
||||||
<div><label>Absender (From)</label><input class="input" type="text" name="twilio_from" value="<?= htmlspecialchars($twilioFrom) ?>" placeholder="+49…"></div>
|
<div><label><?= __('int_sms_from') ?></label><input class="input" type="text" name="twilio_from" value="<?= htmlspecialchars($twilioFrom) ?>" placeholder="+49…"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Single Sign-On (OpenID Connect)</h2>
|
<h2><?= __('int_sso') ?></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>
|
<p class="muted"><?= __('int_sso_hint') ?> <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>
|
<label class="chk"><input type="checkbox" name="oidc_enabled" <?= $oidcEnabled ? 'checked' : '' ?>> <?= __('int_sso_active') ?></label>
|
||||||
<div class="row3" style="margin-top:10px;">
|
<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><?= __('int_sso_button') ?></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 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>Client Secret<?= $oidcSecretSet ? __('int_secret_set') : '' ?></label><input class="input" type="password" name="oidc_client_secret" placeholder="<?= $oidcSecretSet ? __('int_secret_placeholder') : '' ?>"></div>
|
||||||
</div>
|
</div>
|
||||||
<label>Authorization Endpoint</label><input class="input" type="url" name="oidc_auth_url" value="<?= htmlspecialchars($oidcAuthUrl) ?>" placeholder="https://idp/authorize">
|
<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>Token Endpoint</label><input class="input" type="url" name="oidc_token_url" value="<?= htmlspecialchars($oidcTokenUrl) ?>" placeholder="https://idp/token">
|
||||||
|
|
@ -174,18 +174,18 @@ $adminBase = '';
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Datenhaltung & Cleanup (DSGVO)</h2>
|
<h2><?= __('int_cleanup') ?></h2>
|
||||||
<p class="muted">Aufbewahrungsfristen in Tagen (0 = deaktiviert). Ausführung per <code>cron_cleanup.php</code> (täglich empfohlen).
|
<p class="muted"><?= __('int_cleanup_hint') ?> <code>cron_cleanup.php</code> <?= __('int_cleanup_hint2') ?>
|
||||||
<?php if ($lastCleanup): ?><br>Letzter Lauf: <?= htmlspecialchars($lastCleanup) ?><?php endif; ?>
|
<?php if ($lastCleanup): ?><br><?= __('int_cleanup_last') ?> <?= htmlspecialchars($lastCleanup) ?><?php endif; ?>
|
||||||
</p>
|
</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div><label>Abgelaufene Voucher</label><input class="input" type="number" min="0" name="cleanup_expired_days" value="<?= $cleanupExpired ?>"></div>
|
<div><label><?= __('int_cleanup_expired') ?></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><?= __('int_cleanup_audit') ?></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><label><?= __('int_cleanup_logins') ?></label><input class="input" type="number" min="0" name="cleanup_login_days" value="<?= $cleanupLogin ?>"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-primary" type="submit" name="save">Speichern</button>
|
<button class="btn btn-primary" type="submit" name="save"><?= __('btn_save') ?></button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -96,46 +96,46 @@ $adminBase = '';
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Reporting – <?= htmlspecialchars($appTitle) ?></title>
|
<title><?= __('rep_title') ?> – <?= htmlspecialchars($appTitle) ?></title>
|
||||||
<?= Ui::script('assets/vendor/chartjs/chart.umd.min.js', '../') ?>
|
<?= Ui::script('assets/vendor/chartjs/chart.umd.min.js', '../') ?>
|
||||||
<?php require __DIR__ . '/../includes/admin_nav.php'; ?>
|
<?php require __DIR__ . '/../includes/admin_nav.php'; ?>
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="page-title">Reporting</h1>
|
<h1 class="page-title"><?= __('rep_title') ?></h1>
|
||||||
<p class="page-subtitle">Auswertungen nach Zeitraum, Site und Benutzer.</p>
|
<p class="page-subtitle"><?= __('rep_subtitle') ?></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toolbar no-print">
|
<div class="toolbar no-print">
|
||||||
<form method="get" style="display:flex;gap:8px;align-items:center;">
|
<form method="get" style="display:flex;gap:8px;align-items:center;">
|
||||||
<label style="margin:0;">Zeitraum</label>
|
<label style="margin:0;"><?= __('rep_period') ?></label>
|
||||||
<select class="input" name="days" onchange="this.form.submit()">
|
<select class="input" name="days" onchange="this.form.submit()">
|
||||||
<?php foreach ([7,30,90,365] as $d): ?>
|
<?php foreach ([7,30,90,365] as $d): ?>
|
||||||
<option value="<?= $d ?>" <?= $days===$d?'selected':'' ?>><?= $d ?> Tage</option>
|
<option value="<?= $d ?>" <?= $days===$d?'selected':'' ?>><?= $d ?> <?= __('rep_days') ?></option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</form>
|
</form>
|
||||||
<a class="btn btn-secondary" href="?export=daily&days=<?= $days ?>"><i class="fas fa-download"></i> CSV (täglich)</a>
|
<a class="btn btn-secondary" href="?export=daily&days=<?= $days ?>"><i class="fas fa-download"></i> <?= __('rep_csv_daily') ?></a>
|
||||||
<a class="btn btn-secondary" href="?export=per_site"><i class="fas fa-download"></i> CSV (pro Site)</a>
|
<a class="btn btn-secondary" href="?export=per_site"><i class="fas fa-download"></i> <?= __('rep_csv_site') ?></a>
|
||||||
<a class="btn btn-secondary" href="?export=per_user"><i class="fas fa-download"></i> CSV (pro Nutzer)</a>
|
<a class="btn btn-secondary" href="?export=per_user"><i class="fas fa-download"></i> <?= __('rep_csv_user') ?></a>
|
||||||
<button class="btn btn-secondary" onclick="window.print()"><i class="fas fa-print"></i> Drucken/PDF</button>
|
<button class="btn btn-secondary" onclick="window.print()"><i class="fas fa-print"></i> <?= __('rep_print') ?></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid4">
|
<div class="grid4">
|
||||||
<div class="stat"><div class="n"><?= (int)$totals['total'] ?></div><div class="l">Vouchers gesamt</div></div>
|
<div class="stat"><div class="n"><?= (int)$totals['total'] ?></div><div class="l"><?= __('rep_total') ?></div></div>
|
||||||
<div class="stat"><div class="n"><?= (int)$totals['valid'] ?></div><div class="l">Gültig</div></div>
|
<div class="stat"><div class="n"><?= (int)$totals['valid'] ?></div><div class="l"><?= __('status_valid') ?></div></div>
|
||||||
<div class="stat"><div class="n"><?= (int)$totals['used'] ?></div><div class="l">Verwendet</div></div>
|
<div class="stat"><div class="n"><?= (int)$totals['used'] ?></div><div class="l"><?= __('status_used') ?></div></div>
|
||||||
<div class="stat"><div class="n"><?= $inPeriod ?></div><div class="l">In <?= $days ?> Tagen erstellt</div></div>
|
<div class="stat"><div class="n"><?= $inPeriod ?></div><div class="l"><?= str_replace('{days}', (string)$days, __('rep_in_period')) ?></div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Erstellte Voucher (<?= $days ?> Tage)</h2>
|
<h2><?= str_replace('{days}', (string)$days, __('rep_chart_title')) ?></h2>
|
||||||
<canvas id="chart" height="90"></canvas>
|
<canvas id="chart" height="90"></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Pro Site</h2>
|
<h2><?= __('rep_per_site') ?></h2>
|
||||||
<table><tr><th>Site</th><th>Gesamt</th><th>Gültig</th><th>Verwendet</th><th>Abgelaufen</th></tr>
|
<table><tr><th><?= __('label_site') ?></th><th><?= __('label_total') ?></th><th><?= __('status_valid') ?></th><th><?= __('status_used') ?></th><th><?= __('status_expired') ?></th></tr>
|
||||||
<?php foreach ($perSite as $r): ?>
|
<?php foreach ($perSite as $r): ?>
|
||||||
<tr><td><?= htmlspecialchars($r['name']) ?></td><td><?= (int)$r['total'] ?></td><td><?= (int)$r['valid'] ?></td><td><?= (int)$r['used'] ?></td><td><?= (int)$r['expired'] ?></td></tr>
|
<tr><td><?= htmlspecialchars($r['name']) ?></td><td><?= (int)$r['total'] ?></td><td><?= (int)$r['valid'] ?></td><td><?= (int)$r['used'] ?></td><td><?= (int)$r['expired'] ?></td></tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
@ -143,12 +143,12 @@ $adminBase = '';
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Top-Nutzer</h2>
|
<h2><?= __('rep_top_users') ?></h2>
|
||||||
<table><tr><th>Benutzer</th><th>Voucher erstellt</th></tr>
|
<table><tr><th><?= __('label_user') ?></th><th><?= __('rep_col_created') ?></th></tr>
|
||||||
<?php foreach ($perUser as $r): ?>
|
<?php foreach ($perUser as $r): ?>
|
||||||
<tr><td><?= htmlspecialchars($r['name'] ?? '–') ?></td><td><?= (int)$r['c'] ?></td></tr>
|
<tr><td><?= htmlspecialchars($r['name'] ?? '–') ?></td><td><?= (int)$r['c'] ?></td></tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if (empty($perUser)): ?><tr><td colspan="2" style="color:var(--text-muted);">Keine Daten</td></tr><?php endif; ?>
|
<?php if (empty($perUser)): ?><tr><td colspan="2" style="color:var(--text-muted);"><?= __('rep_no_data') ?></td></tr><?php endif; ?>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,11 @@ ini_set('log_errors', 1);
|
||||||
require_once __DIR__ . '/../config.php';
|
require_once __DIR__ . '/../config.php';
|
||||||
require_once __DIR__ . '/../includes/Database.php';
|
require_once __DIR__ . '/../includes/Database.php';
|
||||||
require_once __DIR__ . '/../includes/Auth.php';
|
require_once __DIR__ . '/../includes/Auth.php';
|
||||||
|
require_once __DIR__ . '/../includes/I18n.php';
|
||||||
require_once __DIR__ . '/../includes/Ui.php';
|
require_once __DIR__ . '/../includes/Ui.php';
|
||||||
|
|
||||||
|
I18n::init();
|
||||||
|
|
||||||
$auth = new Auth();
|
$auth = new Auth();
|
||||||
$auth->requireLogin();
|
$auth->requireLogin();
|
||||||
|
|
||||||
|
|
@ -25,20 +28,20 @@ $setupRequired = isset($_GET['setup_required']);
|
||||||
// 2FA aktivieren (Code bestaetigen)
|
// 2FA aktivieren (Code bestaetigen)
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['enable_totp'])) {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['enable_totp'])) {
|
||||||
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
||||||
$error = 'Ungültiges Sicherheits-Token';
|
$error = __('sec_token_invalid');
|
||||||
} else {
|
} else {
|
||||||
$secret = $_SESSION['totp_setup_secret'] ?? '';
|
$secret = $_SESSION['totp_setup_secret'] ?? '';
|
||||||
$code = trim($_POST['code'] ?? '');
|
$code = trim($_POST['code'] ?? '');
|
||||||
if ($secret === '') {
|
if ($secret === '') {
|
||||||
$error = 'Setup abgelaufen, bitte erneut starten.';
|
$error = __('sec_setup_expired');
|
||||||
} elseif (!Totp::verify($secret, $code)) {
|
} elseif (!Totp::verify($secret, $code)) {
|
||||||
$error = 'Code ungültig. Bitte erneut versuchen.';
|
$error = __('sec_code_invalid');
|
||||||
} else {
|
} else {
|
||||||
$backupCodes = $auth->enableTotp($user['id'], $secret);
|
$backupCodes = $auth->enableTotp($user['id'], $secret);
|
||||||
unset($_SESSION['totp_setup_secret']);
|
unset($_SESSION['totp_setup_secret']);
|
||||||
$totpEnabled = true;
|
$totpEnabled = true;
|
||||||
$user = $auth->getCurrentUser();
|
$user = $auth->getCurrentUser();
|
||||||
$success = 'Zwei-Faktor-Authentifizierung wurde aktiviert. Bitte Recovery-Codes sicher speichern!';
|
$success = __('sec_enabled');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -46,32 +49,32 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['enable_totp'])) {
|
||||||
// Überall abmelden (andere Sessions beenden)
|
// Überall abmelden (andere Sessions beenden)
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['logout_others'])) {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['logout_others'])) {
|
||||||
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
||||||
$error = 'Ungültiges Sicherheits-Token';
|
$error = __('sec_token_invalid');
|
||||||
} else {
|
} else {
|
||||||
$auth->logoutOtherSessions();
|
$auth->logoutOtherSessions();
|
||||||
$success = 'Alle anderen Sitzungen wurden beendet.';
|
$success = __('sec_sessions_closed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recovery-Codes neu erzeugen
|
// Recovery-Codes neu erzeugen
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['regen_codes'])) {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['regen_codes'])) {
|
||||||
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
||||||
$error = 'Ungültiges Sicherheits-Token';
|
$error = __('sec_token_invalid');
|
||||||
} elseif (!empty($user['totp_enabled'])) {
|
} elseif (!empty($user['totp_enabled'])) {
|
||||||
$backupCodes = $auth->regenerateBackupCodes($user['id']);
|
$backupCodes = $auth->regenerateBackupCodes($user['id']);
|
||||||
$user = $auth->getCurrentUser();
|
$user = $auth->getCurrentUser();
|
||||||
$success = 'Neue Recovery-Codes erzeugt. Die alten sind jetzt ungültig.';
|
$success = __('sec_codes_new');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2FA deaktivieren
|
// 2FA deaktivieren
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['disable_totp'])) {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['disable_totp'])) {
|
||||||
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
||||||
$error = 'Ungültiges Sicherheits-Token';
|
$error = __('sec_token_invalid');
|
||||||
} else {
|
} else {
|
||||||
$auth->disableTotp($user['id']);
|
$auth->disableTotp($user['id']);
|
||||||
$totpEnabled = false;
|
$totpEnabled = false;
|
||||||
$success = 'Zwei-Faktor-Authentifizierung wurde deaktiviert.';
|
$success = __('sec_disabled');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,11 +91,11 @@ $dbSessions = $db->getSetting('session_driver', 'php') === 'db';
|
||||||
$activeSessions = $dbSessions ? $auth->activeSessionCount() : 0;
|
$activeSessions = $dbSessions ? $auth->activeSessionCount() : 0;
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="<?= I18n::getLanguage() ?>">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Zwei-Faktor-Authentifizierung – <?= htmlspecialchars($appTitle) ?></title>
|
<title><?= __('sec_title') ?> – <?= htmlspecialchars($appTitle) ?></title>
|
||||||
<?php if (!$totpEnabled && $hasPassword): ?>
|
<?php if (!$totpEnabled && $hasPassword): ?>
|
||||||
<?= Ui::script('assets/vendor/qrcodejs/qrcode.min.js', '../') ?>
|
<?= Ui::script('assets/vendor/qrcodejs/qrcode.min.js', '../') ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
@ -103,21 +106,21 @@ $activeSessions = $dbSessions ? $auth->activeSessionCount() : 0;
|
||||||
<div class="focus-head">
|
<div class="focus-head">
|
||||||
<span class="focus-icon"><i class="fas fa-shield-halved"></i></span>
|
<span class="focus-icon"><i class="fas fa-shield-halved"></i></span>
|
||||||
<div>
|
<div>
|
||||||
<h1>Zwei-Faktor-Authentifizierung</h1>
|
<h1><?= __('sec_title') ?></h1>
|
||||||
<p class="sub">Konto: <?= htmlspecialchars($user['email']) ?></p>
|
<p class="sub"><?= __('sec_account') ?> <?= htmlspecialchars($user['email']) ?></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if ($setupRequired && !$totpEnabled): ?>
|
<?php if ($setupRequired && !$totpEnabled): ?>
|
||||||
<div class="alert alert-error">Aus Sicherheitsgründen ist 2FA für Administratoren verpflichtend. Bitte jetzt einrichten.</div>
|
<div class="alert alert-error"><?= __('sec_required_hint') ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($error): ?><div class="alert alert-error"><?= htmlspecialchars($error) ?></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 ($success): ?><div class="alert alert-ok"><?= htmlspecialchars($success) ?></div><?php endif; ?>
|
||||||
|
|
||||||
<?php if (!empty($backupCodes)): ?>
|
<?php if (!empty($backupCodes)): ?>
|
||||||
<div class="codes-box">
|
<div class="codes-box">
|
||||||
<strong><i class="fas fa-key"></i> Recovery-Codes</strong>
|
<strong><i class="fas fa-key"></i> <?= __('sec_recovery_codes') ?></strong>
|
||||||
<p>Bewahren Sie diese sicher auf. Jeder Code funktioniert <em>einmal</em>, falls Sie keinen Zugriff auf Ihre App haben.</p>
|
<p><?= __('sec_recovery_hint') ?></p>
|
||||||
<div class="codes">
|
<div class="codes">
|
||||||
<?php foreach ($backupCodes as $c): ?><span><?= htmlspecialchars($c) ?></span><?php endforeach; ?>
|
<?php foreach ($backupCodes as $c): ?><span><?= htmlspecialchars($c) ?></span><?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -125,34 +128,34 @@ $activeSessions = $dbSessions ? $auth->activeSessionCount() : 0;
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (!$hasPassword): ?>
|
<?php if (!$hasPassword): ?>
|
||||||
<div class="status off"><i class="fas fa-circle-minus"></i> Nicht verfügbar</div>
|
<div class="status off"><i class="fas fa-circle-minus"></i> <?= __('sec_unavailable') ?></div>
|
||||||
<p class="sub">Ihr Konto meldet sich über Microsoft 365 an. 2FA wird dort in Ihrem Microsoft-Konto verwaltet.</p>
|
<p class="sub"><?= __('sec_m365_hint') ?></p>
|
||||||
<?php elseif ($totpEnabled): ?>
|
<?php elseif ($totpEnabled): ?>
|
||||||
<div class="status on"><i class="fas fa-circle-check"></i> Aktiv</div>
|
<div class="status on"><i class="fas fa-circle-check"></i> <?= __('sec_active') ?></div>
|
||||||
<p class="sub">Bei jeder Anmeldung wird zusätzlich ein Code aus Ihrer Authenticator-App abgefragt.<br>
|
<p class="sub"><?= __('sec_active_hint') ?><br>
|
||||||
Verbleibende Recovery-Codes: <strong><?= (int)$auth->backupCodesRemaining($user) ?></strong></p>
|
<?= __('sec_codes_left') ?> <strong><?= (int)$auth->backupCodesRemaining($user) ?></strong></p>
|
||||||
<form method="post" style="margin-bottom:10px;">
|
<form method="post" style="margin-bottom:10px;">
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
||||||
<button type="submit" name="regen_codes" class="btn btn-secondary btn-lg btn-block">Recovery-Codes neu erzeugen</button>
|
<button type="submit" name="regen_codes" class="btn btn-secondary btn-lg btn-block"><?= __('sec_regen_codes') ?></button>
|
||||||
</form>
|
</form>
|
||||||
<form method="post" onsubmit="return confirm('2FA wirklich deaktivieren?');">
|
<form method="post" onsubmit="return confirm('<?= __('sec_disable_confirm') ?>');">
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
||||||
<button type="submit" name="disable_totp" class="btn btn-danger btn-lg btn-block">2FA deaktivieren</button>
|
<button type="submit" name="disable_totp" class="btn btn-danger btn-lg btn-block"><?= __('sec_disable') ?></button>
|
||||||
</form>
|
</form>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="status off"><i class="fas fa-circle-minus"></i> Inaktiv</div>
|
<div class="status off"><i class="fas fa-circle-minus"></i> <?= __('sec_inactive') ?></div>
|
||||||
<ol>
|
<ol>
|
||||||
<li>Authenticator-App öffnen (Google Authenticator, Authy, Microsoft Authenticator …)</li>
|
<li><?= __('sec_step_1') ?></li>
|
||||||
<li>QR-Code scannen <em>oder</em> Secret manuell eingeben</li>
|
<li><?= __('sec_step_2') ?></li>
|
||||||
<li>Den angezeigten 6-stelligen Code unten eingeben</li>
|
<li><?= __('sec_step_3') ?></li>
|
||||||
</ol>
|
</ol>
|
||||||
<div class="qr"><div id="qrcode"></div></div>
|
<div class="qr"><div id="qrcode"></div></div>
|
||||||
<div class="secret"><?= htmlspecialchars($setupSecret) ?></div>
|
<div class="secret"><?= htmlspecialchars($setupSecret) ?></div>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
||||||
<label for="code">6-stelliger Code</label>
|
<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">
|
<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;">2FA aktivieren</button>
|
<button type="submit" name="enable_totp" class="btn btn-primary btn-lg btn-block" style="margin-top:14px;"><?= __('sec_enable') ?></button>
|
||||||
</form>
|
</form>
|
||||||
<script>
|
<script>
|
||||||
new QRCode(document.getElementById('qrcode'), {
|
new QRCode(document.getElementById('qrcode'), {
|
||||||
|
|
@ -165,14 +168,14 @@ $activeSessions = $dbSessions ? $auth->activeSessionCount() : 0;
|
||||||
|
|
||||||
<?php if ($dbSessions): ?>
|
<?php if ($dbSessions): ?>
|
||||||
<hr>
|
<hr>
|
||||||
<p class="sub">Aktive Sitzungen: <strong><?= (int)$activeSessions ?></strong></p>
|
<p class="sub"><?= __('sec_sessions') ?> <strong><?= (int)$activeSessions ?></strong></p>
|
||||||
<form method="post" onsubmit="return confirm('Alle anderen Sitzungen abmelden?');">
|
<form method="post" onsubmit="return confirm('<?= __('sec_logout_others_confirm') ?>');">
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
||||||
<button type="submit" name="logout_others" class="btn btn-secondary btn-lg btn-block">Auf allen anderen Geräten abmelden</button>
|
<button type="submit" name="logout_others" class="btn btn-secondary btn-lg btn-block"><?= __('sec_logout_others') ?></button>
|
||||||
</form>
|
</form>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="auth-links"><a class="back-link" href="../index.php"><i class="fas fa-arrow-left"></i> Zurück</a></div>
|
<div class="auth-links"><a class="back-link" href="../index.php"><i class="fas fa-arrow-left"></i> <?= __('nav_back') ?></a></div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -360,7 +360,7 @@ $adminBase = '';
|
||||||
<!-- Voucher-Standards -->
|
<!-- Voucher-Standards -->
|
||||||
<div id="tab-defaults" class="tab-content">
|
<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>
|
<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>
|
<p style="color: var(--text-muted); font-size: 14px; margin-bottom: 24px;"><?= __('settings_defaults_hint') ?></p>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
|
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
|
||||||
<input type="hidden" name="form_type" value="defaults">
|
<input type="hidden" name="form_type" value="defaults">
|
||||||
|
|
@ -544,7 +544,7 @@ $adminBase = '';
|
||||||
<div id="tab-cron" class="tab-content">
|
<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>
|
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fas fa-clock"></i> <?= __('settings_tab_cron') ?></h2>
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<h4><i class="fas fa-info-circle"></i> Was macht der Cron-Job?</h4>
|
<h4><i class="fas fa-info-circle"></i> <?= __('settings_cron_what') ?></h4>
|
||||||
<p>Der Cron-Job synchronisiert automatisch alle Voucher von Ihren UniFi Controllern in die lokale Datenbank.</p>
|
<p>Der Cron-Job synchronisiert automatisch alle Voucher von Ihren UniFi Controllern in die lokale Datenbank.</p>
|
||||||
</div>
|
</div>
|
||||||
<hr class="section-divider">
|
<hr class="section-divider">
|
||||||
|
|
@ -559,7 +559,7 @@ $adminBase = '';
|
||||||
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px;">
|
<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>
|
<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;"><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>
|
<form method="post" style="display:inline;" onsubmit="return confirm('<?= __('js_confirm_delete_token') ?>');"><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>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php
|
<?php
|
||||||
|
|
@ -618,11 +618,11 @@ $adminBase = '';
|
||||||
<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-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-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>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 class="form-group"><label>Passwort</label><input type="password" name="smtp_password" placeholder="<?= __('settings_leave_empty') ?>"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-grid">
|
<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 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 class="form-group"><label><?= __('settings_smtp_from_name') ?></label><input type="text" name="smtp_from_name" value="<?= htmlspecialchars($cs['smtp_from_name']) ?>"></div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
|
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -643,12 +643,12 @@ $adminBase = '';
|
||||||
<input type="hidden" name="form_type" value="templates">
|
<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>
|
<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">
|
<hr class="section-divider">
|
||||||
<h3 style="margin-bottom:15px; color: var(--text-primary);">Voucher E-Mail</h3>
|
<h3 style="margin-bottom:15px; color: var(--text-primary);"><?= __('settings_tpl_voucher_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="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>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>
|
<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">
|
<hr class="section-divider">
|
||||||
<h3 style="margin-bottom:15px; color: var(--text-primary);">Benutzer-Benachrichtigung</h3>
|
<h3 style="margin-bottom:15px; color: var(--text-primary);"><?= __('settings_tpl_user_notify') ?></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="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>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>
|
<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>
|
||||||
|
|
@ -664,13 +664,13 @@ $adminBase = '';
|
||||||
<input type="hidden" name="form_type" value="system">
|
<input type="hidden" name="form_type" value="system">
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<h4><i class="fas fa-info-circle"></i> WYSIWYG-Editor</h4>
|
<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>
|
<p><?= __('settings_editor_hint') ?> <code>assets/vendor/</code> <?= __('settings_editor_hint2') ?></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="section-divider">
|
<hr class="section-divider">
|
||||||
<h3 style="margin-bottom:15px; color: var(--text-primary);">Druck-Template</h3>
|
<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="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>
|
<div class="form-group"><label><?= __('settings_print_template') ?></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>
|
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
|
||||||
</form>
|
</form>
|
||||||
<hr class="section-divider">
|
<hr class="section-divider">
|
||||||
|
|
@ -761,7 +761,7 @@ async function testSmtp() {
|
||||||
const email = document.getElementById('smtpTestEmail').value.trim();
|
const email = document.getElementById('smtpTestEmail').value.trim();
|
||||||
const btn = document.getElementById('smtpTestBtn');
|
const btn = document.getElementById('smtpTestBtn');
|
||||||
const result = document.getElementById('smtpTestResult');
|
const result = document.getElementById('smtpTestResult');
|
||||||
if (!email) { result.textContent = 'Bitte E-Mail eingeben.'; return; }
|
if (!email) { result.textContent = '<?= __('js_enter_email') ?>'; return; }
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
|
|
@ -780,7 +780,7 @@ async function testCronJob() {
|
||||||
const btn = document.getElementById('testCronBtn');
|
const btn = document.getElementById('testCronBtn');
|
||||||
const result = document.getElementById('testCronResult');
|
const result = document.getElementById('testCronResult');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Läuft...';
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> <?= __('js_running') ?>';
|
||||||
try {
|
try {
|
||||||
const res = await fetch('../cron_sync.php?token=<?= htmlspecialchars($cs['cron_token']) ?>');
|
const res = await fetch('../cron_sync.php?token=<?= htmlspecialchars($cs['cron_token']) ?>');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
@ -789,10 +789,10 @@ async function testCronJob() {
|
||||||
: `<span style="color:var(--danger)"><i class="fas fa-times-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);
|
if (data.success) showToast('success', 'Cron ausgeführt', data.message);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result.innerHTML = `<span style="color:var(--danger)">Fehler: ${e.message}</span>`;
|
result.innerHTML = `<span style="color:var(--danger)"><?= __('js_error') ?>: ${e.message}</span>`;
|
||||||
}
|
}
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = '<i class="fas fa-play"></i> Jetzt ausführen';
|
btn.innerHTML = '<i class="fas fa-play"></i> <?= __('js_run_now') ?>';
|
||||||
}
|
}
|
||||||
|
|
||||||
function initTinyMCE() {
|
function initTinyMCE() {
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,7 @@ $currentPage = 'sites';
|
||||||
</a>
|
</a>
|
||||||
<a href="?delete=<?= $site['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
|
<a href="?delete=<?= $site['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
|
||||||
class="btn btn-danger-soft btn-sm"
|
class="btn btn-danger-soft btn-sm"
|
||||||
onclick="return confirm('Möchten Sie diese Site wirklich löschen?')">
|
onclick="return confirm('<?= __('js_confirm_delete_site') ?>')">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -196,7 +196,7 @@ $currentPage = 'sites';
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label><?= __('sites_site_id') ?></label>
|
<label><?= __('sites_site_id') ?></label>
|
||||||
<input type="text" name="site_id" required placeholder="z.B. default">
|
<input type="text" name="site_id" required placeholder="z.B. default">
|
||||||
<small style="color:var(--text-muted);font-size:12px;">Zu finden in der UniFi Controller URL</small>
|
<small style="color:var(--text-muted);font-size:12px;"><?= __('sites_id_hint') ?></small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label><?= __('sites_controller') ?></label>
|
<label><?= __('sites_controller') ?></label>
|
||||||
|
|
@ -259,7 +259,7 @@ $currentPage = 'sites';
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label><?= __('sites_password_edit') ?></label>
|
<label><?= __('sites_password_edit') ?></label>
|
||||||
<input type="password" id="edit_password" name="password" placeholder="Leer lassen = nicht ändern">
|
<input type="password" id="edit_password" name="password" placeholder="<?= __('sites_pw_unchanged') ?>">
|
||||||
<small style="color:var(--text-muted);font-size:12px;"><?= __('sites_password_hint') ?></small>
|
<small style="color:var(--text-muted);font-size:12px;"><?= __('sites_password_hint') ?></small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@ $adminBase = '';
|
||||||
<button onclick="openEditModal(<?= $t['id'] ?>, '<?= htmlspecialchars($t['name'], ENT_QUOTES) ?>', <?= (int)$t['max_uses'] ?>, <?= (int)$t['expire_minutes'] ?>, '<?= htmlspecialchars($t['description'] ?? '', ENT_QUOTES) ?>', <?= (int)$t['is_active'] ?>, <?= (int)($t['qos_rate_max_down'] ?? 0) ?>, <?= (int)($t['qos_rate_max_up'] ?? 0) ?>, <?= (int)($t['qos_usage_quota'] ?? 0) ?>)"
|
<button onclick="openEditModal(<?= $t['id'] ?>, '<?= htmlspecialchars($t['name'], ENT_QUOTES) ?>', <?= (int)$t['max_uses'] ?>, <?= (int)$t['expire_minutes'] ?>, '<?= htmlspecialchars($t['description'] ?? '', ENT_QUOTES) ?>', <?= (int)$t['is_active'] ?>, <?= (int)($t['qos_rate_max_down'] ?? 0) ?>, <?= (int)($t['qos_rate_max_up'] ?? 0) ?>, <?= (int)($t['qos_usage_quota'] ?? 0) ?>)"
|
||||||
class="btn btn-secondary btn-small"><i class="fas fa-edit"></i></button>
|
class="btn btn-secondary btn-small"><i class="fas fa-edit"></i></button>
|
||||||
<a href="?delete=<?= $t['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
|
<a href="?delete=<?= $t['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
|
||||||
onclick="return confirm('Profil wirklich löschen?')"
|
onclick="return confirm('<?= __('js_confirm_delete_template') ?>')"
|
||||||
class="btn btn-danger-soft btn-small"><i class="fas fa-trash"></i></a>
|
class="btn btn-danger-soft btn-small"><i class="fas fa-trash"></i></a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -210,10 +210,10 @@ $adminBase = '';
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label><?= __('templates_duration') ?> *</label>
|
<label><?= __('templates_duration') ?> *</label>
|
||||||
<input type="number" name="expire_minutes" value="480" min="1" max="525600">
|
<input type="number" name="expire_minutes" value="480" min="1" max="525600">
|
||||||
<div class="help-text">480 = 8 Stunden</div>
|
<div class="help-text"><?= __('templates_minutes_hint') ?></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group"><label><?= __('templates_desc') ?></label><textarea name="description" rows="2" placeholder="Kurze Beschreibung für Ihr Team"></textarea></div>
|
<div class="form-group"><label><?= __('templates_desc') ?></label><textarea name="description" rows="2" placeholder="<?= __('templates_desc_placeholder') ?>"></textarea></div>
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;">
|
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;">
|
||||||
<div class="form-group"><label>Download (kbit/s)</label><input type="number" name="qos_rate_max_down" min="0" placeholder="0 = unbegrenzt"></div>
|
<div class="form-group"><label>Download (kbit/s)</label><input type="number" name="qos_rate_max_down" min="0" placeholder="0 = unbegrenzt"></div>
|
||||||
<div class="form-group"><label>Upload (kbit/s)</label><input type="number" name="qos_rate_max_up" min="0" placeholder="0 = unbegrenzt"></div>
|
<div class="form-group"><label>Upload (kbit/s)</label><input type="number" name="qos_rate_max_up" min="0" placeholder="0 = unbegrenzt"></div>
|
||||||
|
|
|
||||||
|
|
@ -276,7 +276,7 @@ $currentPage = 'users';
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<a href="?delete=<?= $user['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
|
<a href="?delete=<?= $user['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
|
||||||
class="btn btn-danger-soft btn-sm" title="<?= __('btn_delete') ?>"
|
class="btn btn-danger-soft btn-sm" title="<?= __('btn_delete') ?>"
|
||||||
onclick="return confirm('Benutzer wirklich löschen?')">
|
onclick="return confirm('<?= __('js_confirm_delete_user') ?>')">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</a>
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
|
||||||
|
|
@ -384,7 +384,7 @@ async function resendVoucher(voucherId, code) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteVoucher(voucherId) {
|
async function deleteVoucher(voucherId) {
|
||||||
if (!confirm('Voucher wirklich löschen?')) return;
|
if (!confirm('<?= __('js_confirm_delete_voucher') ?>')) return;
|
||||||
const row = document.getElementById(`voucher-${voucherId}`);
|
const row = document.getElementById(`voucher-${voucherId}`);
|
||||||
if (row) row.classList.add('deleting');
|
if (row) row.classList.add('deleting');
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -367,7 +367,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
|
||||||
|
|
||||||
<div class="voucher-result no-print">
|
<div class="voucher-result no-print">
|
||||||
<div class="result-label"><i class="fas fa-circle-check"></i> <?= __('voucher_success_title') ?></div>
|
<div class="result-label"><i class="fas fa-circle-check"></i> <?= __('voucher_success_title') ?></div>
|
||||||
<div class="voucher-code" id="voucherCode" onclick="copyCode()" title="Klicken zum Kopieren">
|
<div class="voucher-code" id="voucherCode" onclick="copyCode()" title="<?= __('js_click_to_copy') ?>">
|
||||||
<?= htmlspecialchars($voucherCode) ?>
|
<?= htmlspecialchars($voucherCode) ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="voucher-info">
|
<div class="voucher-info">
|
||||||
|
|
@ -432,8 +432,8 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
|
||||||
<tr>
|
<tr>
|
||||||
<td><?= $i + 1 ?></td>
|
<td><?= $i + 1 ?></td>
|
||||||
<td>
|
<td>
|
||||||
<code onclick="copyToClipboard('<?= addslashes($bv['code']) ?>', 'Kopiert!')"
|
<code onclick="copyToClipboard('<?= addslashes($bv['code']) ?>', '<?= __('js_copied') ?>')"
|
||||||
title="Klicken zum Kopieren"><?= htmlspecialchars($bv['code']) ?></code>
|
title="<?= __('js_click_to_copy') ?>"><?= htmlspecialchars($bv['code']) ?></code>
|
||||||
</td>
|
</td>
|
||||||
<td><?= htmlspecialchars($bv['site_name']) ?></td>
|
<td><?= htmlspecialchars($bv['site_name']) ?></td>
|
||||||
<td><?= $bv['expiry_date'] ?> <?= $bv['expiry_time'] ?></td>
|
<td><?= $bv['expiry_date'] ?> <?= $bv['expiry_time'] ?></td>
|
||||||
|
|
@ -645,7 +645,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
|
||||||
});
|
});
|
||||||
|
|
||||||
function copyCode() {
|
function copyCode() {
|
||||||
copyToClipboard('<?= addslashes($voucherCode) ?>', 'Code kopiert!');
|
copyToClipboard('<?= addslashes($voucherCode) ?>', '<?= __('js_code_copied') ?>');
|
||||||
}
|
}
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
|
|
||||||
194
lang/de.php
194
lang/de.php
|
|
@ -261,6 +261,200 @@ return [
|
||||||
'settings_login_overlay' => 'Abdunklung des Bildes (%)',
|
'settings_login_overlay' => 'Abdunklung des Bildes (%)',
|
||||||
'settings_login_overlay_hint' => '0–90 %. Höhere Werte machen den Text auf hellen Bildern besser lesbar.',
|
'settings_login_overlay_hint' => '0–90 %. Höhere Werte machen den Text auf hellen Bildern besser lesbar.',
|
||||||
'settings_login_preview' => 'Vorschau öffnen',
|
'settings_login_preview' => 'Vorschau öffnen',
|
||||||
|
'api_title' => 'API-Schlüssel',
|
||||||
|
'api_subtitle' => 'Zugänge für externe Systeme – mit Scope und Rate-Limit.',
|
||||||
|
'api_new_key' => 'Neuer Schlüssel',
|
||||||
|
'api_new_key_hint' => 'Kopieren Sie ihn jetzt – aus Sicherheitsgründen wird er nicht erneut angezeigt.',
|
||||||
|
'api_create_title' => 'Neuen API-Schlüssel erstellen',
|
||||||
|
'api_label_name' => 'Bezeichnung',
|
||||||
|
'api_name_placeholder' => 'z.B. Buchungssystem, Terminal Foyer',
|
||||||
|
'api_label_scope' => 'Berechtigung',
|
||||||
|
'api_scope_write' => 'Lesen + Erstellen',
|
||||||
|
'api_scope_read' => 'Nur Lesen',
|
||||||
|
'api_scope_write_short' => 'Lesen+Erstellen',
|
||||||
|
'api_scope_read_short' => 'nur Lesen',
|
||||||
|
'api_label_limit' => 'Limit (Anfr./min)',
|
||||||
|
'api_limit_title' => '0 = unbegrenzt',
|
||||||
|
'api_existing' => 'Vorhandene Schlüssel',
|
||||||
|
'api_none' => 'Noch keine API-Schlüssel angelegt.',
|
||||||
|
'api_col_prefix' => 'Präfix',
|
||||||
|
'api_col_scope' => 'Scope',
|
||||||
|
'api_col_limit' => 'Limit',
|
||||||
|
'api_col_last_used' => 'Zuletzt genutzt',
|
||||||
|
'api_col_created_by' => 'Erstellt von',
|
||||||
|
'api_state_active' => 'aktiv',
|
||||||
|
'api_state_blocked' => 'gesperrt',
|
||||||
|
'api_action_block' => 'Sperren',
|
||||||
|
'api_action_unblock' => 'Aktivieren',
|
||||||
|
'api_delete_confirm' => 'Schlüssel löschen?',
|
||||||
|
'api_usage' => 'Verwendung',
|
||||||
|
'api_usage_hint' => 'Authentifizierung per Header',
|
||||||
|
'api_openapi' => 'OpenAPI-Spezifikation (Import in Postman/Swagger):',
|
||||||
|
'api_created' => 'API-Schlüssel erstellt.',
|
||||||
|
'api_deleted' => 'API-Schlüssel gelöscht.',
|
||||||
|
'api_toggled' => 'Status geändert.',
|
||||||
|
'api_name_required' => 'Bitte eine Bezeichnung angeben.',
|
||||||
|
'api_created_once' => 'API-Schlüssel erstellt. Bitte JETZT kopieren – er wird nur einmal angezeigt!',
|
||||||
|
'api_status_updated' => 'Status aktualisiert.',
|
||||||
|
'int_title' => 'Integration & Wartung',
|
||||||
|
'int_subtitle' => 'SSO, Webhooks, SMS-Versand und Aufbewahrungsfristen.',
|
||||||
|
'int_security' => 'Sicherheitsrichtlinie',
|
||||||
|
'int_security_hint' => 'Erzwingt Zwei-Faktor-Authentifizierung für alle Administrator-Konten (lokale Accounts). Admins ohne 2FA werden bei der nächsten Aktion zur Einrichtung geleitet.',
|
||||||
|
'int_enforce_2fa' => '2FA für Administratoren verpflichtend',
|
||||||
|
'int_daily_limit' => 'Tageslimit Voucher pro Nicht-Admin-Benutzer (0 = unbegrenzt)',
|
||||||
|
'int_session_driver' => 'Session-Speicher',
|
||||||
|
'int_session_php' => 'PHP-Standard (Dateien)',
|
||||||
|
'int_session_db' => 'Datenbank (ermöglicht „überall abmelden")',
|
||||||
|
'int_captcha' => 'Captcha im öffentlichen Modus',
|
||||||
|
'int_captcha_off' => 'Aus',
|
||||||
|
'int_captcha_math' => 'Rechenaufgabe (ohne externen Dienst)',
|
||||||
|
'int_secret_set' => ' (gesetzt)',
|
||||||
|
'int_secret_placeholder' => '••••••• (leer = unverändert)',
|
||||||
|
'int_proxy' => 'Reverse-Proxy',
|
||||||
|
'int_proxy_hint' => 'IP-Adressen vertrauenswürdiger Proxies (kommasepariert). Nur dann wird die echte Client-IP aus',
|
||||||
|
'int_proxy_hint2' => 'für Rate-Limit & Audit verwendet.',
|
||||||
|
'int_webhook' => 'Webhook-Benachrichtigungen',
|
||||||
|
'int_webhook_hint' => 'Slack-, Microsoft-Teams- oder generische JSON-Webhook-URL. Wird bei Voucher-Erstellung ausgelöst.',
|
||||||
|
'int_webhook_active' => 'Webhook aktiv',
|
||||||
|
'int_webhook_url' => 'Webhook-URL',
|
||||||
|
'int_webhook_test' => 'Test senden',
|
||||||
|
'int_sms' => 'SMS-Versand (Twilio)',
|
||||||
|
'int_sms_hint' => 'Voucher-Codes optional per SMS versenden. Erfordert ein Twilio-Konto.',
|
||||||
|
'int_sms_active' => 'SMS-Versand aktiv',
|
||||||
|
'int_sms_from' => 'Absender (From)',
|
||||||
|
'int_sso' => 'Single Sign-On (OpenID Connect)',
|
||||||
|
'int_sso_hint' => 'Generischer OIDC-Provider (z.B. Keycloak, Authentik, Google, Auth0). Redirect-URI:',
|
||||||
|
'int_sso_active' => 'OIDC-Login aktiv',
|
||||||
|
'int_sso_button' => 'Button-Text',
|
||||||
|
'int_cleanup' => 'Datenhaltung & Cleanup (DSGVO)',
|
||||||
|
'int_cleanup_hint' => 'Aufbewahrungsfristen in Tagen (0 = deaktiviert). Ausführung per',
|
||||||
|
'int_cleanup_hint2' => '(täglich empfohlen).',
|
||||||
|
'int_cleanup_last' => 'Letzter Lauf:',
|
||||||
|
'int_cleanup_expired' => 'Abgelaufene Voucher',
|
||||||
|
'int_cleanup_audit' => 'Audit-Log',
|
||||||
|
'int_cleanup_logins' => 'Login-Versuche',
|
||||||
|
'int_saved' => 'Einstellungen gespeichert.',
|
||||||
|
'int_webhook_sent' => 'Test-Webhook gesendet.',
|
||||||
|
'int_webhook_failed' => 'Test-Webhook fehlgeschlagen.',
|
||||||
|
'int_webhook_test_sent' => 'Test-Benachrichtigung gesendet (sofern Webhook aktiv & URL gültig).',
|
||||||
|
'sec_token_invalid' => 'Ungültiges Sicherheits-Token',
|
||||||
|
'sec_setup_expired' => 'Setup abgelaufen, bitte erneut starten.',
|
||||||
|
'sec_code_invalid' => 'Code ungültig. Bitte erneut versuchen.',
|
||||||
|
'sec_enabled' => 'Zwei-Faktor-Authentifizierung wurde aktiviert. Bitte Recovery-Codes sicher speichern!',
|
||||||
|
'sec_sessions_closed' => 'Alle anderen Sitzungen wurden beendet.',
|
||||||
|
'sec_codes_new' => 'Neue Recovery-Codes erzeugt. Die alten sind jetzt ungültig.',
|
||||||
|
'sec_disabled' => 'Zwei-Faktor-Authentifizierung wurde deaktiviert.',
|
||||||
|
'backup_choose_file' => 'Bitte eine Backup-Datei auswählen.',
|
||||||
|
'backup_invalid_file' => 'Ungültige oder fremde Backup-Datei.',
|
||||||
|
'sec_title' => 'Zwei-Faktor-Authentifizierung',
|
||||||
|
'sec_account' => 'Konto:',
|
||||||
|
'sec_required_hint' => 'Aus Sicherheitsgründen ist 2FA für Administratoren verpflichtend. Bitte jetzt einrichten.',
|
||||||
|
'sec_recovery_codes' => 'Recovery-Codes',
|
||||||
|
'sec_recovery_hint' => 'Bewahren Sie diese sicher auf. Jeder Code funktioniert einmal, falls Sie keinen Zugriff auf Ihre App haben.',
|
||||||
|
'sec_unavailable' => 'Nicht verfügbar',
|
||||||
|
'sec_m365_hint' => 'Ihr Konto meldet sich über Microsoft 365 an. 2FA wird dort in Ihrem Microsoft-Konto verwaltet.',
|
||||||
|
'sec_active' => 'Aktiv',
|
||||||
|
'sec_inactive' => 'Inaktiv',
|
||||||
|
'sec_active_hint' => 'Bei jeder Anmeldung wird zusätzlich ein Code aus Ihrer Authenticator-App abgefragt.',
|
||||||
|
'sec_codes_left' => 'Verbleibende Recovery-Codes:',
|
||||||
|
'sec_regen_codes' => 'Recovery-Codes neu erzeugen',
|
||||||
|
'sec_disable_confirm' => '2FA wirklich deaktivieren?',
|
||||||
|
'sec_disable' => '2FA deaktivieren',
|
||||||
|
'sec_step_1' => 'Authenticator-App öffnen (Google Authenticator, Authy, Microsoft Authenticator …)',
|
||||||
|
'sec_step_2' => 'QR-Code scannen oder Secret manuell eingeben',
|
||||||
|
'sec_step_3' => 'Den angezeigten 6-stelligen Code unten eingeben',
|
||||||
|
'sec_code_label' => '6-stelliger Code',
|
||||||
|
'sec_enable' => '2FA aktivieren',
|
||||||
|
'sec_sessions' => 'Aktive Sitzungen:',
|
||||||
|
'sec_logout_others_confirm' => 'Alle anderen Sitzungen abmelden?',
|
||||||
|
'sec_logout_others' => 'Auf allen anderen Geräten abmelden',
|
||||||
|
'import_title' => 'Voucher-Import',
|
||||||
|
'import_subtitle' => 'Mehrere Vouchers auf einmal aus einer CSV-Liste erstellen.',
|
||||||
|
'import_card_title' => 'Mehrere Voucher erstellen',
|
||||||
|
'import_format_hint' => 'Eine Zeile pro Voucher:',
|
||||||
|
'import_format_hint2' => '– MaxGeräte und Minuten sind optional (Standardwerte greifen). Max. 200 Zeilen. Beispiel:',
|
||||||
|
'import_site' => 'Standort',
|
||||||
|
'import_file' => 'CSV-Datei (optional)',
|
||||||
|
'import_paste' => '… oder direkt einfügen',
|
||||||
|
'import_confirm' => 'Import jetzt starten?',
|
||||||
|
'import_submit' => 'Importieren',
|
||||||
|
'import_result' => 'Ergebnis',
|
||||||
|
'import_col_code' => 'Code / Fehler',
|
||||||
|
'import_created' => '{count} Voucher erstellt.',
|
||||||
|
'backup_title' => 'Backup & Restore',
|
||||||
|
'backup_subtitle' => 'Konfiguration und Daten sichern oder wiederherstellen.',
|
||||||
|
'backup_export' => 'Export',
|
||||||
|
'backup_export_hint' => 'Lädt Einstellungen, Sites und Voucher-Profile als JSON. Site-Passwörter bleiben mit dem',
|
||||||
|
'backup_export_hint2' => 'dieser Installation verschlüsselt – ein Restore auf einer Installation mit anderem APP_KEY kann sie nicht entschlüsseln.',
|
||||||
|
'backup_export_btn' => 'Konfiguration exportieren',
|
||||||
|
'backup_import' => 'Import / Restore',
|
||||||
|
'backup_import_hint' => 'Vorhandene Sites werden anhand von Name + Site-ID aktualisiert, neue hinzugefügt. Profile werden nur angelegt, wenn der Name noch nicht existiert. Der Cron-Token wird nie überschrieben.',
|
||||||
|
'backup_opt_settings' => 'Einstellungen',
|
||||||
|
'backup_opt_sites' => 'Sites',
|
||||||
|
'backup_opt_templates' => 'Voucher-Profile',
|
||||||
|
'backup_import_confirm' => 'Import jetzt durchführen?',
|
||||||
|
'backup_imported' => 'Import abgeschlossen: {settings} Einstellungen, {sites} Sites, {templates} Profile.',
|
||||||
|
'rep_title' => 'Reporting',
|
||||||
|
'rep_subtitle' => 'Auswertungen nach Zeitraum, Site und Benutzer.',
|
||||||
|
'rep_period' => 'Zeitraum',
|
||||||
|
'rep_days' => 'Tage',
|
||||||
|
'rep_csv_daily' => 'CSV (täglich)',
|
||||||
|
'rep_csv_site' => 'CSV (pro Site)',
|
||||||
|
'rep_csv_user' => 'CSV (pro Nutzer)',
|
||||||
|
'rep_print' => 'Drucken/PDF',
|
||||||
|
'rep_total' => 'Vouchers gesamt',
|
||||||
|
'rep_in_period' => 'In {days} Tagen erstellt',
|
||||||
|
'rep_chart_title' => 'Erstellte Voucher ({days} Tage)',
|
||||||
|
'rep_per_site' => 'Pro Site',
|
||||||
|
'rep_top_users' => 'Top-Nutzer',
|
||||||
|
'rep_col_created' => 'Voucher erstellt',
|
||||||
|
'rep_no_data' => 'Keine Daten',
|
||||||
|
'label_total' => 'Gesamt',
|
||||||
|
'label_user' => 'Benutzer',
|
||||||
|
'js_confirm_delete_voucher' => 'Voucher wirklich löschen?',
|
||||||
|
'js_confirm_delete_user' => 'Benutzer wirklich löschen?',
|
||||||
|
'js_confirm_delete_site' => 'Möchten Sie diese Site wirklich löschen?',
|
||||||
|
'js_confirm_delete_template' => 'Profil wirklich löschen?',
|
||||||
|
'js_confirm_delete_token' => 'Token wirklich löschen?',
|
||||||
|
'js_error' => 'Fehler',
|
||||||
|
'js_enter_email' => 'Bitte E-Mail eingeben.',
|
||||||
|
'js_running' => 'Läuft...',
|
||||||
|
'js_run_now' => 'Jetzt ausführen',
|
||||||
|
'js_copied' => 'Kopiert!',
|
||||||
|
'js_code_copied' => 'Code kopiert!',
|
||||||
|
'js_click_to_copy' => 'Klicken zum Kopieren',
|
||||||
|
'settings_defaults_hint' => 'Diese Werte werden als Vorgabe im Voucher-Formular verwendet.',
|
||||||
|
'settings_cron_what' => 'Was macht der Cron-Job?',
|
||||||
|
'settings_smtp_from_name' => 'Absender Name',
|
||||||
|
'settings_tpl_voucher_mail' => 'Voucher E-Mail',
|
||||||
|
'settings_tpl_user_notify' => 'Benutzer-Benachrichtigung',
|
||||||
|
'settings_editor_hint' => 'Der Editor (TinyMCE, GPL-Variante) wird lokal aus',
|
||||||
|
'settings_editor_hint2' => 'geladen – es werden keine externen Dienste aufgerufen.',
|
||||||
|
'settings_print_template' => 'HTML Template für Voucher-Druck',
|
||||||
|
'settings_leave_empty' => 'Leer = nicht ändern',
|
||||||
|
'sites_id_hint' => 'Zu finden in der UniFi Controller URL',
|
||||||
|
'sites_pw_unchanged' => 'Leer lassen = nicht ändern',
|
||||||
|
'templates_minutes_hint' => '480 = 8 Stunden',
|
||||||
|
'templates_desc_placeholder' => 'Kurze Beschreibung für Ihr Team',
|
||||||
|
'audit_all_users' => 'Alle Benutzer',
|
||||||
|
'label_expires_at' => 'Gültig bis:',
|
||||||
|
'label_site_colon' => 'Standort:',
|
||||||
|
'audit_action_voucher_created' => 'Voucher erstellt',
|
||||||
|
'audit_action_voucher_bulk' => 'Bulk Voucher',
|
||||||
|
'audit_action_user_login' => 'Login',
|
||||||
|
'audit_action_user_logout' => 'Logout',
|
||||||
|
'audit_action_user_created' => 'Benutzer erstellt',
|
||||||
|
'audit_action_user_updated' => 'Benutzer geändert',
|
||||||
|
'audit_action_user_deleted' => 'Benutzer gelöscht',
|
||||||
|
'audit_action_site_added' => 'Site hinzugefügt',
|
||||||
|
'audit_action_site_updated' => 'Site geändert',
|
||||||
|
'audit_action_site_deleted' => 'Site gelöscht',
|
||||||
|
'audit_action_settings_saved' => 'Einstellungen gespeichert',
|
||||||
|
'audit_action_password_reset' => 'Passwort-Reset',
|
||||||
|
'audit_action_template_created' => 'Profil erstellt',
|
||||||
|
'audit_action_template_updated' => 'Profil geändert',
|
||||||
|
'audit_action_template_deleted' => 'Profil gelöscht',
|
||||||
|
'audit_page_info' => 'Seite {page} von {pages} ({total} Einträge)',
|
||||||
'settings_tab_general' => 'Allgemein',
|
'settings_tab_general' => 'Allgemein',
|
||||||
'settings_tab_defaults' => 'Voucher-Standards',
|
'settings_tab_defaults' => 'Voucher-Standards',
|
||||||
'settings_tab_cron' => 'Cron-Sync',
|
'settings_tab_cron' => 'Cron-Sync',
|
||||||
|
|
|
||||||
194
lang/en.php
194
lang/en.php
|
|
@ -261,6 +261,200 @@ return [
|
||||||
'settings_login_overlay' => 'Image dimming (%)',
|
'settings_login_overlay' => 'Image dimming (%)',
|
||||||
'settings_login_overlay_hint' => '0–90%. Higher values keep text readable on bright images.',
|
'settings_login_overlay_hint' => '0–90%. Higher values keep text readable on bright images.',
|
||||||
'settings_login_preview' => 'Open preview',
|
'settings_login_preview' => 'Open preview',
|
||||||
|
'api_title' => 'API keys',
|
||||||
|
'api_subtitle' => 'Access for external systems – with scope and rate limit.',
|
||||||
|
'api_new_key' => 'New key',
|
||||||
|
'api_new_key_hint' => 'Copy it now – for security reasons it will not be shown again.',
|
||||||
|
'api_create_title' => 'Create a new API key',
|
||||||
|
'api_label_name' => 'Label',
|
||||||
|
'api_name_placeholder' => 'e.g. booking system, lobby terminal',
|
||||||
|
'api_label_scope' => 'Permission',
|
||||||
|
'api_scope_write' => 'Read + create',
|
||||||
|
'api_scope_read' => 'Read only',
|
||||||
|
'api_scope_write_short' => 'Read+create',
|
||||||
|
'api_scope_read_short' => 'read only',
|
||||||
|
'api_label_limit' => 'Limit (req./min)',
|
||||||
|
'api_limit_title' => '0 = unlimited',
|
||||||
|
'api_existing' => 'Existing keys',
|
||||||
|
'api_none' => 'No API keys created yet.',
|
||||||
|
'api_col_prefix' => 'Prefix',
|
||||||
|
'api_col_scope' => 'Scope',
|
||||||
|
'api_col_limit' => 'Limit',
|
||||||
|
'api_col_last_used' => 'Last used',
|
||||||
|
'api_col_created_by' => 'Created by',
|
||||||
|
'api_state_active' => 'active',
|
||||||
|
'api_state_blocked' => 'blocked',
|
||||||
|
'api_action_block' => 'Block',
|
||||||
|
'api_action_unblock' => 'Activate',
|
||||||
|
'api_delete_confirm' => 'Delete key?',
|
||||||
|
'api_usage' => 'Usage',
|
||||||
|
'api_usage_hint' => 'Authenticate using header',
|
||||||
|
'api_openapi' => 'OpenAPI specification (import into Postman/Swagger):',
|
||||||
|
'api_created' => 'API key created.',
|
||||||
|
'api_deleted' => 'API key deleted.',
|
||||||
|
'api_toggled' => 'Status changed.',
|
||||||
|
'api_name_required' => 'Please enter a label.',
|
||||||
|
'api_created_once' => 'API key created. Copy it NOW – it is shown only once!',
|
||||||
|
'api_status_updated' => 'Status updated.',
|
||||||
|
'int_title' => 'Integration & maintenance',
|
||||||
|
'int_subtitle' => 'SSO, webhooks, SMS delivery and retention periods.',
|
||||||
|
'int_security' => 'Security policy',
|
||||||
|
'int_security_hint' => 'Enforces two-factor authentication for all administrator accounts (local accounts). Admins without 2FA are sent to the setup on their next action.',
|
||||||
|
'int_enforce_2fa' => 'Two-factor authentication mandatory for administrators',
|
||||||
|
'int_daily_limit' => 'Daily voucher limit per non-admin user (0 = unlimited)',
|
||||||
|
'int_session_driver' => 'Session storage',
|
||||||
|
'int_session_php' => 'PHP default (files)',
|
||||||
|
'int_session_db' => 'Database (enables "sign out everywhere")',
|
||||||
|
'int_captcha' => 'Captcha in public mode',
|
||||||
|
'int_captcha_off' => 'Off',
|
||||||
|
'int_captcha_math' => 'Arithmetic question (no external service)',
|
||||||
|
'int_secret_set' => ' (set)',
|
||||||
|
'int_secret_placeholder' => '••••••• (empty = unchanged)',
|
||||||
|
'int_proxy' => 'Reverse proxy',
|
||||||
|
'int_proxy_hint' => 'IP addresses of trusted proxies (comma separated). Only then is the real client IP taken from',
|
||||||
|
'int_proxy_hint2' => 'for rate limiting and the audit log.',
|
||||||
|
'int_webhook' => 'Webhook notifications',
|
||||||
|
'int_webhook_hint' => 'Slack, Microsoft Teams or generic JSON webhook URL. Triggered when a voucher is created.',
|
||||||
|
'int_webhook_active' => 'Webhook active',
|
||||||
|
'int_webhook_url' => 'Webhook URL',
|
||||||
|
'int_webhook_test' => 'Send test',
|
||||||
|
'int_sms' => 'SMS delivery (Twilio)',
|
||||||
|
'int_sms_hint' => 'Optionally send voucher codes by SMS. Requires a Twilio account.',
|
||||||
|
'int_sms_active' => 'SMS delivery active',
|
||||||
|
'int_sms_from' => 'Sender (from)',
|
||||||
|
'int_sso' => 'Single sign-on (OpenID Connect)',
|
||||||
|
'int_sso_hint' => 'Generic OIDC provider (e.g. Keycloak, Authentik, Google, Auth0). Redirect URI:',
|
||||||
|
'int_sso_active' => 'OIDC login active',
|
||||||
|
'int_sso_button' => 'Button label',
|
||||||
|
'int_cleanup' => 'Data retention & cleanup (GDPR)',
|
||||||
|
'int_cleanup_hint' => 'Retention periods in days (0 = disabled). Executed via',
|
||||||
|
'int_cleanup_hint2' => '(daily recommended).',
|
||||||
|
'int_cleanup_last' => 'Last run:',
|
||||||
|
'int_cleanup_expired' => 'Expired vouchers',
|
||||||
|
'int_cleanup_audit' => 'Audit log',
|
||||||
|
'int_cleanup_logins' => 'Login attempts',
|
||||||
|
'int_saved' => 'Settings saved.',
|
||||||
|
'int_webhook_sent' => 'Test webhook sent.',
|
||||||
|
'int_webhook_failed' => 'Test webhook failed.',
|
||||||
|
'int_webhook_test_sent' => 'Test notification sent (provided the webhook is active and the URL valid).',
|
||||||
|
'sec_token_invalid' => 'Invalid security token',
|
||||||
|
'sec_setup_expired' => 'Setup expired, please start again.',
|
||||||
|
'sec_code_invalid' => 'Invalid code. Please try again.',
|
||||||
|
'sec_enabled' => 'Two-factor authentication is now active. Please store the recovery codes safely!',
|
||||||
|
'sec_sessions_closed' => 'All other sessions have been signed out.',
|
||||||
|
'sec_codes_new' => 'New recovery codes generated. The previous ones are now invalid.',
|
||||||
|
'sec_disabled' => 'Two-factor authentication has been disabled.',
|
||||||
|
'backup_choose_file' => 'Please choose a backup file.',
|
||||||
|
'backup_invalid_file' => 'Invalid or foreign backup file.',
|
||||||
|
'sec_title' => 'Two-factor authentication',
|
||||||
|
'sec_account' => 'Account:',
|
||||||
|
'sec_required_hint' => 'For security reasons two-factor authentication is mandatory for administrators. Please set it up now.',
|
||||||
|
'sec_recovery_codes' => 'Recovery codes',
|
||||||
|
'sec_recovery_hint' => 'Keep them somewhere safe. Each code works once if you lose access to your app.',
|
||||||
|
'sec_unavailable' => 'Not available',
|
||||||
|
'sec_m365_hint' => 'Your account signs in via Microsoft 365. Two-factor authentication is managed in your Microsoft account.',
|
||||||
|
'sec_active' => 'Active',
|
||||||
|
'sec_inactive' => 'Inactive',
|
||||||
|
'sec_active_hint' => 'Every sign-in additionally asks for a code from your authenticator app.',
|
||||||
|
'sec_codes_left' => 'Remaining recovery codes:',
|
||||||
|
'sec_regen_codes' => 'Generate new recovery codes',
|
||||||
|
'sec_disable_confirm' => 'Really disable two-factor authentication?',
|
||||||
|
'sec_disable' => 'Disable two-factor authentication',
|
||||||
|
'sec_step_1' => 'Open your authenticator app (Google Authenticator, Authy, Microsoft Authenticator …)',
|
||||||
|
'sec_step_2' => 'Scan the QR code or enter the secret manually',
|
||||||
|
'sec_step_3' => 'Enter the six-digit code shown below',
|
||||||
|
'sec_code_label' => 'Six-digit code',
|
||||||
|
'sec_enable' => 'Enable two-factor authentication',
|
||||||
|
'sec_sessions' => 'Active sessions:',
|
||||||
|
'sec_logout_others_confirm' => 'Sign out all other sessions?',
|
||||||
|
'sec_logout_others' => 'Sign out on all other devices',
|
||||||
|
'import_title' => 'Voucher import',
|
||||||
|
'import_subtitle' => 'Create several vouchers at once from a CSV list.',
|
||||||
|
'import_card_title' => 'Create several vouchers',
|
||||||
|
'import_format_hint' => 'One line per voucher:',
|
||||||
|
'import_format_hint2' => '– max. devices and minutes are optional (defaults apply). 200 lines maximum. Example:',
|
||||||
|
'import_site' => 'Site',
|
||||||
|
'import_file' => 'CSV file (optional)',
|
||||||
|
'import_paste' => '… or paste directly',
|
||||||
|
'import_confirm' => 'Start the import now?',
|
||||||
|
'import_submit' => 'Import',
|
||||||
|
'import_result' => 'Result',
|
||||||
|
'import_col_code' => 'Code / error',
|
||||||
|
'import_created' => '{count} vouchers created.',
|
||||||
|
'backup_title' => 'Backup & restore',
|
||||||
|
'backup_subtitle' => 'Back up or restore configuration and data.',
|
||||||
|
'backup_export' => 'Export',
|
||||||
|
'backup_export_hint' => 'Downloads settings, sites and voucher profiles as JSON. Site passwords stay encrypted with the',
|
||||||
|
'backup_export_hint2' => 'of this installation – a restore on an installation with a different APP_KEY cannot decrypt them.',
|
||||||
|
'backup_export_btn' => 'Export configuration',
|
||||||
|
'backup_import' => 'Import / restore',
|
||||||
|
'backup_import_hint' => 'Existing sites are matched by name + site ID and updated, new ones are added. Profiles are only created if the name does not exist yet. The cron token is never overwritten.',
|
||||||
|
'backup_opt_settings' => 'Settings',
|
||||||
|
'backup_opt_sites' => 'Sites',
|
||||||
|
'backup_opt_templates' => 'Voucher profiles',
|
||||||
|
'backup_import_confirm' => 'Run the import now?',
|
||||||
|
'backup_imported' => 'Import finished: {settings} settings, {sites} sites, {templates} profiles.',
|
||||||
|
'rep_title' => 'Reporting',
|
||||||
|
'rep_subtitle' => 'Analytics by period, site and user.',
|
||||||
|
'rep_period' => 'Period',
|
||||||
|
'rep_days' => 'days',
|
||||||
|
'rep_csv_daily' => 'CSV (daily)',
|
||||||
|
'rep_csv_site' => 'CSV (per site)',
|
||||||
|
'rep_csv_user' => 'CSV (per user)',
|
||||||
|
'rep_print' => 'Print / PDF',
|
||||||
|
'rep_total' => 'Vouchers total',
|
||||||
|
'rep_in_period' => 'Created in {days} days',
|
||||||
|
'rep_chart_title' => 'Vouchers created ({days} days)',
|
||||||
|
'rep_per_site' => 'Per site',
|
||||||
|
'rep_top_users' => 'Top users',
|
||||||
|
'rep_col_created' => 'Vouchers created',
|
||||||
|
'rep_no_data' => 'No data',
|
||||||
|
'label_total' => 'Total',
|
||||||
|
'label_user' => 'User',
|
||||||
|
'js_confirm_delete_voucher' => 'Really delete this voucher?',
|
||||||
|
'js_confirm_delete_user' => 'Really delete this user?',
|
||||||
|
'js_confirm_delete_site' => 'Do you really want to delete this site?',
|
||||||
|
'js_confirm_delete_template' => 'Really delete this profile?',
|
||||||
|
'js_confirm_delete_token' => 'Really delete this token?',
|
||||||
|
'js_error' => 'Error',
|
||||||
|
'js_enter_email' => 'Please enter an email address.',
|
||||||
|
'js_running' => 'Running…',
|
||||||
|
'js_run_now' => 'Run now',
|
||||||
|
'js_copied' => 'Copied!',
|
||||||
|
'js_code_copied' => 'Code copied!',
|
||||||
|
'js_click_to_copy' => 'Click to copy',
|
||||||
|
'settings_defaults_hint' => 'These values are used as defaults in the voucher form.',
|
||||||
|
'settings_cron_what' => 'What does the cron job do?',
|
||||||
|
'settings_smtp_from_name' => 'Sender name',
|
||||||
|
'settings_tpl_voucher_mail' => 'Voucher email',
|
||||||
|
'settings_tpl_user_notify' => 'User notification',
|
||||||
|
'settings_editor_hint' => 'The editor (TinyMCE, GPL build) is served locally from',
|
||||||
|
'settings_editor_hint2' => '– no external services are called.',
|
||||||
|
'settings_print_template' => 'HTML template for voucher printing',
|
||||||
|
'settings_leave_empty' => 'Empty = leave unchanged',
|
||||||
|
'sites_id_hint' => 'Found in the UniFi controller URL',
|
||||||
|
'sites_pw_unchanged' => 'Leave empty = unchanged',
|
||||||
|
'templates_minutes_hint' => '480 = 8 hours',
|
||||||
|
'templates_desc_placeholder' => 'Short description for your team',
|
||||||
|
'audit_all_users' => 'All users',
|
||||||
|
'label_expires_at' => 'Valid until:',
|
||||||
|
'label_site_colon' => 'Site:',
|
||||||
|
'audit_action_voucher_created' => 'Voucher created',
|
||||||
|
'audit_action_voucher_bulk' => 'Bulk vouchers',
|
||||||
|
'audit_action_user_login' => 'Login',
|
||||||
|
'audit_action_user_logout' => 'Logout',
|
||||||
|
'audit_action_user_created' => 'User created',
|
||||||
|
'audit_action_user_updated' => 'User updated',
|
||||||
|
'audit_action_user_deleted' => 'User deleted',
|
||||||
|
'audit_action_site_added' => 'Site added',
|
||||||
|
'audit_action_site_updated' => 'Site updated',
|
||||||
|
'audit_action_site_deleted' => 'Site deleted',
|
||||||
|
'audit_action_settings_saved' => 'Settings saved',
|
||||||
|
'audit_action_password_reset' => 'Password reset',
|
||||||
|
'audit_action_template_created' => 'Profile created',
|
||||||
|
'audit_action_template_updated' => 'Profile updated',
|
||||||
|
'audit_action_template_deleted' => 'Profile deleted',
|
||||||
|
'audit_page_info' => 'Page {page} of {pages} ({total} entries)',
|
||||||
'settings_tab_general' => 'General',
|
'settings_tab_general' => 'General',
|
||||||
'settings_tab_defaults' => 'Voucher Defaults',
|
'settings_tab_defaults' => 'Voucher Defaults',
|
||||||
'settings_tab_cron' => 'Cron Sync',
|
'settings_tab_cron' => 'Cron Sync',
|
||||||
|
|
|
||||||
|
|
@ -88,11 +88,11 @@ class Database {
|
||||||
}
|
}
|
||||||
if (str_contains($s, 'count(v.id) as voucher_count') || str_contains($s, 'from users u left join vouchers')) {
|
if (str_contains($s, 'count(v.id) as voucher_count') || str_contains($s, 'from users u left join vouchers')) {
|
||||||
return [
|
return [
|
||||||
['name'=>'Marie Sander','email'=>'marie.sander@example.com','voucher_count'=>48],
|
['name'=>'Marie Sander','email'=>'marie.sander@example.com','voucher_count'=>48,'c'=>48],
|
||||||
['name'=>'Jonas Weber','email'=>'j.weber@example.com','voucher_count'=>31],
|
['name'=>'Jonas Weber','email'=>'j.weber@example.com','voucher_count'=>31,'c'=>31],
|
||||||
['name'=>'Alina Brandt','email'=>'alina.brandt@example.com','voucher_count'=>27],
|
['name'=>'Alina Brandt','email'=>'alina.brandt@example.com','voucher_count'=>27,'c'=>27],
|
||||||
['name'=>'Tim Kurz','email'=>'tim.kurz@example.com','voucher_count'=>19],
|
['name'=>'Tim Kurz','email'=>'tim.kurz@example.com','voucher_count'=>19,'c'=>19],
|
||||||
['name'=>'Sara Vogt','email'=>'sara.vogt@example.com','voucher_count'=>12],
|
['name'=>'Sara Vogt','email'=>'sara.vogt@example.com','voucher_count'=>12,'c'=>12],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (str_contains($s, 'from vouchers')) {
|
if (str_contains($s, 'from vouchers')) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue