IDK, it's working and we're moving on
This commit is contained in:
151
backend/utils/webhooks.js
Normal file
151
backend/utils/webhooks.js
Normal file
@@ -0,0 +1,151 @@
|
||||
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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user