This commit is contained in:
Dan Milne
2025-11-15 10:51:58 +11:00
parent d9701e4af6
commit 90823a1389
10 changed files with 425 additions and 84 deletions

View File

@@ -113,13 +113,47 @@
</div>
</div>
<!-- Conditions (shown for non-network rules) -->
<!-- Path Pattern (shown for path_pattern rules) -->
<div id="path_pattern_section" class="hidden space-y-6">
<div>
<%= form.label :path_pattern, "Path Pattern", class: "block text-sm font-medium text-gray-700" %>
<%= text_field_tag :path_pattern, "",
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
placeholder: "/admin, /wp-login.php, /.env, /phpmyadmin",
id: "path_pattern_input" %>
<p class="mt-2 text-sm text-gray-500">Enter the path to match (e.g., /admin, /wp-login.php)</p>
</div>
<div>
<%= form.label :match_type, "Match Type", class: "block text-sm font-medium text-gray-700" %>
<%= select_tag :match_type,
options_for_select([
["Exact - Matches path exactly", "exact"],
["Prefix - Matches path and subpaths (e.g., /admin matches /admin/users)", "prefix"],
["Suffix - Matches paths ending with pattern (e.g., /.env matches /backup/.env)", "suffix"],
["Contains - Matches paths containing pattern anywhere", "contains"]
]),
{ class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
id: "match_type_select" } %>
<p class="mt-2 text-sm text-gray-500">How the pattern should be matched against request paths</p>
</div>
<!-- Example Matches (dynamically updated) -->
<div id="match_examples" class="bg-gray-50 border border-gray-200 rounded-md p-4">
<h4 class="text-sm font-medium text-gray-700 mb-2">Example Matches:</h4>
<ul class="text-sm text-gray-600 space-y-1" id="example_list">
<li>Enter a pattern to see examples</li>
</ul>
</div>
</div>
<!-- Conditions (shown for other non-network rules) -->
<div id="conditions_section" class="hidden">
<div>
<%= form.label :conditions, "Conditions", class: "block text-sm font-medium text-gray-700" %>
<%= form.text_area :conditions, rows: 4,
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
placeholder: '{"path_pattern": "/admin/*", "user_agent": "bot*"}' %>
placeholder: '{"user_agent": "bot*"}' %>
<p class="mt-2 text-sm text-gray-500">JSON format with matching conditions</p>
</div>
</div>
@@ -192,19 +226,83 @@ let selectedNetworkData = null;
document.addEventListener('DOMContentLoaded', function() {
const ruleTypeSelect = document.getElementById('rule_type_select');
const networkSection = document.getElementById('network_range_section');
const pathPatternSection = document.getElementById('path_pattern_section');
const conditionsSection = document.getElementById('conditions_section');
const pathPatternInput = document.getElementById('path_pattern_input');
const matchTypeSelect = document.getElementById('match_type_select');
function toggleSections() {
if (ruleTypeSelect.value === 'network') {
const ruleType = ruleTypeSelect.value;
// Hide all sections first
networkSection.classList.add('hidden');
pathPatternSection.classList.add('hidden');
conditionsSection.classList.add('hidden');
// Show appropriate section
if (ruleType === 'network') {
networkSection.classList.remove('hidden');
conditionsSection.classList.add('hidden');
} else if (ruleType === 'path_pattern') {
pathPatternSection.classList.remove('hidden');
} else {
networkSection.classList.add('hidden');
conditionsSection.classList.remove('hidden');
}
}
function updatePathExamples() {
const pattern = pathPatternInput.value.trim();
const matchType = matchTypeSelect.value;
const exampleList = document.getElementById('example_list');
if (!pattern) {
exampleList.innerHTML = '<li>Enter a pattern to see examples</li>';
return;
}
let examples = [];
const cleanPattern = pattern.startsWith('/') ? pattern : '/' + pattern;
switch(matchType) {
case 'exact':
examples = [
`✓ ${cleanPattern}`,
`✗ ${cleanPattern}/users (extra segments)`,
`✗ /api${cleanPattern} (not at root)`
];
break;
case 'prefix':
examples = [
`✓ ${cleanPattern}`,
`✓ ${cleanPattern}/users`,
`✓ ${cleanPattern}/dashboard/settings`,
`✗ /api${cleanPattern} (not at start)`
];
break;
case 'suffix':
examples = [
`✓ ${cleanPattern}`,
`✓ /backup${cleanPattern}`,
`✓ /config/backup${cleanPattern}`,
`✗ ${cleanPattern}/test (extra at end)`
];
break;
case 'contains':
examples = [
`✓ ${cleanPattern}`,
`✓ /api${cleanPattern}/users`,
`✓ /super/secret${cleanPattern}/panel`,
`✗ ${cleanPattern}tool (different segment)`
];
break;
}
exampleList.innerHTML = examples.map(ex => `<li>${ex}</li>`).join('');
}
ruleTypeSelect.addEventListener('change', toggleSections);
pathPatternInput.addEventListener('input', updatePathExamples);
matchTypeSelect.addEventListener('change', updatePathExamples);
toggleSections(); // Initial state
// Pre-select network range if provided