Migrate from file-based configs to database with per-IP site configuration
Major architectural changes: - Replace YAML config files with database-stored ScanConfig model - Remove CIDR block support in favor of individual IP addresses per site - Each IP now has its own expected_ping, expected_tcp_ports, expected_udp_ports - AlertRule now uses config_id FK instead of config_file string API changes: - POST /api/scans now requires config_id instead of config_file - Alert rules API uses config_id with validation - All config dropdowns fetch from /api/configs dynamically Template updates: - scans.html, dashboard.html, alert_rules.html load configs via API - Display format: Config Title (X sites) in dropdowns - Removed Jinja2 config_files loops Migrations: - 008: Expand CIDRs to individual IPs with per-IP port configs - 009: Remove CIDR-related columns - 010: Add config_id to alert_rules, remove config_file
This commit is contained in:
@@ -96,8 +96,8 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if rule.config_file %}
|
||||
<small class="text-muted">{{ rule.config_file }}</small>
|
||||
{% if rule.config %}
|
||||
<small class="text-muted">{{ rule.config.title }}</small>
|
||||
{% else %}
|
||||
<span class="badge bg-primary">All Configs</span>
|
||||
{% endif %}
|
||||
@@ -209,20 +209,9 @@
|
||||
<label for="rule-config" class="form-label">Apply to Config (optional)</label>
|
||||
<select class="form-select" id="rule-config">
|
||||
<option value="">All Configs (Apply to all scans)</option>
|
||||
{% if config_files %}
|
||||
{% for config_file in config_files %}
|
||||
<option value="{{ config_file }}">{{ config_file }}</option>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<option value="" disabled>No config files found</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
<small class="form-text text-muted">
|
||||
{% if config_files %}
|
||||
Select a specific config file to limit this rule, or leave as "All Configs" to apply to all scans
|
||||
{% else %}
|
||||
No config files found. Upload a config in the Configs section to see available options.
|
||||
{% endif %}
|
||||
<small class="form-text text-muted" id="config-help-text">
|
||||
Select a specific config to limit this rule, or leave as "All Configs" to apply to all scans
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -272,12 +261,51 @@
|
||||
<script>
|
||||
let editingRuleId = null;
|
||||
|
||||
// Load available configs for the dropdown
|
||||
async function loadConfigsForRule() {
|
||||
const selectEl = document.getElementById('rule-config');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/configs');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load configurations');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const configs = data.configs || [];
|
||||
|
||||
// Preserve the "All Configs" option and current selection
|
||||
const currentValue = selectEl.value;
|
||||
selectEl.innerHTML = '<option value="">All Configs (Apply to all scans)</option>';
|
||||
|
||||
configs.forEach(config => {
|
||||
const option = document.createElement('option');
|
||||
// Store the config ID as the value
|
||||
option.value = config.id;
|
||||
const siteText = config.site_count === 1 ? 'site' : 'sites';
|
||||
option.textContent = `${config.title} (${config.site_count} ${siteText})`;
|
||||
selectEl.appendChild(option);
|
||||
});
|
||||
|
||||
// Restore selection if it was set
|
||||
if (currentValue) {
|
||||
selectEl.value = currentValue;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading configs:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function showCreateRuleModal() {
|
||||
editingRuleId = null;
|
||||
document.getElementById('ruleModalTitle').textContent = 'Create Alert Rule';
|
||||
document.getElementById('save-rule-text').textContent = 'Create Rule';
|
||||
document.getElementById('ruleForm').reset();
|
||||
document.getElementById('rule-enabled').checked = true;
|
||||
|
||||
// Load configs when modal is shown
|
||||
loadConfigsForRule();
|
||||
|
||||
new bootstrap.Modal(document.getElementById('ruleModal')).show();
|
||||
}
|
||||
|
||||
@@ -286,33 +314,36 @@ function editRule(ruleId) {
|
||||
document.getElementById('ruleModalTitle').textContent = 'Edit Alert Rule';
|
||||
document.getElementById('save-rule-text').textContent = 'Update Rule';
|
||||
|
||||
// Fetch rule details
|
||||
fetch(`/api/alerts/rules`, {
|
||||
headers: {
|
||||
'X-API-Key': localStorage.getItem('api_key') || ''
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const rule = data.rules.find(r => r.id === ruleId);
|
||||
if (rule) {
|
||||
document.getElementById('rule-id').value = rule.id;
|
||||
document.getElementById('rule-name').value = rule.name || '';
|
||||
document.getElementById('rule-type').value = rule.rule_type;
|
||||
document.getElementById('rule-severity').value = rule.severity || 'warning';
|
||||
document.getElementById('rule-threshold').value = rule.threshold || '';
|
||||
document.getElementById('rule-config').value = rule.config_file || '';
|
||||
document.getElementById('rule-email').checked = rule.email_enabled;
|
||||
document.getElementById('rule-webhook').checked = rule.webhook_enabled;
|
||||
document.getElementById('rule-enabled').checked = rule.enabled;
|
||||
// Load configs first, then fetch rule details
|
||||
loadConfigsForRule().then(() => {
|
||||
// Fetch rule details
|
||||
fetch(`/api/alerts/rules`, {
|
||||
headers: {
|
||||
'X-API-Key': localStorage.getItem('api_key') || ''
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const rule = data.rules.find(r => r.id === ruleId);
|
||||
if (rule) {
|
||||
document.getElementById('rule-id').value = rule.id;
|
||||
document.getElementById('rule-name').value = rule.name || '';
|
||||
document.getElementById('rule-type').value = rule.rule_type;
|
||||
document.getElementById('rule-severity').value = rule.severity || 'warning';
|
||||
document.getElementById('rule-threshold').value = rule.threshold || '';
|
||||
document.getElementById('rule-config').value = rule.config_id || '';
|
||||
document.getElementById('rule-email').checked = rule.email_enabled;
|
||||
document.getElementById('rule-webhook').checked = rule.webhook_enabled;
|
||||
document.getElementById('rule-enabled').checked = rule.enabled;
|
||||
|
||||
updateThresholdLabel();
|
||||
new bootstrap.Modal(document.getElementById('ruleModal')).show();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching rule:', error);
|
||||
alert('Failed to load rule details');
|
||||
updateThresholdLabel();
|
||||
new bootstrap.Modal(document.getElementById('ruleModal')).show();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching rule:', error);
|
||||
alert('Failed to load rule details');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -353,7 +384,7 @@ function saveRule() {
|
||||
const ruleType = document.getElementById('rule-type').value;
|
||||
const severity = document.getElementById('rule-severity').value;
|
||||
const threshold = document.getElementById('rule-threshold').value;
|
||||
const configFile = document.getElementById('rule-config').value;
|
||||
const configId = document.getElementById('rule-config').value;
|
||||
const emailEnabled = document.getElementById('rule-email').checked;
|
||||
const webhookEnabled = document.getElementById('rule-webhook').checked;
|
||||
const enabled = document.getElementById('rule-enabled').checked;
|
||||
@@ -368,7 +399,7 @@ function saveRule() {
|
||||
rule_type: ruleType,
|
||||
severity: severity,
|
||||
threshold: threshold ? parseInt(threshold) : null,
|
||||
config_file: configFile || null,
|
||||
config_id: configId ? parseInt(configId) : null,
|
||||
email_enabled: emailEnabled,
|
||||
webhook_enabled: webhookEnabled,
|
||||
enabled: enabled
|
||||
|
||||
@@ -295,7 +295,7 @@ function renderSitesCheckboxes(selectedIds = [], isEditMode = false) {
|
||||
id="${prefix}-${site.id}" ${isChecked ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="${prefix}-${site.id}">
|
||||
${escapeHtml(site.name)}
|
||||
<small class="text-muted">(${site.cidr_count || 0} CIDR${site.cidr_count !== 1 ? 's' : ''})</small>
|
||||
<small class="text-muted">(${site.ip_count || 0} IP${site.ip_count !== 1 ? 's' : ''})</small>
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
@@ -451,7 +451,7 @@ async function viewConfig(id) {
|
||||
<strong>Sites (${config.site_count}):</strong>
|
||||
<ul class="mt-2">
|
||||
${config.sites.map(site => `
|
||||
<li>${escapeHtml(site.name)} <small class="text-muted">(${site.cidr_count} CIDR${site.cidr_count !== 1 ? 's' : ''})</small></li>
|
||||
<li>${escapeHtml(site.name)} <small class="text-muted">(${site.ip_count} IP${site.ip_count !== 1 ? 's' : ''})</small></li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -153,34 +153,28 @@
|
||||
<div class="modal-body">
|
||||
<form id="trigger-scan-form">
|
||||
<div class="mb-3">
|
||||
<label for="config-file" class="form-label">Config File</label>
|
||||
<select class="form-select" id="config-file" name="config_file" required {% if not config_files %}disabled{% endif %}>
|
||||
<option value="">Select a config file...</option>
|
||||
{% for config in config_files %}
|
||||
<option value="{{ config }}">{{ config }}</option>
|
||||
{% endfor %}
|
||||
<label for="config-select" class="form-label">Scan Configuration</label>
|
||||
<select class="form-select" id="config-select" name="config_id" required>
|
||||
<option value="">Loading configurations...</option>
|
||||
</select>
|
||||
{% if config_files %}
|
||||
<div class="form-text text-muted">
|
||||
Select a scan configuration file
|
||||
<div class="form-text text-muted" id="config-help-text">
|
||||
Select a scan configuration
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning mt-2 mb-0" role="alert">
|
||||
<div id="no-configs-warning" class="alert alert-warning mt-2 mb-0" role="alert" style="display: none;">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
<strong>No configurations available</strong>
|
||||
<p class="mb-2 mt-2">You need to create a configuration file before you can trigger a scan.</p>
|
||||
<a href="{{ url_for('main.upload_config') }}" class="btn btn-sm btn-primary">
|
||||
<p class="mb-2 mt-2">You need to create a configuration before you can trigger a scan.</p>
|
||||
<a href="{{ url_for('main.configs') }}" class="btn btn-sm btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Create Configuration
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="trigger-error" class="alert alert-danger" style="display: none;"></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer" style="border-top: 1px solid #334155;">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" onclick="triggerScan()" {% if not config_files %}disabled{% endif %}>
|
||||
<button type="button" class="btn btn-primary" id="trigger-scan-btn" onclick="triggerScan()">
|
||||
<span id="modal-trigger-text">Trigger Scan</span>
|
||||
<span id="modal-trigger-spinner" class="spinner-border spinner-border-sm ms-2" style="display: none;"></span>
|
||||
</button>
|
||||
@@ -323,23 +317,75 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Load available configs
|
||||
async function loadConfigs() {
|
||||
const selectEl = document.getElementById('config-select');
|
||||
const helpTextEl = document.getElementById('config-help-text');
|
||||
const noConfigsWarning = document.getElementById('no-configs-warning');
|
||||
const triggerBtn = document.getElementById('trigger-scan-btn');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/configs');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load configurations');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const configs = data.configs || [];
|
||||
|
||||
// Clear existing options
|
||||
selectEl.innerHTML = '';
|
||||
|
||||
if (configs.length === 0) {
|
||||
selectEl.innerHTML = '<option value="">No configurations available</option>';
|
||||
selectEl.disabled = true;
|
||||
triggerBtn.disabled = true;
|
||||
helpTextEl.style.display = 'none';
|
||||
noConfigsWarning.style.display = 'block';
|
||||
} else {
|
||||
selectEl.innerHTML = '<option value="">Select a configuration...</option>';
|
||||
configs.forEach(config => {
|
||||
const option = document.createElement('option');
|
||||
option.value = config.id;
|
||||
const siteText = config.site_count === 1 ? 'site' : 'sites';
|
||||
option.textContent = `${config.title} (${config.site_count} ${siteText})`;
|
||||
selectEl.appendChild(option);
|
||||
});
|
||||
selectEl.disabled = false;
|
||||
triggerBtn.disabled = false;
|
||||
helpTextEl.style.display = 'block';
|
||||
noConfigsWarning.style.display = 'none';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading configs:', error);
|
||||
selectEl.innerHTML = '<option value="">Error loading configurations</option>';
|
||||
selectEl.disabled = true;
|
||||
triggerBtn.disabled = true;
|
||||
helpTextEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Show trigger scan modal
|
||||
function showTriggerScanModal() {
|
||||
const modal = new bootstrap.Modal(document.getElementById('triggerScanModal'));
|
||||
document.getElementById('trigger-error').style.display = 'none';
|
||||
document.getElementById('trigger-scan-form').reset();
|
||||
|
||||
// Load configs when modal is shown
|
||||
loadConfigs();
|
||||
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Trigger scan
|
||||
async function triggerScan() {
|
||||
const configFile = document.getElementById('config-file').value;
|
||||
const configId = document.getElementById('config-select').value;
|
||||
const errorEl = document.getElementById('trigger-error');
|
||||
const btnText = document.getElementById('modal-trigger-text');
|
||||
const btnSpinner = document.getElementById('modal-trigger-spinner');
|
||||
|
||||
if (!configFile) {
|
||||
errorEl.textContent = 'Please enter a config file path.';
|
||||
if (!configId) {
|
||||
errorEl.textContent = 'Please select a configuration.';
|
||||
errorEl.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
@@ -356,7 +402,7 @@
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
config_file: configFile
|
||||
config_id: parseInt(configId)
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -79,14 +79,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="mb-0">
|
||||
<label class="form-label text-muted">Config File</label>
|
||||
<div id="scan-config-file" class="mono">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -113,34 +113,28 @@
|
||||
<div class="modal-body">
|
||||
<form id="trigger-scan-form">
|
||||
<div class="mb-3">
|
||||
<label for="config-file" class="form-label">Config File</label>
|
||||
<select class="form-select" id="config-file" name="config_file" required {% if not config_files %}disabled{% endif %}>
|
||||
<option value="">Select a config file...</option>
|
||||
{% for config in config_files %}
|
||||
<option value="{{ config }}">{{ config }}</option>
|
||||
{% endfor %}
|
||||
<label for="config-select" class="form-label">Scan Configuration</label>
|
||||
<select class="form-select" id="config-select" name="config_id" required>
|
||||
<option value="">Loading configurations...</option>
|
||||
</select>
|
||||
{% if config_files %}
|
||||
<div class="form-text text-muted">
|
||||
Select a scan configuration file
|
||||
<div class="form-text text-muted" id="config-help-text">
|
||||
Select a scan configuration
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning mt-2 mb-0" role="alert">
|
||||
<div id="no-configs-warning" class="alert alert-warning mt-2 mb-0" role="alert" style="display: none;">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
<strong>No configurations available</strong>
|
||||
<p class="mb-2 mt-2">You need to create a configuration file before you can trigger a scan.</p>
|
||||
<a href="{{ url_for('main.upload_config') }}" class="btn btn-sm btn-primary">
|
||||
<p class="mb-2 mt-2">You need to create a configuration before you can trigger a scan.</p>
|
||||
<a href="{{ url_for('main.configs') }}" class="btn btn-sm btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Create Configuration
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="trigger-error" class="alert alert-danger" style="display: none;"></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer" style="border-top: 1px solid #334155;">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" onclick="triggerScan()" {% if not config_files %}disabled{% endif %}>
|
||||
<button type="button" class="btn btn-primary" id="trigger-scan-btn" onclick="triggerScan()">
|
||||
<span id="modal-trigger-text">Trigger Scan</span>
|
||||
<span id="modal-trigger-spinner" class="spinner-border spinner-border-sm ms-2" style="display: none;"></span>
|
||||
</button>
|
||||
@@ -359,23 +353,75 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Load available configs
|
||||
async function loadConfigs() {
|
||||
const selectEl = document.getElementById('config-select');
|
||||
const helpTextEl = document.getElementById('config-help-text');
|
||||
const noConfigsWarning = document.getElementById('no-configs-warning');
|
||||
const triggerBtn = document.getElementById('trigger-scan-btn');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/configs');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load configurations');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const configs = data.configs || [];
|
||||
|
||||
// Clear existing options
|
||||
selectEl.innerHTML = '';
|
||||
|
||||
if (configs.length === 0) {
|
||||
selectEl.innerHTML = '<option value="">No configurations available</option>';
|
||||
selectEl.disabled = true;
|
||||
triggerBtn.disabled = true;
|
||||
helpTextEl.style.display = 'none';
|
||||
noConfigsWarning.style.display = 'block';
|
||||
} else {
|
||||
selectEl.innerHTML = '<option value="">Select a configuration...</option>';
|
||||
configs.forEach(config => {
|
||||
const option = document.createElement('option');
|
||||
option.value = config.id;
|
||||
const siteText = config.site_count === 1 ? 'site' : 'sites';
|
||||
option.textContent = `${config.title} (${config.site_count} ${siteText})`;
|
||||
selectEl.appendChild(option);
|
||||
});
|
||||
selectEl.disabled = false;
|
||||
triggerBtn.disabled = false;
|
||||
helpTextEl.style.display = 'block';
|
||||
noConfigsWarning.style.display = 'none';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading configs:', error);
|
||||
selectEl.innerHTML = '<option value="">Error loading configurations</option>';
|
||||
selectEl.disabled = true;
|
||||
triggerBtn.disabled = true;
|
||||
helpTextEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Show trigger scan modal
|
||||
function showTriggerScanModal() {
|
||||
const modal = new bootstrap.Modal(document.getElementById('triggerScanModal'));
|
||||
document.getElementById('trigger-error').style.display = 'none';
|
||||
document.getElementById('trigger-scan-form').reset();
|
||||
|
||||
// Load configs when modal is shown
|
||||
loadConfigs();
|
||||
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Trigger scan
|
||||
async function triggerScan() {
|
||||
const configFile = document.getElementById('config-file').value;
|
||||
const configId = document.getElementById('config-select').value;
|
||||
const errorEl = document.getElementById('trigger-error');
|
||||
const btnText = document.getElementById('modal-trigger-text');
|
||||
const btnSpinner = document.getElementById('modal-trigger-spinner');
|
||||
|
||||
if (!configFile) {
|
||||
errorEl.textContent = 'Please enter a config file path.';
|
||||
if (!configId) {
|
||||
errorEl.textContent = 'Please select a configuration.';
|
||||
errorEl.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
@@ -392,13 +438,13 @@
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
config_file: configFile
|
||||
config_id: parseInt(configId)
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.error || 'Failed to trigger scan');
|
||||
throw new Error(data.message || data.error || 'Failed to trigger scan');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="total-cidrs">-</div>
|
||||
<div class="stat-label">Total CIDRs</div>
|
||||
<div class="stat-value" id="total-ips">-</div>
|
||||
<div class="stat-label">Total IPs</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
@@ -66,7 +66,7 @@
|
||||
<tr>
|
||||
<th>Site Name</th>
|
||||
<th>Description</th>
|
||||
<th>CIDRs</th>
|
||||
<th>IPs</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
@@ -79,7 +79,7 @@
|
||||
<div id="empty-state" style="display: none;" class="text-center py-5">
|
||||
<i class="bi bi-globe" style="font-size: 3rem; color: #64748b;"></i>
|
||||
<h5 class="mt-3 text-muted">No sites defined</h5>
|
||||
<p class="text-muted">Create your first site to group CIDR ranges</p>
|
||||
<p class="text-muted">Create your first site to organize your IP addresses</p>
|
||||
<button class="btn btn-primary mt-2" data-bs-toggle="modal" data-bs-target="#createSiteModal">
|
||||
<i class="bi bi-plus-circle"></i> Create Site
|
||||
</button>
|
||||
@@ -109,17 +109,11 @@
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="site-description" class="form-label" style="color: #e2e8f0;">Description</label>
|
||||
<textarea class="form-control" id="site-description" rows="2"
|
||||
placeholder="Optional description"></textarea>
|
||||
<textarea class="form-control" id="site-description" rows="3"
|
||||
placeholder="Optional description of this site"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" style="color: #e2e8f0;">CIDR Ranges *</label>
|
||||
<div id="cidrs-container">
|
||||
<!-- CIDR inputs will be added here -->
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addCidrInput()">
|
||||
<i class="bi bi-plus"></i> Add CIDR
|
||||
</button>
|
||||
<div class="alert alert-info" style="background-color: #1e3a5f; border-color: #2d5a8c; color: #a5d6ff;">
|
||||
<i class="bi bi-info-circle"></i> After creating the site, you'll be able to add IP addresses using CIDRs, individual IPs, or bulk import.
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -147,9 +141,146 @@
|
||||
<h6 style="color: #94a3b8;">Description:</h6>
|
||||
<p id="view-site-description" style="color: #e2e8f0;"></p>
|
||||
|
||||
<h6 class="mt-3" style="color: #94a3b8;">CIDR Ranges:</h6>
|
||||
<div id="view-site-cidrs" class="table-responsive">
|
||||
<!-- Will be populated -->
|
||||
<div class="d-flex justify-content-between align-items-center mt-3 mb-2">
|
||||
<h6 style="color: #94a3b8; margin: 0;">IP Addresses (<span id="ip-count">0</span>):</h6>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-plus-circle"></i> Add IPs
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-dark">
|
||||
<li><a class="dropdown-item" href="#" onclick="showAddIpMethod('cidr'); return false;"><i class="bi bi-diagram-3"></i> From CIDR</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="showAddIpMethod('individual'); return false;"><i class="bi bi- hdd-network"></i> Individual IP</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="showAddIpMethod('bulk'); return false;"><i class="bi bi-file-earmark-text"></i> Bulk Import</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add from CIDR Form -->
|
||||
<div id="add-cidr-form" style="display: none; margin-bottom: 15px; padding: 15px; background-color: #0f172a; border: 1px solid #334155; border-radius: 5px;">
|
||||
<h6 style="color: #60a5fa;"><i class="bi bi-diagram-3"></i> Add IPs from CIDR</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">CIDR Range *</label>
|
||||
<input type="text" class="form-control form-control-sm" id="bulk-cidr" placeholder="e.g., 10.0.0.0/24">
|
||||
<small style="color: #64748b;">Max /24 (256 IPs)</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">Expected Ping</label>
|
||||
<select class="form-select form-select-sm" id="bulk-cidr-ping">
|
||||
<option value="null">Not Set</option>
|
||||
<option value="true">Yes</option>
|
||||
<option value="false" selected>No</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">Expected TCP Ports</label>
|
||||
<input type="text" class="form-control form-control-sm" id="bulk-cidr-tcp-ports" placeholder="e.g., 22,80,443">
|
||||
<small style="color: #64748b;">Comma-separated</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">Expected UDP Ports</label>
|
||||
<input type="text" class="form-control form-control-sm" id="bulk-cidr-udp-ports" placeholder="e.g., 53,123">
|
||||
<small style="color: #64748b;">Comma-separated</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-sm btn-primary" onclick="addIpsFromCidr()">
|
||||
<i class="bi bi-check-circle"></i> Add IPs
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary" onclick="hideAllAddForms()">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Individual IP Form -->
|
||||
<div id="add-individual-form" style="display: none; margin-bottom: 15px; padding: 15px; background-color: #0f172a; border: 1px solid #334155; border-radius: 5px;">
|
||||
<h6 style="color: #60a5fa;"><i class="bi bi-hdd-network"></i> Add Individual IP</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">IP Address *</label>
|
||||
<input type="text" class="form-control form-control-sm" id="individual-ip" placeholder="e.g., 192.168.1.100">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">Expected Ping</label>
|
||||
<select class="form-select form-select-sm" id="individual-ping">
|
||||
<option value="null">Not Set</option>
|
||||
<option value="true">Yes</option>
|
||||
<option value="false" selected>No</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">Expected TCP Ports</label>
|
||||
<input type="text" class="form-control form-control-sm" id="individual-tcp-ports" placeholder="e.g., 22,80,443">
|
||||
<small style="color: #64748b;">Comma-separated</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">Expected UDP Ports</label>
|
||||
<input type="text" class="form-control form-control-sm" id="individual-udp-ports" placeholder="e.g., 53,123">
|
||||
<small style="color: #64748b;">Comma-separated</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-sm btn-primary" onclick="addIndividualIp()">
|
||||
<i class="bi bi-check-circle"></i> Add IP
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary" onclick="hideAllAddForms()">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bulk Import Form -->
|
||||
<div id="add-bulk-form" style="display: none; margin-bottom: 15px; padding: 15px; background-color: #0f172a; border: 1px solid #334155; border-radius: 5px;">
|
||||
<h6 style="color: #60a5fa;"><i class="bi bi-file-earmark-text"></i> Bulk Import IPs</h6>
|
||||
<div class="mb-2">
|
||||
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">IP Addresses *</label>
|
||||
<textarea class="form-control form-control-sm" id="bulk-ips" rows="5" placeholder="Paste IPs here (one per line, or comma/space separated)"></textarea>
|
||||
<small style="color: #64748b;">Supports: one per line, comma-separated, or space-separated</small>
|
||||
</div>
|
||||
<div class="row mt-2">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">Expected Ping</label>
|
||||
<select class="form-select form-select-sm" id="bulk-ping">
|
||||
<option value="null">Not Set</option>
|
||||
<option value="true">Yes</option>
|
||||
<option value="false" selected>No</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">Expected TCP Ports</label>
|
||||
<input type="text" class="form-control form-control-sm" id="bulk-tcp-ports" placeholder="e.g., 22,80,443">
|
||||
<small style="color: #64748b;">Comma-separated</small>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">Expected UDP Ports</label>
|
||||
<input type="text" class="form-control form-control-sm" id="bulk-udp-ports" placeholder="e.g., 53,123">
|
||||
<small style="color: #64748b;">Comma-separated</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-sm btn-primary" onclick="addIpsFromBulk()">
|
||||
<i class="bi bi-check-circle"></i> Import IPs
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary" onclick="hideAllAddForms()">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IP Table -->
|
||||
<div id="view-site-ips-container">
|
||||
<div id="ips-loading" style="display: none;" class="text-center py-3">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
|
||||
<span class="ms-2" style="color: #94a3b8;">Loading IPs...</span>
|
||||
</div>
|
||||
<div id="view-site-ips" class="table-responsive">
|
||||
<!-- Will be populated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h6 class="mt-3" style="color: #94a3b8;">Usage:</h6>
|
||||
@@ -188,7 +319,7 @@
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> To edit CIDRs and IP ranges, please delete and recreate the site.
|
||||
<i class="bi bi-info-circle"></i> To manage IP addresses, use the "View" button on the site.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger" id="edit-site-error" style="display: none;">
|
||||
@@ -205,6 +336,56 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit IP Modal -->
|
||||
<div class="modal fade" id="editIpModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content" style="background-color: #1e293b; border: 1px solid #334155;">
|
||||
<div class="modal-header" style="border-bottom: 1px solid #334155;">
|
||||
<h5 class="modal-title" style="color: #60a5fa;">
|
||||
<i class="bi bi-pencil"></i> Edit IP Settings
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="edit-ip-site-id">
|
||||
<input type="hidden" id="edit-ip-id">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" style="color: #e2e8f0;">IP Address</label>
|
||||
<input type="text" class="form-control" id="edit-ip-address" readonly>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="edit-ip-ping" class="form-label" style="color: #e2e8f0;">Expected Ping</label>
|
||||
<select class="form-select" id="edit-ip-ping">
|
||||
<option value="null">Not Set</option>
|
||||
<option value="true">Yes</option>
|
||||
<option value="false">No</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="edit-ip-tcp-ports" class="form-label" style="color: #e2e8f0;">Expected TCP Ports</label>
|
||||
<input type="text" class="form-control" id="edit-ip-tcp-ports" placeholder="e.g., 22,80,443">
|
||||
<small style="color: #64748b;">Comma-separated port numbers</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="edit-ip-udp-ports" class="form-label" style="color: #e2e8f0;">Expected UDP Ports</label>
|
||||
<input type="text" class="form-control" id="edit-ip-udp-ports" placeholder="e.g., 53,123">
|
||||
<small style="color: #64748b;">Comma-separated port numbers</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" style="border-top: 1px solid #334155;">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="save-ip-btn">
|
||||
<i class="bi bi-check-circle"></i> Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
@@ -240,7 +421,31 @@
|
||||
// Global variables
|
||||
let sitesData = [];
|
||||
let selectedSiteForDeletion = null;
|
||||
let cidrInputCounter = 0;
|
||||
let currentViewingSiteId = null; // Track the site ID currently being viewed
|
||||
let currentSitePage = 1;
|
||||
|
||||
// Helper function to clean up any stray modal backdrops
|
||||
function cleanupModalBackdrops() {
|
||||
// Remove any leftover backdrops
|
||||
document.querySelectorAll('.modal-backdrop').forEach(backdrop => {
|
||||
backdrop.remove();
|
||||
});
|
||||
// Remove modal-open class from body
|
||||
document.body.classList.remove('modal-open');
|
||||
document.body.style.removeProperty('overflow');
|
||||
document.body.style.removeProperty('padding-right');
|
||||
}
|
||||
|
||||
// Helper function to show a modal (reuses existing instance if available)
|
||||
function showModal(modalId) {
|
||||
const modalElement = document.getElementById(modalId);
|
||||
let modal = bootstrap.Modal.getInstance(modalElement);
|
||||
if (!modal) {
|
||||
modal = new bootstrap.Modal(modalElement);
|
||||
}
|
||||
modal.show();
|
||||
return modal;
|
||||
}
|
||||
|
||||
// Format date
|
||||
function formatDate(timestamp) {
|
||||
@@ -249,57 +454,26 @@ function formatDate(timestamp) {
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
// Add CIDR input field
|
||||
function addCidrInput(cidr = '', expectedPing = false, expectedTcpPorts = [], expectedUdpPorts = []) {
|
||||
const container = document.getElementById('cidrs-container');
|
||||
const id = cidrInputCounter++;
|
||||
|
||||
const cidrHtml = `
|
||||
<div class="cidr-input-group mb-2 p-3" style="background-color: #0f172a; border: 1px solid #334155; border-radius: 5px;" id="cidr-${id}">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" style="color: #94a3b8; font-size: 0.875rem;">CIDR *</label>
|
||||
<input type="text" class="form-control form-control-sm cidr-value" placeholder="e.g., 10.0.0.0/24" value="${cidr}" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" style="color: #94a3b8; font-size: 0.875rem;">Expect Ping</label>
|
||||
<select class="form-select form-select-sm cidr-ping">
|
||||
<option value="">Default (No)</option>
|
||||
<option value="true" ${expectedPing ? 'selected' : ''}>Yes</option>
|
||||
<option value="false" ${expectedPing === false ? 'selected' : ''}>No</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" style="color: #94a3b8; font-size: 0.875rem;">Actions</label>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger w-100" onclick="removeCidrInput(${id})">
|
||||
<i class="bi bi-trash"></i> Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" style="color: #94a3b8; font-size: 0.875rem;">Expected TCP Ports</label>
|
||||
<input type="text" class="form-control form-control-sm cidr-tcp-ports" placeholder="e.g., 22,80,443" value="${expectedTcpPorts.join(',')}">
|
||||
<small style="color: #64748b;">Comma-separated port numbers</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" style="color: #94a3b8; font-size: 0.875rem;">Expected UDP Ports</label>
|
||||
<input type="text" class="form-control form-control-sm cidr-udp-ports" placeholder="e.g., 53,123" value="${expectedUdpPorts.join(',')}">
|
||||
<small style="color: #64748b;">Comma-separated port numbers</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.insertAdjacentHTML('beforeend', cidrHtml);
|
||||
// Show/hide add IP form methods
|
||||
function showAddIpMethod(method) {
|
||||
hideAllAddForms();
|
||||
if (method === 'cidr') {
|
||||
document.getElementById('add-cidr-form').style.display = 'block';
|
||||
} else if (method === 'individual') {
|
||||
document.getElementById('add-individual-form').style.display = 'block';
|
||||
} else if (method === 'bulk') {
|
||||
document.getElementById('add-bulk-form').style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Remove CIDR input field
|
||||
function removeCidrInput(id) {
|
||||
const element = document.getElementById(`cidr-${id}`);
|
||||
if (element) {
|
||||
element.remove();
|
||||
}
|
||||
function hideAllAddForms() {
|
||||
document.getElementById('add-cidr-form').style.display = 'none';
|
||||
document.getElementById('add-individual-form').style.display = 'none';
|
||||
document.getElementById('add-bulk-form').style.display = 'none';
|
||||
// Reset forms
|
||||
document.getElementById('bulk-cidr').value = '';
|
||||
document.getElementById('individual-ip').value = '';
|
||||
document.getElementById('bulk-ips').value = '';
|
||||
}
|
||||
|
||||
// Parse port list from comma-separated string
|
||||
@@ -342,10 +516,10 @@ async function loadSites() {
|
||||
// Update summary stats
|
||||
function updateStats() {
|
||||
const totalSites = sitesData.length;
|
||||
const totalCidrs = sitesData.reduce((sum, site) => sum + (site.cidrs?.length || 0), 0);
|
||||
const totalIps = sitesData.reduce((sum, site) => sum + (site.ip_count || 0), 0);
|
||||
|
||||
document.getElementById('total-sites').textContent = totalSites;
|
||||
document.getElementById('total-cidrs').textContent = totalCidrs;
|
||||
document.getElementById('total-ips').textContent = totalIps;
|
||||
document.getElementById('sites-in-use').textContent = '-'; // Will be updated async
|
||||
|
||||
// Count sites in use (async)
|
||||
@@ -380,16 +554,16 @@ function renderSites(sites) {
|
||||
emptyState.style.display = 'none';
|
||||
|
||||
tbody.innerHTML = sites.map(site => {
|
||||
const cidrCount = site.cidrs?.length || 0;
|
||||
const cidrBadge = cidrCount > 0
|
||||
? `<span class="badge bg-info">${cidrCount} CIDR${cidrCount !== 1 ? 's' : ''}</span>`
|
||||
: '<span class="badge bg-secondary">No CIDRs</span>';
|
||||
const ipCount = site.ip_count || 0;
|
||||
const ipBadge = ipCount > 0
|
||||
? `<span class="badge bg-info">${ipCount} IP${ipCount !== 1 ? 's' : ''}</span>`
|
||||
: '<span class="badge bg-secondary">No IPs</span>';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td><strong style="color: #60a5fa;">${site.name}</strong></td>
|
||||
<td style="color: #94a3b8;">${site.description || '<em>No description</em>'}</td>
|
||||
<td>${cidrBadge}</td>
|
||||
<td>${ipBadge}</td>
|
||||
<td>${formatDate(site.created_at)}</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
@@ -420,31 +594,6 @@ async function createSite() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect CIDRs
|
||||
const cidrGroups = document.querySelectorAll('.cidr-input-group');
|
||||
const cidrs = [];
|
||||
|
||||
cidrGroups.forEach(group => {
|
||||
const cidrValue = group.querySelector('.cidr-value').value.trim();
|
||||
if (!cidrValue) return;
|
||||
|
||||
const pingValue = group.querySelector('.cidr-ping').value;
|
||||
const tcpPorts = parsePortList(group.querySelector('.cidr-tcp-ports').value);
|
||||
const udpPorts = parsePortList(group.querySelector('.cidr-udp-ports').value);
|
||||
|
||||
cidrs.push({
|
||||
cidr: cidrValue,
|
||||
expected_ping: pingValue === 'true' ? true : (pingValue === 'false' ? false : null),
|
||||
expected_tcp_ports: tcpPorts,
|
||||
expected_udp_ports: udpPorts
|
||||
});
|
||||
});
|
||||
|
||||
if (cidrs.length === 0) {
|
||||
showAlert('warning', 'At least one CIDR is required');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/sites', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -452,8 +601,7 @@ async function createSite() {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
description: description || null,
|
||||
cidrs: cidrs
|
||||
description: description || null
|
||||
})
|
||||
});
|
||||
|
||||
@@ -462,19 +610,22 @@ async function createSite() {
|
||||
throw new Error(error.message || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
// Hide modal
|
||||
const newSite = await response.json();
|
||||
|
||||
// Hide create modal
|
||||
bootstrap.Modal.getInstance(document.getElementById('createSiteModal')).hide();
|
||||
|
||||
// Reset form
|
||||
document.getElementById('create-site-form').reset();
|
||||
document.getElementById('cidrs-container').innerHTML = '';
|
||||
cidrInputCounter = 0;
|
||||
|
||||
// Reload sites
|
||||
await loadSites();
|
||||
|
||||
// Show success message
|
||||
showAlert('success', `Site "${name}" created successfully`);
|
||||
// Show success message and open view modal
|
||||
showAlert('success', `Site "${name}" created successfully. Now add some IP addresses!`);
|
||||
|
||||
// Open the view modal to add IPs
|
||||
viewSite(newSite.id);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating site:', error);
|
||||
@@ -485,6 +636,7 @@ async function createSite() {
|
||||
// View site details
|
||||
async function viewSite(siteId) {
|
||||
try {
|
||||
currentViewingSiteId = siteId;
|
||||
const response = await fetch(`/api/sites/${siteId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load site: ${response.statusText}`);
|
||||
@@ -495,31 +647,8 @@ async function viewSite(siteId) {
|
||||
document.getElementById('view-site-name').textContent = site.name;
|
||||
document.getElementById('view-site-description').textContent = site.description || 'No description';
|
||||
|
||||
// Render CIDRs
|
||||
const cidrsHtml = site.cidrs && site.cidrs.length > 0
|
||||
? `<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>CIDR</th>
|
||||
<th>Ping</th>
|
||||
<th>TCP Ports</th>
|
||||
<th>UDP Ports</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${site.cidrs.map(cidr => `
|
||||
<tr>
|
||||
<td><code>${cidr.cidr}</code></td>
|
||||
<td>${cidr.expected_ping ? '<span class="badge bg-success">Yes</span>' : '<span class="badge bg-secondary">No</span>'}</td>
|
||||
<td>${cidr.expected_tcp_ports?.length > 0 ? cidr.expected_tcp_ports.join(', ') : '-'}</td>
|
||||
<td>${cidr.expected_udp_ports?.length > 0 ? cidr.expected_udp_ports.join(', ') : '-'}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>`
|
||||
: '<p style="color: #94a3b8;"><em>No CIDRs defined</em></p>';
|
||||
|
||||
document.getElementById('view-site-cidrs').innerHTML = cidrsHtml;
|
||||
// Load IPs
|
||||
await loadSiteIps(siteId);
|
||||
|
||||
// Load usage
|
||||
document.getElementById('view-site-usage').innerHTML = '<p style="color: #94a3b8;"><i class="bi bi-hourglass"></i> Loading usage...</p>';
|
||||
@@ -538,7 +667,7 @@ async function viewSite(siteId) {
|
||||
document.getElementById('view-site-usage').innerHTML = usageHtml;
|
||||
}
|
||||
|
||||
new bootstrap.Modal(document.getElementById('viewSiteModal')).show();
|
||||
showModal('viewSiteModal');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error viewing site:', error);
|
||||
@@ -546,6 +675,250 @@ async function viewSite(siteId) {
|
||||
}
|
||||
}
|
||||
|
||||
// Load IPs for a site
|
||||
async function loadSiteIps(siteId) {
|
||||
try {
|
||||
document.getElementById('ips-loading').style.display = 'block';
|
||||
|
||||
const response = await fetch(`/api/sites/${siteId}/ips?per_page=100`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load IPs');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const ips = data.ips || [];
|
||||
|
||||
document.getElementById('ip-count').textContent = data.total || ips.length;
|
||||
|
||||
// Render flat IP table
|
||||
if (ips.length === 0) {
|
||||
document.getElementById('view-site-ips').innerHTML = `
|
||||
<div class="text-center py-4" style="color: #94a3b8;">
|
||||
<i class="bi bi-hdd-network" style="font-size: 2rem;"></i>
|
||||
<p class="mt-2"><em>No IPs added yet</em></p>
|
||||
<p class="text-muted" style="font-size: 0.875rem;">Use the "Add IPs" button above to get started</p>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
const tableHtml = `
|
||||
<table class="table table-sm table-hover">
|
||||
<thead style="position: sticky; top: 0; background-color: #1e293b;">
|
||||
<tr>
|
||||
<th>IP Address</th>
|
||||
<th>Ping</th>
|
||||
<th>TCP Ports</th>
|
||||
<th>UDP Ports</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${ips.map(ip => `
|
||||
<tr>
|
||||
<td><code>${ip.ip_address}</code></td>
|
||||
<td>${ip.expected_ping ? '<span class="badge bg-success">Yes</span>' : '<span class="badge bg-secondary">No</span>'}</td>
|
||||
<td style="font-size: 0.875rem;">${ip.expected_tcp_ports?.length > 0 ? ip.expected_tcp_ports.join(', ') : '-'}</td>
|
||||
<td style="font-size: 0.875rem;">${ip.expected_udp_ports?.length > 0 ? ip.expected_udp_ports.join(', ') : '-'}</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button class="btn btn-outline-primary" onclick="editIp(${siteId}, ${ip.id}, '${ip.ip_address}', ${ip.expected_ping}, ${JSON.stringify(ip.expected_tcp_ports || [])}, ${JSON.stringify(ip.expected_udp_ports || [])})" title="Edit">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger" onclick="confirmDeleteIp(${siteId}, ${ip.id}, '${ip.ip_address}')" title="Delete IP">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
document.getElementById('view-site-ips').innerHTML = tableHtml;
|
||||
}
|
||||
|
||||
document.getElementById('ips-loading').style.display = 'none';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading IPs:', error);
|
||||
document.getElementById('view-site-ips').innerHTML = `<p style="color: #f87171;">Error loading IPs: ${error.message}</p>`;
|
||||
document.getElementById('ips-loading').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Add IPs from CIDR
|
||||
async function addIpsFromCidr() {
|
||||
try {
|
||||
const cidr = document.getElementById('bulk-cidr').value.trim();
|
||||
if (!cidr) {
|
||||
showAlert('warning', 'CIDR is required');
|
||||
return;
|
||||
}
|
||||
|
||||
const pingValue = document.getElementById('bulk-cidr-ping').value;
|
||||
const expectedPing = pingValue === 'null' ? null : (pingValue === 'true');
|
||||
const expectedTcpPorts = parsePortList(document.getElementById('bulk-cidr-tcp-ports').value);
|
||||
const expectedUdpPorts = parsePortList(document.getElementById('bulk-cidr-udp-ports').value);
|
||||
|
||||
const response = await fetch(`/api/sites/${currentViewingSiteId}/ips/bulk`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
source_type: 'cidr',
|
||||
cidr: cidr,
|
||||
expected_ping: expectedPing,
|
||||
expected_tcp_ports: expectedTcpPorts,
|
||||
expected_udp_ports: expectedUdpPorts
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
hideAllAddForms();
|
||||
await loadSiteIps(currentViewingSiteId);
|
||||
await loadSites(); // Refresh stats
|
||||
|
||||
showAlert('success', `Added ${result.ip_count} IPs from CIDR ${cidr}${result.ips_skipped.length > 0 ? ` (${result.ips_skipped.length} duplicates skipped)` : ''}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error adding IPs from CIDR:', error);
|
||||
showAlert('danger', `Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Add individual IP
|
||||
async function addIndividualIp() {
|
||||
try {
|
||||
const ipAddress = document.getElementById('individual-ip').value.trim();
|
||||
if (!ipAddress) {
|
||||
showAlert('warning', 'IP address is required');
|
||||
return;
|
||||
}
|
||||
|
||||
const pingValue = document.getElementById('individual-ping').value;
|
||||
const expectedPing = pingValue === 'null' ? null : (pingValue === 'true');
|
||||
const expectedTcpPorts = parsePortList(document.getElementById('individual-tcp-ports').value);
|
||||
const expectedUdpPorts = parsePortList(document.getElementById('individual-udp-ports').value);
|
||||
|
||||
const response = await fetch(`/api/sites/${currentViewingSiteId}/ips`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
ip_address: ipAddress,
|
||||
expected_ping: expectedPing,
|
||||
expected_tcp_ports: expectedTcpPorts,
|
||||
expected_udp_ports: expectedUdpPorts
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
hideAllAddForms();
|
||||
await loadSiteIps(currentViewingSiteId);
|
||||
await loadSites(); // Refresh stats
|
||||
|
||||
showAlert('success', `IP ${ipAddress} added successfully`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error adding IP:', error);
|
||||
showAlert('danger', `Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Add IPs from bulk import
|
||||
async function addIpsFromBulk() {
|
||||
try {
|
||||
const bulkText = document.getElementById('bulk-ips').value.trim();
|
||||
if (!bulkText) {
|
||||
showAlert('warning', 'IP list is required');
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse IPs from text (supports newlines, commas, spaces)
|
||||
const ipList = bulkText.split(/[\n,\s]+/).map(ip => ip.trim()).filter(ip => ip);
|
||||
|
||||
if (ipList.length === 0) {
|
||||
showAlert('warning', 'No valid IPs found');
|
||||
return;
|
||||
}
|
||||
|
||||
const pingValue = document.getElementById('bulk-ping').value;
|
||||
const expectedPing = pingValue === 'null' ? null : (pingValue === 'true');
|
||||
const expectedTcpPorts = parsePortList(document.getElementById('bulk-tcp-ports').value);
|
||||
const expectedUdpPorts = parsePortList(document.getElementById('bulk-udp-ports').value);
|
||||
|
||||
const response = await fetch(`/api/sites/${currentViewingSiteId}/ips/bulk`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
source_type: 'list',
|
||||
ips: ipList,
|
||||
expected_ping: expectedPing,
|
||||
expected_tcp_ports: expectedTcpPorts,
|
||||
expected_udp_ports: expectedUdpPorts
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
hideAllAddForms();
|
||||
await loadSiteIps(currentViewingSiteId);
|
||||
await loadSites(); // Refresh stats
|
||||
|
||||
let message = `Added ${result.ip_count} IPs`;
|
||||
if (result.ips_skipped.length > 0) message += ` (${result.ips_skipped.length} duplicates skipped)`;
|
||||
if (result.errors.length > 0) message += ` (${result.errors.length} errors)`;
|
||||
|
||||
showAlert(result.errors.length > 0 ? 'warning' : 'success', message);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error adding IPs from bulk:', error);
|
||||
showAlert('danger', `Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm delete IP
|
||||
function confirmDeleteIp(siteId, ipId, ipAddress) {
|
||||
if (confirm(`Are you sure you want to delete IP ${ipAddress}?`)) {
|
||||
deleteIp(siteId, ipId);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete IP
|
||||
async function deleteIp(siteId, ipId) {
|
||||
try {
|
||||
const response = await fetch(`/api/sites/${siteId}/ips/${ipId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
await loadSiteIps(siteId);
|
||||
await loadSites(); // Refresh stats
|
||||
|
||||
showAlert('success', 'IP deleted successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error deleting IP:', error);
|
||||
showAlert('danger', `Error deleting IP: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Edit site
|
||||
async function editSite(siteId) {
|
||||
try {
|
||||
@@ -563,7 +936,7 @@ async function editSite(siteId) {
|
||||
document.getElementById('edit-site-error').style.display = 'none';
|
||||
|
||||
// Show modal
|
||||
new bootstrap.Modal(document.getElementById('editSiteModal')).show();
|
||||
showModal('editSiteModal');
|
||||
} catch (error) {
|
||||
console.error('Error loading site:', error);
|
||||
showAlert('danger', `Error loading site: ${error.message}`);
|
||||
@@ -638,7 +1011,7 @@ async function confirmDelete(siteId, siteName) {
|
||||
console.error('Error checking site usage:', e);
|
||||
}
|
||||
|
||||
new bootstrap.Modal(document.getElementById('deleteModal')).show();
|
||||
showModal('deleteModal');
|
||||
}
|
||||
|
||||
// Delete site
|
||||
@@ -670,6 +1043,69 @@ async function deleteSite() {
|
||||
}
|
||||
}
|
||||
|
||||
// Edit IP settings
|
||||
function editIp(siteId, ipId, ipAddress, expectedPing, expectedTcpPorts, expectedUdpPorts) {
|
||||
// Populate modal
|
||||
document.getElementById('edit-ip-site-id').value = siteId;
|
||||
document.getElementById('edit-ip-id').value = ipId;
|
||||
document.getElementById('edit-ip-address').value = ipAddress;
|
||||
|
||||
// Set ping value
|
||||
const pingValue = expectedPing === null ? 'null' : (expectedPing ? 'true' : 'false');
|
||||
document.getElementById('edit-ip-ping').value = pingValue;
|
||||
|
||||
// Set ports
|
||||
document.getElementById('edit-ip-tcp-ports').value = expectedTcpPorts && expectedTcpPorts.length > 0 ? expectedTcpPorts.join(',') : '';
|
||||
document.getElementById('edit-ip-udp-ports').value = expectedUdpPorts && expectedUdpPorts.length > 0 ? expectedUdpPorts.join(',') : '';
|
||||
|
||||
// Show modal
|
||||
showModal('editIpModal');
|
||||
}
|
||||
|
||||
// Save IP settings
|
||||
async function saveIp() {
|
||||
try {
|
||||
const siteId = document.getElementById('edit-ip-site-id').value;
|
||||
const ipId = document.getElementById('edit-ip-id').value;
|
||||
const ipAddress = document.getElementById('edit-ip-address').value;
|
||||
|
||||
const pingValue = document.getElementById('edit-ip-ping').value;
|
||||
const expectedPing = pingValue === 'null' ? null : (pingValue === 'true');
|
||||
|
||||
const expectedTcpPorts = parsePortList(document.getElementById('edit-ip-tcp-ports').value);
|
||||
const expectedUdpPorts = parsePortList(document.getElementById('edit-ip-udp-ports').value);
|
||||
|
||||
const response = await fetch(`/api/sites/${siteId}/ips/${ipId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
expected_ping: expectedPing,
|
||||
expected_tcp_ports: expectedTcpPorts,
|
||||
expected_udp_ports: expectedUdpPorts
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
// Hide modal
|
||||
bootstrap.Modal.getInstance(document.getElementById('editIpModal')).hide();
|
||||
|
||||
// Reload the view site modal
|
||||
await viewSite(siteId);
|
||||
|
||||
showAlert('success', `IP ${ipAddress} updated successfully`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error saving IP:', error);
|
||||
showAlert('danger', `Error saving IP: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Show alert
|
||||
function showAlert(type, message) {
|
||||
const alertHtml = `
|
||||
@@ -711,13 +1147,16 @@ document.getElementById('search-input').addEventListener('input', function(e) {
|
||||
// Setup delete button
|
||||
document.getElementById('confirm-delete-btn').addEventListener('click', deleteSite);
|
||||
|
||||
// Initialize modal
|
||||
document.getElementById('createSiteModal').addEventListener('show.bs.modal', function() {
|
||||
// Reset form and add one CIDR input
|
||||
document.getElementById('create-site-form').reset();
|
||||
document.getElementById('cidrs-container').innerHTML = '';
|
||||
cidrInputCounter = 0;
|
||||
addCidrInput();
|
||||
// Setup save IP button
|
||||
document.getElementById('save-ip-btn').addEventListener('click', saveIp);
|
||||
|
||||
// Add cleanup listeners to all modals
|
||||
['createSiteModal', 'viewSiteModal', 'editSiteModal', 'deleteModal', 'editIpModal'].forEach(modalId => {
|
||||
const modalElement = document.getElementById(modalId);
|
||||
modalElement.addEventListener('hidden.bs.modal', function() {
|
||||
// Clean up any stray backdrops when modal is fully hidden
|
||||
setTimeout(cleanupModalBackdrops, 100);
|
||||
});
|
||||
});
|
||||
|
||||
// Load sites on page load
|
||||
|
||||
Reference in New Issue
Block a user