From f9902e8be416f9228abeb13f1f6913ee4b49354b Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Wed, 29 Jan 2020 20:53:01 -0500 Subject: [PATCH] :sparkles: WooCommerce trigger --- .../credentials/WooCommerceApi.credentials.ts | 30 +++ .../nodes/WooCommerce/GenericFunctions.ts | 49 +++++ .../WooCommerce/WooCommerceTrigger.node.ts | 181 ++++++++++++++++++ .../nodes/WooCommerce/woocommerce.png | Bin 0 -> 5942 bytes packages/nodes-base/package.json | 2 + 5 files changed, 262 insertions(+) create mode 100644 packages/nodes-base/credentials/WooCommerceApi.credentials.ts create mode 100644 packages/nodes-base/nodes/WooCommerce/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/WooCommerce/WooCommerceTrigger.node.ts create mode 100644 packages/nodes-base/nodes/WooCommerce/woocommerce.png diff --git a/packages/nodes-base/credentials/WooCommerceApi.credentials.ts b/packages/nodes-base/credentials/WooCommerceApi.credentials.ts new file mode 100644 index 0000000000..da4df3c509 --- /dev/null +++ b/packages/nodes-base/credentials/WooCommerceApi.credentials.ts @@ -0,0 +1,30 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class WooCommerceApi implements ICredentialType { + name = 'wooCommerceApi'; + displayName = 'WooCommerce API'; + properties = [ + { + displayName: 'Consumer Key', + name: 'consumerKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Consumer Secret', + name: 'consumerSecret', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'WooCommerce URL', + name: 'url', + type: 'string' as NodePropertyTypes, + default: '', + placeholder: 'https://example.com', + }, + ]; +} diff --git a/packages/nodes-base/nodes/WooCommerce/GenericFunctions.ts b/packages/nodes-base/nodes/WooCommerce/GenericFunctions.ts new file mode 100644 index 0000000000..5f390fe4f5 --- /dev/null +++ b/packages/nodes-base/nodes/WooCommerce/GenericFunctions.ts @@ -0,0 +1,49 @@ +import { OptionsWithUri } from 'request'; +import { createHash } from 'crypto'; +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IWebhookFunctions, +} from 'n8n-core'; +import { IDataObject, ICredentialDataDecryptedObject } from 'n8n-workflow'; + +export async function woocommerceApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('wooCommerceApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + const base64credentials = Buffer.from(`${credentials.consumerKey}:${credentials.consumerSecret}`).toString('base64'); + let options: OptionsWithUri = { + headers: { + Authorization: `Basic ${base64credentials}`, + }, + method, + qs, + body, + uri: uri ||`${credentials.url}/wp-json/wc/v3${resource}`, + json: true + }; + if (!Object.keys(body).length) { + delete options.form; + } + options = Object.assign({}, options, option); + try { + return await this.helpers.request!(options); + } catch (error) { + throw new Error('WooCommerce Error: ' + error); + } +} + +/** + * Creates a secret from the credentials + * + * @export + * @param {ICredentialDataDecryptedObject} credentials + * @returns + */ +export function getAutomaticSecret(credentials: ICredentialDataDecryptedObject) { + const data = `${credentials.consumerKey},${credentials.consumerSecret}`; + return createHash('md5').update(data).digest("hex"); +} diff --git a/packages/nodes-base/nodes/WooCommerce/WooCommerceTrigger.node.ts b/packages/nodes-base/nodes/WooCommerce/WooCommerceTrigger.node.ts new file mode 100644 index 0000000000..c4313d36a0 --- /dev/null +++ b/packages/nodes-base/nodes/WooCommerce/WooCommerceTrigger.node.ts @@ -0,0 +1,181 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeTypeDescription, + INodeType, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + woocommerceApiRequest, + getAutomaticSecret, +} from './GenericFunctions'; + +import { createHmac } from 'crypto'; + +export class WooCommerceTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'WooCommerce Trigger', + name: 'wooCommerceTrigger', + icon: 'file:woocommerce.png', + group: ['trigger'], + version: 1, + description: 'Handle WooCommerce events via webhooks', + defaults: { + name: 'WooCommerce Trigger', + color: '#96588a', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'wooCommerceApi', + required: true, + } + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Event', + name: 'event', + type: 'options', + required: true, + default: '', + options: [ + { + name: 'coupon.created', + value: 'coupon.created', + }, + { + name: 'coupon.updated', + value: 'coupon.updated', + }, + { + name: 'coupon.deleted', + value: 'coupon.deleted', + }, + { + name: 'customer.created', + value: 'customer.created', + }, + { + name: 'customer.updated', + value: 'customer.updated', + }, + { + name: 'customer.deleted', + value: 'customer.deleted', + }, + { + name: 'order.created', + value: 'order.created', + }, + { + name: 'order.updated', + value: 'order.updated', + }, + { + name: 'order.deleted', + value: 'order.deleted', + }, + { + name: 'product.created', + value: 'product.created', + }, + { + name: 'product.updated', + value: 'product.updated', + }, + { + name: 'product.deleted', + value: 'product.deleted', + }, + ], + description: 'Determines which resource events the webhook is triggered for.', + }, + ], + + }; + + // @ts-ignore + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId === undefined) { + return false; + } + const endpoint = `/webhooks/${webhookData.webhookId}`; + try { + await woocommerceApiRequest.call(this, 'GET', endpoint); + } catch (e) { + return false; + } + return true; + }, + async create(this: IHookFunctions): Promise { + const credentials = this.getCredentials('wooCommerceApi'); + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const event = this.getNodeParameter('event') as string; + const secret = getAutomaticSecret(credentials!); + const endpoint = '/webhooks'; + const body: IDataObject = { + delivery_url: webhookUrl, + topic: event, + secret, + }; + const { id } = await woocommerceApiRequest.call(this, 'POST', endpoint, body); + webhookData.webhookId = id; + webhookData.secret = secret; + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const endpoint = `/webhooks/${webhookData.webhookId}`; + try { + await woocommerceApiRequest.call(this, 'DELETE', endpoint, {}, { force: true }); + } catch(error) { + return false; + } + delete webhookData.webhookId; + delete webhookData.secret; + return true; + }, + }, + }; + + //@ts-ignore + async webhook(this: IWebhookFunctions): Promise { + const req = this.getRequestObject(); + const headerData = this.getHeaderData(); + const webhookData = this.getWorkflowStaticData('node'); + //@ts-ignore + if (headerData['x-wc-webhook-id'] === undefined) { + return {}; + } + //@ts-ignore + const computedSignature = createHmac('sha256',webhookData.secret as string).update(req.rawBody).digest('base64'); + //@ts-ignore + if (headerData['x-wc-webhook-signature'] !== computedSignature) { + // Signature is not valid so ignore call + return {}; + } + return { + workflowData: [ + this.helpers.returnJsonArray(req.body), + ], + } + } +} diff --git a/packages/nodes-base/nodes/WooCommerce/woocommerce.png b/packages/nodes-base/nodes/WooCommerce/woocommerce.png new file mode 100644 index 0000000000000000000000000000000000000000..3495adfe3bbf1ddd29ef44a7e9e80c5533aaee94 GIT binary patch literal 5942 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 JD_@!h{vTs%^Zx(< literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index d0ff3791e6..b9e1e83374 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -89,6 +89,7 @@ "dist/credentials/VeroApi.credentials.js", "dist/credentials/WebflowApi.credentials.js", "dist/credentials/WordpressApi.credentials.js", + "dist/credentials/WooCommerceApi.credentials.js", "dist/credentials/ZendeskApi.credentials.js" ], "nodes": [ @@ -196,6 +197,7 @@ "dist/nodes/Webflow/WebflowTrigger.node.js", "dist/nodes/Webhook.node.js", "dist/nodes/Wordpress/Wordpress.node.js", + "dist/nodes/WooCommerce/WooCommerceTrigger.node.js", "dist/nodes/WriteBinaryFile.node.js", "dist/nodes/Xml.node.js", "dist/nodes/Zendesk/Zendesk.node.js",