n8n-nodes-toggl
Version:
n8n community node for Toggl Track time tracking integration with comprehensive operations including client management and webhook support
242 lines (241 loc) • 9.71 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.TogglWebhookTrigger = void 0;
const crypto = __importStar(require("crypto-js"));
const TogglApi_helper_1 = require("../TogglApi.helper");
class TogglWebhookTrigger {
constructor() {
this.description = {
displayName: 'Toggl Webhook Trigger',
name: 'togglWebhookTrigger',
icon: 'file:toggl.png',
group: ['trigger'],
version: 1,
subtitle: '={{$parameter["events"].join(", ")}}',
description: 'Handle Toggl Track webhook events',
defaults: {
name: 'Toggl Webhook Trigger',
},
inputs: [],
outputs: ["main" /* NodeConnectionType.Main */],
credentials: [
{
name: 'togglApi',
required: true,
},
],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName: 'Workspace ID',
name: 'workspaceId',
type: 'number',
default: 0,
required: true,
description: 'ID of the workspace to create webhook for',
},
{
displayName: 'Events',
name: 'events',
type: 'multiOptions',
options: [
{
name: 'Project Created',
value: 'project:created',
description: 'Triggered when a project is created',
},
{
name: 'Project Updated',
value: 'project:updated',
description: 'Triggered when a project is updated',
},
{
name: 'Project Deleted',
value: 'project:deleted',
description: 'Triggered when a project is deleted',
},
{
name: 'Time Entry Created',
value: 'time_entry:created',
description: 'Triggered when a time entry is created',
},
{
name: 'Time Entry Updated',
value: 'time_entry:updated',
description: 'Triggered when a time entry is updated',
},
{
name: 'Time Entry Deleted',
value: 'time_entry:deleted',
description: 'Triggered when a time entry is deleted',
},
{
name: 'Task Created',
value: 'task:created',
description: 'Triggered when a task is created',
},
{
name: 'Task Updated',
value: 'task:updated',
description: 'Triggered when a task is updated',
},
{
name: 'Task Deleted',
value: 'task:deleted',
description: 'Triggered when a task is deleted',
},
],
default: ['time_entry:created'],
required: true,
description: 'The events to listen for',
},
{
displayName: 'Description',
name: 'description',
type: 'string',
default: 'n8n webhook',
description: 'Description for the webhook subscription',
},
],
};
}
// The function to run when the webhook is activated
async webhookActivate() {
const workspaceId = this.getNodeParameter('workspaceId');
const events = this.getNodeParameter('events');
const description = this.getNodeParameter('description');
const webhookUrl = this.getNodeWebhookUrl('default');
// Generate a random secret for webhook verification
const secret = crypto.lib.WordArray.random(32).toString();
const body = {
url: webhookUrl,
events,
description,
secret,
};
try {
const responseData = await TogglApi_helper_1.togglApiRequest.call(this, 'POST', `/workspaces/${workspaceId}/webhooks`, body);
// Store subscription data
const webhookData = this.getWorkflowStaticData('node');
webhookData.subscriptionId = responseData.id;
webhookData.secret = secret;
// Validate webhook if synchronous echo fails
try {
await TogglApi_helper_1.togglApiRequest.call(this, 'GET', `/workspaces/${workspaceId}/webhooks/${responseData.id}/validate`);
}
catch (error) {
console.warn('Webhook validation failed, but subscription created successfully');
}
}
catch (error) {
throw new Error(`Failed to create webhook: ${error.message}`);
}
}
// The function to run when the webhook is deactivated
async webhookDeactivate() {
const webhookData = this.getWorkflowStaticData('node');
const workspaceId = this.getNodeParameter('workspaceId');
if (webhookData.subscriptionId) {
try {
await TogglApi_helper_1.togglApiRequest.call(this, 'DELETE', `/workspaces/${workspaceId}/webhooks/${webhookData.subscriptionId}`);
}
catch (error) {
console.warn(`Failed to delete webhook: ${error.message}`);
}
// Clear stored data
delete webhookData.subscriptionId;
delete webhookData.secret;
}
}
// The function to run when a webhook request is received
async webhook() {
const req = this.getRequestObject();
const webhookData = this.getWorkflowStaticData('node');
// Verify webhook signature
const signature = req.headers['x-webhook-signature-256'];
const body = JSON.stringify(req.body);
if (!signature || !webhookData.secret) {
return {
webhookResponse: {
status: 401,
body: 'Unauthorized: Missing signature or secret',
},
};
}
// Calculate expected signature
const expectedSignature = crypto.HmacSHA256(body, webhookData.secret).toString();
const receivedSignature = signature.replace('sha256=', '');
// Compare signatures using timing-safe comparison
if (!TogglWebhookTrigger.compareSignatures(expectedSignature, receivedSignature)) {
return {
webhookResponse: {
status: 401,
body: 'Unauthorized: Invalid signature',
},
};
}
// Process the webhook payload
const responseData = req.body;
return {
workflowData: [
[
{
json: responseData,
},
],
],
};
}
// Timing-safe string comparison to prevent timing attacks
static compareSignatures(expected, received) {
if (expected.length !== received.length) {
return false;
}
let result = 0;
for (let i = 0; i < expected.length; i++) {
result |= expected.charCodeAt(i) ^ received.charCodeAt(i);
}
return result === 0;
}
}
exports.TogglWebhookTrigger = TogglWebhookTrigger;