Password Policy/App rename/PM Scheduling Improvements

This commit is contained in:
2026-06-06 02:32:47 -05:00
parent dfd999b6a9
commit f024286b5e
59 changed files with 3814 additions and 299 deletions

View File

@@ -1,11 +1,17 @@
/*
* integration service for managing the connection to a webhook endpoint and delivering alert notifications as JSON POST requests. The service includes functions for building the payload for each alert, posting the payload to the configured webhook URL with optional HMAC-SHA256 signing for authenticity verification, and delivering alerts in a best-effort manner where failures are counted but do not throw errors. Additionally, there is a function for sending a test webhook to verify that the webhook destination is configured correctly, which also audits the action for tracking purposes.
*/
const moment = require('moment');
const crypto = require('crypto');
const { audit } = require('../db');
const { getConfig } = require('./notifications');
const TIMEOUT_MS = 10000;
console.log(`${moment().format()} ✅ Webhooks service initialized with timeout of ${TIMEOUT_MS}ms`);
// The JSON object delivered for each alert. Stable, flat shape for easy consumption.
function buildPayload(alert) {
console.log(`${moment().format()} 🔔 Building webhook payload for alert ID ${alert.id} with title "${alert.title}" and type "${alert.type}"`);
return {
event: 'alert',
id: alert.id,
@@ -25,64 +31,95 @@ function buildPayload(alert) {
}
// 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.
// so the receiver can verify authenticity via the X-DepreCore-Signature header.
async function postJson(url, secret, payload) {
console.log(`${moment().format()} 🚀 Posting JSON payload to webhook URL ${url} with payload: ${JSON.stringify(payload)}`);
const body = JSON.stringify(payload);
const headers = { 'Content-Type': 'application/json', 'User-Agent': 'MixedAssets-Webhook/1' };
const headers = { 'Content-Type': 'application/json', 'User-Agent': 'DepreCore-Webhook/1' };
if (secret) {
// Note: when a webhook secret is configured, we sign the raw JSON body of the request using HMAC-SHA256 with the provided secret, and we include the resulting signature in the X-DepreCore-Signature header in the format "sha256=signature"; this allows the receiver of the webhook to verify the authenticity of the request by computing the HMAC-SHA256 signature on their end using the same secret and comparing it to the signature provided in the header, which helps ensure that the webhook requests are coming from a trusted source and have not been tampered with in transit.
console.log(`${moment().format()} 🔐 Signing webhook payload with HMAC-SHA256 using provided secret`);
const signature = crypto.createHmac('sha256', secret).update(body).digest('hex');
headers['X-MixedAssets-Signature'] = `sha256=${signature}`;
headers['X-DepreCore-Signature'] = `sha256=${signature}`;
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
try {
// Note: we use the Fetch API to send the POST request to the webhook URL with the JSON payload and appropriate headers, and we also set a timeout using AbortController to ensure that the request does not hang indefinitely; if the request takes longer than the configured timeout, it will be aborted and an error will be thrown, which allows us to handle cases where the webhook endpoint is unresponsive or slow to respond.
console.log(`${moment().format()} 📡 Sending POST request to ${url} with headers ${JSON.stringify(headers)} and body ${body}`);
const response = await fetch(url, { method: 'POST', headers, body, signal: controller.signal });
return { ok: response.ok, status: response.status };
} finally {
console.log(`${moment().format()} ⏰ Clearing webhook request timeout`);
clearTimeout(timer);
}
console.log(`${moment().format()} ✅ Webhook POST request completed successfully with status ${result.status}`);
}
// 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) {
console.log(`${moment().format()} 🚚 Delivering ${alerts.length} alerts to webhook endpoint`);
const c = getConfig();
if (!c.webhookEnabled || !c.webhookUrl || !alerts.length) {
// Note: if the webhook is not enabled, the URL is not configured, or there are no alerts to deliver, we skip the delivery process and log a message indicating that the webhook delivery was skipped; this allows us to avoid unnecessary attempts to send webhook requests when the configuration is incomplete or when there are no alerts to send, and it also provides visibility into why webhook deliveries may not be occurring.
console.log(`${moment().format()} 🚫 Webhook delivery skipped: no alerts or webhook not enabled`);
return { attempted: false, delivered: 0, failed: 0 };
}
let delivered = 0;
let failed = 0;
for (const alert of alerts) {
// Note: for each alert, we attempt to post the JSON payload to the configured webhook URL using the postJson function, and we count the number of successful deliveries and failures; if a delivery fails (either due to an error or a non-OK response), we increment the failure count but do not throw an error, which allows us to continue attempting to deliver subsequent alerts even if one or more deliveries fail, ensuring that a single bad endpoint or alert does not disrupt the entire alert delivery process.
console.log(`${moment().format()} 📡 Attempting to deliver alert to webhook: ${alert.title}`);
try {
// Note: we build the payload for the alert using the buildPayload function, which creates a stable and flat JSON object with relevant information about the alert, and then we post it to the webhook URL using the postJson function; if the postJson function returns an OK response, we increment the delivered count, otherwise we increment the failed count, allowing us to track the success and failure of each delivery attempt.
const result = await postJson(c.webhookUrl, c.webhookSecret, buildPayload(alert));
if (result.ok) delivered += 1;
else failed += 1;
} catch {
} catch (error) {
console.log(`${moment().format()} ❌ Failed to deliver alert to webhook: ${error.message}`);
failed += 1;
}
}
console.log(`${moment().format()} 🚚 Webhook delivery complete: ${delivered} delivered, ${failed} failed`);
return { attempted: true, delivered, failed };
}
// Send a test webhook with a simple payload to verify that the webhook URL is configured correctly and can receive requests. This function is intended to be called from an API endpoint where a user can trigger a test webhook, and it will also audit the action for tracking purposes. If the webhook URL is not configured or if the request fails, it throws an error with details about the failure.
async function sendTestWebhook(userId) {
console.log(`${moment().format()} 🚀 Sending test webhook to verify configuration`);
const c = getConfig();
if (!c.webhookUrl) throw Object.assign(new Error('A webhook URL is not configured'), { status: 400 });
if (!c.webhookUrl) {
// Note: if the webhook URL is not configured, we cannot send the test webhook, so we log an error message and throw an error indicating that the webhook URL is missing; this allows us to provide clear feedback about why the test webhook could not be sent and helps users understand that they need to configure the webhook URL before they can successfully send a test webhook.
console.log(`${moment().format()} ❌ Test webhook failed: Webhook URL is not configured`);
throw Object.assign(new Error('A webhook URL is not configured'), { status: 400 });
}
const payload = {
event: 'test',
severity: 'info',
title: 'MixedAssets webhook test',
title: 'DepreCore webhook test',
message: 'If you received this, your webhook destination is configured correctly.',
sent_at: new Date().toISOString()
};
let result;
// Note: we attempt to send the test webhook using the postJson function with the test payload, and we catch any errors that occur during the request; if an error occurs, we log the error message and throw a new error indicating that the webhook request failed, along with the original error message for details; this allows us to provide clear feedback about why the test webhook failed and helps users troubleshoot any issues with their webhook configuration or endpoint.
try {
console.log(`${moment().format()} 📡 Sending test webhook to: ${c.webhookUrl}`);
result = await postJson(c.webhookUrl, c.webhookSecret, payload);
} catch (error) {
console.log(`${moment().format()} ❌ Test webhook failed: ${error.message}`);
throw Object.assign(new Error(`Webhook request failed: ${error.message}`), { status: 400 });
}
console.log(`${moment().format()} 📡 Test webhook sent successfully to: ${c.webhookUrl}`) ;
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 });
if (!result.ok) {
// Note: if the response from the webhook endpoint is not OK (i.e., not in the 200-299 range), we log an error message with the HTTP status code returned by the endpoint and throw an error indicating that the webhook endpoint returned a non-OK status; this allows us to provide clear feedback about why the test webhook failed and helps users understand that their webhook endpoint may be rejecting the request or encountering an error when processing it.
console.log(`${moment().format()} ❌ Test webhook failed: Webhook endpoint returned HTTP ${result.status}`);
throw Object.assign(new Error(`Webhook endpoint returned HTTP ${result.status}`), { status: 400 });
}
console.log(`${moment().format()} ✅ Test webhook delivered successfully with HTTP ${result.status}`);
return { ok: true, status: result.status };
}
// Export the functions for use in other modules. This allows the webhook-related logic to be organized in a single module and reused across different parts of the application, such as in API endpoints for sending test webhooks or in the alert processing logic for delivering alerts to the configured webhook endpoint.
module.exports = { buildPayload, deliverAlerts, sendTestWebhook };