feat: comprehensive UI/UX and feature improvements
- Dark mode: CSS custom properties (global.css) + toggle button, persisted in localStorage - i18n: German/English language switcher (lang/de.php, lang/en.php, includes/I18n.php) - Mobile-responsive admin layout: hamburger menu, sidebar overlay (global.js + global.css) - Shared admin navigation include (includes/admin_nav.php) used across all admin pages - Toast notifications system globally available via global.js - Voucher templates/profiles: CRUD UI at admin/templates.php with voucher_templates DB table - Bulk voucher creation: create 1-20 vouchers at once with multi-print layout on index.php - Configurable voucher defaults: expire time, device limit, max limit in admin settings - Template quick-select on voucher form: auto-fills max_uses and expire_minutes - Password reset flow: forgot_password.php + reset_password.php with token-based reset - Audit log UI: admin/audit_log.php with filter, pagination, audit_log DB table - Audit logging on login, user create/edit/delete, site create/edit/delete - Admin pages updated: index, vouchers, users, sites all use admin_nav.php + dark mode + i18n - Voucher admin: live search input added alongside existing status filter + pagination - Users admin: password-reset-link button per user row (when SMTP enabled) - Login page: i18n, dark mode, language switcher, forgot password link https://claude.ai/code/session_01YN6Bcm1VSi8mpDeyKpyrdJ
This commit is contained in:
parent
bf3e55a967
commit
a1021a0f84
20 changed files with 5281 additions and 5471 deletions
226
admin/audit_log.php
Normal file
226
admin/audit_log.php
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
<?php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
require_once __DIR__ . '/../includes/Database.php';
|
||||
require_once __DIR__ . '/../includes/Auth.php';
|
||||
require_once __DIR__ . '/../includes/I18n.php';
|
||||
|
||||
$auth = new Auth();
|
||||
$auth->requireAdmin();
|
||||
I18n::init();
|
||||
|
||||
$db = Database::getInstance();
|
||||
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
|
||||
|
||||
$filterAction = trim($_GET['action'] ?? '');
|
||||
$filterUser = trim($_GET['user_id'] ?? '');
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
$perPage = 50;
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
if ($filterAction !== '') { $where[] = 'a.action = ?'; $params[] = $filterAction; }
|
||||
if ($filterUser !== '') { $where[] = 'a.user_id = ?'; $params[] = (int)$filterUser; }
|
||||
$whereStr = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
|
||||
$total = (int)$db->fetchOne("SELECT COUNT(*) as c FROM audit_log a $whereStr", $params)['c'];
|
||||
$pages = max(1, (int)ceil($total / $perPage));
|
||||
$logs = $db->fetchAll(
|
||||
"SELECT a.*, u.name as user_name, u.email as user_email
|
||||
FROM audit_log a LEFT JOIN users u ON a.user_id = u.id
|
||||
$whereStr ORDER BY a.created_at DESC LIMIT ? OFFSET ?",
|
||||
array_merge($params, [$perPage, $offset])
|
||||
);
|
||||
|
||||
// Distinct actions for filter
|
||||
$actions = $db->fetchAll("SELECT DISTINCT action FROM audit_log ORDER BY action");
|
||||
|
||||
// User list for filter
|
||||
$users = $db->fetchAll("SELECT id, name FROM users WHERE is_active = 1 ORDER BY name");
|
||||
|
||||
$currentPage = 'audit_log';
|
||||
$adminBase = '';
|
||||
|
||||
$actionLabels = [
|
||||
'voucher_created' => '🎫 Voucher erstellt',
|
||||
'voucher_bulk' => '🎫 Bulk Voucher',
|
||||
'user_login' => '🔐 Login',
|
||||
'user_logout' => '🚪 Logout',
|
||||
'user_created' => '👤 Benutzer erstellt',
|
||||
'user_updated' => '👤 Benutzer geändert',
|
||||
'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>
|
||||
<html lang="<?= I18n::getLanguage() ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= __('audit_title') ?> - <?= htmlspecialchars($appTitle) ?></title>
|
||||
<?php include __DIR__ . '/../includes/admin_nav.php'; ?>
|
||||
<style>
|
||||
.page-header { margin-bottom: 25px; }
|
||||
.page-title { font-size: 28px; font-weight: 600; color: var(--text-primary); margin-bottom: 6px; }
|
||||
.card { background: var(--bg-card); border-radius: 15px; box-shadow: 0 2px 10px var(--shadow); border: 1px solid var(--border-color); overflow: hidden; margin-bottom: 20px; }
|
||||
.card-header { padding: 18px 25px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; }
|
||||
.card-title { font-size: 16px; font-weight: 600; color: var(--text-primary); }
|
||||
.table { width: 100%; border-collapse: collapse; }
|
||||
.table th { text-align: left; padding: 11px 15px; background: var(--bg-table-head); color: var(--text-secondary); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.table td { padding: 12px 15px; border-bottom: 1px solid var(--border-color); font-size: 13px; color: var(--text-primary); }
|
||||
.table tr:last-child td { border-bottom: none; }
|
||||
.table tr:hover td { background: var(--bg-hover); }
|
||||
.badge { display: inline-block; padding: 3px 9px; border-radius: 5px; font-size: 11px; font-weight: 500; }
|
||||
.filter-bar { display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-end; }
|
||||
.filter-bar select { padding: 9px 12px; border: 2px solid var(--border-color); border-radius: 8px; font-size: 13px; background: var(--bg-input); color: var(--text-primary); }
|
||||
.filter-bar select:focus { outline: none; border-color: var(--accent); }
|
||||
.btn-primary { background: var(--accent); color: white; }
|
||||
.btn-primary:hover { background: var(--accent-hover); }
|
||||
.btn-small { padding: 6px 12px; font-size: 12px; }
|
||||
.ip-cell { font-family: monospace; font-size: 12px; color: var(--text-muted); }
|
||||
.details-cell { max-width: 250px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-secondary); }
|
||||
.pagination { display: flex; align-items: center; justify-content: space-between; padding: 14px 25px; border-top: 1px solid var(--border-color); flex-wrap: wrap; gap: 10px; }
|
||||
.page-info { font-size: 13px; color: var(--text-muted); }
|
||||
.page-btns { display: flex; gap: 5px; flex-wrap: wrap; }
|
||||
.page-btn { padding: 6px 12px; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-card); color: var(--text-secondary); cursor: pointer; font-size: 13px; text-decoration: none; transition: all 0.2s; }
|
||||
.page-btn:hover { border-color: var(--accent); color: var(--accent); }
|
||||
.page-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
|
||||
.page-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.empty-state { text-align: center; padding: 60px 20px; color: var(--text-muted); }
|
||||
.empty-state i { font-size: 42px; margin-bottom: 15px; display: block; opacity: 0.3; }
|
||||
.action-chip { display: inline-flex; align-items: center; gap: 5px; padding: 3px 10px; border-radius: 20px; font-size: 12px; font-weight: 500; background: var(--bg-hover); color: var(--text-secondary); }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title"><?= __('audit_title') ?></h1>
|
||||
<p style="color: var(--text-muted); font-size: 14px;"><?= __('audit_subtitle') ?></p>
|
||||
</div>
|
||||
|
||||
<!-- Filter -->
|
||||
<div class="card" style="margin-bottom: 20px;">
|
||||
<div class="card-header"><span class="card-title"><i class="fas fa-filter"></i> <?= __('audit_filter') ?></span></div>
|
||||
<div style="padding: 20px 25px;">
|
||||
<form method="get" class="filter-bar">
|
||||
<div>
|
||||
<label style="display:block;font-size:12px;color:var(--text-muted);margin-bottom:5px;"><?= __('audit_action') ?></label>
|
||||
<select name="action">
|
||||
<option value=""><?= __('audit_filter_all') ?></option>
|
||||
<?php foreach ($actions as $a): ?>
|
||||
<option value="<?= htmlspecialchars($a['action']) ?>" <?= $filterAction === $a['action'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($actionLabels[$a['action']] ?? $a['action']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;font-size:12px;color:var(--text-muted);margin-bottom:5px;"><?= __('audit_user') ?></label>
|
||||
<select name="user_id">
|
||||
<option value="">Alle Benutzer</option>
|
||||
<?php foreach ($users as $u): ?>
|
||||
<option value="<?= $u['id'] ?>" <?= (string)$filterUser === (string)$u['id'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($u['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-small"><i class="fas fa-search"></i> Filtern</button>
|
||||
<a href="audit_log.php" class="btn btn-secondary btn-small"><i class="fas fa-times"></i> Zurücksetzen</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Log-Tabelle -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title"><i class="fas fa-history"></i> <?= __('audit_title') ?></span>
|
||||
<span style="font-size:13px;color:var(--text-muted);"><?= number_format($total) ?> Einträge</span>
|
||||
</div>
|
||||
<?php if (empty($logs)): ?>
|
||||
<div class="empty-state"><i class="fas fa-history"></i><p><?= __('audit_none') ?></p></div>
|
||||
<?php else: ?>
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= __('audit_time') ?></th>
|
||||
<th><?= __('audit_action') ?></th>
|
||||
<th><?= __('audit_user') ?></th>
|
||||
<th><?= __('audit_entity') ?></th>
|
||||
<th><?= __('audit_details') ?></th>
|
||||
<th><?= __('audit_ip') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($logs as $log): ?>
|
||||
<tr>
|
||||
<td style="white-space:nowrap;color:var(--text-muted);">
|
||||
<?= date('d.m.Y', strtotime($log['created_at'])) ?><br>
|
||||
<small><?= date('H:i:s', strtotime($log['created_at'])) ?></small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="action-chip">
|
||||
<?= htmlspecialchars($actionLabels[$log['action']] ?? $log['action']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($log['user_name']): ?>
|
||||
<strong style="font-size:13px;"><?= htmlspecialchars($log['user_name']) ?></strong><br>
|
||||
<small style="color:var(--text-muted);"><?= htmlspecialchars($log['user_email'] ?? '') ?></small>
|
||||
<?php else: ?>
|
||||
<em style="color:var(--text-muted);">System/Anonym</em>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td style="color:var(--text-secondary);">
|
||||
<?php if ($log['entity_type']): ?>
|
||||
<code style="font-size:11px;"><?= htmlspecialchars($log['entity_type']) ?>:<?= htmlspecialchars($log['entity_id'] ?? '') ?></code>
|
||||
<?php else: ?>
|
||||
<span style="color:var(--text-muted);">-</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="details-cell" title="<?= htmlspecialchars($log['details'] ?? '') ?>">
|
||||
<?= htmlspecialchars(mb_strimwidth($log['details'] ?? '-', 0, 80, '…')) ?>
|
||||
</td>
|
||||
<td class="ip-cell"><?= htmlspecialchars($log['ip_address'] ?? '-') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php if ($pages > 1): ?>
|
||||
<div class="pagination">
|
||||
<span class="page-info">Seite <?= $page ?> von <?= $pages ?> (<?= $total ?> Einträge)</span>
|
||||
<div class="page-btns">
|
||||
<?php
|
||||
$baseUrl = '?' . http_build_query(array_filter(['action' => $filterAction, 'user_id' => $filterUser]));
|
||||
if ($page > 1): ?>
|
||||
<a href="<?= $baseUrl ?>&page=<?= $page - 1 ?>" class="page-btn"><i class="fas fa-chevron-left"></i></a>
|
||||
<?php endif;
|
||||
for ($p = max(1, $page - 2); $p <= min($pages, $page + 2); $p++): ?>
|
||||
<a href="<?= $baseUrl ?>&page=<?= $p ?>" class="page-btn <?= $p === $page ? 'active' : '' ?>"><?= $p ?></a>
|
||||
<?php endfor;
|
||||
if ($page < $pages): ?>
|
||||
<a href="<?= $baseUrl ?>&page=<?= $page + 1 ?>" class="page-btn"><i class="fas fa-chevron-right"></i></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
</div><!-- main-content -->
|
||||
<script src="../assets/global.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1262
admin/index.php
1262
admin/index.php
File diff suppressed because it is too large
Load diff
1056
admin/settings.php
1056
admin/settings.php
File diff suppressed because it is too large
Load diff
904
admin/sites.php
904
admin/sites.php
|
|
@ -6,684 +6,354 @@ require_once __DIR__ . '/../config.php';
|
|||
require_once __DIR__ . '/../includes/Database.php';
|
||||
require_once __DIR__ . '/../includes/Auth.php';
|
||||
require_once __DIR__ . '/../includes/UniFiController.php';
|
||||
require_once __DIR__ . '/../includes/I18n.php';
|
||||
|
||||
$auth = new Auth();
|
||||
$auth->requireAdmin();
|
||||
|
||||
$db = Database::getInstance();
|
||||
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
|
||||
I18n::init();
|
||||
|
||||
$error = '';
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
// Site bearbeiten
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['edit_site'])) {
|
||||
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
||||
$error = 'Ungültiges Sicherheits-Token';
|
||||
// Edit site
|
||||
if ($_SERVER['REQUEST_METHOD']==='POST' && isset($_POST['edit_site'])) {
|
||||
if (!$auth->validateCsrfToken($_POST['csrf_token']??'')) {
|
||||
$error = __('error_csrf');
|
||||
} else {
|
||||
try {
|
||||
$siteId = (int)$_POST['site_id'];
|
||||
$name = trim($_POST['name']);
|
||||
$siteIdStr = trim($_POST['site_id_str']);
|
||||
$siteId = (int)$_POST['site_id'];
|
||||
$name = trim($_POST['name']);
|
||||
$siteIdStr = trim($_POST['site_id_str']);
|
||||
$controllerUrl = trim($_POST['controller_url']);
|
||||
$username = trim($_POST['username']);
|
||||
$password = $_POST['password'];
|
||||
$publicAccess = isset($_POST['public_access']) ? 1 : 0;
|
||||
|
||||
if (empty($name) || empty($siteIdStr) || empty($controllerUrl) || empty($username)) {
|
||||
throw new Exception('Bitte füllen Sie alle Pflichtfelder aus');
|
||||
}
|
||||
|
||||
// Wenn neues Passwort, Verbindung testen
|
||||
$username = trim($_POST['username']);
|
||||
$password = $_POST['password'];
|
||||
$publicAccess = isset($_POST['public_access']) ? 1 : 0;
|
||||
if (empty($name)||empty($siteIdStr)||empty($controllerUrl)||empty($username)) throw new Exception(__('error_fill_all'));
|
||||
if (!empty($password)) {
|
||||
$testResult = UniFiController::testConnection($controllerUrl, $username, $password, $siteIdStr);
|
||||
if ($testResult !== true) {
|
||||
throw new Exception('Verbindung fehlgeschlagen: ' . $testResult);
|
||||
}
|
||||
|
||||
// Mit neuem Passwort aktualisieren
|
||||
$db->execute(
|
||||
"UPDATE sites SET name = ?, site_id = ?, unifi_controller_url = ?, unifi_username = ?, unifi_password = ?, public_access = ? WHERE id = ?",
|
||||
[$name, $siteIdStr, $controllerUrl, $username, $password, $publicAccess, $siteId]
|
||||
);
|
||||
$test = UniFiController::testConnection($controllerUrl,$username,$password,$siteIdStr);
|
||||
if ($test !== true) throw new Exception('Verbindung fehlgeschlagen: '.$test);
|
||||
$db->execute("UPDATE sites SET name=?,site_id=?,unifi_controller_url=?,unifi_username=?,unifi_password=?,public_access=? WHERE id=?",
|
||||
[$name,$siteIdStr,$controllerUrl,$username,$password,$publicAccess,$siteId]);
|
||||
} else {
|
||||
// Ohne Passwort-Änderung
|
||||
$db->execute(
|
||||
"UPDATE sites SET name = ?, site_id = ?, unifi_controller_url = ?, unifi_username = ?, public_access = ? WHERE id = ?",
|
||||
[$name, $siteIdStr, $controllerUrl, $username, $publicAccess, $siteId]
|
||||
);
|
||||
$db->execute("UPDATE sites SET name=?,site_id=?,unifi_controller_url=?,unifi_username=?,public_access=? WHERE id=?",
|
||||
[$name,$siteIdStr,$controllerUrl,$username,$publicAccess,$siteId]);
|
||||
}
|
||||
|
||||
$success = 'Site erfolgreich aktualisiert!';
|
||||
|
||||
} catch (Exception $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
$auth->writeAuditLog($_SESSION['user_id'],'site_edit','site',$siteId,"Site {$name} aktualisiert");
|
||||
$success = __('sites_updated');
|
||||
} catch (Exception $e) { $error = $e->getMessage(); }
|
||||
}
|
||||
}
|
||||
|
||||
// Site hinzufügen
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_site'])) {
|
||||
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
||||
$error = 'Ungültiges Sicherheits-Token';
|
||||
// Add site
|
||||
if ($_SERVER['REQUEST_METHOD']==='POST' && isset($_POST['add_site'])) {
|
||||
if (!$auth->validateCsrfToken($_POST['csrf_token']??'')) {
|
||||
$error = __('error_csrf');
|
||||
} else {
|
||||
try {
|
||||
$name = trim($_POST['name']);
|
||||
$siteId = trim($_POST['site_id']);
|
||||
$name = trim($_POST['name']);
|
||||
$siteId = trim($_POST['site_id']);
|
||||
$controllerUrl = trim($_POST['controller_url']);
|
||||
$username = trim($_POST['username']);
|
||||
$password = $_POST['password'];
|
||||
$publicAccess = isset($_POST['public_access']) ? 1 : 0;
|
||||
|
||||
if (empty($name) || empty($siteId) || empty($controllerUrl) || empty($username)) {
|
||||
throw new Exception('Bitte füllen Sie alle Pflichtfelder aus');
|
||||
}
|
||||
|
||||
// Verbindung testen
|
||||
$testResult = UniFiController::testConnection($controllerUrl, $username, $password, $siteId);
|
||||
if ($testResult !== true) {
|
||||
throw new Exception('Verbindung fehlgeschlagen: ' . $testResult);
|
||||
}
|
||||
|
||||
$db->execute(
|
||||
"INSERT INTO sites (name, site_id, unifi_controller_url, unifi_username, unifi_password, public_access)
|
||||
VALUES (?, ?, ?, ?, ?, ?)",
|
||||
[$name, $siteId, $controllerUrl, $username, $password, $publicAccess]
|
||||
);
|
||||
|
||||
$success = 'Site erfolgreich hinzugefügt!';
|
||||
|
||||
} catch (Exception $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
$username = trim($_POST['username']);
|
||||
$password = $_POST['password'];
|
||||
$publicAccess = isset($_POST['public_access']) ? 1 : 0;
|
||||
if (empty($name)||empty($siteId)||empty($controllerUrl)||empty($username)) throw new Exception(__('error_fill_all'));
|
||||
$test = UniFiController::testConnection($controllerUrl,$username,$password,$siteId);
|
||||
if ($test !== true) throw new Exception('Verbindung fehlgeschlagen: '.$test);
|
||||
$newId = $db->execute("INSERT INTO sites (name,site_id,unifi_controller_url,unifi_username,unifi_password,public_access) VALUES (?,?,?,?,?,?)",
|
||||
[$name,$siteId,$controllerUrl,$username,$password,$publicAccess]);
|
||||
$auth->writeAuditLog($_SESSION['user_id'],'site_create','site',$newId,"Site {$name} erstellt");
|
||||
$success = __('sites_added');
|
||||
} catch (Exception $e) { $error = $e->getMessage(); }
|
||||
}
|
||||
}
|
||||
|
||||
// Site löschen
|
||||
// Delete site
|
||||
if (isset($_GET['delete']) && isset($_GET['token'])) {
|
||||
if ($auth->validateCsrfToken($_GET['token'])) {
|
||||
$db->query("DELETE FROM sites WHERE id = ?", [(int)$_GET['delete']]);
|
||||
$success = 'Site erfolgreich gelöscht!';
|
||||
} else {
|
||||
$error = 'Ungültiges Sicherheits-Token';
|
||||
}
|
||||
$delId = (int)$_GET['delete'];
|
||||
$db->query("DELETE FROM sites WHERE id=?", [$delId]);
|
||||
$auth->writeAuditLog($_SESSION['user_id'],'site_delete','site',$delId,'Site gelöscht');
|
||||
$success = __('sites_deleted');
|
||||
} else { $error = __('error_csrf'); }
|
||||
}
|
||||
|
||||
// Site aktivieren/deaktivieren
|
||||
// Toggle site
|
||||
if (isset($_GET['toggle']) && isset($_GET['token'])) {
|
||||
if ($auth->validateCsrfToken($_GET['token'])) {
|
||||
$site = $db->fetchOne("SELECT is_active FROM sites WHERE id = ?", [(int)$_GET['toggle']]);
|
||||
$site = $db->fetchOne("SELECT is_active FROM sites WHERE id=?", [(int)$_GET['toggle']]);
|
||||
if ($site) {
|
||||
$newStatus = $site['is_active'] ? 0 : 1;
|
||||
$db->query("UPDATE sites SET is_active = ? WHERE id = ?", [$newStatus, (int)$_GET['toggle']]);
|
||||
$db->query("UPDATE sites SET is_active=? WHERE id=?", [$site['is_active']?0:1,(int)$_GET['toggle']]);
|
||||
$success = 'Site-Status aktualisiert!';
|
||||
}
|
||||
}
|
||||
} else { $error = __('error_csrf'); }
|
||||
}
|
||||
|
||||
// Alle Sites abrufen
|
||||
$sites = $db->fetchAll("SELECT * FROM sites ORDER BY name");
|
||||
$currentUser = $auth->getCurrentUser();
|
||||
$currentPage = 'sites';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<html lang="<?= I18n::getLanguage() ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sites verwalten - <?= htmlspecialchars($appTitle) ?></title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
.header {
|
||||
background: white;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
padding: 0 30px;
|
||||
height: 70px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||||
}
|
||||
.header-title { font-size: 20px; font-weight: 600; color: #333; }
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 70px;
|
||||
bottom: 0;
|
||||
width: 260px;
|
||||
background: white;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
padding: 30px 0;
|
||||
}
|
||||
.sidebar-nav { list-style: none; }
|
||||
.sidebar-nav a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 30px;
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
font-size: 15px;
|
||||
}
|
||||
.sidebar-nav a:hover,
|
||||
.sidebar-nav a.active {
|
||||
background: #f8f9fa;
|
||||
color: #667eea;
|
||||
}
|
||||
.sidebar-nav i { width: 20px; text-align: center; }
|
||||
.main-content {
|
||||
margin-left: 260px;
|
||||
padding: 30px;
|
||||
min-height: calc(100vh - 70px);
|
||||
}
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.page-title { font-size: 28px; font-weight: 600; color: #333; }
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s;
|
||||
font-size: 14px;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
.btn-primary:hover { background: #5568d3; }
|
||||
.btn-secondary {
|
||||
background: #f8f9fa;
|
||||
color: #666;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
.btn-small {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||||
border: 1px solid #e0e0e0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.card-header {
|
||||
padding: 20px 25px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
.card-title { font-size: 18px; font-weight: 600; color: #333; }
|
||||
.card-body { padding: 25px; }
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-group { margin-bottom: 20px; }
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="url"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.checkbox-group input {
|
||||
width: auto;
|
||||
}
|
||||
.alert {
|
||||
padding: 14px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 25px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.alert-error {
|
||||
background: #fee;
|
||||
border: 1px solid #fcc;
|
||||
color: #c33;
|
||||
}
|
||||
.alert-success {
|
||||
background: #efe;
|
||||
border: 1px solid #cfc;
|
||||
color: #3c3;
|
||||
}
|
||||
.sites-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
.site-card {
|
||||
background: white;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.site-card:hover {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
|
||||
}
|
||||
.site-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.site-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.site-id {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
font-family: monospace;
|
||||
}
|
||||
.site-info {
|
||||
margin: 15px 0;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
.site-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.site-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.badge-success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.badge-warning {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
.badge-info {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.modal.active { display: flex; }
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.modal-header {
|
||||
padding: 25px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.modal-title { font-size: 20px; font-weight: 600; }
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
}
|
||||
.modal-body { padding: 25px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div class="header-title">
|
||||
<i class="fas fa-shield-alt"></i> Administration
|
||||
<title><?= __('sites_title') ?> – <?= htmlspecialchars($appTitle) ?></title>
|
||||
<?php include __DIR__ . '/../includes/admin_nav.php'; ?>
|
||||
<style>
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 28px; flex-wrap: wrap; gap: 12px; }
|
||||
.page-title { font-size: 26px; font-weight: 700; color: var(--text-primary); }
|
||||
.alert { padding: 13px 18px; border-radius: 10px; font-size: 14px; margin-bottom: 20px; }
|
||||
.alert-error { background: #fee; border: 1px solid #fcc; color: #c33; }
|
||||
.alert-success { background: #efe; border: 1px solid #cfc; color: #3c3; }
|
||||
.sites-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)); gap: 18px; }
|
||||
.site-card { background: var(--bg-card); border: 2px solid var(--border-color); border-radius: 14px; padding: 20px; transition: border-color .2s, box-shadow .2s; }
|
||||
.site-card:hover { border-color: var(--accent); box-shadow: 0 4px 14px rgba(102,126,234,.15); }
|
||||
.site-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 14px; }
|
||||
.site-name { font-size: 17px; font-weight: 600; color: var(--text-primary); margin-bottom: 4px; }
|
||||
.site-id-label { font-size: 12px; color: var(--text-muted); font-family: monospace; }
|
||||
.site-info { margin: 14px 0; font-size: 13px; color: var(--text-secondary); }
|
||||
.site-info-item { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; }
|
||||
.site-actions { display: flex; gap: 7px; margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border-color); flex-wrap: wrap; }
|
||||
.badge { display: inline-block; padding: 3px 9px; border-radius: 5px; font-size: 11px; font-weight: 500; margin: 2px; }
|
||||
.badge-success { background: #d4edda; color: #155724; }
|
||||
.badge-warning { background: #fff3cd; color: #856404; }
|
||||
.badge-info { background: var(--bg-badge-info); color: var(--text-badge-info); }
|
||||
.btn { padding: 8px 15px; border-radius: 8px; border: none; font-weight: 500; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; gap: 7px; transition: all .2s; font-size: 13px; }
|
||||
.btn-primary { background: var(--accent); color: white; }
|
||||
.btn-primary:hover { background: var(--accent-hover); }
|
||||
.btn-secondary { background: var(--bg-hover); color: var(--text-secondary); border: 1px solid var(--border-color); }
|
||||
.btn-secondary:hover { background: var(--border-color); }
|
||||
.btn-danger { background: var(--danger); color: white; }
|
||||
.btn-success { background: var(--success); color: white; }
|
||||
.btn-sm { padding: 6px 11px; font-size: 12px; }
|
||||
.modal { display: none; position: fixed; inset: 0; background: var(--modal-overlay); z-index: 1000; align-items: center; justify-content: center; }
|
||||
.modal.active { display: flex; }
|
||||
.modal-content { background: var(--bg-card); border-radius: 14px; max-width: 580px; width: 90%; max-height: 90vh; overflow-y: auto; border: 1px solid var(--border-color); }
|
||||
.modal-header { padding: 22px 25px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
|
||||
.modal-title { font-size: 19px; font-weight: 600; color: var(--text-primary); }
|
||||
.modal-close { background: none; border: none; font-size: 22px; cursor: pointer; color: var(--text-muted); }
|
||||
.modal-body { padding: 22px 25px; }
|
||||
.form-group { margin-bottom: 17px; }
|
||||
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||
label { display: block; margin-bottom: 7px; color: var(--text-secondary); font-weight: 500; font-size: 14px; }
|
||||
input[type="text"], input[type="password"], input[type="url"] { width: 100%; padding: 11px; border: 2px solid var(--border-color); border-radius: 8px; font-size: 14px; background: var(--bg-input); color: var(--text-primary); transition: border-color .2s; }
|
||||
input:focus { outline: none; border-color: var(--accent); }
|
||||
.checkbox-group { display: flex; align-items: center; gap: 10px; }
|
||||
.checkbox-group input { width: auto; }
|
||||
.empty-card { background: var(--bg-card); border-radius: 14px; border: 1px solid var(--border-color); padding: 60px 20px; text-align: center; color: var(--text-muted); }
|
||||
@media(max-width:768px){ .main-content{ margin-left:0!important; } .form-grid{ grid-template-columns:1fr; } }
|
||||
</style>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title"><?= __('sites_title') ?></h1>
|
||||
<button onclick="openModal()" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> <?= __('sites_add') ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> <?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= htmlspecialchars($success) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (empty($sites)): ?>
|
||||
<div class="empty-card">
|
||||
<i class="fas fa-map-marker-alt" style="font-size:48px;margin-bottom:20px;opacity:.3;display:block;"></i>
|
||||
<p><?= __('sites_none') ?></p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="sites-grid">
|
||||
<?php foreach ($sites as $site): ?>
|
||||
<div class="site-card">
|
||||
<div class="site-card-header">
|
||||
<div>
|
||||
<div class="site-name"><?= htmlspecialchars($site['name']) ?></div>
|
||||
<div class="site-id-label">ID: <?= htmlspecialchars($site['site_id']) ?></div>
|
||||
</div>
|
||||
<div>
|
||||
<?php if ($site['is_active']): ?>
|
||||
<span class="badge badge-success"><i class="fas fa-check"></i> <?= __('status_active') ?></span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-warning"><i class="fas fa-pause"></i> <?= __('status_inactive') ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($site['public_access']): ?>
|
||||
<span class="badge badge-info"><i class="fas fa-globe"></i> <?= __('status_public') ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<a href="../index.php" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Zurück
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
<ul>
|
||||
<li><a href="index.php"><i class="fas fa-home"></i> Dashboard</a></li>
|
||||
<li><a href="sites.php" class="active"><i class="fas fa-map-marker-alt"></i> Sites verwalten</a></li>
|
||||
<li><a href="users.php"><i class="fas fa-users"></i> Benutzer verwalten</a></li>
|
||||
<li><a href="vouchers.php"><i class="fas fa-ticket-alt"></i> Voucher-Historie</a></li>
|
||||
<li><a href="settings.php"><i class="fas fa-cog"></i> Einstellungen</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Sites verwalten</h1>
|
||||
<button onclick="openModal()" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Neue Site hinzufügen
|
||||
<div class="site-info">
|
||||
<div class="site-info-item">
|
||||
<i class="fas fa-server" style="color:var(--accent);width:16px;"></i>
|
||||
<span style="word-break:break-all;"><?= htmlspecialchars($site['unifi_controller_url']) ?></span>
|
||||
</div>
|
||||
<div class="site-info-item">
|
||||
<i class="fas fa-user" style="color:var(--accent);width:16px;"></i>
|
||||
<span><?= htmlspecialchars($site['unifi_username']) ?></span>
|
||||
</div>
|
||||
<div class="site-info-item">
|
||||
<i class="fas fa-clock" style="color:var(--text-muted);width:16px;"></i>
|
||||
<span style="color:var(--text-muted);"><?= date('d.m.Y', strtotime($site['created_at'])) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-actions">
|
||||
<button onclick="openEditModal(<?= $site['id'] ?>, '<?= htmlspecialchars($site['name'], ENT_QUOTES) ?>', '<?= htmlspecialchars($site['site_id'], ENT_QUOTES) ?>', '<?= htmlspecialchars($site['unifi_controller_url'], ENT_QUOTES) ?>', '<?= htmlspecialchars($site['unifi_username'], ENT_QUOTES) ?>', <?= $site['public_access'] ?>)"
|
||||
class="btn btn-secondary btn-sm">
|
||||
<i class="fas fa-edit"></i> <?= __('btn_edit') ?>
|
||||
</button>
|
||||
<a href="?toggle=<?= $site['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
|
||||
class="btn btn-secondary btn-sm">
|
||||
<i class="fas fa-<?= $site['is_active'] ? 'pause' : 'play' ?>"></i>
|
||||
<?= $site['is_active'] ? __('sites_deactivate') : __('sites_activate') ?>
|
||||
</a>
|
||||
<a href="?delete=<?= $site['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
|
||||
class="btn btn-danger btn-sm"
|
||||
onclick="return confirm('Möchten Sie diese Site wirklich löschen?')">
|
||||
<i class="fas fa-trash"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> <?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
</div><!-- /main-content -->
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= htmlspecialchars($success) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (empty($sites)): ?>
|
||||
<div class="card">
|
||||
<div class="card-body" style="text-align: center; padding: 60px 20px; color: #999;">
|
||||
<i class="fas fa-map-marker-alt" style="font-size: 48px; margin-bottom: 20px; opacity: 0.3;"></i>
|
||||
<p>Noch keine Sites konfiguriert.<br>Fügen Sie Ihre erste Site hinzu!</p>
|
||||
<!-- Add Site Modal -->
|
||||
<div id="addSiteModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title"><?= __('sites_add_title') ?></h2>
|
||||
<button class="modal-close" onclick="closeModal('addSiteModal')">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post" id="addSiteForm">
|
||||
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
|
||||
<input type="hidden" name="add_site" value="1">
|
||||
<div class="form-group">
|
||||
<label><?= __('sites_name') ?></label>
|
||||
<input type="text" name="name" required placeholder="z.B. Hauptgebäude">
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="sites-grid">
|
||||
<?php foreach ($sites as $site): ?>
|
||||
<div class="site-card">
|
||||
<div class="site-card-header">
|
||||
<div>
|
||||
<div class="site-name"><?= htmlspecialchars($site['name']) ?></div>
|
||||
<div class="site-id">ID: <?= htmlspecialchars($site['site_id']) ?></div>
|
||||
</div>
|
||||
<div>
|
||||
<?php if ($site['is_active']): ?>
|
||||
<span class="badge badge-success"><i class="fas fa-check"></i> Aktiv</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-warning"><i class="fas fa-pause"></i> Inaktiv</span>
|
||||
<?php endif; ?>
|
||||
<?php if ($site['public_access']): ?>
|
||||
<span class="badge badge-info"><i class="fas fa-globe"></i> Öffentlich</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label><?= __('sites_site_id') ?></label>
|
||||
<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>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label><?= __('sites_controller') ?></label>
|
||||
<input type="url" name="controller_url" required placeholder="https://unifi.example.com:11443">
|
||||
<small style="color:var(--text-muted);font-size:12px;">Vollständige URL inkl. Port</small>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label><?= __('sites_username') ?></label>
|
||||
<input type="text" name="username" required placeholder="admin">
|
||||
</div>
|
||||
|
||||
<div class="site-info">
|
||||
<div class="site-info-item">
|
||||
<i class="fas fa-server" style="color: #667eea;"></i>
|
||||
<span><?= htmlspecialchars($site['unifi_controller_url']) ?></span>
|
||||
</div>
|
||||
<div class="site-info-item">
|
||||
<i class="fas fa-user" style="color: #667eea;"></i>
|
||||
<span><?= htmlspecialchars($site['unifi_username']) ?></span>
|
||||
</div>
|
||||
<div class="site-info-item">
|
||||
<i class="fas fa-clock" style="color: #999;"></i>
|
||||
<span>Erstellt: <?= date('d.m.Y', strtotime($site['created_at'])) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="site-actions">
|
||||
<button onclick="openEditModal(<?= $site['id'] ?>, '<?= htmlspecialchars($site['name'], ENT_QUOTES) ?>', '<?= htmlspecialchars($site['site_id'], ENT_QUOTES) ?>', '<?= htmlspecialchars($site['unifi_controller_url'], ENT_QUOTES) ?>', '<?= htmlspecialchars($site['unifi_username'], ENT_QUOTES) ?>', <?= $site['public_access'] ?>)"
|
||||
class="btn btn-secondary btn-small">
|
||||
<i class="fas fa-edit"></i> Bearbeiten
|
||||
</button>
|
||||
<a href="?toggle=<?= $site['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
|
||||
class="btn btn-secondary btn-small">
|
||||
<i class="fas fa-<?= $site['is_active'] ? 'pause' : 'play' ?>"></i>
|
||||
<?= $site['is_active'] ? 'Deaktivieren' : 'Aktivieren' ?>
|
||||
</a>
|
||||
<a href="?delete=<?= $site['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
|
||||
class="btn btn-danger btn-small"
|
||||
onclick="return confirm('Möchten Sie diese Site wirklich löschen?')">
|
||||
<i class="fas fa-trash"></i> Löschen
|
||||
</a>
|
||||
<div class="form-group">
|
||||
<label><?= __('sites_password') ?></label>
|
||||
<input type="password" name="password" required>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Modal für neue Site -->
|
||||
<div id="addSiteModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Neue Site hinzufügen</h2>
|
||||
<button class="modal-close" onclick="closeModal('addSiteModal')">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post" id="addSiteForm">
|
||||
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
|
||||
<input type="hidden" name="add_site" value="1">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Site-Name *</label>
|
||||
<input type="text" id="name" name="name" required
|
||||
placeholder="z.B. Hauptgebäude">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="site_id">UniFi Site ID *</label>
|
||||
<input type="text" id="site_id" name="site_id" required
|
||||
placeholder="z.B. default">
|
||||
<small style="color: #999; font-size: 12px;">
|
||||
Zu finden in der UniFi Controller URL oder in den Site-Einstellungen
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="controller_url">Controller URL *</label>
|
||||
<input type="url" id="controller_url" name="controller_url" required
|
||||
placeholder="https://unifi.example.com:11443">
|
||||
<small style="color: #999; font-size: 12px;">
|
||||
Vollständige URL inklusive Port (meist 11443 für UniFi OS)
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="username">Benutzername *</label>
|
||||
<input type="text" id="username" name="username" required
|
||||
placeholder="admin">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Passwort *</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox-group">
|
||||
<input type="checkbox" id="public_access" name="public_access">
|
||||
<label for="public_access" style="margin: 0;">
|
||||
Öffentlicher Zugriff (ohne Login nutzbar)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 10px; margin-top: 25px;">
|
||||
<button type="submit" class="btn btn-primary" style="flex: 1;" id="addSiteSubmitBtn">
|
||||
<i class="fas fa-save"></i> Site hinzufügen
|
||||
</button>
|
||||
<button type="button" onclick="closeModal('addSiteModal')" class="btn btn-secondary">
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="form-group checkbox-group">
|
||||
<input type="checkbox" id="add_public" name="public_access">
|
||||
<label for="add_public" style="margin:0;"><?= __('sites_public') ?></label>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px;margin-top:20px;">
|
||||
<button type="submit" class="btn btn-primary" style="flex:1;" id="addSiteSubmitBtn">
|
||||
<i class="fas fa-save"></i> <?= __('sites_add') ?>
|
||||
</button>
|
||||
<button type="button" onclick="closeModal('addSiteModal')" class="btn btn-secondary"><?= __('btn_cancel') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal für Site bearbeiten -->
|
||||
<div id="editSiteModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Site bearbeiten</h2>
|
||||
<button class="modal-close" onclick="closeModal('editSiteModal')">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post" id="editSiteForm">
|
||||
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
|
||||
<input type="hidden" name="edit_site" value="1">
|
||||
<input type="hidden" name="site_id" id="edit_site_id">
|
||||
|
||||
<!-- Edit Site Modal -->
|
||||
<div id="editSiteModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title"><?= __('sites_edit_title') ?></h2>
|
||||
<button class="modal-close" onclick="closeModal('editSiteModal')">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post" id="editSiteForm">
|
||||
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
|
||||
<input type="hidden" name="edit_site" value="1">
|
||||
<input type="hidden" name="site_id" id="edit_site_id">
|
||||
<div class="form-group">
|
||||
<label><?= __('sites_name') ?></label>
|
||||
<input type="text" id="edit_name" name="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label><?= __('sites_site_id') ?></label>
|
||||
<input type="text" id="edit_site_id_str" name="site_id_str" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label><?= __('sites_controller') ?></label>
|
||||
<input type="url" id="edit_controller_url" name="controller_url" required>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="edit_name">Site-Name *</label>
|
||||
<input type="text" id="edit_name" name="name" required>
|
||||
<label><?= __('sites_username') ?></label>
|
||||
<input type="text" id="edit_username" name="username" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit_site_id_str">UniFi Site ID *</label>
|
||||
<input type="text" id="edit_site_id_str" name="site_id_str" required>
|
||||
<label><?= __('sites_password_edit') ?></label>
|
||||
<input type="password" id="edit_password" name="password" placeholder="Leer lassen = nicht ändern">
|
||||
<small style="color:var(--text-muted);font-size:12px;"><?= __('sites_password_hint') ?></small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit_controller_url">Controller URL *</label>
|
||||
<input type="url" id="edit_controller_url" name="controller_url" required>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="edit_username">Benutzername *</label>
|
||||
<input type="text" id="edit_username" name="username" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit_password">Neues Passwort</label>
|
||||
<input type="password" id="edit_password" name="password" placeholder="Leer lassen = nicht ändern">
|
||||
<small style="color: #999; font-size: 12px;">
|
||||
Nur ausfüllen wenn Sie das Passwort ändern möchten
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox-group">
|
||||
<input type="checkbox" id="edit_public_access" name="public_access">
|
||||
<label for="edit_public_access" style="margin: 0;">
|
||||
Öffentlicher Zugriff (ohne Login nutzbar)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 10px; margin-top: 25px;">
|
||||
<button type="submit" class="btn btn-primary" style="flex: 1;" id="editSiteSubmitBtn">
|
||||
<i class="fas fa-save"></i> Änderungen speichern
|
||||
</button>
|
||||
<button type="button" onclick="closeModal('editSiteModal')" class="btn btn-secondary">
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group checkbox-group">
|
||||
<input type="checkbox" id="edit_public_access" name="public_access">
|
||||
<label for="edit_public_access" style="margin:0;"><?= __('sites_public') ?></label>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px;margin-top:20px;">
|
||||
<button type="submit" class="btn btn-primary" style="flex:1;" id="editSiteSubmitBtn">
|
||||
<i class="fas fa-save"></i> <?= __('btn_save') ?>
|
||||
</button>
|
||||
<button type="button" onclick="closeModal('editSiteModal')" class="btn btn-secondary"><?= __('btn_cancel') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openModal() {
|
||||
document.getElementById('addSiteModal').classList.add('active');
|
||||
}
|
||||
<div id="toast-container"></div>
|
||||
<script src="../assets/global.js"></script>
|
||||
<script>
|
||||
function openModal() { document.getElementById('addSiteModal').classList.add('active'); }
|
||||
function closeModal(id) { document.getElementById(id).classList.remove('active'); }
|
||||
|
||||
function closeModal(modalId) {
|
||||
document.getElementById(modalId).classList.remove('active');
|
||||
}
|
||||
function openEditModal(id, name, siteIdStr, controllerUrl, username, publicAccess) {
|
||||
document.getElementById('edit_site_id').value = id;
|
||||
document.getElementById('edit_name').value = name;
|
||||
document.getElementById('edit_site_id_str').value = siteIdStr;
|
||||
document.getElementById('edit_controller_url').value = controllerUrl;
|
||||
document.getElementById('edit_username').value = username;
|
||||
document.getElementById('edit_password').value = '';
|
||||
document.getElementById('edit_public_access').checked = publicAccess == 1;
|
||||
document.getElementById('editSiteModal').classList.add('active');
|
||||
}
|
||||
|
||||
function openEditModal(id, name, siteIdStr, controllerUrl, username, publicAccess) {
|
||||
document.getElementById('edit_site_id').value = id;
|
||||
document.getElementById('edit_name').value = name;
|
||||
document.getElementById('edit_site_id_str').value = siteIdStr;
|
||||
document.getElementById('edit_controller_url').value = controllerUrl;
|
||||
document.getElementById('edit_username').value = username;
|
||||
document.getElementById('edit_password').value = '';
|
||||
document.getElementById('edit_public_access').checked = publicAccess == 1;
|
||||
document.getElementById('addSiteForm').addEventListener('submit', function() {
|
||||
const btn = document.getElementById('addSiteSubmitBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> <?= addslashes(__('sites_testing')) ?>';
|
||||
});
|
||||
document.getElementById('editSiteForm').addEventListener('submit', function() {
|
||||
const btn = document.getElementById('editSiteSubmitBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> <?= addslashes(__('sites_testing')) ?>';
|
||||
});
|
||||
|
||||
document.getElementById('editSiteModal').classList.add('active');
|
||||
}
|
||||
|
||||
// Loading-State bei Formular-Absenden
|
||||
document.getElementById('addSiteForm').addEventListener('submit', function() {
|
||||
const btn = document.getElementById('addSiteSubmitBtn');
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Verbindung wird getestet...';
|
||||
}
|
||||
});
|
||||
document.getElementById('editSiteForm').addEventListener('submit', function() {
|
||||
const btn = document.getElementById('editSiteSubmitBtn');
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Verbindung wird getestet...';
|
||||
}
|
||||
});
|
||||
|
||||
// Modal schließen bei Klick außerhalb
|
||||
document.getElementById('addSiteModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeModal('addSiteModal');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('editSiteModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeModal('editSiteModal');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
['addSiteModal','editSiteModal'].forEach(id => {
|
||||
document.getElementById(id).addEventListener('click', function(e) {
|
||||
if (e.target === this) closeModal(id);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
311
admin/templates.php
Normal file
311
admin/templates.php
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
<?php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
require_once __DIR__ . '/../includes/Database.php';
|
||||
require_once __DIR__ . '/../includes/Auth.php';
|
||||
require_once __DIR__ . '/../includes/I18n.php';
|
||||
|
||||
$auth = new Auth();
|
||||
$auth->requireAdmin();
|
||||
I18n::init();
|
||||
|
||||
$db = Database::getInstance();
|
||||
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
|
||||
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
// Profil hinzufügen
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_template'])) {
|
||||
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
||||
$error = __('error_csrf');
|
||||
} else {
|
||||
try {
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$maxUses = (int)($_POST['max_uses'] ?? 1);
|
||||
$expireMin = (int)($_POST['expire_minutes'] ?? 480);
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
|
||||
if (empty($name)) throw new Exception(__('error_name_req'));
|
||||
if ($maxUses < 1) $maxUses = 1;
|
||||
if ($expireMin < 1) $expireMin = 60;
|
||||
|
||||
$db->execute(
|
||||
"INSERT INTO voucher_templates (name, max_uses, expire_minutes, description, created_by) VALUES (?, ?, ?, ?, ?)",
|
||||
[$name, $maxUses, $expireMin, $description, $_SESSION['user_id']]
|
||||
);
|
||||
$success = __('templates_added');
|
||||
} catch (Exception $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Profil bearbeiten
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['edit_template'])) {
|
||||
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
||||
$error = __('error_csrf');
|
||||
} else {
|
||||
try {
|
||||
$id = (int)$_POST['template_id'];
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$maxUses = (int)($_POST['max_uses'] ?? 1);
|
||||
$expireMin = (int)($_POST['expire_minutes'] ?? 480);
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$isActive = isset($_POST['is_active']) ? 1 : 0;
|
||||
|
||||
if (empty($name)) throw new Exception(__('error_name_req'));
|
||||
|
||||
$db->execute(
|
||||
"UPDATE voucher_templates SET name=?, max_uses=?, expire_minutes=?, description=?, is_active=? WHERE id=?",
|
||||
[$name, $maxUses, $expireMin, $description, $isActive, $id]
|
||||
);
|
||||
$success = __('templates_updated');
|
||||
} catch (Exception $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Profil löschen
|
||||
if (isset($_GET['delete']) && isset($_GET['token'])) {
|
||||
if ($auth->validateCsrfToken($_GET['token'])) {
|
||||
$db->execute("DELETE FROM voucher_templates WHERE id = ?", [(int)$_GET['delete']]);
|
||||
$success = __('templates_deleted');
|
||||
} else {
|
||||
$error = __('error_csrf');
|
||||
}
|
||||
}
|
||||
|
||||
$templates = $db->fetchAll("SELECT t.*, u.name as creator FROM voucher_templates t LEFT JOIN users u ON t.created_by = u.id ORDER BY t.is_active DESC, t.name");
|
||||
|
||||
$currentPage = 'templates';
|
||||
$adminBase = '';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?= I18n::getLanguage() ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= __('templates_title') ?> - <?= htmlspecialchars($appTitle) ?></title>
|
||||
<?php include __DIR__ . '/../includes/admin_nav.php'; ?>
|
||||
<style>
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; flex-wrap: wrap; gap: 15px; }
|
||||
.page-title { font-size: 28px; font-weight: 600; color: var(--text-primary); }
|
||||
.alert { padding: 14px 20px; border-radius: 10px; margin-bottom: 25px; font-size: 14px; }
|
||||
.alert-error { background: #fee; border: 1px solid #fcc; color: #c33; }
|
||||
.alert-success { background: #efe; border: 1px solid #cfc; color: #3c3; }
|
||||
.card { background: var(--bg-card); border-radius: 15px; box-shadow: 0 2px 10px var(--shadow); border: 1px solid var(--border-color); overflow: hidden; margin-bottom: 25px; }
|
||||
.card-header { padding: 20px 25px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
|
||||
.card-title { font-size: 18px; font-weight: 600; color: var(--text-primary); }
|
||||
.table { width: 100%; border-collapse: collapse; }
|
||||
.table th { text-align: left; padding: 12px 15px; background: var(--bg-table-head); color: var(--text-secondary); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.table td { padding: 14px 15px; border-bottom: 1px solid var(--border-color); color: var(--text-primary); font-size: 14px; }
|
||||
.table tr:last-child td { border-bottom: none; }
|
||||
.table tr:hover td { background: var(--bg-hover); }
|
||||
.badge { display: inline-block; padding: 3px 10px; border-radius: 6px; font-size: 11px; font-weight: 500; }
|
||||
.badge-success { background: #d4edda; color: #155724; }
|
||||
.badge-secondary { background: var(--bg-hover); color: var(--text-muted); }
|
||||
.btn-primary { background: var(--accent); color: white; }
|
||||
.btn-primary:hover { background: var(--accent-hover); }
|
||||
.btn-danger { background: var(--danger); color: white; }
|
||||
.btn-small { padding: 6px 12px; font-size: 12px; }
|
||||
.modal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: var(--modal-overlay); z-index: 500; align-items: center; justify-content: center; }
|
||||
.modal.active { display: flex; }
|
||||
.modal-content { background: var(--bg-card); border-radius: 15px; max-width: 520px; width: 90%; max-height: 90vh; overflow-y: auto; }
|
||||
.modal-header { padding: 22px 25px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
|
||||
.modal-title { font-size: 18px; font-weight: 600; color: var(--text-primary); }
|
||||
.modal-close { background: none; border: none; font-size: 22px; cursor: pointer; color: var(--text-muted); }
|
||||
.modal-body { padding: 25px; }
|
||||
.form-group { margin-bottom: 18px; }
|
||||
label { display: block; margin-bottom: 7px; color: var(--text-secondary); font-weight: 500; font-size: 14px; }
|
||||
input[type="text"], input[type="number"], textarea, select { width: 100%; padding: 11px 14px; border: 2px solid var(--border-color); border-radius: 8px; font-size: 14px; background: var(--bg-input); color: var(--text-primary); transition: border-color 0.2s; font-family: inherit; }
|
||||
input:focus, textarea:focus { outline: none; border-color: var(--accent); }
|
||||
.checkbox-group { display: flex; align-items: center; gap: 10px; }
|
||||
.checkbox-group input { width: auto; accent-color: var(--accent); }
|
||||
.help-text { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
|
||||
.empty-state { text-align: center; padding: 60px 20px; color: var(--text-muted); }
|
||||
.empty-state i { font-size: 48px; margin-bottom: 20px; opacity: 0.3; display: block; }
|
||||
.duration-badge { display: inline-flex; align-items: center; gap: 5px; background: var(--bg-hover); padding: 3px 10px; border-radius: 20px; font-size: 12px; color: var(--text-secondary); }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title"><?= __('templates_title') ?></h1>
|
||||
<p style="color: var(--text-muted); font-size: 14px; margin-top: 5px;"><?= __('templates_subtitle') ?></p>
|
||||
</div>
|
||||
<button onclick="openAddModal()" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> <?= __('templates_add') ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> <?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= htmlspecialchars($success) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title"><?= __('templates_title') ?></h2>
|
||||
<span style="font-size: 13px; color: var(--text-muted);"><?= count($templates) ?> Profile</span>
|
||||
</div>
|
||||
<?php if (empty($templates)): ?>
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-layer-group"></i>
|
||||
<p style="font-size: 15px; margin-bottom: 8px;"><?= __('templates_none') ?></p>
|
||||
<p style="font-size: 13px;"><?= __('templates_add_hint') ?></p>
|
||||
<button onclick="openAddModal()" class="btn btn-primary" style="margin-top: 20px;"><i class="fas fa-plus"></i> <?= __('templates_add') ?></button>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div style="overflow-x: auto;">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= __('templates_name') ?></th>
|
||||
<th><?= __('templates_devices') ?></th>
|
||||
<th><?= __('templates_duration') ?></th>
|
||||
<th><?= __('templates_desc') ?></th>
|
||||
<th><?= __('label_status') ?></th>
|
||||
<th><?= __('label_actions') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($templates as $t): ?>
|
||||
<tr>
|
||||
<td><strong><?= htmlspecialchars($t['name']) ?></strong></td>
|
||||
<td>
|
||||
<span class="duration-badge"><i class="fas fa-mobile-alt"></i> <?= (int)$t['max_uses'] ?></span>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
$m = (int)$t['expire_minutes'];
|
||||
if ($m >= 1440 && $m % 1440 === 0) {
|
||||
$durLabel = ($m / 1440) . ' Tag' . ($m / 1440 > 1 ? 'e' : '');
|
||||
} elseif ($m >= 60 && $m % 60 === 0) {
|
||||
$durLabel = ($m / 60) . ' Std.';
|
||||
} else {
|
||||
$durLabel = $m . ' Min.';
|
||||
}
|
||||
?>
|
||||
<span class="duration-badge"><i class="fas fa-clock"></i> <?= $durLabel ?></span>
|
||||
</td>
|
||||
<td style="color: var(--text-secondary);"><?= htmlspecialchars($t['description'] ?? '-') ?></td>
|
||||
<td>
|
||||
<?php if ($t['is_active']): ?>
|
||||
<span class="badge badge-success"><?= __('status_active') ?></span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-secondary"><?= __('status_inactive') ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<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'] ?>)"
|
||||
class="btn btn-secondary btn-small"><i class="fas fa-edit"></i></button>
|
||||
<a href="?delete=<?= $t['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
|
||||
onclick="return confirm('Profil wirklich löschen?')"
|
||||
class="btn btn-danger btn-small"><i class="fas fa-trash"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
</div><!-- main-content -->
|
||||
|
||||
<!-- Modal: Hinzufügen -->
|
||||
<div id="addModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title"><i class="fas fa-plus-circle" style="color: var(--accent);"></i> <?= __('templates_add') ?></h2>
|
||||
<button class="modal-close" onclick="closeModal('addModal')">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
|
||||
<div class="form-group"><label><?= __('templates_name') ?> *</label><input type="text" name="name" required placeholder="z.B. Tagespass, Event 4h"></div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
|
||||
<div class="form-group">
|
||||
<label><?= __('templates_devices') ?></label>
|
||||
<input type="number" name="max_uses" value="1" min="1" max="100">
|
||||
<div class="help-text">Max. gleichzeitige Geräte</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label><?= __('templates_duration') ?> *</label>
|
||||
<input type="number" name="expire_minutes" value="480" min="1" max="525600">
|
||||
<div class="help-text">480 = 8 Stunden</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 style="display:flex;gap:10px;margin-top:20px;">
|
||||
<button type="submit" name="add_template" class="btn btn-primary" style="flex:1;"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
|
||||
<button type="button" onclick="closeModal('addModal')" class="btn btn-secondary"><?= __('btn_cancel') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Bearbeiten -->
|
||||
<div id="editModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title"><i class="fas fa-edit" style="color: var(--accent);"></i> <?= __('templates_edit') ?></h2>
|
||||
<button class="modal-close" onclick="closeModal('editModal')">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
|
||||
<input type="hidden" name="template_id" id="editId">
|
||||
<div class="form-group"><label><?= __('templates_name') ?> *</label><input type="text" name="name" id="editName" required></div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
|
||||
<div class="form-group">
|
||||
<label><?= __('templates_devices') ?></label>
|
||||
<input type="number" name="max_uses" id="editMaxUses" min="1" max="100">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label><?= __('templates_duration') ?></label>
|
||||
<input type="number" name="expire_minutes" id="editExpireMin" min="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"><label><?= __('templates_desc') ?></label><textarea name="description" id="editDesc" rows="2"></textarea></div>
|
||||
<div class="checkbox-group" style="margin-bottom:20px;">
|
||||
<input type="checkbox" name="is_active" id="editActive">
|
||||
<label for="editActive" style="margin:0;"><?= __('status_active') ?></label>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px;">
|
||||
<button type="submit" name="edit_template" class="btn btn-primary" style="flex:1;"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
|
||||
<button type="button" onclick="closeModal('editModal')" class="btn btn-secondary"><?= __('btn_cancel') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../assets/global.js"></script>
|
||||
<script>
|
||||
function openAddModal() { document.getElementById('addModal').classList.add('active'); }
|
||||
function closeModal(id) { document.getElementById(id).classList.remove('active'); }
|
||||
function openEditModal(id, name, maxUses, expMin, desc, isActive) {
|
||||
document.getElementById('editId').value = id;
|
||||
document.getElementById('editName').value = name;
|
||||
document.getElementById('editMaxUses').value = maxUses;
|
||||
document.getElementById('editExpireMin').value = expMin;
|
||||
document.getElementById('editDesc').value = desc;
|
||||
document.getElementById('editActive').checked = isActive == 1;
|
||||
document.getElementById('editModal').classList.add('active');
|
||||
}
|
||||
['addModal','editModal'].forEach(id => {
|
||||
document.getElementById(id).addEventListener('click', function(e) {
|
||||
if (e.target === this) closeModal(id);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1032
admin/users.php
1032
admin/users.php
File diff suppressed because it is too large
Load diff
1338
admin/vouchers.php
1338
admin/vouchers.php
File diff suppressed because it is too large
Load diff
471
assets/global.css
Normal file
471
assets/global.css
Normal file
|
|
@ -0,0 +1,471 @@
|
|||
/* === DARK MODE CSS VARIABLES === */
|
||||
:root {
|
||||
--bg-body: #f5f7fa;
|
||||
--bg-card: #ffffff;
|
||||
--bg-header: #ffffff;
|
||||
--bg-sidebar: #ffffff;
|
||||
--bg-hover: #f8f9fa;
|
||||
--bg-input: #ffffff;
|
||||
--bg-table-head: #f8f9fa;
|
||||
--bg-badge-info: #d1ecf1;
|
||||
--text-primary: #333333;
|
||||
--text-secondary: #666666;
|
||||
--text-muted: #999999;
|
||||
--text-badge-info: #0c5460;
|
||||
--border-color: #e0e0e0;
|
||||
--border-hover: #667eea;
|
||||
--shadow: rgba(0,0,0,0.05);
|
||||
--shadow-lg: rgba(0,0,0,0.3);
|
||||
--accent: #667eea;
|
||||
--accent-hover: #5568d3;
|
||||
--danger: #dc3545;
|
||||
--success: #28a745;
|
||||
--warning: #ffc107;
|
||||
--code-bg: #f8f9fa;
|
||||
--input-border: #e0e0e0;
|
||||
--input-focus: #667eea;
|
||||
--modal-overlay: rgba(0,0,0,0.5);
|
||||
--scrollbar-track: #f1f1f1;
|
||||
--scrollbar-thumb: #c1c1c1;
|
||||
--toast-bg: #ffffff;
|
||||
--stat-sub: #f0f0f0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--bg-body: #0f1117;
|
||||
--bg-card: #1a1d27;
|
||||
--bg-header: #1a1d27;
|
||||
--bg-sidebar: #1a1d27;
|
||||
--bg-hover: #22273a;
|
||||
--bg-input: #22273a;
|
||||
--bg-table-head: #22273a;
|
||||
--bg-badge-info: #0c3d47;
|
||||
--text-primary: #e8eaf0;
|
||||
--text-secondary: #a0a8b8;
|
||||
--text-muted: #6b7280;
|
||||
--text-badge-info: #7dd3eb;
|
||||
--border-color: #2e3347;
|
||||
--border-hover: #667eea;
|
||||
--shadow: rgba(0,0,0,0.3);
|
||||
--shadow-lg: rgba(0,0,0,0.6);
|
||||
--accent: #7c8ff5;
|
||||
--accent-hover: #667eea;
|
||||
--danger: #ef4444;
|
||||
--success: #22c55e;
|
||||
--warning: #f59e0b;
|
||||
--code-bg: #22273a;
|
||||
--input-border: #2e3347;
|
||||
--input-focus: #7c8ff5;
|
||||
--modal-overlay: rgba(0,0,0,0.7);
|
||||
--scrollbar-track: #1a1d27;
|
||||
--scrollbar-thumb: #3a3f5a;
|
||||
--toast-bg: #1a1d27;
|
||||
--stat-sub: #22273a;
|
||||
}
|
||||
|
||||
/* === DARK MODE OVERRIDES FOR COMMON ELEMENTS === */
|
||||
[data-theme="dark"] body {
|
||||
background: var(--bg-body);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .header {
|
||||
background: var(--bg-header) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sidebar {
|
||||
background: var(--bg-sidebar) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sidebar-nav a {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sidebar-nav a:hover,
|
||||
[data-theme="dark"] .sidebar-nav a.active {
|
||||
background: var(--bg-hover) !important;
|
||||
color: var(--accent) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .card,
|
||||
[data-theme="dark"] .stat-card,
|
||||
[data-theme="dark"] .site-status-card,
|
||||
[data-theme="dark"] .site-card {
|
||||
background: var(--bg-card) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .card-header,
|
||||
[data-theme="dark"] .modal-header {
|
||||
border-color: var(--border-color) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .card-title,
|
||||
[data-theme="dark"] .page-title,
|
||||
[data-theme="dark"] .modal-title,
|
||||
[data-theme="dark"] .stat-card-value,
|
||||
[data-theme="dark"] h1, [data-theme="dark"] h2, [data-theme="dark"] h3 {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .page-subtitle,
|
||||
[data-theme="dark"] .stat-card-title,
|
||||
[data-theme="dark"] .stat-card-sub,
|
||||
[data-theme="dark"] label {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .table th {
|
||||
background: var(--bg-table-head) !important;
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .table td {
|
||||
color: var(--text-primary) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .table tr:hover {
|
||||
background: var(--bg-hover) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] input[type="text"],
|
||||
[data-theme="dark"] input[type="email"],
|
||||
[data-theme="dark"] input[type="password"],
|
||||
[data-theme="dark"] input[type="number"],
|
||||
[data-theme="dark"] input[type="url"],
|
||||
[data-theme="dark"] select,
|
||||
[data-theme="dark"] textarea {
|
||||
background: var(--bg-input) !important;
|
||||
border-color: var(--input-border) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] input::placeholder,
|
||||
[data-theme="dark"] textarea::placeholder {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] input:focus,
|
||||
[data-theme="dark"] select:focus,
|
||||
[data-theme="dark"] textarea:focus {
|
||||
border-color: var(--input-focus) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .btn-secondary {
|
||||
background: var(--bg-hover) !important;
|
||||
color: var(--text-secondary) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .btn-secondary:hover {
|
||||
background: var(--border-color) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] code {
|
||||
background: var(--code-bg) !important;
|
||||
color: var(--accent) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-content {
|
||||
background: var(--bg-card) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .badge-info {
|
||||
background: var(--bg-badge-info) !important;
|
||||
color: var(--text-badge-info) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .stat-card-icon {
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .info-box {
|
||||
background: #0c2340 !important;
|
||||
border-color: #1a4a7a !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .info-box h4 { color: #60a5fa !important; }
|
||||
[data-theme="dark"] .info-box p { color: #93c5fd !important; }
|
||||
|
||||
[data-theme="dark"] .placeholder-info {
|
||||
background: #1a1500 !important;
|
||||
border-color: #44390a !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .placeholder-info h4 { color: #d4a017 !important; }
|
||||
[data-theme="dark"] .placeholder-info code { background: #2a2000 !important; }
|
||||
|
||||
[data-theme="dark"] .alert-error {
|
||||
background: #2d0a0a !important;
|
||||
border-color: #7f1d1d !important;
|
||||
color: #fca5a5 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .alert-success {
|
||||
background: #052e16 !important;
|
||||
border-color: #14532d !important;
|
||||
color: #86efac !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .user-menu {
|
||||
background: var(--bg-hover) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .filter-btn {
|
||||
background: var(--bg-card) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .filter-btn.active {
|
||||
background: var(--accent) !important;
|
||||
border-color: var(--accent) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tab-navigation {
|
||||
background: var(--bg-table-head) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tab-button {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tab-button.active {
|
||||
background: var(--bg-card) !important;
|
||||
color: var(--accent) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .section-divider {
|
||||
border-color: var(--border-color) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .site-stat {
|
||||
background: var(--bg-hover) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .live-badge {
|
||||
background: #052e16 !important;
|
||||
color: #86efac !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .live-badge.loading {
|
||||
background: #1a0e00 !important;
|
||||
color: #fcd34d !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .live-badge.error {
|
||||
background: #2d0a0a !important;
|
||||
color: #fca5a5 !important;
|
||||
}
|
||||
|
||||
/* === DARK MODE TOGGLE BUTTON === */
|
||||
.dark-mode-toggle {
|
||||
background: none;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
transition: all 0.2s;
|
||||
color: var(--text-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dark-mode-toggle:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
/* === LANGUAGE SWITCHER === */
|
||||
.lang-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: var(--bg-hover);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.lang-btn {
|
||||
padding: 4px 8px;
|
||||
border: none;
|
||||
background: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.lang-btn.active {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
.lang-btn:hover:not(.active) {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* === TOAST NOTIFICATIONS === */
|
||||
#toast-container {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.toast {
|
||||
background: var(--toast-bg);
|
||||
padding: 14px 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px var(--shadow-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
transform: translateX(120%);
|
||||
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
pointer-events: all;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
.toast.show { transform: translateX(0); }
|
||||
.toast.success { border-left: 4px solid var(--success); }
|
||||
.toast.error { border-left: 4px solid var(--danger); }
|
||||
.toast.info { border-left: 4px solid var(--accent); }
|
||||
.toast.warning { border-left: 4px solid var(--warning); }
|
||||
.toast-icon { font-size: 18px; flex-shrink: 0; }
|
||||
.toast.success .toast-icon { color: var(--success); }
|
||||
.toast.error .toast-icon { color: var(--danger); }
|
||||
.toast.info .toast-icon { color: var(--accent); }
|
||||
.toast.warning .toast-icon { color: var(--warning); }
|
||||
.toast-body { flex: 1; }
|
||||
.toast-title { font-weight: 600; font-size: 14px; color: var(--text-primary); }
|
||||
.toast-msg { font-size: 13px; color: var(--text-secondary); margin-top: 2px; }
|
||||
.toast-close {
|
||||
background: none; border: none; cursor: pointer;
|
||||
color: var(--text-muted); font-size: 18px; padding: 0; line-height: 1;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.toast-close:hover { color: var(--text-primary); }
|
||||
|
||||
/* === MOBILE RESPONSIVE === */
|
||||
.mobile-menu-btn {
|
||||
display: none;
|
||||
background: none;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mobile-menu-btn:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.sidebar-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 199;
|
||||
}
|
||||
.sidebar-overlay.active { display: block; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mobile-menu-btn { display: flex; }
|
||||
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease;
|
||||
z-index: 200;
|
||||
}
|
||||
.sidebar.mobile-open { transform: translateX(0); }
|
||||
|
||||
.main-content {
|
||||
margin-left: 0 !important;
|
||||
padding: 15px !important;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 0 15px !important;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
}
|
||||
|
||||
.site-status {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
|
||||
.sites-grid {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start !important;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
gap: 8px !important;
|
||||
}
|
||||
|
||||
.user-menu span { display: none; }
|
||||
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
|
||||
.tab-navigation {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
.lang-switcher { display: none; }
|
||||
}
|
||||
|
||||
/* === SCROLLBAR STYLING === */
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: var(--scrollbar-track); }
|
||||
::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
|
||||
|
||||
/* === SMOOTH TRANSITIONS === */
|
||||
body, .card, .sidebar, .header, input, select, textarea, .btn {
|
||||
transition: background-color 0.2s, border-color 0.2s, color 0.2s;
|
||||
}
|
||||
117
assets/global.js
Normal file
117
assets/global.js
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
/* === DARK MODE === */
|
||||
(function() {
|
||||
const saved = localStorage.getItem('theme') || 'light';
|
||||
document.documentElement.setAttribute('data-theme', saved);
|
||||
})();
|
||||
|
||||
function toggleDarkMode() {
|
||||
const html = document.documentElement;
|
||||
const current = html.getAttribute('data-theme') || 'light';
|
||||
const next = current === 'dark' ? 'light' : 'dark';
|
||||
html.setAttribute('data-theme', next);
|
||||
localStorage.setItem('theme', next);
|
||||
updateDarkModeBtn();
|
||||
}
|
||||
|
||||
function updateDarkModeBtn() {
|
||||
const btn = document.getElementById('darkModeBtn');
|
||||
if (!btn) return;
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
btn.textContent = isDark ? '☀️' : '🌙';
|
||||
btn.title = isDark ? 'Light Mode' : 'Dark Mode';
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', updateDarkModeBtn);
|
||||
|
||||
/* === TOAST NOTIFICATIONS === */
|
||||
(function() {
|
||||
let container = null;
|
||||
|
||||
function getContainer() {
|
||||
if (!container) {
|
||||
container = document.getElementById('toast-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = 'toast-container';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
const icons = {
|
||||
success: '✓',
|
||||
error: '✕',
|
||||
info: 'ℹ',
|
||||
warning: '⚠'
|
||||
};
|
||||
|
||||
window.showToast = function(type, title, message, duration) {
|
||||
duration = duration || 4000;
|
||||
const c = getContainer();
|
||||
const el = document.createElement('div');
|
||||
el.className = 'toast ' + type;
|
||||
el.innerHTML = `
|
||||
<span class="toast-icon">${icons[type] || 'ℹ'}</span>
|
||||
<div class="toast-body">
|
||||
<div class="toast-title">${title}</div>
|
||||
${message ? `<div class="toast-msg">${message}</div>` : ''}
|
||||
</div>
|
||||
<button class="toast-close" onclick="this.parentElement.remove()">×</button>
|
||||
`;
|
||||
c.appendChild(el);
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => el.classList.add('show'));
|
||||
});
|
||||
setTimeout(() => {
|
||||
el.classList.remove('show');
|
||||
setTimeout(() => el.remove(), 350);
|
||||
}, duration);
|
||||
return el;
|
||||
};
|
||||
})();
|
||||
|
||||
/* === MOBILE SIDEBAR === */
|
||||
function toggleMobileSidebar() {
|
||||
const sidebar = document.querySelector('.sidebar');
|
||||
const overlay = document.querySelector('.sidebar-overlay');
|
||||
if (!sidebar) return;
|
||||
sidebar.classList.toggle('mobile-open');
|
||||
if (overlay) overlay.classList.toggle('active');
|
||||
}
|
||||
|
||||
function closeMobileSidebar() {
|
||||
const sidebar = document.querySelector('.sidebar');
|
||||
const overlay = document.querySelector('.sidebar-overlay');
|
||||
if (sidebar) sidebar.classList.remove('mobile-open');
|
||||
if (overlay) overlay.classList.remove('active');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const overlay = document.querySelector('.sidebar-overlay');
|
||||
if (overlay) overlay.addEventListener('click', closeMobileSidebar);
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') closeMobileSidebar();
|
||||
});
|
||||
});
|
||||
|
||||
/* === LANGUAGE SWITCHER === */
|
||||
function switchLanguage(lang) {
|
||||
fetch('?set_lang=' + lang, { method: 'GET' }).then(() => location.reload());
|
||||
}
|
||||
|
||||
/* === CLIPBOARD === */
|
||||
function copyToClipboard(text, successMsg) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showToast('success', successMsg || 'Kopiert!', '');
|
||||
}).catch(() => {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
showToast('success', successMsg || 'Kopiert!', '');
|
||||
});
|
||||
}
|
||||
42
database.sql
42
database.sql
|
|
@ -47,6 +47,19 @@ CREATE TABLE IF NOT EXISTS `user_site_access` (
|
|||
UNIQUE KEY `unique_user_site` (`user_id`, `site_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `voucher_templates` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`max_uses` INT NOT NULL DEFAULT 1,
|
||||
`expire_minutes` INT NOT NULL DEFAULT 480,
|
||||
`description` VARCHAR(500),
|
||||
`is_active` TINYINT(1) DEFAULT 1,
|
||||
`created_by` INT,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `vouchers` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`site_id` INT NOT NULL,
|
||||
|
|
@ -70,15 +83,6 @@ CREATE TABLE IF NOT EXISTS `vouchers` (
|
|||
INDEX `idx_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Migration für bestehende Tabellen (falls bereits vorhanden):
|
||||
-- ALTER TABLE vouchers ADD COLUMN `status` ENUM('valid', 'used', 'expired') DEFAULT 'valid';
|
||||
-- ALTER TABLE vouchers ADD COLUMN `used_count` INT DEFAULT 0;
|
||||
-- ALTER TABLE vouchers ADD COLUMN `expires_at` TIMESTAMP NULL;
|
||||
-- ALTER TABLE vouchers ADD COLUMN `synced_from_unifi` TINYINT(1) DEFAULT 0;
|
||||
-- ALTER TABLE vouchers ADD COLUMN `last_sync` TIMESTAMP NULL;
|
||||
-- ALTER TABLE vouchers ADD INDEX `idx_unifi_id` (`unifi_voucher_id`);
|
||||
-- ALTER TABLE vouchers ADD INDEX `idx_status` (`status`);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `sessions` (
|
||||
`id` VARCHAR(128) PRIMARY KEY,
|
||||
`user_id` INT NOT NULL,
|
||||
|
|
@ -114,5 +118,21 @@ CREATE TABLE IF NOT EXISTS `audit_log` (
|
|||
INDEX `idx_created` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Migration für bestehende Installationen:
|
||||
-- Neue Tabellen werden automatisch erstellt (CREATE TABLE IF NOT EXISTS)
|
||||
CREATE TABLE IF NOT EXISTS `password_reset_tokens` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`user_id` INT NOT NULL,
|
||||
`token` VARCHAR(128) NOT NULL,
|
||||
`expires_at` TIMESTAMP NOT NULL,
|
||||
`used` TINYINT(1) DEFAULT 0,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
|
||||
UNIQUE KEY `unique_token` (`token`),
|
||||
INDEX `idx_expires` (`expires_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Migrations für bestehende Installationen:
|
||||
-- ALTER TABLE vouchers ADD COLUMN IF NOT EXISTS `status` ENUM('valid', 'used', 'expired') DEFAULT 'valid';
|
||||
-- ALTER TABLE vouchers ADD COLUMN IF NOT EXISTS `used_count` INT DEFAULT 0;
|
||||
-- ALTER TABLE vouchers ADD COLUMN IF NOT EXISTS `expires_at` TIMESTAMP NULL;
|
||||
-- ALTER TABLE vouchers ADD COLUMN IF NOT EXISTS `synced_from_unifi` TINYINT(1) DEFAULT 0;
|
||||
-- ALTER TABLE vouchers ADD COLUMN IF NOT EXISTS `last_sync` TIMESTAMP NULL;
|
||||
|
|
|
|||
132
forgot_password.php
Normal file
132
forgot_password.php
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/includes/Database.php';
|
||||
require_once __DIR__ . '/includes/Auth.php';
|
||||
require_once __DIR__ . '/includes/Mailer.php';
|
||||
require_once __DIR__ . '/includes/I18n.php';
|
||||
|
||||
$auth = new Auth();
|
||||
if ($auth->isLoggedIn()) { header('Location: index.php'); exit; }
|
||||
I18n::init();
|
||||
|
||||
$db = Database::getInstance();
|
||||
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
|
||||
$logoUrl = $db->getSetting('logo_url', '');
|
||||
$systemUrl = rtrim($db->getSetting('system_url', ''), '/');
|
||||
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$error = __('error_email_invalid');
|
||||
} else {
|
||||
$user = $db->fetchOne("SELECT * FROM users WHERE email = ? AND is_active = 1 AND password_hash IS NOT NULL", [$email]);
|
||||
|
||||
// Always show success (don't reveal whether email exists)
|
||||
if ($user) {
|
||||
try {
|
||||
// Delete old tokens for this user
|
||||
$db->execute("DELETE FROM password_reset_tokens WHERE user_id = ?", [$user['id']]);
|
||||
|
||||
// Generate token
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$expiresAt = date('Y-m-d H:i:s', strtotime('+1 hour'));
|
||||
|
||||
$db->execute(
|
||||
"INSERT INTO password_reset_tokens (user_id, token, expires_at) VALUES (?, ?, ?)",
|
||||
[$user['id'], $token, $expiresAt]
|
||||
);
|
||||
|
||||
// Send email
|
||||
$resetUrl = $systemUrl . '/reset_password.php?token=' . $token;
|
||||
$mailer = new Mailer();
|
||||
$subject = $appTitle . ' – Passwort zurücksetzen';
|
||||
$body = "Hallo {$user['name']},\n\n" .
|
||||
"Sie haben eine Passwort-Rücksetzung angefordert.\n\n" .
|
||||
"Klicken Sie auf den folgenden Link, um Ihr Passwort zurückzusetzen (gültig für 1 Stunde):\n\n" .
|
||||
$resetUrl . "\n\n" .
|
||||
"Falls Sie dies nicht angefordert haben, ignorieren Sie diese E-Mail.\n\n" .
|
||||
$appTitle;
|
||||
$mailer->sendRaw($user['email'], $subject, $body);
|
||||
|
||||
// Audit log
|
||||
$db->execute(
|
||||
"INSERT INTO audit_log (user_id, action, entity_type, entity_id, details, ip_address) VALUES (?, 'password_reset', 'user', ?, 'Reset-Link angefordert', ?)",
|
||||
[$user['id'], $user['id'], $_SERVER['REMOTE_ADDR'] ?? '']
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
// Silent – don't reveal errors to user
|
||||
}
|
||||
}
|
||||
|
||||
$success = __('reset_success');
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?= I18n::getLanguage() ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= __('reset_title') ?> – <?= htmlspecialchars($appTitle) ?></title>
|
||||
<link rel="stylesheet" href="assets/global.css">
|
||||
<script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; }
|
||||
.box { background: var(--bg-card); border-radius: 20px; box-shadow: 0 20px 60px var(--shadow-lg); max-width: 420px; width: 100%; padding: 45px 40px; text-align: center; }
|
||||
.logo { max-width: 180px; height: auto; margin-bottom: 25px; }
|
||||
h1 { color: var(--text-primary); font-size: 26px; margin-bottom: 8px; }
|
||||
.subtitle { color: var(--text-muted); font-size: 14px; margin-bottom: 28px; line-height: 1.5; }
|
||||
.form-group { margin-bottom: 18px; text-align: left; }
|
||||
label { display: block; margin-bottom: 7px; color: var(--text-secondary); font-weight: 500; font-size: 14px; }
|
||||
input[type="email"] { width: 100%; padding: 13px; border: 2px solid var(--border-color); border-radius: 10px; font-size: 15px; background: var(--bg-input); color: var(--text-primary); transition: border-color 0.2s; }
|
||||
input:focus { outline: none; border-color: var(--accent); }
|
||||
.btn { width: 100%; padding: 14px; background: var(--accent); color: white; border: none; border-radius: 10px; font-size: 15px; font-weight: 600; cursor: pointer; transition: all 0.2s; margin-top: 8px; }
|
||||
.btn:hover { background: var(--accent-hover); transform: translateY(-2px); }
|
||||
.alert { padding: 12px 16px; border-radius: 8px; margin-bottom: 20px; font-size: 14px; text-align: left; }
|
||||
.alert-error { background: #fee; border: 1px solid #fcc; color: #c33; }
|
||||
.alert-success { background: #efe; border: 1px solid #cfc; color: #3c3; }
|
||||
.back-link { display: block; margin-top: 22px; color: var(--accent); text-decoration: none; font-size: 14px; }
|
||||
.back-link:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="box">
|
||||
<?php if ($logoUrl): ?>
|
||||
<img src="<?= htmlspecialchars($logoUrl) ?>" alt="Logo" class="logo">
|
||||
<?php else: ?>
|
||||
<h1><?= htmlspecialchars($appTitle) ?></h1>
|
||||
<?php endif; ?>
|
||||
|
||||
<h1><?= __('reset_title') ?></h1>
|
||||
<p class="subtitle"><?= __('reset_subtitle') ?></p>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-error"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><?= htmlspecialchars($success) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!$success): ?>
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label for="email"><?= __('reset_email_label') ?></label>
|
||||
<input type="email" id="email" name="email" required autofocus placeholder="name@example.com">
|
||||
</div>
|
||||
<button type="submit" class="btn"><?= __('reset_send_btn') ?></button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<a href="login.php" class="back-link"><?= __('reset_back_login') ?></a>
|
||||
</div>
|
||||
<script src="assets/global.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -38,6 +38,7 @@ class Auth {
|
|||
$this->clearLoginAttempts($ip, $email);
|
||||
$this->setUserSession($user);
|
||||
$this->updateLastLogin($user['id']);
|
||||
$this->writeAuditLog($user['id'], 'user_login', 'user', $user['id'], 'Login erfolgreich');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -45,6 +46,17 @@ class Auth {
|
|||
return false;
|
||||
}
|
||||
|
||||
public function writeAuditLog($userId, $action, $entityType = null, $entityId = null, $details = null) {
|
||||
try {
|
||||
$this->db->execute(
|
||||
"INSERT INTO audit_log (user_id, action, entity_type, entity_id, details, ip_address) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
[$userId, $action, $entityType, $entityId !== null ? (string)$entityId : null, $details, $_SERVER['REMOTE_ADDR'] ?? '']
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
// audit_log table may not exist on old installs
|
||||
}
|
||||
}
|
||||
|
||||
private function isRateLimited($ip, $email) {
|
||||
try {
|
||||
$count = $this->db->fetchOne(
|
||||
|
|
|
|||
53
includes/I18n.php
Normal file
53
includes/I18n.php
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
class I18n {
|
||||
private static array $translations = [];
|
||||
private static string $language = 'de';
|
||||
private static bool $initialized = false;
|
||||
|
||||
public static function init(): void {
|
||||
if (self::$initialized) return;
|
||||
|
||||
if (!isset($_SESSION)) {
|
||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||
}
|
||||
|
||||
if (isset($_GET['set_lang']) && in_array($_GET['set_lang'], ['de', 'en'], true)) {
|
||||
$_SESSION['language'] = $_GET['set_lang'];
|
||||
}
|
||||
|
||||
self::$language = $_SESSION['language'] ?? 'de';
|
||||
|
||||
$langFile = __DIR__ . '/../lang/' . self::$language . '.php';
|
||||
if (file_exists($langFile)) {
|
||||
self::$translations = require $langFile;
|
||||
} else {
|
||||
$fallback = __DIR__ . '/../lang/de.php';
|
||||
if (file_exists($fallback)) {
|
||||
self::$translations = require $fallback;
|
||||
}
|
||||
}
|
||||
|
||||
self::$initialized = true;
|
||||
}
|
||||
|
||||
public static function t(string $key, array $replace = []): string {
|
||||
$text = self::$translations[$key] ?? $key;
|
||||
foreach ($replace as $k => $v) {
|
||||
$text = str_replace('{' . $k . '}', (string)$v, $text);
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
public static function getLanguage(): string {
|
||||
return self::$language;
|
||||
}
|
||||
|
||||
public static function getAvailable(): array {
|
||||
return ['de' => 'Deutsch', 'en' => 'English'];
|
||||
}
|
||||
}
|
||||
|
||||
function __($key, array $replace = []): string {
|
||||
return I18n::t($key, $replace);
|
||||
}
|
||||
|
|
@ -26,6 +26,10 @@ class Mailer {
|
|||
$this->fromName = $this->db->getSetting('smtp_from_name', $this->db->getSetting('app_title', 'UniFi Voucher System'));
|
||||
}
|
||||
|
||||
public function sendRaw($to, $subject, $plainBody) {
|
||||
return $this->send($to, $subject, $plainBody, false);
|
||||
}
|
||||
|
||||
public function send($to, $subject, $body, $isHtml = false) {
|
||||
if (!$this->smtpEnabled || empty($this->smtpHost)) {
|
||||
// Fallback auf PHP mail()
|
||||
|
|
|
|||
124
includes/admin_nav.php
Normal file
124
includes/admin_nav.php
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
<?php
|
||||
/**
|
||||
* Shared admin navigation/header snippet.
|
||||
* Expects $currentPage (string), $appTitle (string), $auth, $db to be set before include.
|
||||
* Expects I18n to be initialized.
|
||||
*/
|
||||
$currentPage = $currentPage ?? '';
|
||||
$faviconUrl = isset($db) ? $db->getSetting('favicon_url', '') : '';
|
||||
$currentUser = isset($auth) ? $auth->getCurrentUser() : null;
|
||||
$lang = I18n::getLanguage();
|
||||
?>
|
||||
<?php if ($faviconUrl): ?>
|
||||
<link rel="icon" type="image/x-icon" href="<?= htmlspecialchars($faviconUrl) ?>">
|
||||
<?php endif; ?>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="<?= $adminBase ?? '../' ?>assets/global.css">
|
||||
<script>
|
||||
(function(){
|
||||
const t = localStorage.getItem('theme') || 'light';
|
||||
document.documentElement.setAttribute('data-theme', t);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Base admin layout using CSS variables */
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; background: var(--bg-body); color: var(--text-primary); }
|
||||
.header { background: var(--bg-header); border-bottom: 1px solid var(--border-color); padding: 0 30px; height: 70px; display: flex; align-items: center; justify-content: space-between; position: sticky; top: 0; z-index: 150; box-shadow: 0 2px 10px var(--shadow); }
|
||||
.header-left { display: flex; align-items: center; gap: 12px; }
|
||||
.header-title { font-size: 20px; font-weight: 600; color: var(--text-primary); }
|
||||
.header-right { display: flex; align-items: center; gap: 10px; }
|
||||
.sidebar { position: fixed; left: 0; top: 70px; bottom: 0; width: 260px; background: var(--bg-sidebar); border-right: 1px solid var(--border-color); padding: 20px 0; overflow-y: auto; z-index: 100; }
|
||||
.sidebar-nav { list-style: none; }
|
||||
.sidebar-nav li { margin-bottom: 2px; }
|
||||
.sidebar-nav a { display: flex; align-items: center; gap: 12px; padding: 11px 25px; color: var(--text-secondary); text-decoration: none; transition: all 0.2s; font-size: 14px; border-radius: 0 8px 8px 0; margin-right: 12px; }
|
||||
.sidebar-nav a:hover, .sidebar-nav a.active { background: var(--bg-hover); color: var(--accent); }
|
||||
.sidebar-nav i { width: 18px; text-align: center; font-size: 15px; }
|
||||
.sidebar-section { padding: 16px 25px 6px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; color: var(--text-muted); }
|
||||
.main-content { margin-left: 260px; padding: 30px; min-height: calc(100vh - 70px); }
|
||||
.user-menu { display: flex; align-items: center; gap: 10px; padding: 6px 12px; background: var(--bg-hover); border-radius: 10px; }
|
||||
.user-avatar { width: 32px; height: 32px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-weight: 600; font-size: 14px; flex-shrink: 0; }
|
||||
.user-name { font-weight: 500; font-size: 13px; color: var(--text-primary); }
|
||||
.btn { padding: 8px 16px; border-radius: 8px; border: none; font-weight: 500; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; gap: 7px; transition: all 0.2s; font-size: 13px; }
|
||||
.btn-secondary { background: var(--bg-hover); color: var(--text-secondary); border: 1px solid var(--border-color); }
|
||||
.btn-secondary:hover { background: var(--border-color); color: var(--text-primary); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Sidebar overlay for mobile -->
|
||||
<div class="sidebar-overlay" onclick="closeMobileSidebar()"></div>
|
||||
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<button class="mobile-menu-btn" onclick="toggleMobileSidebar()" title="Menu">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<div class="header-title">
|
||||
<i class="fas fa-shield-alt" style="color: var(--accent);"></i>
|
||||
<?= __('nav_administration') ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<!-- Language switcher -->
|
||||
<div class="lang-switcher">
|
||||
<?php foreach (I18n::getAvailable() as $code => $label): ?>
|
||||
<button class="lang-btn <?= $lang === $code ? 'active' : '' ?>"
|
||||
onclick="switchLanguage('<?= $code ?>')"><?= strtoupper($code) ?></button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<!-- Dark mode toggle -->
|
||||
<button id="darkModeBtn" class="dark-mode-toggle" onclick="toggleDarkMode()" title="Dark Mode">🌙</button>
|
||||
<!-- Back link -->
|
||||
<a href="<?= $adminBase ?? '../' ?>index.php" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
<span class="hide-mobile"><?= __('nav_back') ?></span>
|
||||
</a>
|
||||
<!-- User menu -->
|
||||
<?php if ($currentUser): ?>
|
||||
<div class="user-menu">
|
||||
<div class="user-avatar"><?= strtoupper(mb_substr($currentUser['name'], 0, 1)) ?></div>
|
||||
<div class="user-name hide-mobile"><?= htmlspecialchars($currentUser['name']) ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar" id="adminSidebar">
|
||||
<nav>
|
||||
<div class="sidebar-section">Main</div>
|
||||
<ul class="sidebar-nav">
|
||||
<li><a href="<?= $adminBase ?? '' ?>index.php" class="<?= $currentPage === 'dashboard' ? 'active' : '' ?>">
|
||||
<i class="fas fa-home"></i> <?= __('nav_dashboard') ?>
|
||||
</a></li>
|
||||
<li><a href="<?= $adminBase ?? '' ?>sites.php" class="<?= $currentPage === 'sites' ? 'active' : '' ?>">
|
||||
<i class="fas fa-map-marker-alt"></i> <?= __('nav_sites') ?>
|
||||
</a></li>
|
||||
<li><a href="<?= $adminBase ?? '' ?>users.php" class="<?= $currentPage === 'users' ? 'active' : '' ?>">
|
||||
<i class="fas fa-users"></i> <?= __('nav_users') ?>
|
||||
</a></li>
|
||||
<li><a href="<?= $adminBase ?? '' ?>vouchers.php" class="<?= $currentPage === 'vouchers' ? 'active' : '' ?>">
|
||||
<i class="fas fa-ticket-alt"></i> <?= __('nav_vouchers') ?>
|
||||
</a></li>
|
||||
</ul>
|
||||
<div class="sidebar-section" style="margin-top: 10px;">Tools</div>
|
||||
<ul class="sidebar-nav">
|
||||
<li><a href="<?= $adminBase ?? '' ?>templates.php" class="<?= $currentPage === 'templates' ? 'active' : '' ?>">
|
||||
<i class="fas fa-layer-group"></i> <?= __('nav_templates') ?>
|
||||
</a></li>
|
||||
<li><a href="<?= $adminBase ?? '' ?>audit_log.php" class="<?= $currentPage === 'audit_log' ? 'active' : '' ?>">
|
||||
<i class="fas fa-history"></i> <?= __('nav_audit_log') ?>
|
||||
</a></li>
|
||||
<li><a href="<?= $adminBase ?? '' ?>settings.php" class="<?= $currentPage === 'settings' ? 'active' : '' ?>">
|
||||
<i class="fas fa-cog"></i> <?= __('nav_settings') ?>
|
||||
</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<style>
|
||||
.hide-mobile { }
|
||||
@media(max-width:600px) { .hide-mobile { display: none; } }
|
||||
</style>
|
||||
288
lang/de.php
Normal file
288
lang/de.php
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
<?php
|
||||
return [
|
||||
// Navigation
|
||||
'nav_dashboard' => 'Dashboard',
|
||||
'nav_sites' => 'Sites verwalten',
|
||||
'nav_users' => 'Benutzer verwalten',
|
||||
'nav_vouchers' => 'Live Vouchers',
|
||||
'nav_templates' => 'Voucher-Profile',
|
||||
'nav_audit_log' => 'Audit-Log',
|
||||
'nav_settings' => 'Einstellungen',
|
||||
'nav_back' => 'Zurück zur Startseite',
|
||||
'nav_administration'=> 'Administration',
|
||||
|
||||
// Common buttons
|
||||
'btn_save' => 'Speichern',
|
||||
'btn_cancel' => 'Abbrechen',
|
||||
'btn_add' => 'Hinzufügen',
|
||||
'btn_edit' => 'Bearbeiten',
|
||||
'btn_delete' => 'Löschen',
|
||||
'btn_create' => 'Erstellen',
|
||||
'btn_close' => 'Schließen',
|
||||
'btn_back' => 'Zurück',
|
||||
'btn_login' => 'Anmelden',
|
||||
'btn_logout' => 'Abmelden',
|
||||
'btn_refresh' => 'Aktualisieren',
|
||||
'btn_export_csv' => 'CSV exportieren',
|
||||
'btn_print' => 'Ausdrucken',
|
||||
'btn_send' => 'Senden',
|
||||
'btn_test' => 'Testen',
|
||||
'btn_generate' => 'Generieren',
|
||||
'btn_copy' => 'Kopieren',
|
||||
'btn_new_code' => 'Weiteren Code erstellen',
|
||||
|
||||
// Common labels
|
||||
'label_name' => 'Name',
|
||||
'label_email' => 'E-Mail',
|
||||
'label_password' => 'Passwort',
|
||||
'label_status' => 'Status',
|
||||
'label_actions' => 'Aktionen',
|
||||
'label_created' => 'Erstellt',
|
||||
'label_site' => 'Standort',
|
||||
'label_sites' => 'Sites',
|
||||
'label_role' => 'Rolle',
|
||||
'label_code' => 'Code',
|
||||
'label_note' => 'Notiz',
|
||||
'label_usage' => 'Nutzung',
|
||||
'label_expires' => 'Gültigkeit',
|
||||
'label_devices' => 'Geräte',
|
||||
'label_duration' => 'Dauer',
|
||||
'label_description' => 'Beschreibung',
|
||||
'label_language' => 'Sprache',
|
||||
|
||||
// Status texts
|
||||
'status_active' => 'Aktiv',
|
||||
'status_inactive' => 'Inaktiv',
|
||||
'status_valid' => 'Gültig',
|
||||
'status_used' => 'Verwendet',
|
||||
'status_expired' => 'Abgelaufen',
|
||||
'status_admin' => 'Admin',
|
||||
'status_user' => 'Benutzer',
|
||||
'status_public' => 'Öffentlich',
|
||||
'status_all' => 'Alle',
|
||||
|
||||
// Dashboard
|
||||
'dashboard_title' => 'Dashboard',
|
||||
'dashboard_subtitle'=> 'Übersicht über Ihr UniFi Voucher System',
|
||||
'dashboard_active_sites' => 'Aktive Sites',
|
||||
'dashboard_users' => 'Benutzer',
|
||||
'dashboard_valid' => 'Gültige Vouchers',
|
||||
'dashboard_used' => 'Verwendet',
|
||||
'dashboard_expired' => 'Abgelaufen',
|
||||
'dashboard_total' => 'Gesamt',
|
||||
'dashboard_live_refresh' => 'Live aktualisieren',
|
||||
'dashboard_trend' => 'Voucher-Trend (Letzte 7 Tage)',
|
||||
'dashboard_top_users'=> 'Top 5 Benutzer (Letzte 30 Tage)',
|
||||
'dashboard_recent' => 'Letzte Vouchers (aus Datenbank)',
|
||||
'dashboard_creator' => 'Ersteller',
|
||||
'dashboard_public' => 'Öffentlich',
|
||||
'dashboard_vouchers_per_site' => 'Live: Vouchers pro Site',
|
||||
'dashboard_no_data' => 'Noch keine Daten verfügbar',
|
||||
'dashboard_no_vouchers' => 'Noch keine Vouchers erstellt',
|
||||
|
||||
// Voucher creation
|
||||
'voucher_name_label' => 'Voucher-Name *',
|
||||
'voucher_name_hint' => 'z.B. Besprechung UL, Vertretername Firma XY',
|
||||
'voucher_devices_label' => 'Wie viele Geräte dürfen sich einloggen? *',
|
||||
'voucher_site_label' => 'Standort *',
|
||||
'voucher_site_select' => 'Bitte wählen...',
|
||||
'voucher_email_send' => 'Code per E-Mail versenden',
|
||||
'voucher_email_label' => 'E-Mail-Adresse des Empfängers',
|
||||
'voucher_email_hint' => 'gast@example.com',
|
||||
'voucher_create_btn' => 'Voucher erstellen',
|
||||
'voucher_creating' => 'Erstelle Voucher...',
|
||||
'voucher_success_title' => '✓ Ihr Zugangs-Code',
|
||||
'voucher_validity' => 'Gültig für {minutes} Minuten ab Erstellung',
|
||||
'voucher_qr_label' => 'QR-Code scannen zum Verbinden',
|
||||
'voucher_print_btn' => 'Code ausdrucken',
|
||||
'voucher_no_sites' => 'Keine verfügbaren Sites gefunden.',
|
||||
'voucher_no_sites_admin'=> 'Klicken Sie hier, um Sites anzulegen',
|
||||
'voucher_no_sites_user' => 'Bitte kontaktieren Sie Ihren Administrator.',
|
||||
'voucher_template_select'=> '-- Kein Profil (manuell) --',
|
||||
'voucher_template_label'=> 'Schnellprofil (optional)',
|
||||
|
||||
// Bulk creation
|
||||
'bulk_tab' => 'Einzeln',
|
||||
'bulk_tab_bulk' => 'Mehrere auf einmal',
|
||||
'bulk_quantity' => 'Anzahl Vouchers',
|
||||
'bulk_quantity_hint'=> 'Wie viele Vouchers sollen erstellt werden?',
|
||||
'bulk_name_prefix' => 'Name / Präfix',
|
||||
'bulk_create_btn' => '{count} Vouchers erstellen',
|
||||
'bulk_creating' => 'Erstelle {count} Vouchers...',
|
||||
'bulk_success' => '{count} Vouchers erfolgreich erstellt!',
|
||||
'bulk_print_all' => 'Alle ausdrucken',
|
||||
'bulk_results' => 'Erstellte Vouchers ({count})',
|
||||
|
||||
// Templates
|
||||
'templates_title' => 'Voucher-Profile',
|
||||
'templates_subtitle'=> 'Vordefinierte Voucher-Konfigurationen',
|
||||
'templates_add' => 'Neues Profil',
|
||||
'templates_name' => 'Profilname',
|
||||
'templates_devices' => 'Max. Geräte',
|
||||
'templates_duration'=> 'Gültigkeit (Minuten)',
|
||||
'templates_desc' => 'Beschreibung (optional)',
|
||||
'templates_none' => 'Noch keine Profile vorhanden.',
|
||||
'templates_add_hint'=> 'Erstellen Sie Profile für häufig verwendete Voucher-Konfigurationen.',
|
||||
'templates_edit' => 'Profil bearbeiten',
|
||||
'templates_added' => 'Profil erfolgreich erstellt!',
|
||||
'templates_updated' => 'Profil erfolgreich aktualisiert!',
|
||||
'templates_deleted' => 'Profil erfolgreich gelöscht!',
|
||||
|
||||
// Users
|
||||
'users_title' => 'Benutzer verwalten',
|
||||
'users_add' => 'Neuer Benutzer',
|
||||
'users_all' => 'Alle Benutzer',
|
||||
'users_site_access' => 'Site-Zugriffe',
|
||||
'users_last_login' => 'Letzter Login',
|
||||
'users_never' => 'Noch nie',
|
||||
'users_you' => 'Sie',
|
||||
'users_all_sites' => 'Alle Sites',
|
||||
'users_none' => 'Keine',
|
||||
'users_add_title' => 'Neuen Benutzer anlegen',
|
||||
'users_edit_title' => 'Benutzer bearbeiten',
|
||||
'users_admin_check' => 'Administrator-Rechte',
|
||||
'users_admin_hint' => 'Admins haben Zugriff auf alle Sites und Einstellungen',
|
||||
'users_password_hint'=> 'Mindestens 8 Zeichen',
|
||||
'users_site_hint' => 'Wählen Sie die Sites, auf die dieser Benutzer Zugriff haben soll',
|
||||
'users_no_sites' => 'Keine Sites verfügbar. Bitte zuerst Sites anlegen.',
|
||||
'users_added' => 'Benutzer erfolgreich erstellt!',
|
||||
'users_updated' => 'Benutzer erfolgreich aktualisiert!',
|
||||
'users_deleted' => 'Benutzer erfolgreich gelöscht!',
|
||||
'users_notified' => ' Benachrichtigung wurde versendet.',
|
||||
'users_none_found' => 'Noch keine Benutzer vorhanden',
|
||||
'users_save' => 'Benutzer anlegen',
|
||||
'users_save_edit' => 'Änderungen speichern',
|
||||
'users_reset_pw' => 'Passwort-Reset-Link senden',
|
||||
|
||||
// Sites
|
||||
'sites_title' => 'Sites verwalten',
|
||||
'sites_add' => 'Neue Site hinzufügen',
|
||||
'sites_add_title' => 'Neue Site hinzufügen',
|
||||
'sites_edit_title' => 'Site bearbeiten',
|
||||
'sites_name' => 'Site-Name *',
|
||||
'sites_site_id' => 'UniFi Site ID *',
|
||||
'sites_controller' => 'Controller URL *',
|
||||
'sites_username' => 'Benutzername *',
|
||||
'sites_password' => 'Passwort *',
|
||||
'sites_password_edit'=> 'Neues Passwort',
|
||||
'sites_password_hint'=> 'Nur ausfüllen wenn Sie das Passwort ändern möchten',
|
||||
'sites_public' => 'Öffentlicher Zugriff (ohne Login nutzbar)',
|
||||
'sites_none' => 'Noch keine Sites konfiguriert. Fügen Sie Ihre erste Site hinzu!',
|
||||
'sites_added' => 'Site erfolgreich hinzugefügt!',
|
||||
'sites_updated' => 'Site erfolgreich aktualisiert!',
|
||||
'sites_deleted' => 'Site erfolgreich gelöscht!',
|
||||
'sites_testing' => 'Verbindung wird getestet...',
|
||||
'sites_deactivate' => 'Deaktivieren',
|
||||
'sites_activate' => 'Aktivieren',
|
||||
|
||||
// Voucher admin list
|
||||
'vouchers_title' => 'Live Voucher-Verwaltung',
|
||||
'vouchers_subtitle' => 'Vouchers werden direkt vom UniFi Controller abgerufen',
|
||||
'vouchers_select_site'=> 'Site auswählen',
|
||||
'vouchers_select_hint'=> '-- Site auswählen --',
|
||||
'vouchers_last_sync'=> 'Letzte Sync',
|
||||
'vouchers_no_sites' => 'Keine aktiven Sites vorhanden',
|
||||
'vouchers_search' => 'Suchen (Code, Name...)',
|
||||
'vouchers_filter_all'=> 'Alle',
|
||||
'vouchers_filter_valid'=> 'Gültig',
|
||||
'vouchers_filter_used'=> 'Verwendet',
|
||||
'vouchers_filter_expired'=> 'Abgelaufen',
|
||||
'vouchers_none' => 'Keine Vouchers gefunden.',
|
||||
'vouchers_per_page' => 'pro Seite',
|
||||
'vouchers_page_of' => 'Seite {current} von {total}',
|
||||
|
||||
// Audit Log
|
||||
'audit_title' => 'Audit-Log',
|
||||
'audit_subtitle' => 'Alle System-Aktionen im Überblick',
|
||||
'audit_action' => 'Aktion',
|
||||
'audit_user' => 'Benutzer',
|
||||
'audit_details' => 'Details',
|
||||
'audit_ip' => 'IP-Adresse',
|
||||
'audit_time' => 'Zeitpunkt',
|
||||
'audit_entity' => 'Objekt',
|
||||
'audit_filter_all' => 'Alle Aktionen',
|
||||
'audit_none' => 'Keine Einträge gefunden.',
|
||||
'audit_filter' => 'Filter',
|
||||
|
||||
// Settings
|
||||
'settings_title' => 'Einstellungen',
|
||||
'settings_subtitle' => 'System-Konfiguration und Personalisierung',
|
||||
'settings_tab_general' => 'Allgemein',
|
||||
'settings_tab_defaults' => 'Voucher-Standards',
|
||||
'settings_tab_cron' => 'Cron-Sync',
|
||||
'settings_tab_m365' => 'Microsoft 365',
|
||||
'settings_tab_smtp' => 'SMTP',
|
||||
'settings_tab_templates_email' => 'Templates',
|
||||
'settings_tab_system' => 'System',
|
||||
'settings_tab_password' => 'Passwort',
|
||||
'settings_saved' => 'Einstellungen erfolgreich gespeichert!',
|
||||
'settings_app_title' => 'Anwendungs-Titel *',
|
||||
'settings_logo_url' => 'Logo-URL',
|
||||
'settings_favicon_url' => 'Favicon-URL',
|
||||
'settings_favicon_hint' => 'Icon im Browser-Tab (.ico, .png, .svg)',
|
||||
'settings_instr_header' => 'Anleitung - Überschrift',
|
||||
'settings_instr_text' => 'Anleitung - Text',
|
||||
'settings_public_access'=> 'Öffentlicher Zugriff',
|
||||
'settings_default_expire' => 'Standard-Gültigkeit (Minuten)',
|
||||
'settings_default_expire_hint'=> 'Standard-Ablaufzeit für neue Vouchers (480 = 8 Stunden)',
|
||||
'settings_default_devices' => 'Standard-Geräteanzahl',
|
||||
'settings_default_devices_hint'=> 'Vorausgefüllter Wert im Voucher-Formular',
|
||||
'settings_max_devices' => 'Maximale Geräteanzahl',
|
||||
'settings_max_devices_hint' => 'Maximaler Wert, den ein Benutzer auswählen kann',
|
||||
'settings_pw_current' => 'Aktuelles Passwort',
|
||||
'settings_pw_new' => 'Neues Passwort',
|
||||
'settings_pw_confirm' => 'Passwort bestätigen',
|
||||
'settings_pw_changed' => 'Passwort erfolgreich geändert!',
|
||||
'settings_pw_minlength' => 'Mindestens 8 Zeichen',
|
||||
|
||||
// Login / Auth
|
||||
'login_title' => 'Anmelden',
|
||||
'login_subtitle' => 'Melden Sie sich an, um fortzufahren',
|
||||
'login_email' => 'E-Mail',
|
||||
'login_password' => 'Passwort',
|
||||
'login_btn' => 'Anmelden',
|
||||
'login_ms' => 'Mit Microsoft anmelden',
|
||||
'login_local' => 'Mit Benutzername und Passwort anmelden',
|
||||
'login_back' => '← Zurück zur Code-Erstellung',
|
||||
'login_forgot' => 'Passwort vergessen?',
|
||||
'login_error_empty' => 'Bitte E-Mail und Passwort eingeben',
|
||||
'login_error_rate' => 'Zu viele Fehlversuche. Bitte warten Sie 10 Minuten.',
|
||||
'login_error_creds' => 'Ungültige E-Mail oder Passwort',
|
||||
|
||||
// Password Reset
|
||||
'reset_title' => 'Passwort vergessen',
|
||||
'reset_subtitle' => 'Geben Sie Ihre E-Mail ein – wir senden Ihnen einen Reset-Link.',
|
||||
'reset_email_label' => 'E-Mail-Adresse',
|
||||
'reset_send_btn' => 'Reset-Link senden',
|
||||
'reset_success' => 'Falls ein Konto mit dieser E-Mail existiert, erhalten Sie in Kürze eine E-Mail.',
|
||||
'reset_back_login' => '← Zurück zum Login',
|
||||
'reset_new_pw' => 'Neues Passwort festlegen',
|
||||
'reset_new_pw_label'=> 'Neues Passwort',
|
||||
'reset_confirm_label'=> 'Passwort bestätigen',
|
||||
'reset_set_btn' => 'Passwort festlegen',
|
||||
'reset_invalid' => 'Ungültiger oder abgelaufener Reset-Link.',
|
||||
'reset_done' => 'Ihr Passwort wurde erfolgreich geändert.',
|
||||
|
||||
// Errors
|
||||
'error_csrf' => 'Ungültiges Sicherheits-Token',
|
||||
'error_login_req' => 'Sie müssen angemeldet sein',
|
||||
'error_no_permission'=> 'Keine Berechtigung für diese Aktion',
|
||||
'error_not_found' => 'Nicht gefunden',
|
||||
'error_name_req' => 'Bitte geben Sie einen Namen ein',
|
||||
'error_site_req' => 'Bitte wählen Sie einen Standort',
|
||||
'error_devices_range'=> 'Anzahl der Geräte muss zwischen 1 und {max} liegen',
|
||||
'error_email_invalid'=> 'Ungültige E-Mail-Adresse',
|
||||
'error_site_no_perm'=> 'Keine Berechtigung für diese Site',
|
||||
'error_site_not_found'=> 'Site nicht gefunden',
|
||||
'error_voucher_invalid'=> 'UniFi hat keinen gültigen Voucher zurückgegeben',
|
||||
'error_connection' => 'Verbindung zum UniFi Controller fehlgeschlagen',
|
||||
'error_fill_all' => 'Bitte füllen Sie alle Pflichtfelder aus',
|
||||
|
||||
// Hello / User
|
||||
'hello' => 'Hallo, {name}',
|
||||
'minutes_short' => 'Min.',
|
||||
'hours_short' => 'Std.',
|
||||
'never' => 'Noch nie',
|
||||
'unknown' => 'Unbekannt',
|
||||
'or' => 'oder',
|
||||
];
|
||||
288
lang/en.php
Normal file
288
lang/en.php
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
<?php
|
||||
return [
|
||||
// Navigation
|
||||
'nav_dashboard' => 'Dashboard',
|
||||
'nav_sites' => 'Manage Sites',
|
||||
'nav_users' => 'Manage Users',
|
||||
'nav_vouchers' => 'Live Vouchers',
|
||||
'nav_templates' => 'Voucher Profiles',
|
||||
'nav_audit_log' => 'Audit Log',
|
||||
'nav_settings' => 'Settings',
|
||||
'nav_back' => 'Back to Home',
|
||||
'nav_administration'=> 'Administration',
|
||||
|
||||
// Common buttons
|
||||
'btn_save' => 'Save',
|
||||
'btn_cancel' => 'Cancel',
|
||||
'btn_add' => 'Add',
|
||||
'btn_edit' => 'Edit',
|
||||
'btn_delete' => 'Delete',
|
||||
'btn_create' => 'Create',
|
||||
'btn_close' => 'Close',
|
||||
'btn_back' => 'Back',
|
||||
'btn_login' => 'Sign In',
|
||||
'btn_logout' => 'Sign Out',
|
||||
'btn_refresh' => 'Refresh',
|
||||
'btn_export_csv' => 'Export CSV',
|
||||
'btn_print' => 'Print',
|
||||
'btn_send' => 'Send',
|
||||
'btn_test' => 'Test',
|
||||
'btn_generate' => 'Generate',
|
||||
'btn_copy' => 'Copy',
|
||||
'btn_new_code' => 'Create Another Code',
|
||||
|
||||
// Common labels
|
||||
'label_name' => 'Name',
|
||||
'label_email' => 'Email',
|
||||
'label_password' => 'Password',
|
||||
'label_status' => 'Status',
|
||||
'label_actions' => 'Actions',
|
||||
'label_created' => 'Created',
|
||||
'label_site' => 'Location',
|
||||
'label_sites' => 'Sites',
|
||||
'label_role' => 'Role',
|
||||
'label_code' => 'Code',
|
||||
'label_note' => 'Note',
|
||||
'label_usage' => 'Usage',
|
||||
'label_expires' => 'Expires',
|
||||
'label_devices' => 'Devices',
|
||||
'label_duration' => 'Duration',
|
||||
'label_description' => 'Description',
|
||||
'label_language' => 'Language',
|
||||
|
||||
// Status texts
|
||||
'status_active' => 'Active',
|
||||
'status_inactive' => 'Inactive',
|
||||
'status_valid' => 'Valid',
|
||||
'status_used' => 'Used',
|
||||
'status_expired' => 'Expired',
|
||||
'status_admin' => 'Admin',
|
||||
'status_user' => 'User',
|
||||
'status_public' => 'Public',
|
||||
'status_all' => 'All',
|
||||
|
||||
// Dashboard
|
||||
'dashboard_title' => 'Dashboard',
|
||||
'dashboard_subtitle'=> 'Overview of your UniFi Voucher System',
|
||||
'dashboard_active_sites' => 'Active Sites',
|
||||
'dashboard_users' => 'Users',
|
||||
'dashboard_valid' => 'Valid Vouchers',
|
||||
'dashboard_used' => 'Used',
|
||||
'dashboard_expired' => 'Expired',
|
||||
'dashboard_total' => 'Total',
|
||||
'dashboard_live_refresh' => 'Live Refresh',
|
||||
'dashboard_trend' => 'Voucher Trend (Last 7 Days)',
|
||||
'dashboard_top_users'=> 'Top 5 Users (Last 30 Days)',
|
||||
'dashboard_recent' => 'Recent Vouchers (from Database)',
|
||||
'dashboard_creator' => 'Creator',
|
||||
'dashboard_public' => 'Public',
|
||||
'dashboard_vouchers_per_site' => 'Live: Vouchers per Site',
|
||||
'dashboard_no_data' => 'No data available yet',
|
||||
'dashboard_no_vouchers' => 'No vouchers created yet',
|
||||
|
||||
// Voucher creation
|
||||
'voucher_name_label' => 'Voucher Name *',
|
||||
'voucher_name_hint' => 'e.g. Meeting Room 3, Guest John Doe',
|
||||
'voucher_devices_label' => 'How many devices may connect? *',
|
||||
'voucher_site_label' => 'Location *',
|
||||
'voucher_site_select' => 'Please select...',
|
||||
'voucher_email_send' => 'Send code via email',
|
||||
'voucher_email_label' => 'Recipient email address',
|
||||
'voucher_email_hint' => 'guest@example.com',
|
||||
'voucher_create_btn' => 'Create Voucher',
|
||||
'voucher_creating' => 'Creating Voucher...',
|
||||
'voucher_success_title' => '✓ Your Access Code',
|
||||
'voucher_validity' => 'Valid for {minutes} minutes from creation',
|
||||
'voucher_qr_label' => 'Scan QR code to connect',
|
||||
'voucher_print_btn' => 'Print Code',
|
||||
'voucher_no_sites' => 'No available sites found.',
|
||||
'voucher_no_sites_admin'=> 'Click here to create sites',
|
||||
'voucher_no_sites_user' => 'Please contact your administrator.',
|
||||
'voucher_template_select'=> '-- No Profile (manual) --',
|
||||
'voucher_template_label'=> 'Quick Profile (optional)',
|
||||
|
||||
// Bulk creation
|
||||
'bulk_tab' => 'Single',
|
||||
'bulk_tab_bulk' => 'Create Multiple',
|
||||
'bulk_quantity' => 'Number of Vouchers',
|
||||
'bulk_quantity_hint'=> 'How many vouchers should be created?',
|
||||
'bulk_name_prefix' => 'Name / Prefix',
|
||||
'bulk_create_btn' => 'Create {count} Vouchers',
|
||||
'bulk_creating' => 'Creating {count} vouchers...',
|
||||
'bulk_success' => '{count} vouchers created successfully!',
|
||||
'bulk_print_all' => 'Print All',
|
||||
'bulk_results' => 'Created Vouchers ({count})',
|
||||
|
||||
// Templates
|
||||
'templates_title' => 'Voucher Profiles',
|
||||
'templates_subtitle'=> 'Predefined voucher configurations',
|
||||
'templates_add' => 'New Profile',
|
||||
'templates_name' => 'Profile Name',
|
||||
'templates_devices' => 'Max. Devices',
|
||||
'templates_duration'=> 'Validity (Minutes)',
|
||||
'templates_desc' => 'Description (optional)',
|
||||
'templates_none' => 'No profiles found.',
|
||||
'templates_add_hint'=> 'Create profiles for frequently used voucher configurations.',
|
||||
'templates_edit' => 'Edit Profile',
|
||||
'templates_added' => 'Profile created successfully!',
|
||||
'templates_updated' => 'Profile updated successfully!',
|
||||
'templates_deleted' => 'Profile deleted successfully!',
|
||||
|
||||
// Users
|
||||
'users_title' => 'Manage Users',
|
||||
'users_add' => 'New User',
|
||||
'users_all' => 'All Users',
|
||||
'users_site_access' => 'Site Access',
|
||||
'users_last_login' => 'Last Login',
|
||||
'users_never' => 'Never',
|
||||
'users_you' => 'You',
|
||||
'users_all_sites' => 'All Sites',
|
||||
'users_none' => 'None',
|
||||
'users_add_title' => 'Create New User',
|
||||
'users_edit_title' => 'Edit User',
|
||||
'users_admin_check' => 'Administrator Rights',
|
||||
'users_admin_hint' => 'Admins have access to all sites and settings',
|
||||
'users_password_hint'=> 'At least 8 characters',
|
||||
'users_site_hint' => 'Select the sites this user should have access to',
|
||||
'users_no_sites' => 'No sites available. Please create sites first.',
|
||||
'users_added' => 'User created successfully!',
|
||||
'users_updated' => 'User updated successfully!',
|
||||
'users_deleted' => 'User deleted successfully!',
|
||||
'users_notified' => ' Notification sent.',
|
||||
'users_none_found' => 'No users found',
|
||||
'users_save' => 'Create User',
|
||||
'users_save_edit' => 'Save Changes',
|
||||
'users_reset_pw' => 'Send Password Reset Link',
|
||||
|
||||
// Sites
|
||||
'sites_title' => 'Manage Sites',
|
||||
'sites_add' => 'Add New Site',
|
||||
'sites_add_title' => 'Add New Site',
|
||||
'sites_edit_title' => 'Edit Site',
|
||||
'sites_name' => 'Site Name *',
|
||||
'sites_site_id' => 'UniFi Site ID *',
|
||||
'sites_controller' => 'Controller URL *',
|
||||
'sites_username' => 'Username *',
|
||||
'sites_password' => 'Password *',
|
||||
'sites_password_edit'=> 'New Password',
|
||||
'sites_password_hint'=> 'Leave blank to keep current password',
|
||||
'sites_public' => 'Public Access (usable without login)',
|
||||
'sites_none' => 'No sites configured yet. Add your first site!',
|
||||
'sites_added' => 'Site added successfully!',
|
||||
'sites_updated' => 'Site updated successfully!',
|
||||
'sites_deleted' => 'Site deleted successfully!',
|
||||
'sites_testing' => 'Testing connection...',
|
||||
'sites_deactivate' => 'Deactivate',
|
||||
'sites_activate' => 'Activate',
|
||||
|
||||
// Voucher admin list
|
||||
'vouchers_title' => 'Live Voucher Management',
|
||||
'vouchers_subtitle' => 'Vouchers are fetched directly from the UniFi Controller',
|
||||
'vouchers_select_site'=> 'Select Site',
|
||||
'vouchers_select_hint'=> '-- Select Site --',
|
||||
'vouchers_last_sync'=> 'Last Sync',
|
||||
'vouchers_no_sites' => 'No active sites available',
|
||||
'vouchers_search' => 'Search (code, name...)',
|
||||
'vouchers_filter_all'=> 'All',
|
||||
'vouchers_filter_valid'=> 'Valid',
|
||||
'vouchers_filter_used'=> 'Used',
|
||||
'vouchers_filter_expired'=> 'Expired',
|
||||
'vouchers_none' => 'No vouchers found.',
|
||||
'vouchers_per_page' => 'per page',
|
||||
'vouchers_page_of' => 'Page {current} of {total}',
|
||||
|
||||
// Audit Log
|
||||
'audit_title' => 'Audit Log',
|
||||
'audit_subtitle' => 'All system actions at a glance',
|
||||
'audit_action' => 'Action',
|
||||
'audit_user' => 'User',
|
||||
'audit_details' => 'Details',
|
||||
'audit_ip' => 'IP Address',
|
||||
'audit_time' => 'Time',
|
||||
'audit_entity' => 'Entity',
|
||||
'audit_filter_all' => 'All Actions',
|
||||
'audit_none' => 'No entries found.',
|
||||
'audit_filter' => 'Filter',
|
||||
|
||||
// Settings
|
||||
'settings_title' => 'Settings',
|
||||
'settings_subtitle' => 'System configuration and customization',
|
||||
'settings_tab_general' => 'General',
|
||||
'settings_tab_defaults' => 'Voucher Defaults',
|
||||
'settings_tab_cron' => 'Cron Sync',
|
||||
'settings_tab_m365' => 'Microsoft 365',
|
||||
'settings_tab_smtp' => 'SMTP',
|
||||
'settings_tab_templates_email' => 'Templates',
|
||||
'settings_tab_system' => 'System',
|
||||
'settings_tab_password' => 'Password',
|
||||
'settings_saved' => 'Settings saved successfully!',
|
||||
'settings_app_title' => 'Application Title *',
|
||||
'settings_logo_url' => 'Logo URL',
|
||||
'settings_favicon_url' => 'Favicon URL',
|
||||
'settings_favicon_hint' => 'Browser tab icon (.ico, .png, .svg)',
|
||||
'settings_instr_header' => 'Instructions - Headline',
|
||||
'settings_instr_text' => 'Instructions - Text',
|
||||
'settings_public_access'=> 'Public Access',
|
||||
'settings_default_expire' => 'Default Validity (Minutes)',
|
||||
'settings_default_expire_hint'=> 'Default expiry time for new vouchers (480 = 8 hours)',
|
||||
'settings_default_devices' => 'Default Device Count',
|
||||
'settings_default_devices_hint'=> 'Pre-filled value in the voucher form',
|
||||
'settings_max_devices' => 'Maximum Device Count',
|
||||
'settings_max_devices_hint' => 'Maximum value a user can select',
|
||||
'settings_pw_current' => 'Current Password',
|
||||
'settings_pw_new' => 'New Password',
|
||||
'settings_pw_confirm' => 'Confirm Password',
|
||||
'settings_pw_changed' => 'Password changed successfully!',
|
||||
'settings_pw_minlength' => 'At least 8 characters',
|
||||
|
||||
// Login / Auth
|
||||
'login_title' => 'Sign In',
|
||||
'login_subtitle' => 'Sign in to continue',
|
||||
'login_email' => 'Email',
|
||||
'login_password' => 'Password',
|
||||
'login_btn' => 'Sign In',
|
||||
'login_ms' => 'Sign in with Microsoft',
|
||||
'login_local' => 'Sign in with username and password',
|
||||
'login_back' => '← Back to Code Creation',
|
||||
'login_forgot' => 'Forgot password?',
|
||||
'login_error_empty' => 'Please enter email and password',
|
||||
'login_error_rate' => 'Too many failed attempts. Please wait 10 minutes.',
|
||||
'login_error_creds' => 'Invalid email or password',
|
||||
|
||||
// Password Reset
|
||||
'reset_title' => 'Forgot Password',
|
||||
'reset_subtitle' => 'Enter your email – we will send you a reset link.',
|
||||
'reset_email_label' => 'Email Address',
|
||||
'reset_send_btn' => 'Send Reset Link',
|
||||
'reset_success' => 'If an account with this email exists, you will receive an email shortly.',
|
||||
'reset_back_login' => '← Back to Login',
|
||||
'reset_new_pw' => 'Set New Password',
|
||||
'reset_new_pw_label'=> 'New Password',
|
||||
'reset_confirm_label'=> 'Confirm Password',
|
||||
'reset_set_btn' => 'Set Password',
|
||||
'reset_invalid' => 'Invalid or expired reset link.',
|
||||
'reset_done' => 'Your password has been changed successfully.',
|
||||
|
||||
// Errors
|
||||
'error_csrf' => 'Invalid security token',
|
||||
'error_login_req' => 'You must be logged in',
|
||||
'error_no_permission'=> 'No permission for this action',
|
||||
'error_not_found' => 'Not found',
|
||||
'error_name_req' => 'Please enter a name',
|
||||
'error_site_req' => 'Please select a location',
|
||||
'error_devices_range'=> 'Device count must be between 1 and {max}',
|
||||
'error_email_invalid'=> 'Invalid email address',
|
||||
'error_site_no_perm'=> 'No permission for this site',
|
||||
'error_site_not_found'=> 'Site not found',
|
||||
'error_voucher_invalid'=> 'UniFi did not return a valid voucher',
|
||||
'error_connection' => 'Connection to UniFi Controller failed',
|
||||
'error_fill_all' => 'Please fill in all required fields',
|
||||
|
||||
// Hello / User
|
||||
'hello' => 'Hello, {name}',
|
||||
'minutes_short' => 'min.',
|
||||
'hours_short' => 'hrs.',
|
||||
'never' => 'Never',
|
||||
'unknown' => 'Unknown',
|
||||
'or' => 'or',
|
||||
];
|
||||
361
login.php
361
login.php
|
|
@ -1,45 +1,40 @@
|
|||
<?php
|
||||
// Error Reporting (kann nach erfolgreicher Einrichtung entfernt werden)
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
// Absolute Pfade verwenden
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/includes/Database.php';
|
||||
require_once __DIR__ . '/includes/Auth.php';
|
||||
require_once __DIR__ . '/includes/I18n.php';
|
||||
|
||||
try {
|
||||
$auth = new Auth();
|
||||
|
||||
// Wenn bereits eingeloggt, weiterleiten
|
||||
if ($auth->isLoggedIn()) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
if ($auth->isLoggedIn()) { header('Location: index.php'); exit; }
|
||||
} catch (Exception $e) {
|
||||
die('Fehler beim Initialisieren: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$error = '';
|
||||
I18n::init();
|
||||
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
// Login-Verarbeitung
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
try {
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
if (empty($email) || empty($password)) {
|
||||
$error = 'Bitte E-Mail und Passwort eingeben';
|
||||
$error = __('login_error_empty');
|
||||
} else {
|
||||
$result = $auth->login($email, $password);
|
||||
if ($result === true) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
} elseif ($result === 'rate_limited') {
|
||||
$error = 'Zu viele Fehlversuche. Bitte warten Sie 10 Minuten.';
|
||||
$error = __('login_error_rate');
|
||||
} else {
|
||||
$error = 'Ungültige E-Mail oder Passwort';
|
||||
$error = __('login_error_creds');
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
|
|
@ -48,46 +43,36 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
}
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$db = Database::getInstance();
|
||||
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
|
||||
$logoUrl = $db->getSetting('logo_url', '');
|
||||
$logoUrl = $db->getSetting('logo_url', '');
|
||||
|
||||
// M365 aktiviert prüfen - ALLE drei Felder müssen ausgefüllt sein
|
||||
$m365ClientId = $db->getSetting('m365_client_id', '');
|
||||
$m365ClientId = $db->getSetting('m365_client_id', '');
|
||||
$m365ClientSecret = $db->getSetting('m365_client_secret', '');
|
||||
$m365TenantId = $db->getSetting('m365_tenant_id', '');
|
||||
$m365TenantId = $db->getSetting('m365_tenant_id', '');
|
||||
$m365Enabled = !empty($m365ClientId) && !empty($m365ClientSecret) && !empty($m365TenantId);
|
||||
$publicAccess = $db->getSetting('public_access', 0);
|
||||
$smtpEnabled = $db->getSetting('smtp_enabled', '0') === '1';
|
||||
|
||||
$m365Enabled = !empty($m365ClientId) &&
|
||||
!empty($m365ClientSecret) &&
|
||||
!empty($m365TenantId);
|
||||
|
||||
$publicAccess = $db->getSetting('public_access', 0);
|
||||
|
||||
// M365 OAuth URL generieren falls aktiviert
|
||||
$m365LoginUrl = '';
|
||||
if ($m365Enabled) {
|
||||
// Dynamische Redirect URI basierend auf aktuellem Pfad
|
||||
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'];
|
||||
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'];
|
||||
$scriptPath = dirname($_SERVER['SCRIPT_NAME']);
|
||||
$scriptPath = $scriptPath === '/' ? '' : $scriptPath;
|
||||
$redirectUri = $protocol . '://' . $host . $scriptPath . '/m365_callback.php';
|
||||
|
||||
$params = [
|
||||
'client_id' => $m365ClientId,
|
||||
'client_id' => $m365ClientId,
|
||||
'response_type' => 'code',
|
||||
'redirect_uri' => $redirectUri,
|
||||
'redirect_uri' => $redirectUri,
|
||||
'response_mode' => 'query',
|
||||
'scope' => 'openid profile email User.Read',
|
||||
'state' => bin2hex(random_bytes(16))
|
||||
'scope' => 'openid profile email User.Read',
|
||||
'state' => bin2hex(random_bytes(16))
|
||||
];
|
||||
|
||||
$_SESSION['m365_state'] = $params['state'];
|
||||
|
||||
$m365LoginUrl = "https://login.microsoftonline.com/$m365TenantId/oauth2/v2.0/authorize?" . http_build_query($params);
|
||||
}
|
||||
|
||||
// Prüfen ob alternative Login-Form (Benutzername/Passwort) angezeigt werden soll
|
||||
$showLocalLogin = isset($_GET['local']) && $_GET['local'] === '1';
|
||||
|
||||
} catch (Exception $e) {
|
||||
|
|
@ -95,239 +80,117 @@ try {
|
|||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<html lang="<?= I18n::getLanguage() ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - <?= htmlspecialchars($appTitle) ?></title>
|
||||
<title><?= __('login_title') ?> – <?= htmlspecialchars($appTitle) ?></title>
|
||||
<link rel="stylesheet" href="assets/global.css">
|
||||
<script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.login-container {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
padding: 50px 40px;
|
||||
text-align: center;
|
||||
}
|
||||
.logo {
|
||||
max-width: 200px;
|
||||
height: auto;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
font-size: 28px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.subtitle {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
input[type="email"],
|
||||
input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.btn:hover {
|
||||
background: #5568d3;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
.btn-microsoft {
|
||||
background: #2f2f2f;
|
||||
color: white;
|
||||
border: none;
|
||||
margin-top: 0;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
.btn-microsoft:hover {
|
||||
background: #1a1a1a;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
text-decoration: none;
|
||||
}
|
||||
.btn-microsoft svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.divider {
|
||||
margin: 25px 0;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
.divider::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: #e0e0e0;
|
||||
}
|
||||
.divider span {
|
||||
background: white;
|
||||
padding: 0 15px;
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.alert {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.alert-error {
|
||||
background: #fee;
|
||||
border: 1px solid #fcc;
|
||||
color: #c33;
|
||||
}
|
||||
.alert-success {
|
||||
background: #efe;
|
||||
border: 1px solid #cfc;
|
||||
color: #3c3;
|
||||
}
|
||||
.back-link {
|
||||
display: block;
|
||||
margin-top: 20px;
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
.back-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.local-login-link {
|
||||
display: block;
|
||||
margin-top: 25px;
|
||||
color: #999;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
.local-login-link:hover {
|
||||
color: #667eea;
|
||||
text-decoration: underline;
|
||||
}
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; }
|
||||
.login-container { background: var(--bg-card); border-radius: 20px; box-shadow: 0 20px 60px var(--shadow-lg); max-width: 420px; width: 100%; padding: 50px 40px; text-align: center; }
|
||||
.logo { max-width: 200px; height: auto; margin-bottom: 30px; }
|
||||
h1 { color: var(--text-primary); font-size: 28px; margin-bottom: 10px; }
|
||||
.subtitle { color: var(--text-muted); font-size: 14px; margin-bottom: 30px; }
|
||||
.form-group { margin-bottom: 20px; text-align: left; }
|
||||
label { display: block; margin-bottom: 8px; color: var(--text-secondary); font-weight: 500; font-size: 14px; }
|
||||
input[type="email"], input[type="password"] { width: 100%; padding: 14px; border: 2px solid var(--border-color); border-radius: 10px; font-size: 15px; background: var(--bg-input); color: var(--text-primary); transition: all 0.2s; }
|
||||
input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(102,126,234,0.1); }
|
||||
.btn { width: 100%; padding: 14px; background: var(--accent); color: white; border: none; border-radius: 10px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.2s; margin-top: 10px; }
|
||||
.btn:hover { background: var(--accent-hover); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(102,126,234,0.4); }
|
||||
.btn-microsoft { background: #2f2f2f; color: white; border: none; margin-top: 0; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; gap: 12px; padding: 16px 24px; border-radius: 10px; width: 100%; font-size: 15px; font-weight: 500; cursor: pointer; transition: all 0.2s; }
|
||||
.btn-microsoft:hover { background: #1a1a1a; transform: translateY(-2px); }
|
||||
.btn-microsoft svg { width: 20px; height: 20px; }
|
||||
.divider { margin: 22px 0; text-align: center; position: relative; }
|
||||
.divider::before { content: ''; position: absolute; top: 50%; left: 0; right: 0; height: 1px; background: var(--border-color); }
|
||||
.divider span { background: var(--bg-card); padding: 0 15px; color: var(--text-muted); font-size: 13px; position: relative; z-index: 1; }
|
||||
.alert { padding: 12px 16px; border-radius: 8px; margin-bottom: 20px; font-size: 14px; }
|
||||
.alert-error { background: #fee; border: 1px solid #fcc; color: #c33; }
|
||||
.alert-success { background: #efe; border: 1px solid #cfc; color: #3c3; }
|
||||
.back-link { display: block; margin-top: 20px; color: var(--accent); text-decoration: none; font-size: 14px; }
|
||||
.back-link:hover { text-decoration: underline; }
|
||||
.local-login-link { display: block; margin-top: 20px; color: var(--text-muted); text-decoration: none; font-size: 13px; }
|
||||
.local-login-link:hover { color: var(--accent); text-decoration: underline; }
|
||||
.forgot-link { display: block; margin-top: 12px; text-align: right; color: var(--text-muted); font-size: 13px; text-decoration: none; }
|
||||
.forgot-link:hover { color: var(--accent); text-decoration: underline; }
|
||||
.header-tools { position: absolute; top: 20px; right: 20px; display: flex; gap: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<?php if ($logoUrl): ?>
|
||||
<img src="<?= htmlspecialchars($logoUrl) ?>" alt="Logo" class="logo">
|
||||
<?php else: ?>
|
||||
<h1><?= htmlspecialchars($appTitle) ?></h1>
|
||||
<?php endif; ?>
|
||||
<div style="position:fixed;top:15px;right:20px;display:flex;gap:8px;z-index:10;">
|
||||
<div class="lang-switcher">
|
||||
<?php foreach (I18n::getAvailable() as $code => $label): ?>
|
||||
<button class="lang-btn <?= I18n::getLanguage() === $code ? 'active' : '' ?>"
|
||||
onclick="switchLanguage('<?= $code ?>')"><?= strtoupper($code) ?></button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<button id="darkModeBtn" class="dark-mode-toggle" onclick="toggleDarkMode()" title="Dark Mode">🌙</button>
|
||||
</div>
|
||||
|
||||
<p class="subtitle">Melden Sie sich an, um fortzufahren</p>
|
||||
<div class="login-container">
|
||||
<?php if ($logoUrl): ?>
|
||||
<img src="<?= htmlspecialchars($logoUrl) ?>" alt="Logo" class="logo">
|
||||
<?php else: ?>
|
||||
<h1><?= htmlspecialchars($appTitle) ?></h1>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-error"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<p class="subtitle"><?= __('login_subtitle') ?></p>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><?= htmlspecialchars($success) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-error"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><?= htmlspecialchars($success) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($m365Enabled && !$showLocalLogin): ?>
|
||||
<!-- M365 Login als Hauptoption -->
|
||||
<a href="<?= htmlspecialchars($m365LoginUrl) ?>" class="btn btn-microsoft">
|
||||
<?php if ($m365Enabled && !$showLocalLogin): ?>
|
||||
<a href="<?= htmlspecialchars($m365LoginUrl) ?>" class="btn-microsoft">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
|
||||
<path fill="#f35325" d="M1 1h10v10H1z"/>
|
||||
<path fill="#81bc06" d="M12 1h10v10H12z"/>
|
||||
<path fill="#05a6f0" d="M1 12h10v10H1z"/>
|
||||
<path fill="#ffba08" d="M12 12h10v10H12z"/>
|
||||
</svg>
|
||||
<?= __('login_ms') ?>
|
||||
</a>
|
||||
<a href="?local=1" class="local-login-link"><?= __('login_local') ?></a>
|
||||
<?php else: ?>
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label for="email"><?= __('login_email') ?></label>
|
||||
<input type="email" id="email" name="email" required autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password"><?= __('login_password') ?></label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<?php if ($smtpEnabled): ?>
|
||||
<a href="forgot_password.php" class="forgot-link"><?= __('login_forgot') ?></a>
|
||||
<?php endif; ?>
|
||||
<button type="submit" class="btn"><?= __('login_btn') ?></button>
|
||||
</form>
|
||||
|
||||
<?php if ($m365Enabled): ?>
|
||||
<div class="divider"><span><?= __('or') ?></span></div>
|
||||
<a href="<?= htmlspecialchars($m365LoginUrl) ?>" class="btn-microsoft">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
|
||||
<path fill="#f35325" d="M1 1h10v10H1z"/>
|
||||
<path fill="#81bc06" d="M12 1h10v10H12z"/>
|
||||
<path fill="#05a6f0" d="M1 12h10v10H1z"/>
|
||||
<path fill="#ffba08" d="M12 12h10v10H12z"/>
|
||||
</svg>
|
||||
Mit Microsoft anmelden
|
||||
<?= __('login_ms') ?>
|
||||
</a>
|
||||
|
||||
<a href="?local=1" class="local-login-link">Mit Benutzername und Passwort anmelden</a>
|
||||
|
||||
<?php else: ?>
|
||||
<!-- Lokales Login-Formular -->
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label for="email">E-Mail</label>
|
||||
<input type="email" id="email" name="email" required autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Passwort</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn">Anmelden</button>
|
||||
</form>
|
||||
|
||||
<?php if ($m365Enabled): ?>
|
||||
<div class="divider"><span>oder</span></div>
|
||||
<a href="<?= htmlspecialchars($m365LoginUrl) ?>" class="btn btn-microsoft">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
|
||||
<path fill="#f35325" d="M1 1h10v10H1z"/>
|
||||
<path fill="#81bc06" d="M12 1h10v10H12z"/>
|
||||
<path fill="#05a6f0" d="M1 12h10v10H1z"/>
|
||||
<path fill="#ffba08" d="M12 12h10v10H12z"/>
|
||||
</svg>
|
||||
Mit Microsoft anmelden
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($publicAccess): ?>
|
||||
<a href="index.php" class="back-link">← Zurück zur Code-Erstellung</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if ($publicAccess): ?>
|
||||
<a href="index.php" class="back-link"><?= __('login_back') ?></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<script src="assets/global.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
151
reset_password.php
Normal file
151
reset_password.php
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
<?php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/includes/Database.php';
|
||||
require_once __DIR__ . '/includes/Auth.php';
|
||||
require_once __DIR__ . '/includes/I18n.php';
|
||||
|
||||
$auth = new Auth();
|
||||
if ($auth->isLoggedIn()) { header('Location: index.php'); exit; }
|
||||
I18n::init();
|
||||
|
||||
$db = Database::getInstance();
|
||||
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
|
||||
$logoUrl = $db->getSetting('logo_url', '');
|
||||
|
||||
$token = trim($_GET['token'] ?? '');
|
||||
$error = '';
|
||||
$success = '';
|
||||
$valid = false;
|
||||
$tokenRow = null;
|
||||
|
||||
if (empty($token)) {
|
||||
$error = __('reset_invalid');
|
||||
} else {
|
||||
$tokenRow = $db->fetchOne(
|
||||
"SELECT prt.*, u.email, u.name FROM password_reset_tokens prt
|
||||
JOIN users u ON prt.user_id = u.id
|
||||
WHERE prt.token = ? AND prt.used = 0 AND prt.expires_at > NOW()",
|
||||
[$token]
|
||||
);
|
||||
if (!$tokenRow) {
|
||||
$error = __('reset_invalid');
|
||||
} else {
|
||||
$valid = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($valid && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$newPw = $_POST['new_password'] ?? '';
|
||||
$confirm= $_POST['confirm_password'] ?? '';
|
||||
|
||||
if (strlen($newPw) < 8) {
|
||||
$error = __('settings_pw_minlength');
|
||||
$valid = true; // keep form visible
|
||||
} elseif ($newPw !== $confirm) {
|
||||
$error = 'Passwörter stimmen nicht überein';
|
||||
$valid = true;
|
||||
} else {
|
||||
$hash = password_hash($newPw, PASSWORD_DEFAULT);
|
||||
$db->execute("UPDATE users SET password_hash = ? WHERE id = ?", [$hash, $tokenRow['user_id']]);
|
||||
$db->execute("UPDATE password_reset_tokens SET used = 1 WHERE token = ?", [$token]);
|
||||
|
||||
$db->execute(
|
||||
"INSERT INTO audit_log (user_id, action, entity_type, entity_id, details, ip_address) VALUES (?, 'password_reset', 'user', ?, 'Passwort erfolgreich geändert', ?)",
|
||||
[$tokenRow['user_id'], $tokenRow['user_id'], $_SERVER['REMOTE_ADDR'] ?? '']
|
||||
);
|
||||
|
||||
$success = __('reset_done');
|
||||
$valid = false;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?= I18n::getLanguage() ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= __('reset_new_pw') ?> – <?= htmlspecialchars($appTitle) ?></title>
|
||||
<link rel="stylesheet" href="assets/global.css">
|
||||
<script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; }
|
||||
.box { background: var(--bg-card); border-radius: 20px; box-shadow: 0 20px 60px var(--shadow-lg); max-width: 420px; width: 100%; padding: 45px 40px; text-align: center; }
|
||||
.logo { max-width: 180px; height: auto; margin-bottom: 25px; }
|
||||
h1 { color: var(--text-primary); font-size: 24px; margin-bottom: 8px; }
|
||||
.subtitle { color: var(--text-muted); font-size: 14px; margin-bottom: 28px; }
|
||||
.form-group { margin-bottom: 18px; text-align: left; }
|
||||
label { display: block; margin-bottom: 7px; color: var(--text-secondary); font-weight: 500; font-size: 14px; }
|
||||
input[type="password"] { width: 100%; padding: 13px; border: 2px solid var(--border-color); border-radius: 10px; font-size: 15px; background: var(--bg-input); color: var(--text-primary); transition: border-color 0.2s; }
|
||||
input:focus { outline: none; border-color: var(--accent); }
|
||||
.btn { width: 100%; padding: 14px; background: var(--accent); color: white; border: none; border-radius: 10px; font-size: 15px; font-weight: 600; cursor: pointer; transition: all 0.2s; margin-top: 8px; }
|
||||
.btn:hover { background: var(--accent-hover); transform: translateY(-2px); }
|
||||
.alert { padding: 12px 16px; border-radius: 8px; margin-bottom: 20px; font-size: 14px; text-align: left; }
|
||||
.alert-error { background: #fee; border: 1px solid #fcc; color: #c33; }
|
||||
.alert-success { background: #efe; border: 1px solid #cfc; color: #3c3; }
|
||||
.back-link { display: block; margin-top: 22px; color: var(--accent); text-decoration: none; font-size: 14px; }
|
||||
.back-link:hover { text-decoration: underline; }
|
||||
.pw-strength { height: 4px; border-radius: 2px; margin-top: 6px; transition: all 0.3s; background: var(--border-color); }
|
||||
.pw-strength.weak { background: var(--danger); width: 30%; }
|
||||
.pw-strength.medium { background: var(--warning); width: 65%; }
|
||||
.pw-strength.strong { background: var(--success); width: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="box">
|
||||
<?php if ($logoUrl): ?>
|
||||
<img src="<?= htmlspecialchars($logoUrl) ?>" alt="Logo" class="logo">
|
||||
<?php else: ?>
|
||||
<div style="font-size:28px;font-weight:700;color:var(--text-primary);margin-bottom:10px;"><?= htmlspecialchars($appTitle) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<h1><?= __('reset_new_pw') ?></h1>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-error"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><?= htmlspecialchars($success) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($valid): ?>
|
||||
<p class="subtitle">Für <strong><?= htmlspecialchars($tokenRow['email']) ?></strong></p>
|
||||
<form method="post" action="reset_password.php?token=<?= htmlspecialchars($token) ?>">
|
||||
<div class="form-group">
|
||||
<label><?= __('reset_new_pw_label') ?></label>
|
||||
<input type="password" name="new_password" id="pw" required minlength="8" oninput="checkPw(this.value)">
|
||||
<div class="pw-strength" id="pwBar"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label><?= __('reset_confirm_label') ?></label>
|
||||
<input type="password" name="confirm_password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn"><?= __('reset_set_btn') ?></button>
|
||||
</form>
|
||||
<?php elseif ($success): ?>
|
||||
<a href="login.php" class="btn" style="display:block;text-decoration:none;text-align:center;"><?= __('btn_login') ?></a>
|
||||
<?php endif; ?>
|
||||
|
||||
<a href="login.php" class="back-link"><?= __('reset_back_login') ?></a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function checkPw(val) {
|
||||
const bar = document.getElementById('pwBar');
|
||||
if (!bar) return;
|
||||
if (val.length >= 12 && /[A-Z]/.test(val) && /[0-9]/.test(val)) {
|
||||
bar.className = 'pw-strength strong';
|
||||
} else if (val.length >= 8) {
|
||||
bar.className = 'pw-strength medium';
|
||||
} else if (val.length > 0) {
|
||||
bar.className = 'pw-strength weak';
|
||||
} else {
|
||||
bar.className = 'pw-strength';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue