From 19e96103c8b180c48fd445d6c51ca752d6b3c92a Mon Sep 17 00:00:00 2001 From: Michael Kret Date: Tue, 22 Mar 2022 14:39:26 +0200 Subject: [PATCH 1/2] :hammer: fix for deprecated targets --- .../nodes/Zendesk/GenericFunctions.ts | 18 ++++-- .../nodes/Zendesk/ZendeskTrigger.node.ts | 58 +++++++++++-------- 2 files changed, 48 insertions(+), 28 deletions(-) diff --git a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts index 379a99fe281f5..2ee266527b0b0 100644 --- a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts @@ -10,7 +10,7 @@ import { } from 'n8n-core'; import { - IDataObject, NodeApiError, NodeOperationError, + IDataObject, JsonObject, NodeApiError, NodeOperationError, } from 'n8n-workflow'; export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any @@ -42,7 +42,13 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions } const base64Key = Buffer.from(`${credentials.email}/token:${credentials.apiToken}`).toString('base64'); - options.uri = uri || `https://${credentials.subdomain}.zendesk.com/api/v2${resource}.json`; + + if (resource.includes('webhooks')) { + options.uri = uri || `https://${credentials.subdomain}.zendesk.com/api/v2${resource}`; + } else { + options.uri = uri || `https://${credentials.subdomain}.zendesk.com/api/v2${resource}.json`; + } + options.headers!['Authorization'] = `Basic ${base64Key}`; return await this.helpers.request!(options); } else { @@ -52,12 +58,16 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions throw new NodeOperationError(this.getNode(), 'No credentials got returned!'); } - options.uri = uri || `https://${credentials.subdomain}.zendesk.com/api/v2${resource}.json`; + if (resource.includes('webhooks')) { + options.uri = uri || `https://${credentials.subdomain}.zendesk.com/api/v2${resource}`; + } else { + options.uri = uri || `https://${credentials.subdomain}.zendesk.com/api/v2${resource}.json`; + } return await this.helpers.requestOAuth2!.call(this, 'zendeskOAuth2Api', options); } } catch(error) { - throw new NodeApiError(this.getNode(), error); + throw new NodeApiError(this.getNode(), error as JsonObject); } } diff --git a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts index 14c5734072655..62d1619403b39 100644 --- a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts +++ b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts @@ -246,11 +246,11 @@ export class ZendeskTrigger implements INodeType { const webhookUrl = this.getNodeWebhookUrl('default') as string; const webhookData = this.getWorkflowStaticData('node'); const conditions = this.getNodeParameter('conditions') as IDataObject; - const conditionsAll = conditions.all as [IDataObject]; let endpoint = ''; const resultAll = [], resultAny = []; + const conditionsAll = conditions.all as [IDataObject]; if (conditionsAll) { for (const conditionAll of conditionsAll) { const aux: IDataObject = {}; @@ -282,12 +282,12 @@ export class ZendeskTrigger implements INodeType { } } - // check if there is a target already created - endpoint = `/targets`; - const targets = await zendeskApiRequestAllItems.call(this, 'targets', 'GET', endpoint); - for (const target of targets) { - if (target.target_url === webhookUrl) { - webhookData.targetId = target.id.toString(); + // get all webhooks + // https://developer.zendesk.com/api-reference/event-connectors/webhooks/webhooks/#list-webhooks + const { webhooks } = await zendeskApiRequest.call(this, 'GET', '/webhooks'); + for (const webhook of webhooks) { + if (webhook.endpoint === webhookUrl) { + webhookData.targetId = webhook.id; break; } } @@ -299,6 +299,7 @@ export class ZendeskTrigger implements INodeType { endpoint = `/triggers/active`; const triggers = await zendeskApiRequestAllItems.call(this, 'triggers', 'GET', endpoint); + for (const trigger of triggers) { const toDeleteTriggers = []; // this trigger belong to the current target @@ -317,23 +318,26 @@ export class ZendeskTrigger implements INodeType { const webhookUrl = this.getNodeWebhookUrl('default') as string; const webhookData = this.getWorkflowStaticData('node'); const service = this.getNodeParameter('service') as string; + if (service === 'support') { const message: IDataObject = {}; const resultAll = [], resultAny = []; const conditions = this.getNodeParameter('conditions') as IDataObject; const options = this.getNodeParameter('options') as IDataObject; + if (Object.keys(conditions).length === 0) { throw new NodeOperationError(this.getNode(), 'You must have at least one condition'); } + if (options.fields) { - // @ts-ignore - for (const field of options.fields) { + for (const field of options.fields as string[]) { // @ts-ignore message[field] = `{{${field}}}`; } } else { message['ticket.id'] = '{{ticket.id}}'; } + const conditionsAll = conditions.all as [IDataObject]; if (conditionsAll) { for (const conditionAll of conditionsAll) { @@ -349,6 +353,7 @@ export class ZendeskTrigger implements INodeType { resultAll.push(aux); } } + const conditionsAny = conditions.any as [IDataObject]; if (conditionsAny) { for (const conditionAny of conditionsAny) { @@ -364,30 +369,35 @@ export class ZendeskTrigger implements INodeType { resultAny.push(aux); } } - const urlParts = urlParse(webhookUrl); + + const urlParts = new URL(webhookUrl); + const bodyTrigger: IDataObject = { trigger: { - title: `n8n-webhook:${urlParts.path}`, + title: `n8n-webhook:${urlParts.pathname}`, conditions: { all: resultAll, any: resultAny, }, actions: [ { - field: 'notification_target', + field: 'notification_webhook', value: [], }, ], }, }; + const bodyTarget: IDataObject = { - target: { - title: 'n8n webhook', - type: 'http_target', - target_url: webhookUrl, - method: 'POST', - active: true, - content_type: 'application/json', + webhook: { + name:'n8n webhook', + endpoint: webhookUrl, + http_method:'POST', + status:'active', + request_format:'json', + subscriptions: [ + 'conditional_ticket_events', + ], }, }; let target: IDataObject = {}; @@ -397,14 +407,14 @@ export class ZendeskTrigger implements INodeType { if (webhookData.targetId !== undefined) { target.id = webhookData.targetId; } else { - target = await zendeskApiRequest.call(this, 'POST', '/targets', bodyTarget); - target = target.target as IDataObject; + // create a webhook + // https://developer.zendesk.com/api-reference/event-connectors/webhooks/webhooks/#create-or-clone-webhook + target = (await zendeskApiRequest.call(this, 'POST', '/webhooks', bodyTarget)).webhook as IDataObject; } // @ts-ignore bodyTrigger.trigger.actions[0].value = [target.id, JSON.stringify(message)]; - //@ts-ignore const { trigger } = await zendeskApiRequest.call(this, 'POST', '/triggers', bodyTrigger); webhookData.webhookId = trigger.id; webhookData.targetId = target.id; @@ -415,11 +425,11 @@ export class ZendeskTrigger implements INodeType { const webhookData = this.getWorkflowStaticData('node'); try { await zendeskApiRequest.call(this, 'DELETE', `/triggers/${webhookData.webhookId}`); - await zendeskApiRequest.call(this, 'DELETE', `/targets/${webhookData.targetId}`); + await zendeskApiRequest.call(this, 'DELETE', `/webhooks/${webhookData.targetId}`); } catch(error) { return false; } - delete webhookData.webhookId; + delete webhookData.triggerId; delete webhookData.targetId; return true; }, From 6adebdab849184e5f3387b03ae27b3155bc9c79d Mon Sep 17 00:00:00 2001 From: ricardo Date: Mon, 18 Apr 2022 10:29:59 -0400 Subject: [PATCH 2/2] :zap: Move crendentials injection to the credential file --- .../credentials/ZendeskApi.credentials.ts | 16 ++++++ .../nodes/Zendesk/GenericFunctions.ts | 54 ++++++++----------- .../nodes-base/nodes/Zendesk/Zendesk.node.ts | 37 ------------- 3 files changed, 39 insertions(+), 68 deletions(-) diff --git a/packages/nodes-base/credentials/ZendeskApi.credentials.ts b/packages/nodes-base/credentials/ZendeskApi.credentials.ts index 879b59c63d500..f4c2898669a6f 100644 --- a/packages/nodes-base/credentials/ZendeskApi.credentials.ts +++ b/packages/nodes-base/credentials/ZendeskApi.credentials.ts @@ -1,5 +1,8 @@ import { + ICredentialDataDecryptedObject, + ICredentialTestRequest, ICredentialType, + IHttpRequestOptions, INodeProperties, } from 'n8n-workflow'; @@ -29,4 +32,17 @@ export class ZendeskApi implements ICredentialType { default: '', }, ]; + async authenticate(credentials: ICredentialDataDecryptedObject, requestOptions: IHttpRequestOptions): Promise { + requestOptions.auth = { + username: `${credentials.email}/token`, + password: credentials.apiToken as string, + }; + return requestOptions; + } + test: ICredentialTestRequest = { + request: { + baseURL: '=https://{{$credentials.subdomain}}.zendesk.com/api/v2', + url: '/ticket_fields.json', + }, + }; } diff --git a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts index 648578e244533..0b16128529735 100644 --- a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts @@ -10,19 +10,27 @@ import { } from 'n8n-core'; import { - IDataObject, JsonObject, NodeApiError, NodeOperationError, + IDataObject, + JsonObject, + NodeApiError, } from 'n8n-workflow'; export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any const authenticationMethod = this.getNodeParameter('authentication', 0); + let credentials; + + if (authenticationMethod === 'apiToken') { + credentials = await this.getCredentials('zendeskApi') as { subdomain: string }; + } else { + credentials = await this.getCredentials('zendeskOAuth2Api') as { subdomain: string }; + } + let options: OptionsWithUri = { - headers: {}, method, qs, body, - //@ts-ignore - uri, + uri: uri || getUri(resource, credentials.subdomain), json: true, qsStringifyOptions: { arrayFormat: 'brackets', @@ -33,35 +41,11 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions if (Object.keys(options.body).length === 0) { delete options.body; } - try { - if (authenticationMethod === 'apiToken') { - const credentials = await this.getCredentials('zendeskApi'); - - const base64Key = Buffer.from(`${credentials.email}/token:${credentials.apiToken}`).toString('base64'); - - if (resource.includes('webhooks')) { - options.uri = uri || `https://${credentials.subdomain}.zendesk.com/api/v2${resource}`; - } else { - options.uri = uri || `https://${credentials.subdomain}.zendesk.com/api/v2${resource}.json`; - } - - options.headers!['Authorization'] = `Basic ${base64Key}`; - return await this.helpers.request!(options); - } else { - const credentials = await this.getCredentials('zendeskOAuth2Api'); - if (credentials === undefined) { - throw new NodeOperationError(this.getNode(), 'No credentials got returned!'); - } + const credentialType = authenticationMethod === 'apiToken' ? 'zendeskApi' : 'zendeskOAuth2Api'; - if (resource.includes('webhooks')) { - options.uri = uri || `https://${credentials.subdomain}.zendesk.com/api/v2${resource}`; - } else { - options.uri = uri || `https://${credentials.subdomain}.zendesk.com/api/v2${resource}.json`; - } - - return await this.helpers.requestOAuth2!.call(this, 'zendeskOAuth2Api', options); - } + try { + return await this.helpers.requestWithAuthentication.call(this, credentialType, options); } catch(error) { throw new NodeApiError(this.getNode(), error as JsonObject); } @@ -103,3 +87,11 @@ export function validateJSON(json: string | undefined): any { // tslint:disable- } return result; } + +function getUri(resource: string, subdomain: string) { + if (resource.includes('webhooks')) { + return `https://${subdomain}.zendesk.com/api/v2${resource}`; + } else { + return `https://${subdomain}.zendesk.com/api/v2${resource}.json`; + } +} diff --git a/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts index 1761debb93235..8e8c3b7a88e5c 100644 --- a/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts +++ b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts @@ -76,7 +76,6 @@ export class Zendesk implements INodeType { ], }, }, - testedBy: 'zendeskSoftwareApiTest', }, { name: 'zendeskOAuth2Api', @@ -153,42 +152,6 @@ export class Zendesk implements INodeType { }; methods = { - credentialTest: { - async zendeskSoftwareApiTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { - const credentials = credential.data; - const subdomain = credentials!.subdomain; - const email = credentials!.email; - const apiToken = credentials!.apiToken; - - const base64Key = Buffer.from(`${email}/token:${apiToken}`).toString('base64'); - const options: OptionsWithUri = { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Basic ${base64Key}`, - }, - method: 'GET', - uri: `https://${subdomain}.zendesk.com/api/v2/ticket_fields.json`, - qs: { - recent: 0, - }, - json: true, - timeout: 5000, - }; - - try { - await this.helpers.request!(options); - } catch (error) { - return { - status: 'Error', - message: `Connection details not valid: ${error.message}`, - }; - } - return { - status: 'OK', - message: 'Authentication successful!', - }; - }, - }, loadOptions: { // Get all the custom fields to display them to user so that he can // select them easily