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 0000000000..3495adfe3b Binary files /dev/null and b/packages/nodes-base/nodes/WooCommerce/woocommerce.png differ 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",