Unifi-Voucher-Tool/tools/demo/overlay/includes/Database.php
Friederich Loheide 61810eb050 Display-Seiten: Gäste holen sich den Zugang selbst
Für Empfang, Lobby oder Tagungsraum lässt sich je Site eine öffentliche
Seite anlegen (kiosk.php), die auf einem Bildschirm oder Tablet läuft:
ein großer Knopf, ein Klick, ein Zugangscode mit QR-Code. Nach einer
einstellbaren Anzeigedauer springt der Bildschirm zurück, damit der
nächste Gast nicht den Code seines Vorgängers sieht.

Verwaltung unter Administration → Display-Seiten:
- Site und optionales Voucher-Profil (bestimmt Laufzeit, Geräte, QoS)
- eigene Überschrift und Text für den Bildschirm
- Codes pro Tag, Wartezeit zwischen zwei Codes, Anzeigedauer
- geheimer Link zum Kopieren, als QR-Code anzeigbar und jederzeit
  erneuerbar (der alte Link gilt dann sofort nicht mehr)

Absicherung: der Link ist der Zugang, deshalb Tageslimit und Wartezeit
je Display, CSRF-Token am Formular, `noindex` im Kopf und ein Eintrag im
Audit-Log für jeden ausgegebenen Code. Webhooks werden für Kiosk-Codes
bewusst nicht ausgelöst – ein Empfangsdisplay würde den Kanal fluten.

Technik:
- neue Tabelle `kiosks`, `vouchers.kiosk_id` hält die Herkunft fest
  (Migration 0005, database.sql nachgezogen)
- includes/Kiosk.php kapselt Token, Limits und Profil-Auflösung
- includes/VoucherService.php bündelt die Voucher-Erstellung, die vorher
  in index.php lag und für den Kiosk ein zweites Mal nötig gewesen wäre
- Startbildschirm zeigt zusätzlich einen QR auf sich selbst, damit Gäste
  die Seite am eigenen Handy öffnen können

Tests: 9 neue Fälle für Token-Prüfung, Wartezeit, Tageslimit und
Profil-Auflösung (38 Tests gesamt), PHPStan deckt Kiosk.php mit ab.

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

