Add CSRF, CAPTCHA, and input validation improvements
Introduces CSRF protection to all sensitive controller actions, integrates configurable CAPTCHA (reCAPTCHA v2/v3, Turnstile) for authentication and registration flows, and centralizes input validation via a new InputValidator helper. Adds new helpers and services for CSRF and CAPTCHA, updates settings and migration for CAPTCHA configuration, and enhances logging and error handling in TLD registry import processes. Also improves validation for user, domain, group, and profile inputs throughout the application.
This commit is contained in:
86
app/Views/auth/captcha-widget.php
Normal file
86
app/Views/auth/captcha-widget.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
/**
|
||||
* CAPTCHA Widget Component
|
||||
* Renders the appropriate CAPTCHA widget based on settings
|
||||
*
|
||||
* Required variables:
|
||||
* - $captchaSettings: Array with 'provider' and 'site_key'
|
||||
*/
|
||||
|
||||
$provider = $captchaSettings['provider'] ?? 'disabled';
|
||||
$siteKey = $captchaSettings['site_key'] ?? '';
|
||||
|
||||
if ($provider === 'disabled' || empty($siteKey)) {
|
||||
return; // No CAPTCHA to render
|
||||
}
|
||||
?>
|
||||
|
||||
<!-- CAPTCHA Widget -->
|
||||
<div class="captcha-container mb-4">
|
||||
<?php if ($provider === 'recaptcha_v2'): ?>
|
||||
<!-- reCAPTCHA v2 -->
|
||||
<div class="g-recaptcha" data-sitekey="<?= htmlspecialchars($siteKey) ?>"></div>
|
||||
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
|
||||
|
||||
<?php elseif ($provider === 'recaptcha_v3'): ?>
|
||||
<!-- reCAPTCHA v3 (Invisible) -->
|
||||
<input type="hidden" id="captcha_response" name="captcha_response">
|
||||
<script src="https://www.google.com/recaptcha/api.js?render=<?= htmlspecialchars($siteKey) ?>"></script>
|
||||
|
||||
<?php elseif ($provider === 'turnstile'): ?>
|
||||
<!-- Cloudflare Turnstile -->
|
||||
<div class="cf-turnstile" data-sitekey="<?= htmlspecialchars($siteKey) ?>"></div>
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($provider === 'recaptcha_v3'): ?>
|
||||
<!-- reCAPTCHA v3 Form Submission Handler -->
|
||||
<script>
|
||||
// Store the original form submission handler
|
||||
const form = document.querySelector('form');
|
||||
const originalSubmit = form.onsubmit;
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
grecaptcha.ready(function() {
|
||||
grecaptcha.execute('<?= htmlspecialchars($siteKey) ?>', {action: 'submit'}).then(function(token) {
|
||||
document.getElementById('captcha_response').value = token;
|
||||
|
||||
// Call original submit handler if it exists
|
||||
if (originalSubmit && originalSubmit.call(form, e) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Submit the form
|
||||
form.submit();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php elseif ($provider === 'recaptcha_v2' || $provider === 'turnstile'): ?>
|
||||
<!-- reCAPTCHA v2 / Turnstile Response Handler -->
|
||||
<script>
|
||||
// Add hidden input to capture response
|
||||
const form = document.querySelector('form');
|
||||
const captchaInput = document.createElement('input');
|
||||
captchaInput.type = 'hidden';
|
||||
captchaInput.name = 'captcha_response';
|
||||
captchaInput.id = 'captcha_response';
|
||||
form.appendChild(captchaInput);
|
||||
|
||||
// Capture response on form submit
|
||||
form.addEventListener('submit', function(e) {
|
||||
<?php if ($provider === 'recaptcha_v2'): ?>
|
||||
const response = grecaptcha.getResponse();
|
||||
<?php else: // turnstile ?>
|
||||
const response = turnstile.getResponse();
|
||||
<?php endif; ?>
|
||||
|
||||
captchaInput.value = response;
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -35,6 +35,7 @@ ob_start();
|
||||
|
||||
<!-- Forgot Password Form -->
|
||||
<form method="POST" action="/forgot-password" class="space-y-5">
|
||||
<?= csrf_field() ?>
|
||||
<!-- Email Field -->
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
@@ -56,6 +57,9 @@ ob_start();
|
||||
<p class="text-xs text-gray-500 mt-1">Enter the email associated with your account</p>
|
||||
</div>
|
||||
|
||||
<!-- CAPTCHA Widget -->
|
||||
<?php include __DIR__ . '/captcha-widget.php'; ?>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
@@ -25,6 +25,7 @@ ob_start();
|
||||
|
||||
<!-- Login Form -->
|
||||
<form method="POST" action="/login" class="space-y-5">
|
||||
<?= csrf_field() ?>
|
||||
<!-- Username Field -->
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
@@ -85,6 +86,9 @@ ob_start();
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- CAPTCHA Widget -->
|
||||
<?php include __DIR__ . '/captcha-widget.php'; ?>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
@@ -35,6 +35,7 @@ ob_start();
|
||||
|
||||
<!-- Registration Form -->
|
||||
<form method="POST" action="/register" class="space-y-4">
|
||||
<?= csrf_field() ?>
|
||||
<!-- Full Name Field -->
|
||||
<div>
|
||||
<label for="full_name" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
@@ -163,6 +164,9 @@ ob_start();
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- CAPTCHA Widget -->
|
||||
<?php include __DIR__ . '/captcha-widget.php'; ?>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
@@ -25,6 +25,7 @@ ob_start();
|
||||
|
||||
<!-- Reset Password Form -->
|
||||
<form method="POST" action="/reset-password" class="space-y-4">
|
||||
<?= csrf_field() ?>
|
||||
<!-- Hidden token field -->
|
||||
<input type="hidden" name="token" value="<?= htmlspecialchars($token ?? '') ?>">
|
||||
|
||||
@@ -95,6 +96,9 @@ ob_start();
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- CAPTCHA Widget -->
|
||||
<?php include __DIR__ . '/captcha-widget.php'; ?>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
@@ -18,6 +18,7 @@ ob_start();
|
||||
|
||||
<div class="p-6">
|
||||
<form method="POST" action="/domains/bulk-add" class="space-y-5">
|
||||
<?= csrf_field() ?>
|
||||
<!-- Domains Textarea -->
|
||||
<div>
|
||||
<label for="domains" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
|
||||
@@ -18,6 +18,7 @@ ob_start();
|
||||
|
||||
<div class="p-6">
|
||||
<form method="POST" action="/domains/store" class="space-y-5">
|
||||
<?= csrf_field() ?>
|
||||
<!-- Domain Name -->
|
||||
<div>
|
||||
<label for="domain_name" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
|
||||
@@ -18,6 +18,7 @@ ob_start();
|
||||
|
||||
<div class="p-6">
|
||||
<form method="POST" action="/domains/<?= $domain['id'] ?>/update" class="space-y-5">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<!-- Domain Name (Read-only) -->
|
||||
<div>
|
||||
@@ -98,6 +99,7 @@ ob_start();
|
||||
<span class="text-sm font-medium text-gray-700">View Details</span>
|
||||
</a>
|
||||
<form method="POST" action="/domains/<?= $domain['id'] ?>/refresh" class="m-0">
|
||||
<?= csrf_field() ?>
|
||||
<button type="submit"
|
||||
class="w-full flex items-center justify-center p-3 bg-white border border-gray-200 rounded-lg hover:border-green-300 hover:bg-green-50 transition-colors group">
|
||||
<i class="fas fa-sync-alt text-green-600 mr-2 text-sm"></i>
|
||||
@@ -105,6 +107,7 @@ ob_start();
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="/domains/<?= $domain['id'] ?>/delete" onsubmit="return confirm('Delete this domain permanently?')" class="m-0">
|
||||
<?= csrf_field() ?>
|
||||
<button type="submit"
|
||||
class="w-full flex items-center justify-center p-3 bg-white border border-gray-200 rounded-lg hover:border-red-300 hover:bg-red-50 transition-colors group">
|
||||
<i class="fas fa-trash text-red-600 mr-2 text-sm"></i>
|
||||
|
||||
@@ -47,6 +47,7 @@ $currentFilters = $filters ?? ['search' => '', 'status' => '', 'group' => '', 's
|
||||
</button>
|
||||
<div id="assign-group-dropdown" class="hidden absolute left-0 mt-2 w-64 bg-white rounded-lg shadow-lg border border-gray-200 z-10">
|
||||
<form method="POST" action="/domains/bulk-assign-group" id="bulk-assign-form">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="domain_ids" id="bulk-assign-ids">
|
||||
<div class="p-3">
|
||||
<select name="group_id" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
@@ -83,6 +84,7 @@ $currentFilters = $filters ?? ['search' => '', 'status' => '', 'group' => '', 's
|
||||
<div class="flex gap-2">
|
||||
<?php if (!empty($domains)): ?>
|
||||
<form method="POST" action="/domains/bulk-refresh" id="refresh-all-form">
|
||||
<?= csrf_field() ?>
|
||||
<?php foreach ($domains as $domain): ?>
|
||||
<input type="hidden" name="domain_ids[]" value="<?= $domain['id'] ?>">
|
||||
<?php endforeach; ?>
|
||||
@@ -299,6 +301,7 @@ $currentFilters = $filters ?? ['search' => '', 'status' => '', 'group' => '', 's
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<form method="POST" action="/domains/<?= $domain['id'] ?>/refresh" class="inline">
|
||||
<?= csrf_field() ?>
|
||||
<button type="submit" class="text-green-600 hover:text-green-800" title="Refresh WHOIS">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
@@ -307,6 +310,7 @@ $currentFilters = $filters ?? ['search' => '', 'status' => '', 'group' => '', 's
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<form method="POST" action="/domains/<?= $domain['id'] ?>/delete" class="inline" onsubmit="return confirm('Delete this domain?')">
|
||||
<?= csrf_field() ?>
|
||||
<button type="submit" class="text-red-600 hover:text-red-800" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
|
||||
@@ -39,6 +39,7 @@ ob_start();
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<form method="POST" action="/domains/<?= $domain['id'] ?>/refresh" class="inline">
|
||||
<?= csrf_field() ?>
|
||||
<button type="submit" class="inline-flex items-center justify-center px-3 py-2 bg-green-600 text-white text-xs rounded-lg hover:bg-green-700 transition-colors font-medium min-w-[80px] h-[32px]">
|
||||
<i class="fas fa-sync-alt mr-1.5"></i>
|
||||
Refresh
|
||||
@@ -49,6 +50,7 @@ ob_start();
|
||||
Edit
|
||||
</a>
|
||||
<form method="POST" action="/domains/<?= $domain['id'] ?>/delete" onsubmit="return confirm('Delete this domain?')" class="inline">
|
||||
<?= csrf_field() ?>
|
||||
<button type="submit" class="inline-flex items-center justify-center px-3 py-2 bg-red-600 text-white text-xs rounded-lg hover:bg-red-700 transition-colors font-medium min-w-[80px] h-[32px]">
|
||||
<i class="fas fa-trash mr-1.5"></i>
|
||||
Delete
|
||||
@@ -293,6 +295,7 @@ ob_start();
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<form method="POST" action="/domains/<?= $domain['id'] ?>/update-notes" id="notes-form">
|
||||
<?= csrf_field() ?>
|
||||
<textarea
|
||||
name="notes"
|
||||
id="notes-textarea"
|
||||
|
||||
@@ -18,6 +18,7 @@ ob_start();
|
||||
|
||||
<div class="p-6">
|
||||
<form method="POST" action="/groups/store" class="space-y-5">
|
||||
<?= csrf_field() ?>
|
||||
<!-- Group Name -->
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
|
||||
@@ -18,6 +18,7 @@ ob_start();
|
||||
|
||||
<div class="p-6">
|
||||
<form method="POST" action="/groups/update" class="space-y-5">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= $group['id'] ?>">
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
@@ -129,6 +130,7 @@ ob_start();
|
||||
</h3>
|
||||
|
||||
<form method="POST" action="/channels/add" id="channelForm" class="space-y-5">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="group_id" value="<?= $group['id'] ?>">
|
||||
|
||||
<!-- Channel Type -->
|
||||
|
||||
@@ -82,6 +82,7 @@ ob_start();
|
||||
</div>
|
||||
|
||||
<form method="POST" action="/profile/update" class="p-6">
|
||||
<?= csrf_field() ?>
|
||||
<div class="space-y-5">
|
||||
<!-- Full Name -->
|
||||
<div>
|
||||
@@ -180,6 +181,7 @@ ob_start();
|
||||
</div>
|
||||
|
||||
<form method="POST" action="/profile/change-password" class="p-6">
|
||||
<?= csrf_field() ?>
|
||||
<div class="space-y-4">
|
||||
<!-- Current Password -->
|
||||
<div>
|
||||
@@ -242,6 +244,7 @@ ob_start();
|
||||
</div>
|
||||
<?php if (count($sessions ?? []) > 1): ?>
|
||||
<form method="POST" action="/profile/logout-other-sessions" onsubmit="return confirm('Logout all other sessions?')" class="inline">
|
||||
<?= csrf_field() ?>
|
||||
<button type="submit" class="inline-flex items-center px-3 py-2 bg-red-600 text-white text-xs rounded-lg hover:bg-red-700 transition-colors font-medium">
|
||||
<i class="fas fa-sign-out-alt mr-1.5"></i>
|
||||
Logout Others
|
||||
@@ -332,6 +335,7 @@ ob_start();
|
||||
<!-- Delete Button (only for non-current sessions) -->
|
||||
<?php if (!$isCurrent): ?>
|
||||
<form method="POST" action="/profile/logout-session/<?= htmlspecialchars($session['id']) ?>" onsubmit="return confirm('Terminate this session?\n\nThat device will be logged out immediately.')" class="ml-3">
|
||||
<?= csrf_field() ?>
|
||||
<button type="submit" class="flex items-center justify-center w-8 h-8 bg-red-100 text-red-600 rounded-lg hover:bg-red-600 hover:text-white transition-colors" title="Terminate session">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
|
||||
@@ -48,6 +48,10 @@ foreach ($notificationPresets as $key => $preset) {
|
||||
<i class="fas fa-bell mr-2"></i>
|
||||
Monitoring
|
||||
</button>
|
||||
<button onclick="switchTab('security')" id="tab-security" class="tab-button px-6 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap">
|
||||
<i class="fas fa-shield-alt mr-2"></i>
|
||||
Security
|
||||
</button>
|
||||
<button onclick="switchTab('system')" id="tab-system" class="tab-button px-6 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap">
|
||||
<i class="fas fa-server mr-2"></i>
|
||||
System
|
||||
@@ -69,6 +73,7 @@ foreach ($notificationPresets as $key => $preset) {
|
||||
</div>
|
||||
|
||||
<form method="POST" action="/settings/update-app" class="p-6">
|
||||
<?= csrf_field() ?>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="app_name" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
@@ -174,6 +179,7 @@ foreach ($notificationPresets as $key => $preset) {
|
||||
</div>
|
||||
|
||||
<form method="POST" action="/settings/update-email" class="p-6">
|
||||
<?= csrf_field() ?>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="mail_host" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
@@ -278,6 +284,7 @@ foreach ($notificationPresets as $key => $preset) {
|
||||
Send a test email to verify your SMTP settings are configured correctly.
|
||||
</p>
|
||||
<form method="POST" action="/settings/test-email" id="testEmailForm" class="flex gap-2">
|
||||
<?= csrf_field() ?>
|
||||
<input type="email" name="test_email" id="test_email" required
|
||||
placeholder="Enter email address to receive test"
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary text-sm">
|
||||
@@ -302,6 +309,7 @@ foreach ($notificationPresets as $key => $preset) {
|
||||
</div>
|
||||
|
||||
<form method="POST" action="/settings/update" id="settingsForm" class="p-6">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<!-- Notification Settings -->
|
||||
<div class="mb-6">
|
||||
@@ -411,6 +419,120 @@ foreach ($notificationPresets as $key => $preset) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content: Security Settings -->
|
||||
<div id="content-security" class="tab-content hidden">
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Security Settings</h3>
|
||||
<p class="text-sm text-gray-600 mt-1">Configure CAPTCHA protection for authentication forms</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="/settings/update-captcha" class="p-6">
|
||||
<?= csrf_field() ?>
|
||||
<div class="space-y-4">
|
||||
<!-- CAPTCHA Provider Selection -->
|
||||
<div>
|
||||
<label for="captcha_provider" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
CAPTCHA Provider
|
||||
</label>
|
||||
<select id="captcha_provider" name="captcha_provider"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
|
||||
<option value="disabled" <?= ($captchaSettings['provider'] ?? 'disabled') === 'disabled' ? 'selected' : '' ?>>
|
||||
Disabled (No CAPTCHA)
|
||||
</option>
|
||||
<option value="recaptcha_v2" <?= ($captchaSettings['provider'] ?? '') === 'recaptcha_v2' ? 'selected' : '' ?>>
|
||||
Google reCAPTCHA v2 (Checkbox)
|
||||
</option>
|
||||
<option value="recaptcha_v3" <?= ($captchaSettings['provider'] ?? '') === 'recaptcha_v3' ? 'selected' : '' ?>>
|
||||
Google reCAPTCHA v3 (Invisible)
|
||||
</option>
|
||||
<option value="turnstile" <?= ($captchaSettings['provider'] ?? '') === 'turnstile' ? 'selected' : '' ?>>
|
||||
Cloudflare Turnstile
|
||||
</option>
|
||||
</select>
|
||||
<p class="text-xs text-gray-500 mt-1">CAPTCHA protects login, registration, and password reset forms</p>
|
||||
</div>
|
||||
|
||||
<!-- CAPTCHA Configuration Fields (shown when enabled) -->
|
||||
<div id="captcha_config" style="display: <?= ($captchaSettings['provider'] ?? 'disabled') !== 'disabled' ? 'block' : 'none' ?>;">
|
||||
<div class="border-t border-gray-200 pt-4 mt-4">
|
||||
<!-- Site Key -->
|
||||
<div class="mb-4">
|
||||
<label for="captcha_site_key" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Site Key (Public Key)
|
||||
</label>
|
||||
<input type="text" id="captcha_site_key" name="captcha_site_key"
|
||||
value="<?= htmlspecialchars($captchaSettings['site_key'] ?? '') ?>"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary"
|
||||
placeholder="Enter your site/public key">
|
||||
<p class="text-xs text-gray-500 mt-1">Public key visible in HTML source</p>
|
||||
</div>
|
||||
|
||||
<!-- Secret Key -->
|
||||
<div class="mb-4">
|
||||
<label for="captcha_secret_key" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Secret Key
|
||||
</label>
|
||||
<input type="password" id="captcha_secret_key" name="captcha_secret_key"
|
||||
value=""
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary"
|
||||
placeholder="<?= !empty($captchaSettings['secret_key']) ? '••••••••••••••••' : 'Enter your secret key' ?>">
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
<i class="fas fa-lock text-green-600 mr-1"></i>
|
||||
Encrypted before storing in database. Leave blank to keep existing key.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- reCAPTCHA v3 Score Threshold (only for v3) -->
|
||||
<div id="recaptcha_v3_threshold" style="display: <?= ($captchaSettings['provider'] ?? '') === 'recaptcha_v3' ? 'block' : 'none' ?>;">
|
||||
<label for="recaptcha_v3_score_threshold" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
reCAPTCHA v3 Score Threshold
|
||||
</label>
|
||||
<input type="number" id="recaptcha_v3_score_threshold" name="recaptcha_v3_score_threshold"
|
||||
value="<?= htmlspecialchars($captchaSettings['score_threshold'] ?? '0.5') ?>"
|
||||
min="0.0" max="1.0" step="0.1"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
|
||||
<p class="text-xs text-gray-500 mt-1">Minimum score required (0.0 to 1.0). Default: 0.5. Lower = more permissive.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Provider-specific Documentation -->
|
||||
<div id="captcha_docs" class="bg-blue-50 border border-blue-200 rounded-lg p-4 mt-4">
|
||||
<p class="text-sm font-medium text-gray-900 mb-2">
|
||||
<i class="fas fa-info-circle text-blue-500 mr-1"></i>
|
||||
<span id="captcha_docs_title">Setup Instructions</span>
|
||||
</p>
|
||||
<div id="docs_recaptcha_v2" class="text-sm text-gray-700" style="display: none;">
|
||||
<p class="mb-1">1. Visit <a href="https://www.google.com/recaptcha/admin" target="_blank" class="text-primary hover:underline">Google reCAPTCHA Admin Console</a></p>
|
||||
<p class="mb-1">2. Register a new site with reCAPTCHA v2 "I'm not a robot" Checkbox</p>
|
||||
<p>3. Copy the Site Key and Secret Key to the fields above</p>
|
||||
</div>
|
||||
<div id="docs_recaptcha_v3" class="text-sm text-gray-700" style="display: none;">
|
||||
<p class="mb-1">1. Visit <a href="https://www.google.com/recaptcha/admin" target="_blank" class="text-primary hover:underline">Google reCAPTCHA Admin Console</a></p>
|
||||
<p class="mb-1">2. Register a new site with reCAPTCHA v3</p>
|
||||
<p class="mb-1">3. Copy the Site Key and Secret Key to the fields above</p>
|
||||
<p>4. Adjust the score threshold based on your security needs (0.5 is recommended)</p>
|
||||
</div>
|
||||
<div id="docs_turnstile" class="text-sm text-gray-700" style="display: none;">
|
||||
<p class="mb-1">1. Visit <a href="https://dash.cloudflare.com/?to=/:account/turnstile" target="_blank" class="text-primary hover:underline">Cloudflare Turnstile Dashboard</a></p>
|
||||
<p class="mb-1">2. Create a new Turnstile widget</p>
|
||||
<p class="mb-1">3. Choose "Managed" mode for best user experience</p>
|
||||
<p>4. Copy the Site Key and Secret Key to the fields above</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pt-6 mt-6 border-t border-gray-200">
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2.5 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
|
||||
<i class="fas fa-save mr-2"></i>
|
||||
Save Security Settings
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content: System Information -->
|
||||
<div id="content-system" class="tab-content hidden">
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
@@ -499,6 +621,7 @@ foreach ($notificationPresets as $key => $preset) {
|
||||
</div>
|
||||
|
||||
<form method="POST" action="/settings/clear-logs" onsubmit="return confirm('Are you sure you want to clear logs older than 30 days? This action cannot be undone.')">
|
||||
<?= csrf_field() ?>
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2.5 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700 transition-colors font-medium">
|
||||
<i class="fas fa-trash-alt mr-2"></i>
|
||||
Clear Old Logs
|
||||
@@ -547,7 +670,7 @@ function switchTab(tabName) {
|
||||
// Load tab from URL hash on page load
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
const hash = window.location.hash.substring(1); // Remove the #
|
||||
const validTabs = ['app', 'email', 'monitoring', 'system', 'maintenance'];
|
||||
const validTabs = ['app', 'email', 'monitoring', 'security', 'system', 'maintenance'];
|
||||
|
||||
if (hash && validTabs.includes(hash)) {
|
||||
switchTab(hash);
|
||||
@@ -610,6 +733,51 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// CAPTCHA provider selection logic
|
||||
const captchaProvider = document.getElementById('captcha_provider');
|
||||
if (captchaProvider) {
|
||||
const captchaConfig = document.getElementById('captcha_config');
|
||||
const v3Threshold = document.getElementById('recaptcha_v3_threshold');
|
||||
const docsV2 = document.getElementById('docs_recaptcha_v2');
|
||||
const docsV3 = document.getElementById('docs_recaptcha_v3');
|
||||
const docsTurnstile = document.getElementById('docs_turnstile');
|
||||
|
||||
function updateCaptchaUI() {
|
||||
const selectedProvider = captchaProvider.value;
|
||||
|
||||
// Show/hide configuration section
|
||||
if (selectedProvider === 'disabled') {
|
||||
captchaConfig.style.display = 'none';
|
||||
} else {
|
||||
captchaConfig.style.display = 'block';
|
||||
}
|
||||
|
||||
// Show/hide v3 threshold field
|
||||
if (selectedProvider === 'recaptcha_v3') {
|
||||
v3Threshold.style.display = 'block';
|
||||
} else {
|
||||
v3Threshold.style.display = 'none';
|
||||
}
|
||||
|
||||
// Update documentation
|
||||
docsV2.style.display = 'none';
|
||||
docsV3.style.display = 'none';
|
||||
docsTurnstile.style.display = 'none';
|
||||
|
||||
if (selectedProvider === 'recaptcha_v2') {
|
||||
docsV2.style.display = 'block';
|
||||
} else if (selectedProvider === 'recaptcha_v3') {
|
||||
docsV3.style.display = 'block';
|
||||
} else if (selectedProvider === 'turnstile') {
|
||||
docsTurnstile.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
captchaProvider.addEventListener('change', updateCaptchaUI);
|
||||
// Initialize on page load
|
||||
updateCaptchaUI();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ ob_start();
|
||||
'check_updates' => 'Checking for IANA updates',
|
||||
'complete_workflow' => 'Complete TLD import workflow (TLD List → RDAP → WHOIS & Registry Data)'
|
||||
];
|
||||
echo $descriptions[$import_type] ?? 'Processing import';
|
||||
echo htmlspecialchars($descriptions[$import_type] ?? 'Processing import');
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
@@ -194,7 +194,10 @@ function updateProgress(data) {
|
||||
statusBadge.className = 'inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800';
|
||||
statusText.innerHTML = '<i class="fas fa-check mr-2"></i>Complete';
|
||||
isComplete = true;
|
||||
addLogMessage('Import completed successfully!', 'success');
|
||||
|
||||
// Show the actual completion message from API
|
||||
const completionMessage = data.message || 'Import completed successfully!';
|
||||
addLogMessage(completionMessage, 'success');
|
||||
|
||||
// Mark all steps as completed for complete workflow
|
||||
if (importType === 'complete_workflow') {
|
||||
|
||||
@@ -33,6 +33,7 @@ $currentFilters = $filters ?? ['search' => '', 'sort' => 'tld', 'order' => 'asc'
|
||||
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<form method="POST" action="/tld-registry/start-progressive-import" class="inline">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="import_type" value="complete_workflow">
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2.5 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors font-medium" title="Complete TLD import workflow: TLD List → RDAP → WHOIS → Registry URLs">
|
||||
<i class="fas fa-rocket mr-2"></i>
|
||||
@@ -40,6 +41,7 @@ $currentFilters = $filters ?? ['search' => '', 'sort' => 'tld', 'order' => 'asc'
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="/tld-registry/start-progressive-import" class="inline">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="import_type" value="check_updates">
|
||||
<button type="submit" <?= $stats['total'] == 0 ? 'disabled' : '' ?> class="inline-flex items-center px-4 py-2.5 <?= $stats['total'] == 0 ? 'bg-gray-400 cursor-not-allowed' : 'bg-purple-600 hover:bg-purple-700' ?> text-white text-sm rounded-lg transition-colors font-medium" title="<?= $stats['total'] == 0 ? 'Import TLDs first' : 'Check for IANA updates' ?>">
|
||||
<i class="fas fa-sync-alt mr-2"></i>
|
||||
@@ -205,6 +207,7 @@ $currentFilters = $filters ?? ['search' => '', 'sort' => 'tld', 'order' => 'asc'
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="text-sm text-gray-600">Bulk Actions:</span>
|
||||
<form method="POST" action="/tld-registry/bulk-delete" id="bulk-delete-form" class="inline">
|
||||
<?= csrf_field() ?>
|
||||
<button type="button" onclick="confirmBulkDelete()" class="inline-flex items-center px-3 py-2 border border-red-300 text-red-700 text-sm rounded-lg hover:bg-red-50 transition-colors font-medium">
|
||||
<i class="fas fa-trash mr-2"></i>
|
||||
Delete Selected
|
||||
|
||||
@@ -7,6 +7,7 @@ ob_start();
|
||||
?>
|
||||
|
||||
<form method="POST" action="/users/store" class="max-w-2xl">
|
||||
<?= csrf_field() ?>
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
||||
<h3 class="text-lg font-semibold text-gray-900">User Information</h3>
|
||||
|
||||
@@ -7,6 +7,7 @@ ob_start();
|
||||
?>
|
||||
|
||||
<form method="POST" action="/users/update" class="max-w-2xl">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= $user['id'] ?>">
|
||||
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
|
||||
Reference in New Issue
Block a user