634 lines
29 KiB
HTML
634 lines
29 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ 'Edit' if mode == 'edit' else 'New' }} Webhook - SneakyScanner{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row mt-4">
|
|
<div class="col-12 mb-4">
|
|
<h1 style="color: #60a5fa;">{{ 'Edit' if mode == 'edit' else 'Create' }} Webhook</h1>
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb">
|
|
<li class="breadcrumb-item"><a href="{{ url_for('webhooks.list_webhooks') }}">Webhooks</a></li>
|
|
<li class="breadcrumb-item active">{{ 'Edit' if mode == 'edit' else 'New' }}</li>
|
|
</ol>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-lg-8">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<form id="webhook-form">
|
|
<!-- Basic Information -->
|
|
<h5 class="card-title mb-3">Basic Information</h5>
|
|
|
|
<div class="mb-3">
|
|
<label for="name" class="form-label">Webhook Name <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" id="name" name="name" required
|
|
placeholder="e.g., Slack Notifications">
|
|
<div class="form-text">A descriptive name for this webhook</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="url" class="form-label">Webhook URL <span class="text-danger">*</span></label>
|
|
<input type="url" class="form-control" id="url" name="url" required
|
|
placeholder="https://hooks.example.com/webhook">
|
|
<div class="form-text">The endpoint where alerts will be sent</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" id="enabled" name="enabled" checked>
|
|
<label class="form-check-label" for="enabled">Enabled</label>
|
|
</div>
|
|
<div class="form-text">Disabled webhooks will not receive notifications</div>
|
|
</div>
|
|
|
|
<hr class="my-4">
|
|
|
|
<!-- Authentication -->
|
|
<h5 class="card-title mb-3">Authentication</h5>
|
|
|
|
<div class="mb-3">
|
|
<label for="auth_type" class="form-label">Authentication Type</label>
|
|
<select class="form-select" id="auth_type" name="auth_type">
|
|
<option value="none">None</option>
|
|
<option value="bearer">Bearer Token</option>
|
|
<option value="basic">Basic Auth (username:password)</option>
|
|
<option value="custom">Custom Headers</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-3" id="auth_token_field" style="display: none;">
|
|
<label for="auth_token" class="form-label">Authentication Token</label>
|
|
<input type="password" class="form-control" id="auth_token" name="auth_token"
|
|
placeholder="Enter token or username:password">
|
|
<div class="form-text" id="auth_token_help">Will be encrypted when stored</div>
|
|
</div>
|
|
|
|
<div class="mb-3" id="custom_headers_field" style="display: none;">
|
|
<label for="custom_headers" class="form-label">Custom Headers (JSON)</label>
|
|
<textarea class="form-control font-monospace" id="custom_headers" name="custom_headers" rows="4"
|
|
placeholder='{"X-API-Key": "your-key", "X-Custom-Header": "value"}'></textarea>
|
|
<div class="form-text">JSON object with custom HTTP headers</div>
|
|
</div>
|
|
|
|
<hr class="my-4">
|
|
|
|
<!-- Filters -->
|
|
<h5 class="card-title mb-3">Alert Filters</h5>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Alert Types</label>
|
|
<div class="form-text mb-2">Select which alert types trigger this webhook (leave all unchecked for all types)</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input alert-type-check" type="checkbox" value="unexpected_port" id="type_unexpected">
|
|
<label class="form-check-label" for="type_unexpected">Unexpected Port</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input alert-type-check" type="checkbox" value="drift_detection" id="type_drift">
|
|
<label class="form-check-label" for="type_drift">Drift Detection</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input alert-type-check" type="checkbox" value="cert_expiry" id="type_cert">
|
|
<label class="form-check-label" for="type_cert">Certificate Expiry</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input alert-type-check" type="checkbox" value="weak_tls" id="type_tls">
|
|
<label class="form-check-label" for="type_tls">Weak TLS</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input alert-type-check" type="checkbox" value="ping_failed" id="type_ping">
|
|
<label class="form-check-label" for="type_ping">Ping Failed</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Severity Filter</label>
|
|
<div class="form-text mb-2">Select which severities trigger this webhook (leave all unchecked for all severities)</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input severity-check" type="checkbox" value="critical" id="severity_critical">
|
|
<label class="form-check-label" for="severity_critical">Critical</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input severity-check" type="checkbox" value="warning" id="severity_warning">
|
|
<label class="form-check-label" for="severity_warning">Warning</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input severity-check" type="checkbox" value="info" id="severity_info">
|
|
<label class="form-check-label" for="severity_info">Info</label>
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="my-4">
|
|
|
|
<!-- Webhook Template -->
|
|
<h5 class="card-title mb-3">Webhook Template</h5>
|
|
<div class="alert alert-info small">
|
|
<i class="bi bi-info-circle"></i>
|
|
Customize the webhook payload using Jinja2 templates. Leave empty to use the default JSON format.
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="preset_selector" class="form-label">Load Preset Template</label>
|
|
<select class="form-select" id="preset_selector">
|
|
<option value="">-- Select a preset --</option>
|
|
</select>
|
|
<div class="form-text">Choose from pre-built templates for popular services</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="template_format" class="form-label">Template Format</label>
|
|
<select class="form-select" id="template_format" name="template_format">
|
|
<option value="json">JSON</option>
|
|
<option value="text">Plain Text</option>
|
|
</select>
|
|
<div class="form-text">Output format of the rendered template</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="template" class="form-label">Template</label>
|
|
<textarea class="form-control font-monospace" id="template" name="template" rows="12"
|
|
placeholder="Leave empty for default format, or enter custom Jinja2 template..."></textarea>
|
|
<div class="form-text">
|
|
Available variables: <code>{{ "{{" }} alert.* {{ "}}" }}</code>, <code>{{ "{{" }} scan.* {{ "}}" }}</code>, <code>{{ "{{" }} rule.* {{ "}}" }}</code>
|
|
<a href="#" data-bs-toggle="modal" data-bs-target="#variablesModal">View all variables</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="content_type_override" class="form-label">Custom Content-Type (optional)</label>
|
|
<input type="text" class="form-control font-monospace" id="content_type_override" name="content_type_override"
|
|
placeholder="e.g., application/json, text/plain, text/markdown">
|
|
<div class="form-text">Override the default Content-Type header (auto-detected from template format if not set)</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" id="preview-template-btn">
|
|
<i class="bi bi-eye"></i> Preview Template
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm ms-2" id="clear-template-btn">
|
|
<i class="bi bi-x-circle"></i> Clear Template
|
|
</button>
|
|
</div>
|
|
|
|
<hr class="my-4">
|
|
|
|
<!-- Advanced Settings -->
|
|
<h5 class="card-title mb-3">Advanced Settings</h5>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label for="timeout" class="form-label">Timeout (seconds)</label>
|
|
<input type="number" class="form-control" id="timeout" name="timeout" min="1" max="60" value="10">
|
|
<div class="form-text">Maximum time to wait for response</div>
|
|
</div>
|
|
|
|
<div class="col-md-6 mb-3">
|
|
<label for="retry_count" class="form-label">Retry Count</label>
|
|
<input type="number" class="form-control" id="retry_count" name="retry_count" min="0" max="5" value="3">
|
|
<div class="form-text">Number of retry attempts on failure</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="my-4">
|
|
|
|
<!-- Submit Buttons -->
|
|
<div class="d-flex justify-content-between">
|
|
<a href="{{ url_for('webhooks.list_webhooks') }}" class="btn btn-secondary">Cancel</a>
|
|
<div>
|
|
<button type="button" class="btn btn-outline-primary me-2" id="test-btn">
|
|
<i class="bi bi-send"></i> Test Webhook
|
|
</button>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="bi bi-check-circle"></i> Save Webhook
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Help Sidebar -->
|
|
<div class="col-lg-4">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h5 class="card-title"><i class="bi bi-info-circle"></i> Help</h5>
|
|
|
|
<h6 class="mt-3">Payload Format</h6>
|
|
<p class="small text-muted">Default JSON payload format (can be customized with templates):</p>
|
|
<pre class="small bg-dark text-light p-2 rounded"><code>{
|
|
"event": "alert.created",
|
|
"alert": {
|
|
"id": 123,
|
|
"type": "cert_expiry",
|
|
"severity": "warning",
|
|
"message": "...",
|
|
"ip_address": "192.168.1.10",
|
|
"port": 443
|
|
},
|
|
"scan": {...},
|
|
"rule": {...}
|
|
}</code></pre>
|
|
|
|
<h6 class="mt-3">Custom Templates</h6>
|
|
<p class="small text-muted">Use Jinja2 templates to customize payloads for services like Slack, Discord, Gotify, or create your own format. Select a preset or write a custom template.</p>
|
|
|
|
<h6 class="mt-3">Authentication Types</h6>
|
|
<ul class="small">
|
|
<li><strong>None:</strong> No authentication</li>
|
|
<li><strong>Bearer:</strong> Add Authorization header with token</li>
|
|
<li><strong>Basic:</strong> Use username:password format</li>
|
|
<li><strong>Custom:</strong> Define custom HTTP headers</li>
|
|
</ul>
|
|
|
|
<h6 class="mt-3">Retry Logic</h6>
|
|
<p class="small text-muted">Failed webhooks are retried with exponential backoff (2^attempt seconds, max 60s).</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Template Variables Modal -->
|
|
<div class="modal fade" id="variablesModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Available Template Variables</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<h6>Alert Variables</h6>
|
|
<ul class="small font-monospace">
|
|
<li>{{ "{{" }} alert.id {{ "}}" }} - Alert ID</li>
|
|
<li>{{ "{{" }} alert.type {{ "}}" }} - Alert type (unexpected_port, cert_expiry, etc.)</li>
|
|
<li>{{ "{{" }} alert.severity {{ "}}" }} - Severity level (critical, warning, info)</li>
|
|
<li>{{ "{{" }} alert.message {{ "}}" }} - Human-readable alert message</li>
|
|
<li>{{ "{{" }} alert.ip_address {{ "}}" }} - IP address (if applicable)</li>
|
|
<li>{{ "{{" }} alert.port {{ "}}" }} - Port number (if applicable)</li>
|
|
<li>{{ "{{" }} alert.acknowledged {{ "}}" }} - Boolean: is acknowledged</li>
|
|
<li>{{ "{{" }} alert.created_at {{ "}}" }} - Alert creation timestamp</li>
|
|
</ul>
|
|
|
|
<h6 class="mt-3">Scan Variables</h6>
|
|
<ul class="small font-monospace">
|
|
<li>{{ "{{" }} scan.id {{ "}}" }} - Scan ID</li>
|
|
<li>{{ "{{" }} scan.title {{ "}}" }} - Scan title from config</li>
|
|
<li>{{ "{{" }} scan.timestamp {{ "}}" }} - Scan start time</li>
|
|
<li>{{ "{{" }} scan.duration {{ "}}" }} - Scan duration in seconds</li>
|
|
<li>{{ "{{" }} scan.status {{ "}}" }} - Scan status (running, completed, failed)</li>
|
|
<li>{{ "{{" }} scan.triggered_by {{ "}}" }} - How scan was triggered (manual, scheduled, api)</li>
|
|
</ul>
|
|
|
|
<h6 class="mt-3">Rule Variables</h6>
|
|
<ul class="small font-monospace">
|
|
<li>{{ "{{" }} rule.id {{ "}}" }} - Rule ID</li>
|
|
<li>{{ "{{" }} rule.name {{ "}}" }} - Rule name</li>
|
|
<li>{{ "{{" }} rule.type {{ "}}" }} - Rule type</li>
|
|
<li>{{ "{{" }} rule.threshold {{ "}}" }} - Rule threshold value</li>
|
|
<li>{{ "{{" }} rule.severity {{ "}}" }} - Rule severity</li>
|
|
</ul>
|
|
|
|
<h6 class="mt-3">App Variables</h6>
|
|
<ul class="small font-monospace">
|
|
<li>{{ "{{" }} app.name {{ "}}" }} - Application name</li>
|
|
<li>{{ "{{" }} app.version {{ "}}" }} - Application version</li>
|
|
<li>{{ "{{" }} app.url {{ "}}" }} - Repository URL</li>
|
|
</ul>
|
|
|
|
<h6 class="mt-3">Other Variables</h6>
|
|
<ul class="small font-monospace">
|
|
<li>{{ "{{" }} timestamp {{ "}}" }} - Current UTC timestamp</li>
|
|
</ul>
|
|
|
|
<h6 class="mt-3">Jinja2 Features</h6>
|
|
<p class="small">Templates support Jinja2 syntax including:</p>
|
|
<ul class="small">
|
|
<li>Conditionals: <code>{{ "{%" }} if alert.severity == 'critical' {{ "%}" }}...{{ "{%" }} endif {{ "%}" }}</code></li>
|
|
<li>Filters: <code>{{ "{{" }} alert.type|upper {{ "}}" }}</code>, <code>{{ "{{" }} alert.created_at.isoformat() {{ "}}" }}</code></li>
|
|
<li>Default values: <code>{{ "{{" }} alert.port|default('N/A') {{ "}}" }}</code></li>
|
|
</ul>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Template Preview Modal -->
|
|
<div class="modal fade" id="previewModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Template Preview</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="small text-muted">Preview using sample data:</p>
|
|
<pre class="bg-dark text-light p-3 rounded" id="preview-output" style="max-height: 500px; overflow-y: auto;"><code></code></pre>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
const webhookId = {{ webhook.id if webhook else 'null' }};
|
|
const mode = '{{ mode }}';
|
|
|
|
// Load template presets on page load
|
|
async function loadPresets() {
|
|
try {
|
|
const response = await fetch('/api/webhooks/template-presets');
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'success') {
|
|
const selector = document.getElementById('preset_selector');
|
|
data.presets.forEach(preset => {
|
|
const option = document.createElement('option');
|
|
option.value = JSON.stringify({
|
|
template: preset.template,
|
|
format: preset.format,
|
|
content_type: preset.content_type
|
|
});
|
|
option.textContent = `${preset.name} - ${preset.description}`;
|
|
selector.appendChild(option);
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load presets:', error);
|
|
}
|
|
}
|
|
|
|
// Handle preset selection
|
|
document.getElementById('preset_selector').addEventListener('change', function() {
|
|
if (!this.value) return;
|
|
|
|
try {
|
|
const preset = JSON.parse(this.value);
|
|
document.getElementById('template').value = preset.template;
|
|
document.getElementById('template_format').value = preset.format;
|
|
document.getElementById('content_type_override').value = preset.content_type;
|
|
} catch (error) {
|
|
console.error('Failed to load preset:', error);
|
|
}
|
|
});
|
|
|
|
// Handle preview template button
|
|
document.getElementById('preview-template-btn').addEventListener('click', async function() {
|
|
const template = document.getElementById('template').value.trim();
|
|
if (!template) {
|
|
alert('Please enter a template first');
|
|
return;
|
|
}
|
|
|
|
const templateFormat = document.getElementById('template_format').value;
|
|
|
|
try {
|
|
const response = await fetch('/api/webhooks/preview-template', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
template: template,
|
|
template_format: templateFormat
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.status === 'success') {
|
|
// Display preview in modal
|
|
const output = document.querySelector('#preview-output code');
|
|
if (templateFormat === 'json') {
|
|
// Pretty print JSON
|
|
try {
|
|
const parsed = JSON.parse(result.rendered);
|
|
output.textContent = JSON.stringify(parsed, null, 2);
|
|
} catch (e) {
|
|
output.textContent = result.rendered;
|
|
}
|
|
} else {
|
|
output.textContent = result.rendered;
|
|
}
|
|
|
|
// Show modal
|
|
const modal = new bootstrap.Modal(document.getElementById('previewModal'));
|
|
modal.show();
|
|
} else {
|
|
alert(`Preview failed: ${result.message}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error previewing template:', error);
|
|
alert('Failed to preview template');
|
|
}
|
|
});
|
|
|
|
// Handle clear template button
|
|
document.getElementById('clear-template-btn').addEventListener('click', function() {
|
|
if (confirm('Clear template and reset to default format?')) {
|
|
document.getElementById('template').value = '';
|
|
document.getElementById('template_format').value = 'json';
|
|
document.getElementById('content_type_override').value = '';
|
|
document.getElementById('preset_selector').value = '';
|
|
}
|
|
});
|
|
|
|
// Load presets on page load
|
|
loadPresets();
|
|
|
|
// Show/hide auth fields based on type
|
|
document.getElementById('auth_type').addEventListener('change', function() {
|
|
const authType = this.value;
|
|
const tokenField = document.getElementById('auth_token_field');
|
|
const headersField = document.getElementById('custom_headers_field');
|
|
const tokenHelp = document.getElementById('auth_token_help');
|
|
|
|
tokenField.style.display = 'none';
|
|
headersField.style.display = 'none';
|
|
|
|
if (authType === 'bearer') {
|
|
tokenField.style.display = 'block';
|
|
document.getElementById('auth_token').placeholder = 'Enter bearer token';
|
|
tokenHelp.textContent = 'Bearer token for Authorization header (encrypted when stored)';
|
|
} else if (authType === 'basic') {
|
|
tokenField.style.display = 'block';
|
|
document.getElementById('auth_token').placeholder = 'username:password';
|
|
tokenHelp.textContent = 'Format: username:password (encrypted when stored)';
|
|
} else if (authType === 'custom') {
|
|
headersField.style.display = 'block';
|
|
}
|
|
});
|
|
|
|
// Load existing webhook data if editing
|
|
if (mode === 'edit' && webhookId) {
|
|
loadWebhookData(webhookId);
|
|
}
|
|
|
|
async function loadWebhookData(id) {
|
|
try {
|
|
const response = await fetch(`/api/webhooks/${id}`);
|
|
const data = await response.json();
|
|
const webhook = data.webhook;
|
|
|
|
// Populate form fields
|
|
document.getElementById('name').value = webhook.name || '';
|
|
document.getElementById('url').value = webhook.url || '';
|
|
document.getElementById('enabled').checked = webhook.enabled;
|
|
document.getElementById('auth_type').value = webhook.auth_type || 'none';
|
|
document.getElementById('timeout').value = webhook.timeout || 10;
|
|
document.getElementById('retry_count').value = webhook.retry_count || 3;
|
|
|
|
// Trigger auth type change to show relevant fields
|
|
document.getElementById('auth_type').dispatchEvent(new Event('change'));
|
|
|
|
// Don't populate auth_token (it's encrypted)
|
|
if (webhook.custom_headers) {
|
|
document.getElementById('custom_headers').value = JSON.stringify(webhook.custom_headers, null, 2);
|
|
}
|
|
|
|
// Check alert types
|
|
if (webhook.alert_types && webhook.alert_types.length > 0) {
|
|
webhook.alert_types.forEach(type => {
|
|
const checkbox = document.querySelector(`.alert-type-check[value="${type}"]`);
|
|
if (checkbox) checkbox.checked = true;
|
|
});
|
|
}
|
|
|
|
// Check severities
|
|
if (webhook.severity_filter && webhook.severity_filter.length > 0) {
|
|
webhook.severity_filter.forEach(sev => {
|
|
const checkbox = document.querySelector(`.severity-check[value="${sev}"]`);
|
|
if (checkbox) checkbox.checked = true;
|
|
});
|
|
}
|
|
|
|
// Load template fields
|
|
if (webhook.template) {
|
|
document.getElementById('template').value = webhook.template;
|
|
}
|
|
if (webhook.template_format) {
|
|
document.getElementById('template_format').value = webhook.template_format;
|
|
}
|
|
if (webhook.content_type_override) {
|
|
document.getElementById('content_type_override').value = webhook.content_type_override;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading webhook:', error);
|
|
alert('Failed to load webhook data');
|
|
}
|
|
}
|
|
|
|
// Form submission
|
|
document.getElementById('webhook-form').addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
|
|
const formData = {
|
|
name: document.getElementById('name').value,
|
|
url: document.getElementById('url').value,
|
|
enabled: document.getElementById('enabled').checked,
|
|
auth_type: document.getElementById('auth_type').value,
|
|
timeout: parseInt(document.getElementById('timeout').value),
|
|
retry_count: parseInt(document.getElementById('retry_count').value)
|
|
};
|
|
|
|
// Add auth token if provided
|
|
const authToken = document.getElementById('auth_token').value;
|
|
if (authToken) {
|
|
formData.auth_token = authToken;
|
|
}
|
|
|
|
// Add custom headers if provided
|
|
const customHeaders = document.getElementById('custom_headers').value;
|
|
if (customHeaders.trim()) {
|
|
try {
|
|
formData.custom_headers = JSON.parse(customHeaders);
|
|
} catch (e) {
|
|
alert('Invalid JSON in custom headers');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Collect selected alert types
|
|
const alertTypes = Array.from(document.querySelectorAll('.alert-type-check:checked'))
|
|
.map(cb => cb.value);
|
|
if (alertTypes.length > 0) {
|
|
formData.alert_types = alertTypes;
|
|
}
|
|
|
|
// Collect selected severities
|
|
const severities = Array.from(document.querySelectorAll('.severity-check:checked'))
|
|
.map(cb => cb.value);
|
|
if (severities.length > 0) {
|
|
formData.severity_filter = severities;
|
|
}
|
|
|
|
// Add template fields
|
|
const template = document.getElementById('template').value.trim();
|
|
if (template) {
|
|
formData.template = template;
|
|
formData.template_format = document.getElementById('template_format').value;
|
|
|
|
const contentTypeOverride = document.getElementById('content_type_override').value.trim();
|
|
if (contentTypeOverride) {
|
|
formData.content_type_override = contentTypeOverride;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const url = mode === 'edit' ? `/api/webhooks/${webhookId}` : '/api/webhooks';
|
|
const method = mode === 'edit' ? 'PUT' : 'POST';
|
|
|
|
const response = await fetch(url, {
|
|
method: method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(formData)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.status === 'success') {
|
|
alert('Webhook saved successfully!');
|
|
window.location.href = '{{ url_for("webhooks.list_webhooks") }}';
|
|
} else {
|
|
alert(`Failed to save webhook: ${result.message}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving webhook:', error);
|
|
alert('Failed to save webhook');
|
|
}
|
|
});
|
|
|
|
// Test webhook button
|
|
document.getElementById('test-btn').addEventListener('click', async function() {
|
|
if (mode !== 'edit' || !webhookId) {
|
|
alert('Please save the webhook first before testing');
|
|
return;
|
|
}
|
|
|
|
if (!confirm('Send a test payload to this webhook?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/webhooks/${webhookId}/test`, { method: 'POST' });
|
|
const result = await response.json();
|
|
|
|
if (result.status === 'success') {
|
|
alert(`Test successful!\nHTTP ${result.status_code}\n${result.message}`);
|
|
} else {
|
|
alert(`Test failed:\n${result.message}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error testing webhook:', error);
|
|
alert('Failed to test webhook');
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|