179 lines
13 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/** Demo-Stub: liefert feste Beispieldaten statt echter DB-Zugriffe. */
class Database {
private static $instance = null;
private $settings = [
'app_title' => 'UniFi Voucher System',
'logo_url' => '', 'favicon_url' => '',
'public_access' => '1', 'smtp_enabled' => '1', 'sms_enabled' => '1',
'm365_enabled' => '1', 'oidc_enabled' => '0', 'oidc_name' => 'Keycloak',
'default_expire_minutes' => '480', 'default_max_uses' => '2', 'max_uses_limit' => '10',
'instruction_header' => 'So verbinden Sie sich',
'instruction_text' => 'WLAN „Gast-WiFi" wählen, Code eingeben und bestätigen. Bei Fragen hilft der Empfang gerne weiter.',
'last_cron_sync' => '2026-09-22 14:35:00',
'session_driver' => 'db', 'captcha_mode' => 'math',
'enforce_2fa_admins' => '1', 'user_daily_voucher_limit' => '25',
'webhook_enabled' => '1', 'webhook_url' => 'https://hooks.slack.com/services/T024/B0197/XYZ',
'twilio_sid' => 'AC8f2c1d94e77b40a2', 'twilio_from' => '+4915112345678',
'trusted_proxy' => '10.0.0.1, 172.18.0.1',
'cleanup_expired_days' => '90', 'cleanup_audit_days' => '365', 'cleanup_login_days' => '30',
'last_cleanup' => '2026-09-22 03:00:00',
];
public function __construct()
{
// Screenshot-Varianten: ?brand=custom|gradient|nopanel
$brand = $_GET['brand'] ?? '';
if ($brand === 'custom') {
$this->settings = array_merge($this->settings, [
'app_title' => 'Hotel Seeblick',
'login_brand_name' => 'Hotel Seeblick',
'login_logo_url' => '/demo-assets/logo.svg',
'login_claim_title' => 'Willkommen im Hotel Seeblick.',
'login_claim_text' => 'Gäste-WLAN für Zimmer, Tagungsräume und Restaurant Zugangscodes direkt an der Rezeption erstellen.',
'login_features' => "Code direkt beim Check-in ausdrucken\nTagungsgäste per E-Mail versorgen\nAuswertung je Haus und Etage",
'login_footer' => '© 2026 Hotel Seeblick GmbH · Datenschutz · Impressum',
'login_bg_image' => '/demo-assets/background.svg',
'login_bg_overlay' => '45',
'brand_accent' => '#0f766e',
'brand_accent_dark' => '#2dd4bf',
'brand_gradient_from' => '#0f766e',
'brand_gradient_to' => '#0ea5e9',
]);
} elseif ($brand === 'nopanel') {
$this->settings = array_merge($this->settings, [
'login_panel_enabled' => '0',
'login_brand_name' => 'Stadtwerke Nordheim',
]);
}
}
public static function getInstance() { return self::$instance ??= new self(); }
public function getConnection() { return null; }
public function getSetting($key, $default = null) { return $this->settings[$key] ?? $default; }
public function setSetting($key, $value) { $this->settings[$key] = $value; }
public function execute($sql, $params = []) { return 1; }
public function query($sql, $params = []) { return new DemoStmt(); }
public function fetchOne($sql, $params = []) { $r = $this->fetchAll($sql, $params); return $r[0] ?? false; }
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]];
if (str_contains($s, 'count(*) as count from vouchers where date(created_at)=')) {
$day = $params[0] ?? '';
$map = [6,9,5,12,8,4,7,14,11,16,9,6,13,18];
$idx = abs((int)((strtotime($day) - strtotime('today')) / 86400));
return [['count' => $map[13 - min($idx, 13)]]];
}
if (str_contains($s, 'from vouchers where site_id=?') && str_contains($s, 'count(*) as total')) {
$per = [1=>['total'=>128,'valid'=>74,'used'=>39,'expired'=>15],
2=>['total'=>63,'valid'=>41,'used'=>18,'expired'=>4],
3=>['total'=>22,'valid'=>14,'used'=>6,'expired'=>2],
4=>['total'=>0,'valid'=>0,'used'=>0,'expired'=>0]];
return [$per[(int)($params[0] ?? 1)] ?? $per[4]];
}
if (str_contains($s, 'count(*) as total') && str_contains($s, 'from vouchers')) {
return [['total'=>213,'valid'=>129,'used'=>63,'expired'=>21]];
}
if (str_contains($s, 'from sites')) {
return [
['id'=>1,'name'=>'Hauptstandort Nord','site_id'=>'default','unifi_controller_url'=>'https://unifi.example.com:8443','unifi_username'=>'voucher-api','unifi_password'=>'x','is_active'=>1,'use_ssl'=>1,'created_at'=>'2026-01-12 09:00:00'],
['id'=>2,'name'=>'Campus West','site_id'=>'campus-west','unifi_controller_url'=>'https://unifi.example.com:8443','unifi_username'=>'voucher-api','unifi_password'=>'x','is_active'=>1,'use_ssl'=>1,'created_at'=>'2026-02-03 11:30:00'],
['id'=>3,'name'=>'Showroom Berlin','site_id'=>'showroom','unifi_controller_url'=>'https://unifi-b.example.com:8443','unifi_username'=>'voucher-api','unifi_password'=>'x','is_active'=>1,'use_ssl'=>1,'created_at'=>'2026-04-21 15:10:00'],
['id'=>4,'name'=>'Lager Süd','site_id'=>'lager','unifi_controller_url'=>'https://unifi-s.example.com:8443','unifi_username'=>'voucher-api','unifi_password'=>'x','is_active'=>1,'use_ssl'=>0,'created_at'=>'2026-06-08 08:45:00'],
];
}
if (str_contains($s, 'count(v.id) as voucher_count') || str_contains($s, 'from users u left join vouchers')) {
return [
['name'=>'Marie Sander','email'=>'marie.sander@example.com','voucher_count'=>48,'c'=>48],
['name'=>'Jonas Weber','email'=>'j.weber@example.com','voucher_count'=>31,'c'=>31],
['name'=>'Alina Brandt','email'=>'alina.brandt@example.com','voucher_count'=>27,'c'=>27],
['name'=>'Tim Kurz','email'=>'tim.kurz@example.com','voucher_count'=>19,'c'=>19],
['name'=>'Sara Vogt','email'=>'sara.vogt@example.com','voucher_count'=>12,'c'=>12],
];
}
if (str_contains($s, 'from vouchers')) {
if (str_contains($s, 'group by date')) {
$out = []; $vals = [6,9,5,12,8,4,7,14,11,16,9,6,13,18];
foreach ($vals as $i => $c) { $out[] = ['date'=>date('d.m', strtotime('-'.(13-$i).' days')), 'count'=>$c]; }
return $out;
}
$codes = [['H7K2-M9QF',1,'Hauptstandort Nord','valid'],['P3RT-8ZXC',2,'Campus West','used'],
['QW4E-7TYU',1,'Hauptstandort Nord','valid'],['LM2N-5BVD',3,'Showroom Berlin','expired'],
['ZX9C-3PLO',2,'Campus West','valid'],['FG6H-1JKL',1,'Hauptstandort Nord','used']];
$names = ['Besuch Agentur Nordlicht','Workshop Raum 2','Empfang Tagesgast','Messe-Stand','Handwerker Haustechnik','Bewerbungsgespräch'];
$out = [];
foreach ($codes as $i => [$c,$sid,$sname,$st]) {
$out[] = ['id'=>$i+1,'voucher_code'=>$c,'site_id'=>$sid,'site_name'=>$sname,'user_name'=>'Marie Sander',
'status'=>$st,'created_at'=>date('Y-m-d H:i:s', strtotime("-{$i} hours")),
'voucher_name'=>$names[$i],'unifi_voucher_id'=>'65f1a'.$i,
'expires_at'=>date('Y-m-d H:i:s', strtotime('+'.(8-$i).' hours')),
'max_uses'=>[2,5,1,3,2,1][$i],'used_count'=>[2,1,0,3,0,1][$i],'expire_minutes'=>480];
}
return $out;
}
if (str_contains($s, 'from users')) {
return [
['id'=>1,'name'=>'Marie Sander','email'=>'marie.sander@example.com','is_admin'=>1,'is_active'=>1,'created_at'=>'2026-01-10 09:00:00','last_login'=>'2026-09-22 08:12:00','totp_enabled'=>1,'auth_source'=>'local','voucher_count'=>48,'password_hash'=>'x'],
['id'=>2,'name'=>'Jonas Weber','email'=>'j.weber@example.com','is_admin'=>0,'is_active'=>1,'created_at'=>'2026-03-02 13:20:00','last_login'=>'2026-09-21 17:40:00','totp_enabled'=>0,'auth_source'=>'m365','voucher_count'=>31,'password_hash'=>''],
['id'=>3,'name'=>'Alina Brandt','email'=>'alina.brandt@example.com','is_admin'=>0,'is_active'=>1,'created_at'=>'2026-05-18 10:05:00','last_login'=>'2026-09-20 09:31:00','totp_enabled'=>1,'auth_source'=>'local','voucher_count'=>27,'password_hash'=>'x'],
];
}
if (str_contains($s, 'from api_keys')) {
return [
['id'=>1,'name'=>'Buchungssystem','key_prefix'=>'3f9a2c','scope'=>'write','rate_limit'=>60,'is_active'=>1,'last_used_at'=>'2026-09-22 09:12:00','creator'=>'Marie Sander','created_at'=>'2026-06-01 10:00:00'],
['id'=>2,'name'=>'Terminal Foyer','key_prefix'=>'b71e04','scope'=>'read','rate_limit'=>0,'is_active'=>1,'last_used_at'=>'2026-09-21 17:40:00','creator'=>'Marie Sander','created_at'=>'2026-07-14 16:20:00'],
['id'=>3,'name'=>'Altes Kassensystem','key_prefix'=>'c0d5f8','scope'=>'write','rate_limit'=>30,'is_active'=>0,'last_used_at'=>null,'creator'=>'Jonas Weber','created_at'=>'2026-02-09 08:05:00'],
];
}
if (str_contains($s, 'from audit_log')) {
if (str_contains($s, 'count(')) return [['total'=>1284,'c'=>1284,'count'=>1284]];
if (str_contains($s, 'distinct action')) {
return [['action'=>'voucher_created'],['action'=>'user_login'],['action'=>'settings_saved'],['action'=>'user_created'],['action'=>'template_updated']];
}
$rows = [['2026-09-22 14:32:00','Marie Sander','voucher_created','Hauptstandort Nord · H7K2-M9QF','192.168.10.42'],
['2026-09-22 13:58:00','Jonas Weber','user_login','Microsoft 365','83.112.4.17'],
['2026-09-22 11:04:00','Marie Sander','settings_saved','Tab: E-Mail (SMTP)','192.168.10.42'],
['2026-09-21 17:40:00','Alina Brandt','user_created','tim.kurz@example.com','192.168.10.88'],
['2026-09-21 09:12:00','Marie Sander','template_updated','Tagesgast','192.168.10.42'],
['2026-09-20 16:03:00','Jonas Weber','voucher_bulk','5 Vouchers · Campus West','83.112.4.17']];
$out = [];
foreach ($rows as $i => [$d,$u,$a,$det,$ip]) {
$out[] = ['id'=>$i+1,'created_at'=>$d,'user_name'=>$u,'user_email'=>'demo@example.com','action'=>$a,'details'=>$det,'ip_address'=>$ip,'entity_type'=>'voucher','entity_id'=>$i+1];
}
return $out;
}
if (str_contains($s, 'templates')) {
return [
['id'=>1,'name'=>'Tagesgast','description'=>'Standardprofil für Besucher am Empfang','max_uses'=>2,'expire_minutes'=>480,'qos_rate_max_down'=>20000,'qos_rate_max_up'=>5000,'qos_usage_quota'=>0,'is_active'=>1,'is_default'=>1,'created_at'=>'2026-01-15 10:00:00'],
['id'=>2,'name'=>'Konferenz','description'=>'Mehrtägige Veranstaltungen','max_uses'=>5,'expire_minutes'=>4320,'qos_rate_max_down'=>50000,'qos_rate_max_up'=>10000,'qos_usage_quota'=>0,'is_active'=>1,'is_default'=>0,'created_at'=>'2026-02-20 11:30:00'],
['id'=>3,'name'=>'Handwerker','description'=>'Kurzzugang für Dienstleister','max_uses'=>1,'expire_minutes'=>240,'qos_rate_max_down'=>10000,'qos_rate_max_up'=>2000,'qos_usage_quota'=>1024,'is_active'=>1,'is_default'=>0,'created_at'=>'2026-03-08 09:15:00'],
];
}
return [];
}
}
class DemoStmt {
public function fetch($m = null) { return false; }
public function fetchAll($m = null) { return []; }
public function rowCount() { return 1; }
public function execute($p = []) { return true; }
}