CAPTCHA für öffentlichen Modus (Rechenaufgabe oder hCaptcha)
- includes/Captcha.php (math self-contained / hcaptcha via siteverify) - index.php: Verifikation bei anonymer Erstellung + Anzeige in beiden Formularen - Settings in admin/integrations.php (Modus, Site-Key, Secret)
This commit is contained in:
parent
546accc185
commit
ce75215498
3 changed files with 95 additions and 0 deletions
|
|
@ -24,6 +24,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save'])) {
|
||||||
$error = __('error_csrf');
|
$error = __('error_csrf');
|
||||||
} else {
|
} else {
|
||||||
$db->setSetting('enforce_2fa_admins', isset($_POST['enforce_2fa_admins']) ? '1' : '0');
|
$db->setSetting('enforce_2fa_admins', isset($_POST['enforce_2fa_admins']) ? '1' : '0');
|
||||||
|
$cm = in_array($_POST['captcha_mode'] ?? 'off', ['off','math','hcaptcha'], true) ? $_POST['captcha_mode'] : 'off';
|
||||||
|
$db->setSetting('captcha_mode', $cm);
|
||||||
|
$db->setSetting('captcha_site_key', trim($_POST['captcha_site_key'] ?? ''));
|
||||||
|
if (!empty($_POST['captcha_secret'])) { $db->setSetting('captcha_secret', trim($_POST['captcha_secret'])); }
|
||||||
$db->setSetting('user_daily_voucher_limit', max(0, (int)($_POST['user_daily_voucher_limit'] ?? 0)));
|
$db->setSetting('user_daily_voucher_limit', max(0, (int)($_POST['user_daily_voucher_limit'] ?? 0)));
|
||||||
$db->setSetting('trusted_proxy', trim($_POST['trusted_proxy'] ?? ''));
|
$db->setSetting('trusted_proxy', trim($_POST['trusted_proxy'] ?? ''));
|
||||||
$db->setSetting('webhook_enabled', isset($_POST['webhook_enabled']) ? '1' : '0');
|
$db->setSetting('webhook_enabled', isset($_POST['webhook_enabled']) ? '1' : '0');
|
||||||
|
|
@ -42,6 +46,9 @@ if (isset($_GET['test_webhook']) && isset($_GET['token']) && $auth->validateCsrf
|
||||||
}
|
}
|
||||||
|
|
||||||
$enforce2fa = $db->getSetting('enforce_2fa_admins', '0') === '1';
|
$enforce2fa = $db->getSetting('enforce_2fa_admins', '0') === '1';
|
||||||
|
$captchaMode = $db->getSetting('captcha_mode', 'off');
|
||||||
|
$captchaSiteKey = $db->getSetting('captcha_site_key', '');
|
||||||
|
$captchaSecretSet = $db->getSetting('captcha_secret', '') !== '';
|
||||||
$dailyLimit = (int)$db->getSetting('user_daily_voucher_limit', 0);
|
$dailyLimit = (int)$db->getSetting('user_daily_voucher_limit', 0);
|
||||||
$trustedProxy = $db->getSetting('trusted_proxy', '');
|
$trustedProxy = $db->getSetting('trusted_proxy', '');
|
||||||
$webhookEnabled = $db->getSetting('webhook_enabled', '0') === '1';
|
$webhookEnabled = $db->getSetting('webhook_enabled', '0') === '1';
|
||||||
|
|
@ -90,6 +97,16 @@ label { display:block; font-size:14px; color:var(--text-secondary); margin:14px
|
||||||
<label class="chk"><input type="checkbox" name="enforce_2fa_admins" <?= $enforce2fa ? 'checked' : '' ?>> 2FA für Administratoren verpflichtend</label>
|
<label class="chk"><input type="checkbox" name="enforce_2fa_admins" <?= $enforce2fa ? 'checked' : '' ?>> 2FA für Administratoren verpflichtend</label>
|
||||||
<label>Tageslimit Voucher pro Nicht-Admin-Benutzer (0 = unbegrenzt)</label>
|
<label>Tageslimit Voucher pro Nicht-Admin-Benutzer (0 = unbegrenzt)</label>
|
||||||
<input class="input" type="number" min="0" name="user_daily_voucher_limit" value="<?= $dailyLimit ?>" style="max-width:200px;">
|
<input class="input" type="number" min="0" name="user_daily_voucher_limit" value="<?= $dailyLimit ?>" style="max-width:200px;">
|
||||||
|
<label>Captcha im öffentlichen Modus</label>
|
||||||
|
<select class="input" name="captcha_mode" style="max-width:240px;">
|
||||||
|
<option value="off" <?= $captchaMode==='off'?'selected':'' ?>>Aus</option>
|
||||||
|
<option value="math" <?= $captchaMode==='math'?'selected':'' ?>>Rechenaufgabe (ohne externen Dienst)</option>
|
||||||
|
<option value="hcaptcha" <?= $captchaMode==='hcaptcha'?'selected':'' ?>>hCaptcha</option>
|
||||||
|
</select>
|
||||||
|
<div class="row3" style="margin-top:10px;">
|
||||||
|
<div><label>hCaptcha Site-Key</label><input class="input" type="text" name="captcha_site_key" value="<?= htmlspecialchars($captchaSiteKey) ?>"></div>
|
||||||
|
<div><label>hCaptcha Secret<?= $captchaSecretSet ? ' (gesetzt)' : '' ?></label><input class="input" type="password" name="captcha_secret" placeholder="<?= $captchaSecretSet ? '••••••• (leer = unverändert)' : '' ?>"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
|
||||||
56
includes/Captcha.php
Normal file
56
includes/Captcha.php
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Captcha – Schutz der öffentlichen (anonymen) Voucher-Erstellung.
|
||||||
|
*
|
||||||
|
* Modi (Setting captcha_mode):
|
||||||
|
* 'off' – deaktiviert
|
||||||
|
* 'math' – selbst-enthaltenes Rechen-Captcha (keine externen Dienste)
|
||||||
|
* 'hcaptcha' – hCaptcha (Setting captcha_site_key / captcha_secret)
|
||||||
|
*
|
||||||
|
* Reine PHP-Standardlib + cURL (für hCaptcha-Verifizierung).
|
||||||
|
*/
|
||||||
|
class Captcha {
|
||||||
|
public static function mode($db) {
|
||||||
|
$m = (string)$db->getSetting('captcha_mode', 'off');
|
||||||
|
return in_array($m, ['off', 'math', 'hcaptcha'], true) ? $m : 'off';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Frage für das Math-Captcha erzeugen und Antwort in Session hinterlegen. */
|
||||||
|
public static function newMathChallenge() {
|
||||||
|
$a = random_int(1, 9);
|
||||||
|
$b = random_int(1, 9);
|
||||||
|
$_SESSION['captcha_answer'] = (string)($a + $b);
|
||||||
|
return "$a + $b";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prüft die Captcha-Antwort des aktuellen Requests. */
|
||||||
|
public static function verify($db) {
|
||||||
|
$mode = self::mode($db);
|
||||||
|
if ($mode === 'off') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ($mode === 'math') {
|
||||||
|
$expected = $_SESSION['captcha_answer'] ?? null;
|
||||||
|
unset($_SESSION['captcha_answer']); // einmalig
|
||||||
|
$given = trim((string)($_POST['captcha'] ?? ''));
|
||||||
|
return $expected !== null && hash_equals((string)$expected, $given);
|
||||||
|
}
|
||||||
|
if ($mode === 'hcaptcha') {
|
||||||
|
$resp = $_POST['h-captcha-response'] ?? '';
|
||||||
|
if ($resp === '') return false;
|
||||||
|
$secret = (string)$db->getSetting('captcha_secret', '');
|
||||||
|
$ch = curl_init('https://hcaptcha.com/siteverify');
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => http_build_query(['secret' => $secret, 'response' => $resp]),
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => 8,
|
||||||
|
]);
|
||||||
|
$out = curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
$data = json_decode((string)$out, true);
|
||||||
|
return is_array($data) && !empty($data['success']);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
index.php
22
index.php
|
|
@ -17,6 +17,7 @@ require_once __DIR__ . '/includes/Auth.php';
|
||||||
require_once __DIR__ . '/includes/UniFiController.php';
|
require_once __DIR__ . '/includes/UniFiController.php';
|
||||||
require_once __DIR__ . '/includes/Mailer.php';
|
require_once __DIR__ . '/includes/Mailer.php';
|
||||||
require_once __DIR__ . '/includes/Notifier.php';
|
require_once __DIR__ . '/includes/Notifier.php';
|
||||||
|
require_once __DIR__ . '/includes/Captcha.php';
|
||||||
require_once __DIR__ . '/includes/I18n.php';
|
require_once __DIR__ . '/includes/I18n.php';
|
||||||
|
|
||||||
$auth = new Auth();
|
$auth = new Auth();
|
||||||
|
|
@ -151,6 +152,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_voucher'])) {
|
||||||
$error = __('error_csrf');
|
$error = __('error_csrf');
|
||||||
} elseif (!$auth->isLoggedIn() && isVoucherRateLimited()) {
|
} elseif (!$auth->isLoggedIn() && isVoucherRateLimited()) {
|
||||||
$error = 'Zu viele Anfragen. Bitte warten Sie einen Moment.';
|
$error = 'Zu viele Anfragen. Bitte warten Sie einen Moment.';
|
||||||
|
} elseif (!$auth->isLoggedIn() && !Captcha::verify($db)) {
|
||||||
|
$error = 'Captcha-Prüfung fehlgeschlagen. Bitte erneut versuchen.';
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
$siteId = (int)($_POST['site_id'] ?? 0);
|
$siteId = (int)($_POST['site_id'] ?? 0);
|
||||||
|
|
@ -203,6 +206,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_bulk'])) {
|
||||||
$error = __('error_csrf');
|
$error = __('error_csrf');
|
||||||
} elseif (!$auth->isLoggedIn() && isVoucherRateLimited()) {
|
} elseif (!$auth->isLoggedIn() && isVoucherRateLimited()) {
|
||||||
$error = 'Zu viele Anfragen. Bitte warten Sie einen Moment.';
|
$error = 'Zu viele Anfragen. Bitte warten Sie einen Moment.';
|
||||||
|
} elseif (!$auth->isLoggedIn() && !Captcha::verify($db)) {
|
||||||
|
$error = 'Captcha-Prüfung fehlgeschlagen. Bitte erneut versuchen.';
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
$siteId = (int)($_POST['site_id'] ?? 0);
|
$siteId = (int)($_POST['site_id'] ?? 0);
|
||||||
|
|
@ -242,6 +247,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_bulk'])) {
|
||||||
|
|
||||||
$currentUser = $auth->isLoggedIn() ? $auth->getCurrentUser() : null;
|
$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', '') : '';
|
||||||
|
$captchaHtml = '';
|
||||||
|
if ($captchaMode === 'math') {
|
||||||
|
$captchaHtml = '<div class="form-group"><label>Sicherheitsfrage: Wie viel ist ' . htmlspecialchars($captchaQuestion) . '?</label>'
|
||||||
|
. '<input type="text" name="captcha" inputmode="numeric" required></div>';
|
||||||
|
} elseif ($captchaMode === 'hcaptcha' && $hcaptchaSiteKey !== '') {
|
||||||
|
$captchaHtml = '<div class="form-group"><div class="h-captcha" data-sitekey="' . htmlspecialchars($hcaptchaSiteKey) . '"></div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
// Build print HTML for each voucher
|
// Build print HTML for each voucher
|
||||||
function buildPrintCard($template, $data, $instructionHeader, $instructionText, $appTitle) {
|
function buildPrintCard($template, $data, $instructionHeader, $instructionText, $appTitle) {
|
||||||
$instructions = $instructionHeader || $instructionText
|
$instructions = $instructionHeader || $instructionText
|
||||||
|
|
@ -261,6 +278,9 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title><?= htmlspecialchars($appTitle) ?></title>
|
<title><?= htmlspecialchars($appTitle) ?></title>
|
||||||
<link rel="stylesheet" href="assets/global.css">
|
<link rel="stylesheet" href="assets/global.css">
|
||||||
|
<?php if ($captchaMode === 'hcaptcha' && $hcaptchaSiteKey !== ''): ?>
|
||||||
|
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
|
||||||
|
<?php endif; ?>
|
||||||
<script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script>
|
<script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script>
|
||||||
<?php if ($voucherCreated): ?>
|
<?php if ($voucherCreated): ?>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" integrity="sha512-CNgIRecGo7nphbeZ04Sc13ka07paqdeTu0WR1IM4kNcpmBAUSHSe2keRB6Q5pBUtIxCY7bQMsVB0ANBpd6JDg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" integrity="sha512-CNgIRecGo7nphbeZ04Sc13ka07paqdeTu0WR1IM4kNcpmBAUSHSe2keRB6Q5pBUtIxCY7bQMsVB0ANBpd6JDg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
|
@ -510,6 +530,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
|
||||||
<input type="hidden" name="qos_down" class="qos-down-field" value="0">
|
<input type="hidden" name="qos_down" class="qos-down-field" value="0">
|
||||||
<input type="hidden" name="qos_up" class="qos-up-field" value="0">
|
<input type="hidden" name="qos_up" class="qos-up-field" value="0">
|
||||||
<input type="hidden" name="qos_quota" class="qos-quota-field" value="0">
|
<input type="hidden" name="qos_quota" class="qos-quota-field" value="0">
|
||||||
|
<?= $captchaHtml ?>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="voucher_name"><?= __('voucher_name_label') ?></label>
|
<label for="voucher_name"><?= __('voucher_name_label') ?></label>
|
||||||
|
|
@ -570,6 +591,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
|
||||||
<input type="hidden" name="qos_down" class="qos-down-field" value="0">
|
<input type="hidden" name="qos_down" class="qos-down-field" value="0">
|
||||||
<input type="hidden" name="qos_up" class="qos-up-field" value="0">
|
<input type="hidden" name="qos_up" class="qos-up-field" value="0">
|
||||||
<input type="hidden" name="qos_quota" class="qos-quota-field" value="0">
|
<input type="hidden" name="qos_quota" class="qos-quota-field" value="0">
|
||||||
|
<?= $captchaHtml ?>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="bulk_count"><?= __('bulk_quantity') ?></label>
|
<label for="bulk_count"><?= __('bulk_quantity') ?></label>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue