381 lines
13 KiB
JavaScript
381 lines
13 KiB
JavaScript
const BASE_URL = window.location.origin;
|
|
|
|
const APP_VARIANTS = {
|
|
'raw-cidr': [
|
|
{
|
|
value: 'txt',
|
|
text: 'Plain Text (.txt)',
|
|
description: 'Simple list of CIDR blocks, one per line'
|
|
},
|
|
{
|
|
value: 'csv',
|
|
text: 'CSV Format (.csv)',
|
|
description: 'Structured CSV with country codes and networks'
|
|
}
|
|
],
|
|
nginx: [
|
|
{
|
|
value: 'deny',
|
|
text: 'Deny Directives',
|
|
description: 'Simple and fast. Works everywhere. Recommended for large lists.',
|
|
recommended: true
|
|
},
|
|
{
|
|
value: 'geo',
|
|
text: 'Geo Module',
|
|
description: 'Fast with native CIDR support. Requires http_geo_module compiled in nginx.'
|
|
},
|
|
{
|
|
value: 'map',
|
|
text: 'Map Module (regex)',
|
|
description: 'Slow with 10k+ rules. Uses regex patterns. Not recommended for production.',
|
|
warning: true
|
|
}
|
|
],
|
|
apache: [
|
|
{
|
|
value: '24',
|
|
text: 'Apache 2.4 (Require)',
|
|
description: 'Modern Apache 2.4+ syntax using Require directives'
|
|
},
|
|
{
|
|
value: '22',
|
|
text: 'Apache 2.2 (Allow/Deny)',
|
|
description: 'Legacy Apache 2.2 syntax with Allow/Deny directives'
|
|
}
|
|
],
|
|
haproxy: [
|
|
{
|
|
value: 'acl',
|
|
text: 'ACL Rules',
|
|
description: 'Native HAProxy ACL rules for frontend/backend blocking'
|
|
},
|
|
{
|
|
value: 'lua',
|
|
text: 'Lua Script',
|
|
description: 'Lua-based blocking script for advanced HAProxy setups'
|
|
},
|
|
{
|
|
value: 'map',
|
|
text: 'Map File',
|
|
description: 'HAProxy map format (for use with -m ip / map files)'
|
|
}
|
|
]
|
|
};
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
updateVariants();
|
|
checkDatabaseStatus();
|
|
});
|
|
|
|
|
|
function updateVariants() {
|
|
const appType = document.getElementById('appType').value;
|
|
const variantSelect = document.getElementById('appVariant');
|
|
const variantSection = document.getElementById('variantSection');
|
|
const variants = APP_VARIANTS[appType] || [];
|
|
|
|
variantSelect.innerHTML = '';
|
|
variantSection.style.display = 'block';
|
|
|
|
variants.forEach(variant => {
|
|
const option = document.createElement('option');
|
|
option.value = variant.value;
|
|
option.textContent = variant.text;
|
|
option.dataset.description = variant.description || '';
|
|
option.dataset.warning = variant.warning || false;
|
|
option.dataset.recommended = variant.recommended || false;
|
|
variantSelect.appendChild(option);
|
|
});
|
|
|
|
updateVariantDescription();
|
|
}
|
|
|
|
|
|
function updateVariantDescription() {
|
|
const variantSelect = document.getElementById('appVariant');
|
|
const descriptionDiv = document.getElementById('variantDescription');
|
|
|
|
if (!descriptionDiv) return;
|
|
|
|
const selectedOption = variantSelect.options[variantSelect.selectedIndex];
|
|
|
|
if (selectedOption && selectedOption.dataset.description) {
|
|
const isWarning = selectedOption.dataset.warning === 'true';
|
|
const isRecommended = selectedOption.dataset.recommended === 'true';
|
|
|
|
let alertClass = 'alert-info';
|
|
let borderClass = 'border-info';
|
|
let icon = 'fa-info-circle';
|
|
|
|
if (isRecommended) {
|
|
alertClass = 'alert-success';
|
|
borderClass = 'border-success';
|
|
icon = 'fa-check-circle';
|
|
} else if (isWarning) {
|
|
alertClass = 'alert-warning';
|
|
borderClass = 'border-warning';
|
|
icon = 'fa-exclamation-triangle';
|
|
}
|
|
|
|
descriptionDiv.innerHTML = `
|
|
<div class="alert ${alertClass} border-start border-4 ${borderClass} mb-0 py-2">
|
|
<small>
|
|
<i class="fas ${icon} me-2"></i>
|
|
${selectedOption.dataset.description}
|
|
</small>
|
|
</div>
|
|
`;
|
|
descriptionDiv.style.display = 'block';
|
|
} else {
|
|
descriptionDiv.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
|
|
function checkDatabaseStatus() {
|
|
fetch(BASE_URL + '/api/database/status')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const statusDiv = document.getElementById('dbStatus');
|
|
|
|
if (data.success) {
|
|
if (data.exists && !data.needs_update) {
|
|
statusDiv.className = 'alert alert-success mb-0';
|
|
statusDiv.innerHTML = '<i class="fas fa-check-circle me-2"></i>Database ready (Last update: ' + formatDate(data.last_update) + ')';
|
|
} else if (data.needs_update) {
|
|
statusDiv.className = 'alert alert-warning mb-0';
|
|
statusDiv.innerHTML = '<i class="fas fa-exclamation-triangle me-2"></i>Database needs update <button class="btn btn-sm btn-warning ms-2" onclick="updateDatabase()">Update Now</button>';
|
|
} else {
|
|
statusDiv.className = 'alert alert-info mb-0';
|
|
statusDiv.innerHTML = '<i class="fas fa-download me-2"></i>Downloading database...';
|
|
}
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
});
|
|
}
|
|
|
|
|
|
function updateDatabase() {
|
|
const statusDiv = document.getElementById('dbStatus');
|
|
statusDiv.className = 'alert alert-info mb-0';
|
|
statusDiv.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Updating database...';
|
|
|
|
fetch(BASE_URL + '/api/database/update', { method: 'POST' })
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
statusDiv.className = 'alert alert-success mb-0';
|
|
statusDiv.innerHTML = '<i class="fas fa-check-circle me-2"></i>Database updated successfully';
|
|
setTimeout(checkDatabaseStatus, 2000);
|
|
} else {
|
|
statusDiv.className = 'alert alert-danger mb-0';
|
|
statusDiv.innerHTML = '<i class="fas fa-times-circle me-2"></i>Update failed: ' + data.error;
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
function selectAll() {
|
|
const checkboxes = document.querySelectorAll('input[name="countries"]');
|
|
checkboxes.forEach(cb => cb.checked = true);
|
|
}
|
|
|
|
|
|
function deselectAll() {
|
|
const checkboxes = document.querySelectorAll('input[name="countries"]');
|
|
checkboxes.forEach(cb => cb.checked = false);
|
|
}
|
|
|
|
|
|
function formatDate(dateString) {
|
|
if (!dateString) return 'Never';
|
|
try {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
|
|
} catch (e) {
|
|
return dateString;
|
|
}
|
|
}
|
|
|
|
|
|
function copyToClipboard() {
|
|
const content = document.getElementById('previewContent').textContent;
|
|
|
|
navigator.clipboard.writeText(content).then(() => {
|
|
showResult('Copied to clipboard!', 'success');
|
|
}).catch(err => {
|
|
const textarea = document.createElement('textarea');
|
|
textarea.value = content;
|
|
textarea.style.position = 'fixed';
|
|
textarea.style.opacity = '0';
|
|
document.body.appendChild(textarea);
|
|
textarea.select();
|
|
|
|
try {
|
|
document.execCommand('copy');
|
|
showResult('Copied to clipboard!', 'success');
|
|
} catch (e) {
|
|
showResult('Failed to copy to clipboard', 'danger');
|
|
}
|
|
|
|
document.body.removeChild(textarea);
|
|
});
|
|
}
|
|
|
|
|
|
function getFormData() {
|
|
const countries = Array.from(document.querySelectorAll('input[name="countries"]:checked'))
|
|
.map(input => input.value);
|
|
|
|
if (countries.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const useCacheCheckbox = document.getElementById('useCache');
|
|
|
|
return {
|
|
countries: countries,
|
|
app_type: document.getElementById('appType').value,
|
|
app_variant: document.getElementById('appVariant').value,
|
|
aggregate: document.getElementById('aggregate').checked,
|
|
use_cache: useCacheCheckbox ? useCacheCheckbox.checked : true
|
|
};
|
|
}
|
|
|
|
|
|
function showCacheBadge(fromCache, generatedAt) {
|
|
if (fromCache) {
|
|
const badge = document.createElement('div');
|
|
badge.className = 'alert alert-success alert-dismissible fade show mt-3';
|
|
badge.innerHTML = `
|
|
<i class="fas fa-bolt me-2"></i>
|
|
<strong>Lightning fast!</strong> Config loaded from Redis cache in <100ms
|
|
<small class="d-block mt-1">Generated: ${new Date(generatedAt).toLocaleString()}</small>
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
|
|
const container = document.querySelector('.container > .row > .col-lg-10');
|
|
container.insertBefore(badge, container.firstChild);
|
|
|
|
setTimeout(() => {
|
|
badge.classList.remove('show');
|
|
setTimeout(() => badge.remove(), 150);
|
|
}, 5000);
|
|
}
|
|
}
|
|
|
|
|
|
async function previewConfiguration() {
|
|
const formData = getFormData();
|
|
|
|
if (!formData) {
|
|
showResult('Please select at least one country to continue', 'warning');
|
|
return;
|
|
}
|
|
|
|
showProgress();
|
|
|
|
try {
|
|
const endpoint = formData.app_type === 'raw-cidr'
|
|
? '/api/generate/raw'
|
|
: '/api/generate/preview';
|
|
|
|
const response = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(formData)
|
|
});
|
|
|
|
hideProgress();
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
showResult('Error: ' + (errorData.error || 'Request failed'), 'danger');
|
|
return;
|
|
}
|
|
|
|
if (formData.app_type === 'raw-cidr') {
|
|
const text = await response.text();
|
|
const fromCache = response.headers.get('X-From-Cache') === 'true';
|
|
const generatedAt = response.headers.get('X-Generated-At');
|
|
|
|
const contentDisposition = response.headers.get('Content-Disposition');
|
|
let filename = 'blocklist.txt';
|
|
if (contentDisposition) {
|
|
const matches = /filename="?(.+)"?/.exec(contentDisposition);
|
|
if (matches) filename = matches[1];
|
|
}
|
|
|
|
document.getElementById('previewContent').textContent = text;
|
|
|
|
const cacheIndicator = document.getElementById('cacheIndicator');
|
|
if (fromCache) {
|
|
cacheIndicator.innerHTML = '<span class="badge bg-success ms-2"><i class="fas fa-bolt"></i> From Cache</span>';
|
|
showCacheBadge(true, generatedAt);
|
|
} else {
|
|
cacheIndicator.innerHTML = '<span class="badge bg-info ms-2"><i class="fas fa-sync"></i> Fresh</span>';
|
|
}
|
|
|
|
window.lastGeneratedConfig = text;
|
|
window.lastGeneratedFilename = filename;
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('previewModal'));
|
|
modal.show();
|
|
} else {
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
document.getElementById('previewContent').textContent = result.config;
|
|
|
|
const cacheIndicator = document.getElementById('cacheIndicator');
|
|
if (result.from_cache) {
|
|
cacheIndicator.innerHTML = '<span class="badge bg-success ms-2"><i class="fas fa-bolt"></i> From Cache</span>';
|
|
showCacheBadge(true, result.generated_at);
|
|
} else {
|
|
cacheIndicator.innerHTML = '<span class="badge bg-info ms-2"><i class="fas fa-sync"></i> Fresh</span>';
|
|
}
|
|
|
|
if (result.stats) {
|
|
const statsText = `${result.stats.countries} countries, ${result.stats.total_networks.toLocaleString()} networks`;
|
|
document.getElementById('previewStats').textContent = statsText;
|
|
}
|
|
|
|
window.lastGeneratedConfig = result.config;
|
|
window.currentStats = result.stats;
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('previewModal'));
|
|
modal.show();
|
|
} else {
|
|
showResult(result.error || 'An error occurred while generating the preview', 'danger');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
hideProgress();
|
|
showResult('Network error: ' + error.message, 'danger');
|
|
console.error('Preview error:', error);
|
|
}
|
|
}
|
|
|
|
|
|
async function downloadFromPreview() {
|
|
const formData = getFormData();
|
|
|
|
if (!formData) {
|
|
showResult('Please select at least one country', 'warning');
|
|
return;
|
|
}
|
|
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('previewModal'));
|
|
if (modal) {
|
|
modal.hide();
|
|
}
|
|
|
|
await downloadConfiguration(formData);
|
|
}
|