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>
This commit is contained in:
parent
5d72febadc
commit
61810eb050
30 changed files with 1360 additions and 29 deletions
222
kiosk.php
Normal file
222
kiosk.php
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
<?php
|
||||
/**
|
||||
* Öffentliche Display-Seite ("Kiosk").
|
||||
*
|
||||
* Aufruf: kiosk.php?k=<token>
|
||||
*
|
||||
* 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']) : '';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?= I18n::getLanguage() ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<title><?= htmlspecialchars($appTitle) ?></title>
|
||||
<?= Ui::head($db) ?>
|
||||
<?php if (!$notFound): ?>
|
||||
<?= Ui::script('assets/vendor/qrcodejs/qrcode.min.js') ?>
|
||||
<?php endif; ?>
|
||||
</head>
|
||||
<body class="kiosk-body">
|
||||
|
||||
<?php if ($notFound): ?>
|
||||
<main class="kiosk-stage">
|
||||
<div class="kiosk-card">
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon"><i class="fas fa-link-slash" aria-hidden="true"></i></div>
|
||||
<h1><?= __('kiosk_unknown') ?></h1>
|
||||
<p><?= __('kiosk_unknown_hint') ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<?php elseif ($voucher): ?>
|
||||
<main class="kiosk-stage">
|
||||
<div class="kiosk-card kiosk-result">
|
||||
<p class="kiosk-eyebrow"><i class="fas fa-circle-check" aria-hidden="true"></i> <?= __('kiosk_ready') ?></p>
|
||||
<div class="kiosk-code" id="voucherCode"><?= htmlspecialchars($voucher['code']) ?></div>
|
||||
<div class="kiosk-meta">
|
||||
<span><i class="fas fa-location-dot" aria-hidden="true"></i> <?= htmlspecialchars($voucher['site_name']) ?></span>
|
||||
<span><i class="fas fa-clock" aria-hidden="true"></i> <?= (int)$voucher['expire_min'] ?> <?= __('minutes_short') ?></span>
|
||||
<span><i class="fas fa-mobile-screen" aria-hidden="true"></i> <?= (int)$voucher['max_uses'] ?> <?= __('label_devices') ?></span>
|
||||
</div>
|
||||
<div class="kiosk-qr">
|
||||
<div id="qrcode"></div>
|
||||
<p class="kiosk-qr-label"><?= __('kiosk_scan_code') ?></p>
|
||||
</div>
|
||||
<p class="kiosk-countdown">
|
||||
<?= str_replace('{seconds}', '<span id="countdown">' . $display . '</span>', __('kiosk_reset_in')) ?>
|
||||
</p>
|
||||
<a class="btn btn-secondary btn-lg" href="?k=<?= htmlspecialchars($kiosk['token']) ?>"><?= __('kiosk_done') ?></a>
|
||||
</div>
|
||||
</main>
|
||||
<?php else: ?>
|
||||
<main class="kiosk-stage">
|
||||
<div class="kiosk-card">
|
||||
<?php if ($logoUrl): ?>
|
||||
<img class="kiosk-logo" src="<?= htmlspecialchars(Ui::mediaUrl($logoUrl)) ?>" alt="<?= htmlspecialchars($appTitle) ?>">
|
||||
<?php else: ?>
|
||||
<span class="brand-mark kiosk-mark"><i class="fas fa-wifi" aria-hidden="true"></i></span>
|
||||
<?php endif; ?>
|
||||
|
||||
<h1 class="kiosk-headline"><?= htmlspecialchars($headline) ?></h1>
|
||||
<p class="kiosk-subline"><?= htmlspecialchars($subline) ?></p>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-error kiosk-alert"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" id="kioskForm">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($auth->getCsrfToken()) ?>">
|
||||
<button type="submit" class="btn btn-primary kiosk-button" id="kioskButton"
|
||||
<?= $waitSecs > 0 ? 'disabled' : '' ?>>
|
||||
<i class="fas fa-wifi" aria-hidden="true"></i>
|
||||
<span><?= __('kiosk_button') ?></span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="kiosk-phone">
|
||||
<div id="selfQr" class="kiosk-phone-qr"></div>
|
||||
<p><?= __('kiosk_phone_hint') ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<?php endif; ?>
|
||||
|
||||
<footer class="kiosk-footer"><?= Ui::credit() ?></footer>
|
||||
|
||||
<?php if (!$notFound): ?>
|
||||
<script>
|
||||
(function () {
|
||||
<?php if ($voucher): ?>
|
||||
// Code als QR – lokal erzeugt, ohne externen Dienst.
|
||||
new QRCode(document.getElementById('qrcode'), {
|
||||
text: <?= json_encode(str_replace('-', '', $voucher['code'])) ?>,
|
||||
width: 240, height: 240,
|
||||
colorDark: '#101625', colorLight: '#ffffff',
|
||||
correctLevel: QRCode.CorrectLevel.M
|
||||
});
|
||||
|
||||
// Nach der Anzeigedauer zurück zum Startbildschirm, damit der nächste
|
||||
// Gast nicht den Code seines Vorgängers sieht.
|
||||
var left = <?= $display ?>;
|
||||
var out = document.getElementById('countdown');
|
||||
setInterval(function () {
|
||||
left -= 1;
|
||||
if (out) out.textContent = left > 0 ? left : 0;
|
||||
if (left <= 0) location.href = '?k=<?= htmlspecialchars($kiosk['token'], ENT_QUOTES) ?>';
|
||||
}, 1000);
|
||||
<?php else: ?>
|
||||
// QR auf diese Seite selbst: Gäste öffnen sie am eigenen Handy.
|
||||
new QRCode(document.getElementById('selfQr'), {
|
||||
text: <?= json_encode($selfUrl) ?>,
|
||||
width: 150, height: 150,
|
||||
colorDark: '#101625', colorLight: '#ffffff',
|
||||
correctLevel: QRCode.CorrectLevel.M
|
||||
});
|
||||
|
||||
// Doppelklicks auf dem Touch-Display verhindern.
|
||||
var form = document.getElementById('kioskForm');
|
||||
var button = document.getElementById('kioskButton');
|
||||
if (form && button) {
|
||||
form.addEventListener('submit', function () {
|
||||
button.disabled = true;
|
||||
button.querySelector('span').textContent = <?= json_encode(__('kiosk_working')) ?>;
|
||||
});
|
||||
}
|
||||
|
||||
<?php if ($waitSecs > 0): ?>
|
||||
// Nach der Wartezeit wieder freigeben.
|
||||
setTimeout(function () { location.href = '?k=<?= htmlspecialchars($kiosk['token'], ENT_QUOTES) ?>'; }, <?= $waitSecs * 1000 ?>);
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue