= $maxRequests) {
$_SESSION['voucher_create_times'] = $timestamps;
return true;
}
$timestamps[] = $now;
$_SESSION['voucher_create_times'] = $timestamps;
return false;
}
/**
* Optionales Tageslimit pro (Nicht-Admin-)Benutzer (Setting
* user_daily_voucher_limit, 0 = aus). Verhindert übermäßige Erstellung.
*/
function userDailyLimitExceeded($db, $auth, $additional = 1) {
if (!$auth->isLoggedIn() || $auth->isAdmin()) {
return false;
}
$limit = (int)$db->getSetting('user_daily_voucher_limit', 0);
if ($limit <= 0) {
return false;
}
$uid = $_SESSION['user_id'] ?? 0;
$today = (int)($db->fetchOne(
"SELECT COUNT(*) c FROM vouchers WHERE user_id=? AND DATE(created_at)=CURDATE()",
[$uid]
)['c'] ?? 0);
return ($today + $additional) > $limit;
}
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
$logoUrl = $db->getSetting('logo_url', '');
$instructionHeader = $db->getSetting('instruction_header', '');
$instructionText = $db->getSetting('instruction_text', '');
$publicAccess = $db->getSetting('public_access', 0);
$smtpEnabled = $db->getSetting('smtp_enabled', '0') === '1';
$printTemplate = $db->getSetting('print_template', Ui::defaultPrintTemplate());
$defaultExpire = max(1, (int)$db->getSetting('default_expire_minutes', 480));
$defaultMaxUses = max(1, (int)$db->getSetting('default_max_uses', 1));
$maxUsesLimit = max(1, (int)$db->getSetting('max_uses_limit', 10));
if (!$publicAccess && !$auth->isLoggedIn()) {
header('Location: login.php');
exit;
}
try {
$templates = $db->fetchAll("SELECT * FROM voucher_templates WHERE is_active = 1 ORDER BY name");
} catch (Exception $e) {
$templates = [];
}
$error = '';
$success = '';
$voucherCode = '';
$voucherCreated = false;
$voucherData = [];
$bulkVouchers = [];
$bulkCreated = false;
if ($auth->isLoggedIn()) {
if ($auth->isAdmin()) {
$sites = $db->fetchAll("SELECT * FROM sites WHERE is_active = 1 ORDER BY name");
} else {
$sites = $db->fetchAll(
"SELECT s.* FROM sites s INNER JOIN user_site_access usa ON s.id = usa.site_id
WHERE s.is_active = 1 AND usa.user_id = ? ORDER BY s.name",
[$_SESSION['user_id']]
);
}
} else {
$sites = $db->fetchAll("SELECT * FROM sites WHERE is_active = 1 AND public_access = 1 ORDER BY name");
}
$autoSelectSite = (count($sites) === 1) ? $sites[0]['id'] : 0;
// Helper: create one voucher and save to DB
function doCreateVoucher($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
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_voucher'])) {
if (!$publicAccess && !$auth->isLoggedIn()) {
$error = __('error_login_req');
} elseif (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
// CSRF fuer ALLE (auch anonyme oeffentliche Erstellung)
$error = __('error_csrf');
} elseif (!$auth->isLoggedIn() && isVoucherRateLimited()) {
$error = 'Zu viele Anfragen. Bitte warten Sie einen Moment.';
} elseif (!$auth->isLoggedIn() && !Captcha::verify($db)) {
$error = 'Captcha-Prüfung fehlgeschlagen. Bitte erneut versuchen.';
} else {
try {
$siteId = (int)($_POST['site_id'] ?? 0);
$voucherName = trim((string)($_POST['voucher_name'] ?? ''));
$maxUses = (int)($_POST['max_uses'] ?? $defaultMaxUses);
$expireMinutes = max(1, (int)($_POST['expire_minutes'] ?? $defaultExpire));
$sendEmail = isset($_POST['send_email']) && !empty($_POST['recipient_email']);
$recipientEmail= trim((string)($_POST['recipient_email'] ?? ''));
if (empty($voucherName)) throw new Exception(__('error_name_req'));
if ($maxUses < 1 || $maxUses > $maxUsesLimit) throw new Exception(__('error_devices_range', ['max' => $maxUsesLimit]));
if ($siteId <= 0) throw new Exception(__('error_site_req'));
if ($sendEmail && !filter_var($recipientEmail, FILTER_VALIDATE_EMAIL)) throw new Exception(__('error_email_invalid'));
if ($auth->isLoggedIn() && !$auth->hasAccessToSite($siteId)) throw new Exception(__('error_site_no_perm'));
if (userDailyLimitExceeded($db, $auth, 1)) throw new Exception('Tageslimit für Voucher erreicht.');
$site = $db->fetchOne("SELECT * FROM sites WHERE id = ? AND is_active = 1", [$siteId]);
if (!$site) throw new Exception(__('error_site_not_found'));
$userId = $auth->isLoggedIn() ? ($_SESSION['user_id'] ?? null) : null;
$qos = [
'down' => max(0, (int)($_POST['qos_down'] ?? 0)),
'up' => max(0, (int)($_POST['qos_up'] ?? 0)),
'quota_mb' => max(0, (int)($_POST['qos_quota'] ?? 0)),
];
$voucherData = doCreateVoucher($db, $site, $voucherName, $maxUses, $expireMinutes, $userId, $qos);
$voucherCode = $voucherData['code'];
$voucherCreated = true;
Notifier::voucherCreated(1, $site['name'], $_SESSION['user_name'] ?? null);
$success = 'Voucher erfolgreich erstellt!';
if ($sendEmail && !empty($recipientEmail)) {
$mailer->sendVoucherEmail($recipientEmail, $voucherCode, $site['name'], $maxUses);
$success .= ' E-Mail versendet.';
}
// Optional: Code per SMS (Twilio)
$recipientPhone = trim((string)($_POST['recipient_phone'] ?? ''));
if (isset($_POST['send_sms']) && $recipientPhone !== '' && Sms::enabled($db)) {
$smsText = ($appTitle ? $appTitle . ': ' : '') . 'WLAN-Code ' . $voucherCode;
$success .= Sms::send($db, $recipientPhone, $smsText) ? ' SMS versendet.' : ' (SMS fehlgeschlagen)';
}
} catch (Exception $e) {
$error = 'Fehler: ' . $e->getMessage();
}
}
}
// Bulk voucher creation
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_bulk'])) {
if (!$publicAccess && !$auth->isLoggedIn()) {
$error = __('error_login_req');
} elseif (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
// CSRF fuer ALLE (auch anonyme oeffentliche Erstellung)
$error = __('error_csrf');
} elseif (!$auth->isLoggedIn() && isVoucherRateLimited()) {
$error = 'Zu viele Anfragen. Bitte warten Sie einen Moment.';
} elseif (!$auth->isLoggedIn() && !Captcha::verify($db)) {
$error = 'Captcha-Prüfung fehlgeschlagen. Bitte erneut versuchen.';
} else {
try {
$siteId = (int)($_POST['site_id'] ?? 0);
$voucherName = trim((string)($_POST['voucher_name'] ?? ''));
$maxUses = (int)($_POST['max_uses'] ?? $defaultMaxUses);
$expireMinutes = max(1, (int)($_POST['expire_minutes'] ?? $defaultExpire));
$bulkCount = max(1, min(20, (int)($_POST['bulk_count'] ?? 1)));
if (empty($voucherName)) throw new Exception(__('error_name_req'));
if ($maxUses < 1 || $maxUses > $maxUsesLimit) throw new Exception(__('error_devices_range', ['max' => $maxUsesLimit]));
if ($siteId <= 0) throw new Exception(__('error_site_req'));
if ($auth->isLoggedIn() && !$auth->hasAccessToSite($siteId)) throw new Exception(__('error_site_no_perm'));
if (userDailyLimitExceeded($db, $auth, $bulkCount)) throw new Exception('Tageslimit für Voucher erreicht.');
$site = $db->fetchOne("SELECT * FROM sites WHERE id = ? AND is_active = 1", [$siteId]);
if (!$site) throw new Exception(__('error_site_not_found'));
$userId = $auth->isLoggedIn() ? ($_SESSION['user_id'] ?? null) : null;
$qos = [
'down' => max(0, (int)($_POST['qos_down'] ?? 0)),
'up' => max(0, (int)($_POST['qos_up'] ?? 0)),
'quota_mb' => max(0, (int)($_POST['qos_quota'] ?? 0)),
];
for ($i = 0; $i < $bulkCount; $i++) {
$bulkVouchers[] = doCreateVoucher($db, $site, $voucherName . '_' . ($i + 1), $maxUses, $expireMinutes, $userId, $qos);
}
Notifier::voucherCreated($bulkCount, $site['name'], $_SESSION['user_name'] ?? null);
$bulkCreated = true;
$success = str_replace('{count}', $bulkCount, __('bulk_success'));
} catch (Exception $e) {
$error = 'Fehler: ' . $e->getMessage();
}
}
}
$currentUser = $auth->isLoggedIn() ? $auth->getCurrentUser() : null;
// Captcha nur für anonyme öffentliche Erstellung
$captchaMode = !$auth->isLoggedIn() ? Captcha::mode($db) : 'off';
$captchaQuestion = $captchaMode === 'math' ? Captcha::newMathChallenge() : '';
$hcaptchaSiteKey = $captchaMode === 'hcaptcha' ? $db->getSetting('captcha_site_key', '') : '';
$smsEnabled = Sms::enabled($db);
$captchaHtml = '';
if ($captchaMode === 'math') {
$captchaHtml = '
'
. '
';
} elseif ($captchaMode === 'hcaptcha' && $hcaptchaSiteKey !== '') {
$captchaHtml = '';
}
// Build print HTML for each voucher
function buildPrintCard($template, $data, $instructionHeader, $instructionText, $appTitle) {
$instructions = $instructionHeader || $instructionText
? htmlspecialchars($instructionHeader) . "\n" . $instructionText
: '';
// {QR_CODE} wird erst im Browser gefuellt (siehe renderPrintQr()).
$qr = '';
return str_replace(
['{QR_CODE}', '{VOUCHER_CODE}', '{SITE_NAME}', '{MAX_USES}', '{APP_TITLE}', '{INSTRUCTIONS}', '{EXPIRY_DATE}', '{EXPIRY_TIME}'],
[$qr, $data['code'], htmlspecialchars($data['site_name']), $data['max_uses'], htmlspecialchars($appTitle), $instructions, $data['expiry_date'], $data['expiry_time']],
$template
);
}
?>
= htmlspecialchars($appTitle) ?>
= Ui::head($db) ?>
= Ui::script('assets/vendor/qrcodejs/qrcode.min.js') ?>
= __('a11y_skip') ?>
= htmlspecialchars($appTitle) ?>
= __('app_subtitle') ?>
= htmlspecialchars($error) ?>
= buildPrintCard($printTemplate, $voucherData, $instructionHeader, $instructionText, $appTitle) ?>
= __('voucher_success_title') ?>
= htmlspecialchars($voucherCode) ?>
= __('voucher_copy_hint') ?>
= str_replace('{minutes}', $voucherData['expire_min'], __('voucher_validity')) ?>
= htmlspecialchars($voucherData['site_name']) ?>
= (int)$voucherData['max_uses'] ?> = __('label_devices') ?>
= __('voucher_qr_label') ?>
= htmlspecialchars($instructionHeader) ?>
= $instructionText ?>
= str_replace('{count}', count($bulkVouchers), __('bulk_results')) ?>
= __('bulk_results_hint') ?>
$bv): ?>
= buildPrintCard($printTemplate, $bv, $instructionHeader, $instructionText, $appTitle) ?>
| # |
= __('label_code') ?> |
= __('label_site') ?> |
= __('label_expires') ?> |
$bv): ?>
| = $i + 1 ?> |
= htmlspecialchars($bv['code']) ?>
|
= htmlspecialchars($bv['site_name']) ?> |
= $bv['expiry_date'] ?> = $bv['expiry_time'] ?> |
= __('voucher_copy_hint') ?>
isLoggedIn()): ?>
= htmlspecialchars($instructionHeader) ?>
= $instructionText ?>