Files
jackboxpartypack-gamepicker/backend/utils/webhooks.js
2025-11-02 16:06:31 -05:00

152 lines
4.1 KiB
JavaScript

const crypto = require('crypto');
const db = require('../database');
/**
* Trigger webhooks for a specific event type
* @param {string} eventType - The event type (e.g., 'game.added')
* @param {object} data - The payload data to send
*/
async function triggerWebhook(eventType, data) {
try {
// Get all enabled webhooks that are subscribed to this event
const webhooks = db.prepare(`
SELECT * FROM webhooks
WHERE enabled = 1
`).all();
if (webhooks.length === 0) {
return; // No webhooks configured
}
// Filter webhooks that are subscribed to this event
const subscribedWebhooks = webhooks.filter(webhook => {
try {
const events = JSON.parse(webhook.events);
return events.includes(eventType);
} catch (err) {
console.error(`Invalid events JSON for webhook ${webhook.id}:`, err);
return false;
}
});
if (subscribedWebhooks.length === 0) {
return; // No webhooks subscribed to this event
}
// Build the payload
const payload = {
event: eventType,
timestamp: new Date().toISOString(),
data: data
};
// Send to each webhook asynchronously (non-blocking)
subscribedWebhooks.forEach(webhook => {
sendWebhook(webhook, payload, eventType).catch(err => {
console.error(`Error sending webhook ${webhook.id}:`, err);
});
});
} catch (err) {
console.error('Error triggering webhooks:', err);
}
}
/**
* Send a webhook to a specific URL
* @param {object} webhook - The webhook configuration
* @param {object} payload - The payload to send
* @param {string} eventType - The event type
*/
async function sendWebhook(webhook, payload, eventType) {
const payloadString = JSON.stringify(payload);
// Generate HMAC signature
const signature = 'sha256=' + crypto
.createHmac('sha256', webhook.secret)
.update(payloadString)
.digest('hex');
const startTime = Date.now();
let responseStatus = null;
let errorMessage = null;
try {
// Send the webhook
const response = await fetch(webhook.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature,
'X-Webhook-Event': eventType,
'User-Agent': 'Jackbox-Game-Picker-Webhook/1.0'
},
body: payloadString,
// Set a timeout of 5 seconds
signal: AbortSignal.timeout(5000)
});
responseStatus = response.status;
if (!response.ok) {
errorMessage = `HTTP ${response.status}: ${response.statusText}`;
}
} catch (err) {
errorMessage = err.message;
responseStatus = 0; // Indicates connection/network error
}
// Log the webhook call
try {
db.prepare(`
INSERT INTO webhook_logs (webhook_id, event_type, payload, response_status, error_message)
VALUES (?, ?, ?, ?, ?)
`).run(webhook.id, eventType, payloadString, responseStatus, errorMessage);
} catch (logErr) {
console.error('Error logging webhook call:', logErr);
}
const duration = Date.now() - startTime;
if (errorMessage) {
console.error(`Webhook ${webhook.id} (${webhook.name}) failed: ${errorMessage} (${duration}ms)`);
} else {
console.log(`Webhook ${webhook.id} (${webhook.name}) sent successfully: ${responseStatus} (${duration}ms)`);
}
}
/**
* Verify a webhook signature
* @param {string} signature - The signature from the X-Webhook-Signature header
* @param {string} payload - The raw request body as a string
* @param {string} secret - The webhook secret
* @returns {boolean} - True if signature is valid
*/
function verifyWebhookSignature(signature, payload, secret) {
if (!signature || !signature.startsWith('sha256=')) {
return false;
}
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Use timing-safe comparison to prevent timing attacks
try {
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
} catch (err) {
return false;
}
}
module.exports = {
triggerWebhook,
verifyWebhookSignature
};