Updated A LOT
This commit is contained in:
88
server/services/webhooks.js
Normal file
88
server/services/webhooks.js
Normal file
@@ -0,0 +1,88 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user