- Implemented across bootstrap.php, chat frontend, and dashboard:
Per-tier audio bitrate: Free 64, Plus 96, Pro 128 kbps. Configurable text/voice reply threads and depth. Registration/login-only honeypot and internal CAPTCHA. Optional Google reCAPTCHA v2/v3 with server verification. Archive/export support for replies and bitrate. Dashboard controls for all settings. Verified JavaScript, JSON, SQLite migrations, desktop/mobile UI, and browser console. Native PHP lint was unavailable because PHP is not installed. Google implementation follows Siteverify and reCAPTCHA v3.
This commit is contained in:
+94
-3
@@ -4,8 +4,33 @@ function handleAdminPlatformAction(string $action): ?string {
|
||||
if ($action === 'save_platform_services') {
|
||||
$voiceProfile = (string)($_POST['voice_transcoding_profile'] ?? 'm4a_aac');
|
||||
if (!in_array($voiceProfile, ['m4a_aac', 'mp4_h264_aac'], true)) $voiceProfile = 'm4a_aac';
|
||||
$captchaVersion = (string)($_POST['captcha_google_version'] ?? 'v2');
|
||||
if (!in_array($captchaVersion, ['v2', 'v3'], true)) $captchaVersion = 'v2';
|
||||
$captchaDifficulty = (string)($_POST['captcha_internal_difficulty'] ?? 'easy');
|
||||
if (!in_array($captchaDifficulty, ['easy', 'medium'], true)) $captchaDifficulty = 'easy';
|
||||
$captchaGoogleEnabled = !empty($_POST['captcha_google_enabled']);
|
||||
$captchaGoogleSiteKey = trim((string)($_POST['captcha_google_site_key'] ?? ''));
|
||||
$captchaGoogleSecretKey = trim((string)($_POST['captcha_google_secret_key'] ?? ''));
|
||||
if ($captchaGoogleEnabled && ($captchaGoogleSiteKey === '' || $captchaGoogleSecretKey === '')) {
|
||||
throw new RuntimeException('Google reCAPTCHA requires both a site key and secret key.');
|
||||
}
|
||||
setConfigValues([
|
||||
'subscriptions.enabled' => !empty($_POST['subscriptions_enabled']),
|
||||
'chat.replies_enabled' => !empty($_POST['replies_enabled']),
|
||||
'chat.reply_max_depth' => max(1, min(20, (int)($_POST['reply_max_depth'] ?? 4))),
|
||||
'security.captcha.registration_enabled' => !empty($_POST['captcha_registration_enabled']),
|
||||
'security.captcha.login_enabled' => !empty($_POST['captcha_login_enabled']),
|
||||
'security.captcha.honeypot.enabled' => !empty($_POST['captcha_honeypot_enabled']),
|
||||
'security.captcha.internal.enabled' => !empty($_POST['captcha_internal_enabled']),
|
||||
'security.captcha.internal.difficulty' => $captchaDifficulty,
|
||||
'security.captcha.internal.ttl_seconds' => max(60, min(1800, (int)($_POST['captcha_internal_ttl'] ?? 300))),
|
||||
'security.captcha.internal.max_attempts' => max(1, min(10, (int)($_POST['captcha_internal_attempts'] ?? 3))),
|
||||
'security.captcha.google.enabled' => $captchaGoogleEnabled,
|
||||
'security.captcha.google.version' => $captchaVersion,
|
||||
'security.captcha.google.site_key' => $captchaGoogleSiteKey,
|
||||
'security.captcha.google.secret_key' => $captchaGoogleSecretKey,
|
||||
'security.captcha.google.v3_min_score' => max(0.0, min(1.0, (float)($_POST['captcha_google_v3_score'] ?? 0.5))),
|
||||
'security.captcha.google.hostname' => trim((string)($_POST['captcha_google_hostname'] ?? '')),
|
||||
'mailgun.api_key' => trim((string)($_POST['mailgun_api_key'] ?? '')),
|
||||
'mailgun.domain' => trim((string)($_POST['mailgun_domain'] ?? '')),
|
||||
'mailgun.from' => trim((string)($_POST['mailgun_from'] ?? '')),
|
||||
@@ -64,6 +89,9 @@ function handleAdminPlatformAction(string $action): ?string {
|
||||
foreach ($boolFeatures as $key) {
|
||||
$upsertFeature->execute([$id, $key, json_encode(!empty($plan['features'][$key]))]);
|
||||
}
|
||||
$upsertFeature->execute([$id, 'voice_bitrate_kbps', json_encode(
|
||||
max(48, min(320, (int)($plan['features']['voice_bitrate_kbps'] ?? 64)))
|
||||
)]);
|
||||
$upsertFeature->execute([$id, 'text_archive_days', json_encode(max(30, min(3650, (int)($plan['features']['text_archive_days'] ?? 30))))]);
|
||||
}
|
||||
$db->commit();
|
||||
@@ -87,7 +115,8 @@ function handleAdminPlatformAction(string $action): ?string {
|
||||
->execute([$slug, $name, trim((string)($_POST['description'] ?? '')), time(), time()]);
|
||||
$id = (int)$db->lastInsertId();
|
||||
$features = [
|
||||
'voice_messages' => true, 'recording_status' => false, 'auto_voice_send' => false,
|
||||
'voice_messages' => true, 'voice_bitrate_kbps' => 64,
|
||||
'recording_status' => false, 'auto_voice_send' => false,
|
||||
'auto_voice_play' => false, 'username_color' => false, 'text_archive_days' => 30,
|
||||
'text_export' => true, 'voice_archive' => false, 'voice_export' => false, 'email_2fa' => false,
|
||||
];
|
||||
@@ -124,7 +153,7 @@ function renderAdminPlatformTab(): void {
|
||||
$events = $db->query("SELECT event_type,COUNT(*) AS total FROM visitor_events
|
||||
WHERE created_at>" . (time() - 2592000) . " GROUP BY event_type ORDER BY total DESC")->fetchAll();
|
||||
$featureKeys = [
|
||||
'voice_messages', 'recording_status', 'auto_voice_send', 'auto_voice_play',
|
||||
'voice_messages', 'voice_bitrate_kbps', 'recording_status', 'auto_voice_send', 'auto_voice_play',
|
||||
'username_color', 'text_archive_days', 'text_export', 'voice_archive', 'voice_export', 'email_2fa',
|
||||
];
|
||||
?>
|
||||
@@ -144,6 +173,64 @@ function renderAdminPlatformTab(): void {
|
||||
<div class="panel-body"><label class="cb-row"><input type="checkbox" name="subscriptions_enabled" <?= getConfigVal('subscriptions.enabled', true) ? 'checked' : '' ?>>
|
||||
<span>Allow and display new subscriptions. When off, existing subscribers can still manage billing.</span></label></div></div>
|
||||
|
||||
<div class="panel"><div class="panel-head"><span class="panel-title">// REPLY THREADS</span></div><div class="panel-body">
|
||||
<div class="form-row">
|
||||
<label class="cb-row"><input type="checkbox" name="replies_enabled" <?= getConfigVal('chat.replies_enabled', true) ? 'checked' : '' ?>>
|
||||
<span>Enable reply-to controls for text and voice messages</span></label>
|
||||
<div class="field"><label>Maximum Reply Depth</label><input type="number" min="1" max="20"
|
||||
name="reply_max_depth" value="<?= (int)getConfigVal('chat.reply_max_depth', 4) ?>"></div>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<div class="panel"><div class="panel-head"><span class="panel-title">// REGISTRATION / LOGIN CAPTCHA</span></div><div class="panel-body">
|
||||
<p class="dim">CAPTCHA checks apply only to initial registration and password login. Email 2FA verification is not challenged.</p>
|
||||
<div class="form-row">
|
||||
<label class="cb-row"><input type="checkbox" name="captcha_registration_enabled"
|
||||
<?= getConfigVal('security.captcha.registration_enabled', true) ? 'checked' : '' ?>><span>Protect registration</span></label>
|
||||
<label class="cb-row"><input type="checkbox" name="captcha_login_enabled"
|
||||
<?= getConfigVal('security.captcha.login_enabled', true) ? 'checked' : '' ?>><span>Protect login</span></label>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="cb-row"><input type="checkbox" name="captcha_honeypot_enabled"
|
||||
<?= getConfigVal('security.captcha.honeypot.enabled', true) ? 'checked' : '' ?>>
|
||||
<span>Invisible honeypot fields that deny bot-like submissions</span></label>
|
||||
<label class="cb-row"><input type="checkbox" name="captcha_internal_enabled"
|
||||
<?= getConfigVal('security.captcha.internal.enabled', true) ? 'checked' : '' ?>>
|
||||
<span>Internal arithmetic challenge</span></label>
|
||||
</div>
|
||||
<div class="form-row3">
|
||||
<div class="field"><label>Internal Difficulty</label><select name="captcha_internal_difficulty">
|
||||
<?php $captchaDifficulty = getConfigVal('security.captcha.internal.difficulty', 'easy'); ?>
|
||||
<option value="easy" <?= $captchaDifficulty === 'easy' ? 'selected' : '' ?>>Easy (1-9)</option>
|
||||
<option value="medium" <?= $captchaDifficulty === 'medium' ? 'selected' : '' ?>>Medium (1-25)</option>
|
||||
</select></div>
|
||||
<div class="field"><label>Challenge TTL (seconds)</label><input type="number" min="60" max="1800"
|
||||
name="captcha_internal_ttl" value="<?= (int)getConfigVal('security.captcha.internal.ttl_seconds', 300) ?>"></div>
|
||||
<div class="field"><label>Maximum Attempts</label><input type="number" min="1" max="10"
|
||||
name="captcha_internal_attempts" value="<?= (int)getConfigVal('security.captcha.internal.max_attempts', 3) ?>"></div>
|
||||
</div>
|
||||
<label class="cb-row"><input type="checkbox" name="captcha_google_enabled"
|
||||
<?= getConfigVal('security.captcha.google.enabled', false) ? 'checked' : '' ?>>
|
||||
<span>Also require Google reCAPTCHA</span></label>
|
||||
<div class="form-row3">
|
||||
<div class="field"><label>Google Version</label><select name="captcha_google_version">
|
||||
<?php $captchaVersion = getConfigVal('security.captcha.google.version', 'v2'); ?>
|
||||
<option value="v2" <?= $captchaVersion === 'v2' ? 'selected' : '' ?>>v2 checkbox</option>
|
||||
<option value="v3" <?= $captchaVersion === 'v3' ? 'selected' : '' ?>>v3 score</option>
|
||||
</select></div>
|
||||
<div class="field"><label>Site Key</label><input name="captcha_google_site_key"
|
||||
value="<?= esc(getConfigVal('security.captcha.google.site_key', '')) ?>"></div>
|
||||
<div class="field"><label>Secret Key</label><input type="password" name="captcha_google_secret_key"
|
||||
value="<?= esc(getConfigVal('security.captcha.google.secret_key', '')) ?>"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="field"><label>v3 Minimum Score</label><input type="number" min="0" max="1" step="0.1"
|
||||
name="captcha_google_v3_score" value="<?= esc(getConfigVal('security.captcha.google.v3_min_score', 0.5)) ?>"></div>
|
||||
<div class="field"><label>Expected Hostname (optional)</label><input name="captcha_google_hostname"
|
||||
value="<?= esc(getConfigVal('security.captcha.google.hostname', '')) ?>" placeholder="chat.example.com"></div>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<div class="panel"><div class="panel-head"><span class="panel-title">// MAILGUN</span></div><div class="panel-body">
|
||||
<div class="form-row3">
|
||||
<div class="field"><label>API Key</label><input type="password" name="mailgun_api_key" value="<?= esc(getConfigVal('mailgun.api_key', '')) ?>"></div>
|
||||
@@ -178,7 +265,7 @@ function renderAdminPlatformTab(): void {
|
||||
<option value="m4a_aac" <?= $voiceProfile === 'm4a_aac' ? 'selected' : '' ?>>M4A / AAC-LC (recommended)</option>
|
||||
<option value="mp4_h264_aac" <?= $voiceProfile === 'mp4_h264_aac' ? 'selected' : '' ?>>MP4 / H.264 + AAC</option>
|
||||
</select></div>
|
||||
<div class="field"><label>Audio Bitrate (kbps)</label><input type="number" min="48" max="320"
|
||||
<div class="field"><label>Fallback Audio Bitrate (kbps)</label><input type="number" min="48" max="320"
|
||||
name="voice_audio_bitrate" value="<?= (int)getConfigVal('voice.transcoding.audio_bitrate_kbps', 128) ?>"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
@@ -230,6 +317,10 @@ function renderAdminPlatformTab(): void {
|
||||
<div class="field"><label><?= esc(adminFeatureLabel($key)) ?></label><input type="number"
|
||||
min="30"
|
||||
name="plans[<?= $plan['id'] ?>][features][<?= $key ?>]" value="<?= (int)($plan['features'][$key] ?? 0) ?>"></div>
|
||||
<?php elseif ($key === 'voice_bitrate_kbps'): ?>
|
||||
<div class="field"><label><?= esc(adminFeatureLabel($key)) ?></label><input type="number"
|
||||
min="48" max="320"
|
||||
name="plans[<?= $plan['id'] ?>][features][<?= $key ?>]" value="<?= (int)($plan['features'][$key] ?? 64) ?>"></div>
|
||||
<?php else: ?>
|
||||
<label class="cb-row"><input type="checkbox" name="plans[<?= $plan['id'] ?>][features][<?= $key ?>]"
|
||||
<?= !empty($plan['features'][$key]) ? 'checked' : '' ?>><span><?= esc(adminFeatureLabel($key)) ?></span></label>
|
||||
|
||||
Reference in New Issue
Block a user