89 lines
3.1 KiB
JavaScript
89 lines
3.1 KiB
JavaScript
const crypto = require('crypto');
|
|
const { audit } = require('../db');
|
|
const { getConfig } = require('./notifications');
|
|
|
|
const TIMEOUT_MS = 10000;
|
|
|
|
// The JSON object delivered for each alert. Stable, flat shape for easy consumption.
|
|
function buildPayload(alert) {
|
|
return {
|
|
event: 'alert',
|
|
id: alert.id,
|
|
type: alert.type,
|
|
severity: alert.severity,
|
|
title: alert.title,
|
|
message: alert.message || null,
|
|
due_date: alert.due_date || null,
|
|
status: alert.status,
|
|
reference_type: alert.reference_type,
|
|
reference_id: alert.reference_id,
|
|
asset_id: alert.asset_id || null,
|
|
asset_code: alert.asset_code || null,
|
|
created_at: alert.created_at,
|
|
sent_at: new Date().toISOString()
|
|
};
|
|
}
|
|
|
|
// POST a single JSON object. When a secret is set, sign the raw body with HMAC-SHA256
|
|
// so the receiver can verify authenticity via the X-MixedAssets-Signature header.
|
|
async function postJson(url, secret, payload) {
|
|
const body = JSON.stringify(payload);
|
|
const headers = { 'Content-Type': 'application/json', 'User-Agent': 'MixedAssets-Webhook/1' };
|
|
if (secret) {
|
|
const signature = crypto.createHmac('sha256', secret).update(body).digest('hex');
|
|
headers['X-MixedAssets-Signature'] = `sha256=${signature}`;
|
|
}
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
try {
|
|
const response = await fetch(url, { method: 'POST', headers, body, signal: controller.signal });
|
|
return { ok: response.ok, status: response.status };
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
// Deliver each alert as its own JSON POST. Best-effort: a failed delivery is counted
|
|
// but never throws, so one bad endpoint can't break the alert cycle.
|
|
async function deliverAlerts(alerts) {
|
|
const c = getConfig();
|
|
if (!c.webhookEnabled || !c.webhookUrl || !alerts.length) {
|
|
return { attempted: false, delivered: 0, failed: 0 };
|
|
}
|
|
let delivered = 0;
|
|
let failed = 0;
|
|
for (const alert of alerts) {
|
|
try {
|
|
const result = await postJson(c.webhookUrl, c.webhookSecret, buildPayload(alert));
|
|
if (result.ok) delivered += 1;
|
|
else failed += 1;
|
|
} catch {
|
|
failed += 1;
|
|
}
|
|
}
|
|
return { attempted: true, delivered, failed };
|
|
}
|
|
|
|
async function sendTestWebhook(userId) {
|
|
const c = getConfig();
|
|
if (!c.webhookUrl) throw Object.assign(new Error('A webhook URL is not configured'), { status: 400 });
|
|
const payload = {
|
|
event: 'test',
|
|
severity: 'info',
|
|
title: 'MixedAssets webhook test',
|
|
message: 'If you received this, your webhook destination is configured correctly.',
|
|
sent_at: new Date().toISOString()
|
|
};
|
|
let result;
|
|
try {
|
|
result = await postJson(c.webhookUrl, c.webhookSecret, payload);
|
|
} catch (error) {
|
|
throw Object.assign(new Error(`Webhook request failed: ${error.message}`), { status: 400 });
|
|
}
|
|
audit(userId, 'send', 'test_webhook', null, null, { url: c.webhookUrl, status: result.status });
|
|
if (!result.ok) throw Object.assign(new Error(`Webhook endpoint returned HTTP ${result.status}`), { status: 400 });
|
|
return { ok: true, status: result.status };
|
|
}
|
|
|
|
module.exports = { buildPayload, deliverAlerts, sendTestWebhook };
|