152 lines
4.1 KiB
JavaScript
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
|
|
};
|
|
|