diff --git a/Readme.md b/Readme.md index 8cc7143..a9725d3 100644 --- a/Readme.md +++ b/Readme.md @@ -10,7 +10,7 @@ Entwickelt von **[Loheide.eu](https://loheide.eu)** ![MySQL](https://img.shields.io/badge/MySQL-5.7%2B%20%2F%20MariaDB-4479A1?logo=mysql&logoColor=white) ![UniFi OS](https://img.shields.io/badge/UniFi%20OS-7.0%2B-0559C9?logo=ubiquiti&logoColor=white) ![License](https://img.shields.io/badge/Lizenz-MIT-green) -![Version](https://img.shields.io/badge/Version-2.7.0-blueviolet) +![Version](https://img.shields.io/badge/Version-2.6.0-blueviolet) ![Tests](https://img.shields.io/badge/Tests-PHPUnit%20%2B%20PHPStan-brightgreen) [![Repository](https://img.shields.io/badge/Code-git.loheide.cloud-4b3ec4)](https://git.loheide.cloud/friloo/Unifi-Voucher-Tool) @@ -28,7 +28,6 @@ Entwickelt von **[Loheide.eu](https://loheide.eu)** ## ✨ Features - 🎟️ **Voucher-Erstellung** mit sofortiger QR-Code-Anzeige, Druckvorlage und E-Mail-Versand -- 🖥️ **Display-Seiten (Kiosk)** – öffentliche Seite je Site, an der Gäste sich mit einem Klick selbst einen Zugang holen - 📦 **Bulk-Erstellung** – bis zu 20 Vouchers auf einmal, inkl. Sammeldruck-Layout - 🧩 **Voucher-Profile/Templates** – vordefinierte Laufzeiten & Gerätelimits per Schnellauswahl - 🏢 **Multi-Site-Support** – beliebig viele UniFi-Standorte zentral verwalten @@ -84,13 +83,6 @@ Entwickelt von **[Loheide.eu](https://loheide.eu)** Einstellungen der Login-Seite -### Display-Seite für Gäste - -
- Display-Seite im Ruhezustand - Ausgegebener Zugangscode auf dem Display -
-
Voucher-Ergebnis mit QR-Code Bulk-Voucher-Erstellung @@ -222,47 +214,6 @@ Während eines Updates wird die Anwendung kurz in den **Wartungsmodus** versetzt --- -## 🖥️ Display-Seiten für Gäste - -Für Empfang, Lobby oder Tagungsraum lässt sich je Site eine **öffentliche Seite** -anlegen, die auf einem Bildschirm oder Tablet läuft. Gäste tippen auf einen -Knopf und bekommen sofort einen eigenen Zugangscode – ohne Anmeldung, ohne -Personal am Tresen. - -**Anlegen:** Administration → **Display-Seiten** → *Display-Seite anlegen* - -
- Verwaltung der Display-Seiten -
- -| Einstellung | Wirkung | -|---|---| -| Site | für welchen Standort die Codes erzeugt werden | -| Voucher-Profil | Laufzeit, Geräteanzahl und Bandbreite der Codes (leer = Standardwerte) | -| Überschrift / Text | was auf dem Bildschirm steht | -| Codes pro Tag | Obergrenze je Kalendertag (0 = unbegrenzt) | -| Wartezeit | Abstand zwischen zwei Codes an diesem Display | -| Anzeigedauer | danach springt der Bildschirm automatisch zurück | - -Jede Seite hat einen **eigenen, geheimen Link** (`kiosk.php?k=…`). Er lässt sich -kopieren, als QR-Code anzeigen (praktisch, um ihn am Tablet zu öffnen) und -jederzeit erneuern – der alte Link ist dann sofort ungültig. Den Link nicht -öffentlich verbreiten: wer ihn hat, kann im Rahmen der Limits Codes ziehen. - -Auf dem Startbildschirm steht zusätzlich ein QR-Code, der auf dieselbe Seite -zeigt. Gäste können sie damit **am eigenen Handy** öffnen – praktisch bei -Bildschirmen ohne Touch. - -Die ausgegebenen Codes erscheinen normal in *Live Vouchers*, im *Reporting* und -im *Audit-Log* (Aktion „Voucher am Display geholt"), sodass jederzeit -nachvollziehbar bleibt, woher ein Zugang stammt. - -> Display-Seiten funktionieren unabhängig vom globalen öffentlichen Modus – der -> geheime Link ist der Zugang. Webhook-Benachrichtigungen werden für diese Codes -> bewusst **nicht** ausgelöst, sonst wäre der Slack-Kanal voll. - ---- - ## ⚙️ Konfiguration ### `config.php` @@ -654,13 +605,12 @@ dem Git-Hoster. - [x] Branding über die Oberfläche (Farben, Logo, Login-Seite) - [x] Assets lokal ausliefern (keine Drittanbieter-CDNs) - [x] Vollständige englische Übersetzung des Admin-Bereichs -- [x] Display-Seiten: Selbstbedienung für Gäste am Bildschirm ---
-**Version 2.7.0** · Autor: **Friederich Loheide** · Lizenz: **MIT** +**Version 2.6.0** · Autor: **Friederich Loheide** · Lizenz: **MIT** Entwickelt von **[Loheide.eu](https://loheide.eu)** diff --git a/VERSION b/VERSION index 24ba9a3..e70b452 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.7.0 +2.6.0 diff --git a/admin/audit_log.php b/admin/audit_log.php index 8edff87..52d2937 100644 --- a/admin/audit_log.php +++ b/admin/audit_log.php @@ -50,8 +50,7 @@ $actionLabels = []; foreach (['voucher_created', 'voucher_bulk', 'user_login', 'user_logout', 'user_created', 'user_updated', 'user_deleted', 'site_added', 'site_updated', 'site_deleted', 'settings_saved', 'password_reset', 'template_created', 'template_updated', - 'template_deleted', 'voucher_kiosk', 'kiosk_created', 'kiosk_updated', - 'kiosk_deleted'] as $action) { + 'template_deleted'] as $action) { $actionLabels[$action] = __('audit_action_' . $action); } ?> diff --git a/admin/kiosks.php b/admin/kiosks.php deleted file mode 100644 index 5597422..0000000 --- a/admin/kiosks.php +++ /dev/null @@ -1,411 +0,0 @@ -requireAdmin(); -I18n::init(); - -$db = Database::getInstance(); -$appTitle = $db->getSetting('app_title', 'UniFi Voucher System'); - -$error = ''; -$success = ''; - -/** Formularwerte einsammeln – für Anlegen und Bearbeiten identisch. */ -function kioskInput(): array -{ - return [ - 'site_id' => (int)($_POST['site_id'] ?? 0), - 'template_id' => (int)($_POST['template_id'] ?? 0) ?: null, - 'name' => trim((string)($_POST['name'] ?? '')), - 'headline' => trim((string)($_POST['headline'] ?? '')), - 'subline' => trim((string)($_POST['subline'] ?? '')), - 'daily_limit' => max(0, (int)($_POST['daily_limit'] ?? Kiosk::DEFAULT_DAILY_LIMIT)), - 'cooldown_seconds' => max(0, min(3600, (int)($_POST['cooldown_seconds'] ?? Kiosk::DEFAULT_COOLDOWN))), - 'display_seconds' => max(10, min(600, (int)($_POST['display_seconds'] ?? Kiosk::DEFAULT_DISPLAY_SECONDS))), - 'is_active' => isset($_POST['is_active']) ? 1 : 0, - ]; -} - -if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_kiosk'])) { - if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) { - $error = __('error_csrf'); - } else { - try { - $in = kioskInput(); - if ($in['name'] === '') throw new Exception(__('error_name_req')); - if ($in['site_id'] <= 0) throw new Exception(__('error_site_req')); - - $db->execute( - "INSERT INTO kiosks (site_id, template_id, name, token, headline, subline, daily_limit, cooldown_seconds, display_seconds, is_active, created_by) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)", - [$in['site_id'], $in['template_id'], $in['name'], Kiosk::newToken(), - $in['headline'], $in['subline'], $in['daily_limit'], $in['cooldown_seconds'], - $in['display_seconds'], $_SESSION['user_id']] - ); - $auth->writeAuditLog($_SESSION['user_id'], 'kiosk_created', 'kiosk', null, $in['name']); - $success = __('kiosks_added'); - } catch (Exception $e) { - $error = $e->getMessage(); - } - } -} - -if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['edit_kiosk'])) { - if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) { - $error = __('error_csrf'); - } else { - try { - $id = (int)($_POST['kiosk_id'] ?? 0); - $in = kioskInput(); - if ($in['name'] === '') throw new Exception(__('error_name_req')); - if ($in['site_id'] <= 0) throw new Exception(__('error_site_req')); - - $db->execute( - "UPDATE kiosks SET site_id=?, template_id=?, name=?, headline=?, subline=?, - daily_limit=?, cooldown_seconds=?, display_seconds=?, is_active=? - WHERE id=?", - [$in['site_id'], $in['template_id'], $in['name'], $in['headline'], $in['subline'], - $in['daily_limit'], $in['cooldown_seconds'], $in['display_seconds'], $in['is_active'], $id] - ); - $auth->writeAuditLog($_SESSION['user_id'], 'kiosk_updated', 'kiosk', $id, $in['name']); - $success = __('kiosks_updated'); - } catch (Exception $e) { - $error = $e->getMessage(); - } - } -} - -// Neuen Link erzeugen – der alte gilt damit sofort nicht mehr. -if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['renew_token'])) { - if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) { - $error = __('error_csrf'); - } else { - $id = (int)($_POST['kiosk_id'] ?? 0); - $db->execute("UPDATE kiosks SET token = ? WHERE id = ?", [Kiosk::newToken(), $id]); - $auth->writeAuditLog($_SESSION['user_id'], 'kiosk_updated', 'kiosk', $id, 'Link erneuert'); - $success = __('kiosks_token_renewed'); - } -} - -if (isset($_GET['delete'], $_GET['token'])) { - if ($auth->validateCsrfToken($_GET['token'])) { - $db->execute("DELETE FROM kiosks WHERE id = ?", [(int)$_GET['delete']]); - $auth->writeAuditLog($_SESSION['user_id'], 'kiosk_deleted', 'kiosk', (int)$_GET['delete'], ''); - $success = __('kiosks_deleted'); - } else { - $error = __('error_csrf'); - } -} - -$sites = $db->fetchAll("SELECT id, name FROM sites WHERE is_active = 1 ORDER BY name"); -$templates = $db->fetchAll("SELECT id, name, max_uses, expire_minutes FROM voucher_templates WHERE is_active = 1 ORDER BY name"); -$kiosks = $db->fetchAll( - "SELECT k.*, s.name AS site_name, t.name AS template_name, - (SELECT COUNT(*) FROM vouchers v WHERE v.kiosk_id = k.id) AS total_vouchers, - (SELECT COUNT(*) FROM vouchers v WHERE v.kiosk_id = k.id AND DATE(v.created_at) = CURDATE()) AS today_vouchers - FROM kiosks k - INNER JOIN sites s ON s.id = k.site_id - LEFT JOIN voucher_templates t ON t.id = k.template_id - ORDER BY k.is_active DESC, k.name" -); - -$csrf = $auth->getCsrfToken(); -$currentPage = 'kiosks'; -$adminBase = ''; -?> - - - - - -<?= __('kiosks_title') ?> – <?= htmlspecialchars($appTitle) ?> - - - - - -
-
- - -
-
-

- - - -
- -
-
-

- -
- -
- - -
-
-
-
-
-
- - - -
- -
-
- - -
-
- - 0 ? ' / ' . (int)$k['daily_limit'] : '' ?> - -
-
- - -
-
- - - - -
- - - - - -
- - - -
- - - -
-
- -
- - - - - - - - - - -
- - - - diff --git a/assets/global.css b/assets/global.css index 4de34b5..ce41678 100644 --- a/assets/global.css +++ b/assets/global.css @@ -1610,128 +1610,6 @@ input.search-bar, .site-selector .search-bar { min-width: 240px; width: auto; fl } } -/* ========================================================================= - 16. KIOSK – öffentliche Display-Seite - Große Typografie: der Code muss aus einigen Metern Entfernung lesbar sein. - ========================================================================= */ -.kiosk-body { - min-height: 100vh; - display: flex; - flex-direction: column; - background: - radial-gradient(900px 500px at 12% -10%, var(--accent-soft), transparent 62%), - radial-gradient(700px 460px at 100% 0%, rgba(139,92,246,.10), transparent 64%), - var(--bg-body); -} -.kiosk-stage { - flex: 1; - display: flex; - align-items: center; - justify-content: center; - padding: 40px 24px; -} -.kiosk-card { - width: 100%; - max-width: 680px; - padding: 48px 44px; - text-align: center; - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--r-xl); - box-shadow: var(--shadow-xl); -} -.kiosk-logo { max-height: 84px; max-width: 320px; margin: 0 auto 26px; display: block; } -.kiosk-mark { width: 62px; height: 62px; margin: 0 auto 26px; font-size: 26px; } -.kiosk-headline { - font-size: clamp(30px, 4.6vw, 46px); - line-height: 1.15; - letter-spacing: -0.03em; -} -.kiosk-subline { - margin-top: 14px; - font-size: clamp(16px, 1.8vw, 20px); - color: var(--text-secondary); -} -.kiosk-alert { justify-content: center; margin: 24px 0 0; text-align: left; } -.kiosk-button { - width: 100%; - margin-top: 34px; - padding: 26px 32px; - gap: 14px; - font-size: clamp(20px, 2.4vw, 26px); - font-weight: 620; - border-radius: var(--r-lg); -} -.kiosk-button i { font-size: 0.95em; } -.kiosk-phone { - display: flex; - align-items: center; - justify-content: center; - gap: 16px; - margin-top: 34px; - padding-top: 26px; - border-top: 1px solid var(--border-color); - color: var(--text-muted); - font-size: 13.5px; -} -.kiosk-phone-qr { line-height: 0; } -.kiosk-phone-qr img, .kiosk-phone-qr canvas { border-radius: var(--r-sm); } - -.kiosk-result { max-width: 880px; } -.kiosk-eyebrow { - display: inline-flex; align-items: center; gap: 9px; - padding: 6px 16px; - border-radius: var(--r-pill); - background: var(--success-soft); - border: 1px solid var(--success-border); - color: var(--success); - font-size: 14px; font-weight: 600; -} -.kiosk-code { - margin: 26px 0 18px; - font-family: var(--font-mono); - /* Muss aus einigen Metern lesbar sein, aber in einer Zeile bleiben. */ - font-size: clamp(38px, 7vw, 76px); - font-weight: 700; - letter-spacing: .06em; - white-space: nowrap; - line-height: 1.05; - color: var(--text-primary); - word-break: break-word; -} -.kiosk-meta { - display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; - font-size: 15px; color: var(--text-secondary); -} -.kiosk-meta span { - display: inline-flex; align-items: center; gap: 8px; - padding: 6px 14px; - background: var(--bg-subtle); - border: 1px solid var(--border-color); - border-radius: var(--r-pill); -} -.kiosk-qr { margin: 30px 0 8px; } -.kiosk-qr #qrcode { - display: inline-block; - padding: 16px; - background: #fff; - border-radius: var(--r-lg); - box-shadow: var(--shadow-sm); - line-height: 0; -} -.kiosk-qr-label { margin-top: 14px; font-size: 15px; color: var(--text-secondary); } -.kiosk-countdown { margin: 18px 0 22px; font-size: 13.5px; color: var(--text-muted); } -.kiosk-footer { padding: 0 24px 22px; text-align: center; } - -/* Link-Zeile in der Kiosk-Verwaltung */ -.kiosk-link-row { display: flex; gap: 8px; align-items: center; } -.kiosk-link-row .input { font-family: var(--font-mono); font-size: 12px; } - -@media (max-width: 560px) { - .kiosk-card { padding: 32px 22px; } - .kiosk-phone { flex-direction: column; } -} - /* ========================================================================= 13. RESPONSIVE ========================================================================= */ diff --git a/database.sql b/database.sql index 4d7fcc0..6dfa5d4 100644 --- a/database.sql +++ b/database.sql @@ -66,29 +66,6 @@ CREATE TABLE IF NOT EXISTS `voucher_templates` ( FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -CREATE TABLE IF NOT EXISTS `kiosks` ( - `id` INT PRIMARY KEY AUTO_INCREMENT, - `site_id` INT NOT NULL, - `template_id` INT NULL, - `name` VARCHAR(255) NOT NULL, - `token` VARCHAR(64) NOT NULL, - `headline` VARCHAR(255) NULL, - `subline` VARCHAR(500) NULL, - `is_active` TINYINT(1) NOT NULL DEFAULT 1, - `daily_limit` INT NOT NULL DEFAULT 100, - `cooldown_seconds` INT NOT NULL DEFAULT 20, - `display_seconds` INT NOT NULL DEFAULT 90, - `last_used_at` TIMESTAMP NULL, - `created_by` INT NULL, - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - UNIQUE KEY `uniq_token` (`token`), - INDEX `idx_site` (`site_id`), - FOREIGN KEY (`site_id`) REFERENCES `sites`(`id`) ON DELETE CASCADE, - FOREIGN KEY (`template_id`) REFERENCES `voucher_templates`(`id`) ON DELETE SET NULL, - FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON DELETE SET NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - CREATE TABLE IF NOT EXISTS `api_keys` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(255) NOT NULL, @@ -116,7 +93,6 @@ CREATE TABLE IF NOT EXISTS `vouchers` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `site_id` INT NOT NULL, `user_id` INT, - `kiosk_id` INT NULL, `voucher_code` VARCHAR(50) NOT NULL, `voucher_name` VARCHAR(255) NOT NULL, `max_uses` INT NOT NULL, @@ -133,8 +109,7 @@ CREATE TABLE IF NOT EXISTS `vouchers` ( INDEX `idx_site` (`site_id`), INDEX `idx_created` (`created_at`), INDEX `idx_unifi_id` (`unifi_voucher_id`), - INDEX `idx_status` (`status`), - INDEX `idx_kiosk` (`kiosk_id`) + INDEX `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS `sessions` ( diff --git a/docs/screenshots/admin-dashboard-dark.png b/docs/screenshots/admin-dashboard-dark.png index e81cd9e..24cb92e 100644 Binary files a/docs/screenshots/admin-dashboard-dark.png and b/docs/screenshots/admin-dashboard-dark.png differ diff --git a/docs/screenshots/admin-dashboard.png b/docs/screenshots/admin-dashboard.png index b7e1fe6..006348c 100644 Binary files a/docs/screenshots/admin-dashboard.png and b/docs/screenshots/admin-dashboard.png differ diff --git a/docs/screenshots/api-keys.png b/docs/screenshots/api-keys.png index 0594942..c8664c7 100644 Binary files a/docs/screenshots/api-keys.png and b/docs/screenshots/api-keys.png differ diff --git a/docs/screenshots/integrations.png b/docs/screenshots/integrations.png index 62c4171..57edb8d 100644 Binary files a/docs/screenshots/integrations.png and b/docs/screenshots/integrations.png differ diff --git a/docs/screenshots/kiosk-code.png b/docs/screenshots/kiosk-code.png deleted file mode 100644 index f961cfc..0000000 Binary files a/docs/screenshots/kiosk-code.png and /dev/null differ diff --git a/docs/screenshots/kiosk-display.png b/docs/screenshots/kiosk-display.png deleted file mode 100644 index b786146..0000000 Binary files a/docs/screenshots/kiosk-display.png and /dev/null differ diff --git a/docs/screenshots/kiosks-admin.png b/docs/screenshots/kiosks-admin.png deleted file mode 100644 index 19d8595..0000000 Binary files a/docs/screenshots/kiosks-admin.png and /dev/null differ diff --git a/docs/screenshots/settings-branding.png b/docs/screenshots/settings-branding.png index aa99af6..b826df4 100644 Binary files a/docs/screenshots/settings-branding.png and b/docs/screenshots/settings-branding.png differ diff --git a/docs/screenshots/settings-login.png b/docs/screenshots/settings-login.png index a69a747..cdc97f9 100644 Binary files a/docs/screenshots/settings-login.png and b/docs/screenshots/settings-login.png differ diff --git a/docs/screenshots/settings.png b/docs/screenshots/settings.png index fe7f67f..aabd60f 100644 Binary files a/docs/screenshots/settings.png and b/docs/screenshots/settings.png differ diff --git a/docs/screenshots/two-factor.png b/docs/screenshots/two-factor.png index 0507434..809a849 100644 Binary files a/docs/screenshots/two-factor.png and b/docs/screenshots/two-factor.png differ diff --git a/docs/screenshots/vouchers.png b/docs/screenshots/vouchers.png index a81b909..38d79d9 100644 Binary files a/docs/screenshots/vouchers.png and b/docs/screenshots/vouchers.png differ diff --git a/includes/Kiosk.php b/includes/Kiosk.php deleted file mode 100644 index 847da0d..0000000 --- a/includes/Kiosk.php +++ /dev/null @@ -1,140 +0,0 @@ -fetchOne( - "SELECT k.*, s.name AS site_name, s.is_active AS site_active, - t.name AS template_name, t.max_uses AS tpl_max_uses, t.expire_minutes AS tpl_expire_minutes, - t.qos_rate_max_down, t.qos_rate_max_up, t.qos_usage_quota - FROM kiosks k - INNER JOIN sites s ON s.id = k.site_id - LEFT JOIN voucher_templates t ON t.id = k.template_id - WHERE k.token = ? AND k.is_active = 1", - [$token] - ); - - if (!$row || (int)$row['site_active'] !== 1) { - return null; - } - - return $row; - } - - /** Wie viele Codes hat dieser Kiosk heute schon ausgegeben? */ - public static function usedToday($db, int $kioskId): int - { - $row = $db->fetchOne( - "SELECT COUNT(*) AS c FROM vouchers WHERE kiosk_id = ? AND DATE(created_at) = CURDATE()", - [$kioskId] - ); - - return (int)($row['c'] ?? 0); - } - - /** - * Darf gerade ein Code geholt werden? - * - * @return array{allowed:bool,reason:string,wait:int} - * reason: '' | 'cooldown' | 'daily_limit' - */ - public static function checkLimits($db, array $kiosk): array - { - $cooldown = max(0, (int)$kiosk['cooldown_seconds']); - if ($cooldown > 0 && !empty($kiosk['last_used_at'])) { - $elapsed = time() - strtotime((string)$kiosk['last_used_at']); - if ($elapsed >= 0 && $elapsed < $cooldown) { - return ['allowed' => false, 'reason' => 'cooldown', 'wait' => $cooldown - $elapsed]; - } - } - - $limit = max(0, (int)$kiosk['daily_limit']); - if ($limit > 0 && self::usedToday($db, (int)$kiosk['id']) >= $limit) { - return ['allowed' => false, 'reason' => 'daily_limit', 'wait' => 0]; - } - - return ['allowed' => true, 'reason' => '', 'wait' => 0]; - } - - /** Nach erfolgreicher Ausgabe den Zeitstempel fortschreiben. */ - public static function markUsed($db, int $kioskId): void - { - $db->execute("UPDATE kiosks SET last_used_at = NOW() WHERE id = ?", [$kioskId]); - } - - /** - * Voucher-Eckdaten eines Kiosks: entweder aus dem verknüpften Profil - * oder aus den globalen Standardwerten. - */ - public static function voucherSettings($db, array $kiosk): array - { - $maxUses = (int)($kiosk['tpl_max_uses'] ?? 0); - $expire = (int)($kiosk['tpl_expire_minutes'] ?? 0); - - if ($maxUses < 1) { - $maxUses = max(1, (int)$db->getSetting('default_max_uses', 1)); - } - if ($expire < 1) { - $expire = max(1, (int)$db->getSetting('default_expire_minutes', 480)); - } - - return [ - 'max_uses' => $maxUses, - 'expire_minutes' => $expire, - 'qos' => [ - 'down' => max(0, (int)($kiosk['qos_rate_max_down'] ?? 0)), - 'up' => max(0, (int)($kiosk['qos_rate_max_up'] ?? 0)), - 'quota_mb' => max(0, (int)($kiosk['qos_usage_quota'] ?? 0)), - ], - ]; - } - - /** Öffentliche Adresse eines Kiosks. */ - public static function publicUrl(string $token, string $baseUrl = ''): string - { - if ($baseUrl === '') { - $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; - $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; - $path = dirname($_SERVER['SCRIPT_NAME'] ?? '/', 2); - $path = $path === '/' || $path === '\\' ? '' : $path; - $baseUrl = $protocol . '://' . $host . $path; - } - - return rtrim($baseUrl, '/') . '/kiosk.php?k=' . $token; - } -} diff --git a/includes/VoucherService.php b/includes/VoucherService.php deleted file mode 100644 index 9ddcaa7..0000000 --- a/includes/VoucherService.php +++ /dev/null @@ -1,63 +0,0 @@ - kbit, 'up' => kbit, 'quota_mb' => MB] - * @param int|null $userId angemeldeter Benutzer, sonst null - * @param int|null $kioskId Herkunft, falls ueber eine Display-Seite geholt - * - * @return array{code:string,site_name:string,max_uses:int,expire_min:int,expiry_date:string,expiry_time:string} - * @throws Exception wenn der Controller keinen gueltigen Voucher liefert - */ - public static function create( - $db, - array $site, - string $voucherName, - int $maxUses, - int $expireMinutes, - ?int $userId = null, - array $qos = [], - ?int $kioskId = null - ): array { - $fullName = date('Y-m-d') . '_' . $voucherName; - - $controller = new UniFiController( - $site['unifi_controller_url'], - $site['unifi_username'], - Crypto::decrypt($site['unifi_password']), - $site['site_id'] - ); - - $voucher = $controller->createVoucher($fullName, $maxUses, $expireMinutes, $qos); - if (!is_array($voucher) || empty($voucher['formatted_code'])) { - throw new Exception(function_exists('__') ? __('error_voucher_invalid') : 'Ungueltige Antwort des Controllers'); - } - - $db->execute( - "INSERT INTO vouchers (site_id, user_id, kiosk_id, voucher_code, voucher_name, max_uses, expire_minutes, unifi_voucher_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - [$site['id'], $userId, $kioskId, $voucher['code'], $fullName, $maxUses, $expireMinutes, $voucher['unifi_id'] ?? null] - ); - - $expiryTs = time() + ($expireMinutes * 60); - - return [ - 'code' => $voucher['formatted_code'], - 'site_name' => $site['name'], - 'max_uses' => $maxUses, - 'expire_min' => $expireMinutes, - 'expiry_date' => date('d.m.Y', $expiryTs), - 'expiry_time' => date('H:i', $expiryTs), - ]; - } -} diff --git a/includes/admin_nav.php b/includes/admin_nav.php index 167c9ff..5a5a5a1 100644 --- a/includes/admin_nav.php +++ b/includes/admin_nav.php @@ -23,7 +23,6 @@ $navGroups = [ ['vouchers', 'vouchers.php', 'fa-ticket', 'nav_vouchers'], ['templates', 'templates.php', 'fa-layer-group', 'nav_templates'], ['import', 'import.php', 'fa-file-arrow-up', 'nav_import'], - ['kiosks', 'kiosks.php', 'fa-display', 'nav_kiosks'], ['sites', 'sites.php', 'fa-location-dot', 'nav_sites'], ['users', 'users.php', 'fa-users', 'nav_users'], ], diff --git a/index.php b/index.php index 020c796..5b0b516 100644 --- a/index.php +++ b/index.php @@ -20,7 +20,6 @@ require_once __DIR__ . '/includes/Notifier.php'; require_once __DIR__ . '/includes/Captcha.php'; require_once __DIR__ . '/includes/Sms.php'; require_once __DIR__ . '/includes/Ui.php'; -require_once __DIR__ . '/includes/VoucherService.php'; require_once __DIR__ . '/includes/I18n.php'; $auth = new Auth(); @@ -116,9 +115,34 @@ if ($auth->isLoggedIn()) { $autoSelectSite = (count($sites) === 1) ? $sites[0]['id'] : 0; -// Voucher-Erstellung liegt gebuendelt in includes/VoucherService.php. +// Helper: create one voucher and save to DB function doCreateVoucher($db, $site, $voucherName, $maxUses, $expireMinutes, $userId, $qos = []) { - return VoucherService::create($db, $site, $voucherName, $maxUses, $expireMinutes, $userId, $qos); + $datum = date('Y-m-d'); + $fullName = $datum . '_' . $voucherName; + $controller = new UniFiController( + $site['unifi_controller_url'], + $site['unifi_username'], + Crypto::decrypt($site['unifi_password']), + $site['site_id'] + ); + $voucher = $controller->createVoucher($fullName, $maxUses, $expireMinutes, $qos); + if (!is_array($voucher) || empty($voucher['formatted_code'])) { + throw new Exception(__('error_voucher_invalid')); + } + $db->execute( + "INSERT INTO vouchers (site_id, user_id, voucher_code, voucher_name, max_uses, expire_minutes, unifi_voucher_id) + VALUES (?, ?, ?, ?, ?, ?, ?)", + [$site['id'], $userId, $voucher['code'], $fullName, $maxUses, $expireMinutes, $voucher['unifi_id'] ?? null] + ); + $expiryTs = time() + ($expireMinutes * 60); + return [ + 'code' => $voucher['formatted_code'], + 'site_name' => $site['name'], + 'max_uses' => $maxUses, + 'expire_min' => $expireMinutes, + 'expiry_date' => date('d.m.Y', $expiryTs), + 'expiry_time' => date('H:i', $expiryTs), + ]; } // Single voucher diff --git a/kiosk.php b/kiosk.php deleted file mode 100644 index 665651d..0000000 --- a/kiosk.php +++ /dev/null @@ -1,222 +0,0 @@ - - * - * Gedacht für ein Tablet oder einen Bildschirm im Empfangsbereich: ein großer - * Knopf, ein Klick, ein Zugangscode. Gäste ohne Zugriff auf den Bildschirm - * können denselben Link über den QR-Code am Handy öffnen. - */ -error_reporting(E_ALL); -ini_set('display_errors', 0); -ini_set('log_errors', 1); - -require_once __DIR__ . '/config.php'; -require_once __DIR__ . '/includes/Database.php'; -require_once __DIR__ . '/includes/Auth.php'; -require_once __DIR__ . '/includes/I18n.php'; -require_once __DIR__ . '/includes/Ui.php'; -require_once __DIR__ . '/includes/Kiosk.php'; -require_once __DIR__ . '/includes/VoucherService.php'; - -I18n::init(); - -try { - $db = Database::getInstance(); - $auth = new Auth(); -} catch (Exception $e) { - http_response_code(500); - die('Datenbankfehler'); -} - -$appTitle = $db->getSetting('app_title', 'UniFi Voucher System'); -$token = Kiosk::sanitizeToken($_GET['k'] ?? ''); -$kiosk = $token !== '' ? Kiosk::findByToken($db, $token) : null; - -if (!$kiosk) { - http_response_code(404); - $notFound = true; -} else { - $notFound = false; -} - -$voucher = null; // erzeugter Code -$error = ''; -$waitSecs = 0; - -if (!$notFound && $_SERVER['REQUEST_METHOD'] === 'POST') { - if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) { - $error = __('error_csrf'); - } else { - $limits = Kiosk::checkLimits($db, $kiosk); - if (!$limits['allowed']) { - $waitSecs = (int)$limits['wait']; - $error = $limits['reason'] === 'cooldown' - ? str_replace('{seconds}', (string)$waitSecs, __('kiosk_error_cooldown')) - : __('kiosk_error_limit'); - } else { - try { - $site = $db->fetchOne("SELECT * FROM sites WHERE id = ? AND is_active = 1", [(int)$kiosk['site_id']]); - if (!$site) { - throw new Exception(__('error_site_not_found')); - } - - $settings = Kiosk::voucherSettings($db, $kiosk); - $voucher = VoucherService::create( - $db, - $site, - $kiosk['name'], - $settings['max_uses'], - $settings['expire_minutes'], - null, - $settings['qos'], - (int)$kiosk['id'] - ); - - Kiosk::markUsed($db, (int)$kiosk['id']); - $auth->writeAuditLog(null, 'voucher_kiosk', 'kiosk', (int)$kiosk['id'], - $kiosk['name'] . ' · ' . $voucher['code']); - } catch (Exception $e) { - error_log('Kiosk-Fehler: ' . $e->getMessage()); - $error = __('kiosk_error_generic'); - } - } - } -} - -$headline = trim((string)($kiosk['headline'] ?? '')) ?: __('kiosk_default_headline'); -$subline = trim((string)($kiosk['subline'] ?? '')) ?: __('kiosk_default_subline'); -$logoUrl = $db->getSetting('logo_url', ''); -$display = max(10, (int)($kiosk['display_seconds'] ?? Kiosk::DEFAULT_DISPLAY_SECONDS)); -$selfUrl = $kiosk ? Kiosk::publicUrl($kiosk['token']) : ''; -?> - - - - - - - <?= htmlspecialchars($appTitle) ?> - - - - - - - - -
-
-
-
-

-

-
-
-
- -
-
-

-
-
- - - -
-
-
-

-
-

- ' . $display . '', __('kiosk_reset_in')) ?> -

- -
-
- -
-
- - - - - - -

-

- - -
- - -
- - -
- -
-
-

-
-
-
- - -
- - - - - - diff --git a/lang/de.php b/lang/de.php index c83e6a5..3a84eab 100644 --- a/lang/de.php +++ b/lang/de.php @@ -470,59 +470,6 @@ return [ 'print_valid_until' => 'Gültig bis', 'print_devices' => 'Geräte', 'credit_by' => 'Entwickelt von', - 'kiosk_default_headline' => 'Kostenloses Gäste-WLAN', - 'kiosk_default_subline' => 'Tippen Sie auf den Knopf – Sie erhalten sofort einen persönlichen Zugangscode.', - 'kiosk_button' => 'Zugangscode holen', - 'kiosk_working' => 'Einen Moment …', - 'kiosk_ready' => 'Ihr Zugangscode', - 'kiosk_scan_code' => 'QR-Code scannen oder Code eintippen', - 'kiosk_reset_in' => 'Der Bildschirm wird in {seconds} Sekunden zurückgesetzt.', - 'kiosk_done' => 'Fertig', - 'kiosk_phone_hint' => 'Oder mit dem Handy scannen und dort öffnen', - 'kiosk_error_cooldown' => 'Gerade wurde ein Code ausgegeben. Bitte {seconds} Sekunden warten.', - 'kiosk_error_limit' => 'Für heute sind keine Zugänge mehr verfügbar. Bitte wenden Sie sich an den Empfang.', - 'kiosk_error_generic' => 'Der Zugang konnte gerade nicht erstellt werden. Bitte erneut versuchen.', - 'kiosk_unknown' => 'Diese Seite ist nicht verfügbar', - 'kiosk_unknown_hint' => 'Der Link ist ungültig oder wurde deaktiviert.', - 'audit_action_voucher_kiosk' => 'Voucher am Display geholt', - 'nav_kiosks' => 'Display-Seiten', - 'kiosks_title' => 'Display-Seiten', - 'kiosks_subtitle' => 'Öffentliche Seiten für Bildschirme und Tablets – Gäste holen sich den Zugang selbst.', - 'kiosks_add' => 'Display-Seite anlegen', - 'kiosks_edit' => 'Display-Seite bearbeiten', - 'kiosks_empty' => 'Noch keine Display-Seite angelegt.', - 'kiosks_no_sites' => 'Legen Sie zuerst eine Site an, dann können Sie dafür eine Display-Seite erstellen.', - 'kiosks_name' => 'Bezeichnung', - 'kiosks_name_placeholder' => 'z.B. Empfang Erdgeschoss', - 'kiosks_name_hint' => 'Erscheint im Audit-Log und als Voucher-Name.', - 'kiosks_template' => 'Voucher-Profil', - 'kiosks_no_template' => 'Standardwerte verwenden', - 'kiosks_template_hint' => 'Bestimmt Laufzeit, Geräteanzahl und Bandbreite der ausgegebenen Codes.', - 'kiosks_headline' => 'Überschrift auf dem Bildschirm', - 'kiosks_subline' => 'Text darunter', - 'kiosks_daily_limit' => 'Codes pro Tag', - 'kiosks_daily_limit_hint' => '0 = unbegrenzt. Schützt vor Missbrauch, wenn der Link weitergegeben wird.', - 'kiosks_cooldown' => 'Wartezeit (Sekunden)', - 'kiosks_cooldown_hint' => 'Abstand zwischen zwei Codes an diesem Display.', - 'kiosks_display' => 'Anzeigedauer (Sekunden)', - 'kiosks_display_hint' => 'Danach springt der Bildschirm zurück auf den Startbildschirm.', - 'kiosks_active' => 'Display-Seite aktiv', - 'kiosks_link' => 'Öffentlicher Link', - 'kiosks_open' => 'Öffnen', - 'kiosks_qr' => 'QR-Code', - 'kiosks_qr_hint' => 'Am Bildschirm aufhängen oder abfotografieren, um die Seite auf einem Tablet zu öffnen.', - 'kiosks_today' => 'heute', - 'kiosks_total' => 'insgesamt', - 'kiosks_renew' => 'Link erneuern', - 'kiosks_renew_confirm' => 'Neuen Link erzeugen? Der bisherige Link funktioniert danach nicht mehr.', - 'kiosks_delete_confirm' => 'Display-Seite wirklich löschen?', - 'kiosks_added' => 'Display-Seite angelegt.', - 'kiosks_updated' => 'Display-Seite gespeichert.', - 'kiosks_deleted' => 'Display-Seite gelöscht.', - 'kiosks_token_renewed' => 'Neuer Link erzeugt – der alte ist ab sofort ungültig.', - 'audit_action_kiosk_created' => 'Display-Seite angelegt', - 'audit_action_kiosk_updated' => 'Display-Seite geändert', - 'audit_action_kiosk_deleted' => 'Display-Seite gelöscht', 'settings_tab_general' => 'Allgemein', 'settings_tab_defaults' => 'Voucher-Standards', 'settings_tab_cron' => 'Cron-Sync', diff --git a/lang/en.php b/lang/en.php index 11f1eeb..7a0ee39 100644 --- a/lang/en.php +++ b/lang/en.php @@ -470,59 +470,6 @@ return [ 'print_valid_until' => 'Valid until', 'print_devices' => 'devices', 'credit_by' => 'Developed by', - 'kiosk_default_headline' => 'Free guest Wi-Fi', - 'kiosk_default_subline' => 'Tap the button – you will get your personal access code right away.', - 'kiosk_button' => 'Get access code', - 'kiosk_working' => 'One moment…', - 'kiosk_ready' => 'Your access code', - 'kiosk_scan_code' => 'Scan the QR code or type the code', - 'kiosk_reset_in' => 'This screen resets in {seconds} seconds.', - 'kiosk_done' => 'Done', - 'kiosk_phone_hint' => 'Or scan with your phone and open it there', - 'kiosk_error_cooldown' => 'A code was just issued. Please wait {seconds} seconds.', - 'kiosk_error_limit' => 'No more access codes available today. Please ask at the reception desk.', - 'kiosk_error_generic' => 'The access code could not be created. Please try again.', - 'kiosk_unknown' => 'This page is not available', - 'kiosk_unknown_hint' => 'The link is invalid or has been deactivated.', - 'audit_action_voucher_kiosk' => 'Voucher taken at display', - 'nav_kiosks' => 'Display pages', - 'kiosks_title' => 'Display pages', - 'kiosks_subtitle' => 'Public pages for screens and tablets – guests get their access themselves.', - 'kiosks_add' => 'Add display page', - 'kiosks_edit' => 'Edit display page', - 'kiosks_empty' => 'No display page created yet.', - 'kiosks_no_sites' => 'Create a site first, then you can add a display page for it.', - 'kiosks_name' => 'Label', - 'kiosks_name_placeholder' => 'e.g. reception ground floor', - 'kiosks_name_hint' => 'Appears in the audit log and as the voucher name.', - 'kiosks_template' => 'Voucher profile', - 'kiosks_no_template' => 'Use default values', - 'kiosks_template_hint' => 'Defines duration, device count and bandwidth of the codes issued.', - 'kiosks_headline' => 'Headline on screen', - 'kiosks_subline' => 'Text below', - 'kiosks_daily_limit' => 'Codes per day', - 'kiosks_daily_limit_hint' => '0 = unlimited. Protects against misuse if the link gets shared.', - 'kiosks_cooldown' => 'Cooldown (seconds)', - 'kiosks_cooldown_hint' => 'Delay between two codes on this display.', - 'kiosks_display' => 'Display duration (seconds)', - 'kiosks_display_hint' => 'After that the screen returns to the start screen.', - 'kiosks_active' => 'Display page active', - 'kiosks_link' => 'Public link', - 'kiosks_open' => 'Open', - 'kiosks_qr' => 'QR code', - 'kiosks_qr_hint' => 'Put it up next to the screen or photograph it to open the page on a tablet.', - 'kiosks_today' => 'today', - 'kiosks_total' => 'in total', - 'kiosks_renew' => 'Renew link', - 'kiosks_renew_confirm' => 'Generate a new link? The previous link will stop working.', - 'kiosks_delete_confirm' => 'Really delete this display page?', - 'kiosks_added' => 'Display page created.', - 'kiosks_updated' => 'Display page saved.', - 'kiosks_deleted' => 'Display page deleted.', - 'kiosks_token_renewed' => 'New link generated – the old one is no longer valid.', - 'audit_action_kiosk_created' => 'Display page created', - 'audit_action_kiosk_updated' => 'Display page updated', - 'audit_action_kiosk_deleted' => 'Display page deleted', 'settings_tab_general' => 'General', 'settings_tab_defaults' => 'Voucher Defaults', 'settings_tab_cron' => 'Cron Sync', diff --git a/phpstan.neon b/phpstan.neon index 2d305ab..95edbd1 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,4 +9,3 @@ parameters: - includes/ApiKey.php - includes/Ui.php - includes/Upload.php - - includes/Kiosk.php diff --git a/tests/KioskTest.php b/tests/KioskTest.php deleted file mode 100644 index bdc19f3..0000000 --- a/tests/KioskTest.php +++ /dev/null @@ -1,155 +0,0 @@ - */ - private array $settings; - - /** @param array $settings */ - public function __construct(array $settings = []) - { - $this->settings = $settings; - } - - public function fetchOne($sql, $params = []) - { - return ['c' => $this->usedToday]; - } - - public function getSetting($key, $default = null) - { - return $this->settings[$key] ?? $default; - } -} - -/** - * Die Grenzen der Kiosk-Seite sind das, was sie vor Missbrauch schützt – - * der Link ist öffentlich, also muss diese Logik stimmen. - */ -class KioskTest extends TestCase -{ - /** @param array $overrides */ - private function kiosk(array $overrides = []): array - { - return array_merge([ - 'id' => 1, - 'daily_limit' => 100, - 'cooldown_seconds' => 20, - 'last_used_at' => null, - ], $overrides); - } - - public function testTokenHasFixedShape(): void - { - $token = \Kiosk::newToken(); - - $this->assertMatchesRegularExpression('/^[0-9a-f]{32}$/', $token); - $this->assertNotSame($token, \Kiosk::newToken(), 'Tokens dürfen sich nicht wiederholen'); - } - - public function testSanitizeTokenRejectsAnythingElse(): void - { - $valid = \Kiosk::newToken(); - - $this->assertSame($valid, \Kiosk::sanitizeToken($valid)); - $this->assertSame($valid, \Kiosk::sanitizeToken(strtoupper($valid))); - $this->assertSame('', \Kiosk::sanitizeToken('kurz')); - $this->assertSame('', \Kiosk::sanitizeToken("' OR 1=1 --")); - $this->assertSame('', \Kiosk::sanitizeToken(null)); - $this->assertSame('', \Kiosk::sanitizeToken($valid . 'ff')); - } - - public function testCooldownBlocksSecondCode(): void - { - $db = new FakeKioskDb(); - $kiosk = $this->kiosk(['last_used_at' => date('Y-m-d H:i:s', time() - 5)]); - - $result = \Kiosk::checkLimits($db, $kiosk); - - $this->assertFalse($result['allowed']); - $this->assertSame('cooldown', $result['reason']); - $this->assertGreaterThan(0, $result['wait']); - $this->assertLessThanOrEqual(20, $result['wait']); - } - - public function testCooldownExpires(): void - { - $db = new FakeKioskDb(); - $kiosk = $this->kiosk(['last_used_at' => date('Y-m-d H:i:s', time() - 60)]); - - $this->assertTrue(\Kiosk::checkLimits($db, $kiosk)['allowed']); - } - - public function testDailyLimitBlocks(): void - { - $db = new FakeKioskDb(); - $db->usedToday = 100; - - $result = \Kiosk::checkLimits($db, $this->kiosk()); - - $this->assertFalse($result['allowed']); - $this->assertSame('daily_limit', $result['reason']); - } - - public function testZeroMeansUnlimited(): void - { - $db = new FakeKioskDb(); - $db->usedToday = 5000; - - $kiosk = $this->kiosk(['daily_limit' => 0, 'cooldown_seconds' => 0]); - - $this->assertTrue(\Kiosk::checkLimits($db, $kiosk)['allowed']); - } - - public function testVoucherSettingsPreferTemplate(): void - { - $db = new FakeKioskDb(['default_max_uses' => '1', 'default_expire_minutes' => '480']); - $kiosk = $this->kiosk([ - 'tpl_max_uses' => 5, - 'tpl_expire_minutes' => 240, - 'qos_rate_max_down' => 20000, - 'qos_rate_max_up' => 5000, - 'qos_usage_quota' => 1024, - ]); - - $settings = \Kiosk::voucherSettings($db, $kiosk); - - $this->assertSame(5, $settings['max_uses']); - $this->assertSame(240, $settings['expire_minutes']); - $this->assertSame(20000, $settings['qos']['down']); - $this->assertSame(1024, $settings['qos']['quota_mb']); - } - - public function testVoucherSettingsFallBackToDefaults(): void - { - $db = new FakeKioskDb(['default_max_uses' => '3', 'default_expire_minutes' => '120']); - - $settings = \Kiosk::voucherSettings($db, $this->kiosk()); - - $this->assertSame(3, $settings['max_uses']); - $this->assertSame(120, $settings['expire_minutes']); - $this->assertSame(0, $settings['qos']['down']); - } - - public function testPublicUrl(): void - { - $token = \Kiosk::newToken(); - - $this->assertSame( - 'https://wlan.example.com/kiosk.php?k=' . $token, - \Kiosk::publicUrl($token, 'https://wlan.example.com/') - ); - } -} diff --git a/tools/demo/build.py b/tools/demo/build.py index b01afca..565c069 100755 --- a/tools/demo/build.py +++ b/tools/demo/build.py @@ -89,16 +89,6 @@ document.addEventListener('DOMContentLoaded', function () { patch(os.path.join(target, 'admin', 'api_keys.php'), '$keys = $db->fetchAll(', "if (($_GET['demo'] ?? '') === 'new') { $newKey = 'uvt_3f9a2c7d41e8b60592af18cc4d7e0b3a95f2617c'; }\n$keys = $db->fetchAll(") - # Kiosk: ausgegebenen Code zeigen, ohne echten Controller (?demo=code) - patch(os.path.join(target, 'kiosk.php'), - "$headline = trim((string)($kiosk['headline'] ?? ''))", - '''if (($_GET['demo'] ?? '') === 'code' && $kiosk) { - $voucher = ['code' => '4829-17364', 'site_name' => $kiosk['site_name'], 'max_uses' => 2, - 'expire_min' => 480, 'expiry_date' => '24.09.2026', 'expiry_time' => '08:00']; -} - -$headline = trim((string)($kiosk['headline'] ?? ''))''') - # Theme per Query-Parameter erzwingen (fuer Dark-Mode-Screenshots) ui = os.path.join(target, 'includes', 'Ui.php') patch(ui, 'var s=localStorage.getItem("theme");', diff --git a/tools/demo/overlay/includes/Database.php b/tools/demo/overlay/includes/Database.php index 9f2729e..6552806 100644 --- a/tools/demo/overlay/includes/Database.php +++ b/tools/demo/overlay/includes/Database.php @@ -58,22 +58,6 @@ class Database { public function fetchAll($sql, $params = []) { $s = preg_replace('/\s+/', ' ', strtolower($sql)); - if (str_contains($s, 'from kiosks')) { - return [ - ['id'=>1,'site_id'=>1,'template_id'=>1,'name'=>'Empfang Erdgeschoss','token'=>'a1b2c3d4e5f60718293a4b5c6d7e8f90', - 'headline'=>'Willkommen im Hotel Seeblick','subline'=>'Tippen Sie auf den Knopf – Ihr WLAN-Code erscheint sofort.', - 'is_active'=>1,'daily_limit'=>150,'cooldown_seconds'=>15,'display_seconds'=>90,'last_used_at'=>null, - 'site_name'=>'Hauptstandort Nord','site_active'=>1,'template_name'=>'Tagesgast', - 'tpl_max_uses'=>2,'tpl_expire_minutes'=>480,'qos_rate_max_down'=>20000,'qos_rate_max_up'=>5000,'qos_usage_quota'=>0, - 'total_vouchers'=>412,'today_vouchers'=>23,'created_at'=>'2026-06-01 10:00:00'], - ['id'=>2,'site_id'=>2,'template_id'=>null,'name'=>'Tagungsraum West','token'=>'0f1e2d3c4b5a69788796a5b4c3d2e1f0', - 'headline'=>'','subline'=>'','is_active'=>1,'daily_limit'=>0,'cooldown_seconds'=>30,'display_seconds'=>60, - 'last_used_at'=>null,'site_name'=>'Campus West','site_active'=>1,'template_name'=>null, - 'tpl_max_uses'=>0,'tpl_expire_minutes'=>0,'qos_rate_max_down'=>0,'qos_rate_max_up'=>0,'qos_usage_quota'=>0, - 'total_vouchers'=>87,'today_vouchers'=>4,'created_at'=>'2026-07-12 09:30:00'], - ]; - } - if (str_contains($s, 'count(*) as count from sites')) return [['count'=>4]]; if (str_contains($s, 'count(*) as count from users')) return [['count'=>12]]; if (str_contains($s, 'count(*) as count from vouchers where date(created_at)=curdate()')) return [['count'=>18]]; diff --git a/tools/screenshots.py b/tools/screenshots.py index 6f4338b..95938df 100755 --- a/tools/screenshots.py +++ b/tools/screenshots.py @@ -35,9 +35,6 @@ SHOTS = [ ('two-factor.png', 'admin/security.php', 1200, 900), ('updater.png', 'admin/update.php', 1200, 780), ('updater-available.png', 'admin/update.php?demo=available', 1200, 780), - ('kiosk-display.png', 'kiosk.php?k=a1b2c3d4e5f60718293a4b5c6d7e8f90', 1200, 900), - ('kiosk-code.png', 'kiosk.php?k=a1b2c3d4e5f60718293a4b5c6d7e8f90&demo=code', 1200, 900), - ('kiosks-admin.png', 'admin/kiosks.php', 1200, 780), ('mobile-vouchers.png', 'admin/users.php', 430, 860), ('maintenance.png', 'updater/templates/maintenance.html', 1200, 700), ] diff --git a/updater/migrations/0005_kiosk.sql b/updater/migrations/0005_kiosk.sql deleted file mode 100644 index 9f48aa8..0000000 --- a/updater/migrations/0005_kiosk.sql +++ /dev/null @@ -1,29 +0,0 @@ --- Öffentliche Display-Seiten ("Kiosk"): pro Site eine Seite mit festem Link, --- über die Gäste sich mit einem Klick selbst einen Zugangscode holen. - -CREATE TABLE IF NOT EXISTS `kiosks` ( - `id` INT PRIMARY KEY AUTO_INCREMENT, - `site_id` INT NOT NULL, - `template_id` INT NULL, - `name` VARCHAR(255) NOT NULL, - `token` VARCHAR(64) NOT NULL, - `headline` VARCHAR(255) NULL, - `subline` VARCHAR(500) NULL, - `is_active` TINYINT(1) NOT NULL DEFAULT 1, - `daily_limit` INT NOT NULL DEFAULT 100, - `cooldown_seconds` INT NOT NULL DEFAULT 20, - `display_seconds` INT NOT NULL DEFAULT 90, - `last_used_at` TIMESTAMP NULL, - `created_by` INT NULL, - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - UNIQUE KEY `uniq_token` (`token`), - INDEX `idx_site` (`site_id`), - FOREIGN KEY (`site_id`) REFERENCES `sites`(`id`) ON DELETE CASCADE, - FOREIGN KEY (`template_id`) REFERENCES `voucher_templates`(`id`) ON DELETE SET NULL, - FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON DELETE SET NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- Herkunft eines Vouchers festhalten: über welchen Kiosk wurde er geholt? -ALTER TABLE `vouchers` ADD COLUMN `kiosk_id` INT NULL AFTER `user_id`; -ALTER TABLE `vouchers` ADD INDEX `idx_kiosk` (`kiosk_id`);