import type {
IHookFunctions,
IWebhookFunctions,
IDataObject,
ILoadOptionsFunctions,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
IWebhookResponseData,
} from 'n8n-workflow';
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
import { asanaApiRequest, getWorkspaces } from './GenericFunctions';
// import {
// createHmac,
// } from 'crypto';
export class AsanaTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Asana Trigger',
name: 'asanaTrigger',
icon: 'file:asana.svg',
group: ['trigger'],
version: 1,
description: 'Starts the workflow when Asana events occur.',
defaults: {
name: 'Asana Trigger',
},
inputs: [],
outputs: [NodeConnectionType.Main],
credentials: [
{
name: 'asanaApi',
required: true,
displayOptions: {
show: {
authentication: ['accessToken'],
},
},
},
{
name: 'asanaOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: ['oAuth2'],
},
},
},
],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Access Token',
value: 'accessToken',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'accessToken',
},
{
displayName: 'Resource',
name: 'resource',
type: 'string',
default: '',
required: true,
description: 'The resource ID to subscribe to. The resource can be a task or project.',
},
{
displayName: 'Workspace Name or ID',
name: 'workspace',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getWorkspaces',
},
options: [],
default: '',
description:
'The workspace ID the resource is registered under. This is only required if you want to allow overriding existing webhooks. Choose from the list, or specify an ID using an expression.',
},
],
};
methods = {
loadOptions: {
// Get all the available workspaces to display them to user so that they can
// select them easily
async getWorkspaces(this: ILoadOptionsFunctions): Promise {
const workspaces = await getWorkspaces.call(this);
workspaces.unshift({
name: '',
value: '',
});
return workspaces;
},
},
};
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise {
const webhookData = this.getWorkflowStaticData('node');
const webhookUrl = this.getNodeWebhookUrl('default') as string;
const resource = this.getNodeParameter('resource') as string;
const workspace = this.getNodeParameter('workspace') as string;
const { data } = await asanaApiRequest.call(this, 'GET', '/webhooks', {}, { workspace });
for (const webhook of data) {
if (webhook.resource.gid === resource && webhook.target === webhookUrl) {
webhookData.webhookId = webhook.gid;
return true;
}
}
// If it did not error then the webhook exists
return false;
},
async create(this: IHookFunctions): Promise {
const webhookData = this.getWorkflowStaticData('node');
const webhookUrl = this.getNodeWebhookUrl('default') as string;
if (webhookUrl.includes('%20')) {
throw new NodeOperationError(
this.getNode(),
'The name of the Asana Trigger Node is not allowed to contain any spaces!',
);
}
const resource = this.getNodeParameter('resource') as string;
const body = {
resource,
target: webhookUrl,
};
const responseData = await asanaApiRequest.call(this, 'POST', '/webhooks', body);
if (responseData.data === undefined || responseData.data.gid === undefined) {
// Required data is missing so was not successful
return false;
}
webhookData.webhookId = responseData.data.gid as string;
return true;
},
async delete(this: IHookFunctions): Promise {
const webhookData = this.getWorkflowStaticData('node');
if (webhookData.webhookId !== undefined) {
const body = {};
try {
await asanaApiRequest.call(this, 'DELETE', `/webhooks/${webhookData.webhookId}`, body);
} catch (error) {
return false;
}
// Remove from the static workflow data so that it is clear
// that no webhooks are registered anymore
delete webhookData.webhookId;
delete webhookData.webhookEvents;
delete webhookData.hookSecret;
}
return true;
},
},
};
async webhook(this: IWebhookFunctions): Promise {
const bodyData = this.getBodyData();
const headerData = this.getHeaderData() as IDataObject;
const req = this.getRequestObject();
const webhookData = this.getWorkflowStaticData('node');
if (headerData['x-hook-secret'] !== undefined) {
// Is a create webhook confirmation request
webhookData.hookSecret = headerData['x-hook-secret'];
const res = this.getResponseObject();
res.set('X-Hook-Secret', webhookData.hookSecret as string);
res.status(200).end();
return {
noWebhookResponse: true,
};
}
// Is regular webhook call
// Check if it contains any events
if (
bodyData.events === undefined ||
!Array.isArray(bodyData.events) ||
bodyData.events.length === 0
) {
// Does not contain any event data so nothing to process so no reason to
// start the workflow
return {};
}
// TODO: Had to be deactivated as it is currently not possible to get the secret
// in production mode as the static data overwrites each other because the
// two exist at the same time (create webhook [with webhookId] and receive
// webhook [with secret])
// // Check if the request is valid
// // (if the signature matches to data and hookSecret)
// const computedSignature = createHmac('sha256', webhookData.hookSecret as string).update(JSON.stringify(req.body)).digest('hex');
// if (headerData['x-hook-signature'] !== computedSignature) {
// // Signature is not valid so ignore call
// return {};
// }
return {
workflowData: [this.helpers.returnJsonArray(req.body.events as IDataObject[])],
};
}
}