diff --git a/packages/nodes-base/credentials/CloudflareApi.credentials.ts b/packages/nodes-base/credentials/CloudflareApi.credentials.ts new file mode 100644 index 0000000000..a034af4cbd --- /dev/null +++ b/packages/nodes-base/credentials/CloudflareApi.credentials.ts @@ -0,0 +1,35 @@ +import { + IAuthenticateGeneric, + ICredentialTestRequest, + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +export class CloudflareApi implements ICredentialType { + name = 'cloudflareApi'; + displayName = 'Cloudflare API'; + documentationUrl = 'cloudflare'; + properties: INodeProperties[] = [ + { + displayName: 'API Token', + name: 'apiToken', + type: 'string', + default: '', + }, + ]; + + authenticate: IAuthenticateGeneric = { + type: 'generic', + properties: { + headers: { + 'Authorization': '=Bearer {{$credentials.apiToken}}', + }, + }, + }; + + test: ICredentialTestRequest = { + request: { + baseURL: 'https://api.cloudflare.com/client/v4/user/tokens/verify', + }, + }; +} diff --git a/packages/nodes-base/nodes/Cloudflare/Cloudflare.node.json b/packages/nodes-base/nodes/Cloudflare/Cloudflare.node.json new file mode 100644 index 0000000000..d2077e5520 --- /dev/null +++ b/packages/nodes-base/nodes/Cloudflare/Cloudflare.node.json @@ -0,0 +1,18 @@ +{ + "node": "n8n-nodes-base.cloudflare", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Development"], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/cloudflare" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.cloudflare/" + } + ] + } +} diff --git a/packages/nodes-base/nodes/Cloudflare/Cloudflare.node.ts b/packages/nodes-base/nodes/Cloudflare/Cloudflare.node.ts new file mode 100644 index 0000000000..356159e851 --- /dev/null +++ b/packages/nodes-base/nodes/Cloudflare/Cloudflare.node.ts @@ -0,0 +1,179 @@ +import { IExecuteFunctions } from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { cloudflareApiRequest, cloudflareApiRequestAllItems } from './GenericFunctions'; + +import { zoneCertificateFields, zoneCertificateOperations } from './ZoneCertificateDescription'; + +export class Cloudflare implements INodeType { + description: INodeTypeDescription = { + displayName: 'Cloudflare', + name: 'cloudflare', + icon: 'file:cloudflare.svg', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Cloudflare API', + defaults: { + name: 'Cloudflare', + color: '#000000', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'cloudflareApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Zone Certificate', + value: 'zoneCertificate', + }, + ], + default: 'zoneCertificate', + }, + ...zoneCertificateOperations, + ...zoneCertificateFields, + ], + }; + + methods = { + loadOptions: { + async getZones(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const { result: zones } = await cloudflareApiRequest.call(this, 'GET', '/zones'); + for (const zone of zones) { + returnData.push({ + name: zone.name, + value: zone.id, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + for (let i = 0; i < length; i++) { + try { + if (resource === 'zoneCertificate') { + //https://api.cloudflare.com/#zone-level-authenticated-origin-pulls-delete-certificate + if (operation === 'delete') { + const zoneId = this.getNodeParameter('zoneId', i) as string; + const certificateId = this.getNodeParameter('certificateId', i) as string; + + responseData = await cloudflareApiRequest.call( + this, + 'DELETE', + `/zones/${zoneId}/origin_tls_client_auth/${certificateId}`, + {}, + ); + responseData = responseData.result; + } + //https://api.cloudflare.com/#zone-level-authenticated-origin-pulls-get-certificate-details + if (operation === 'get') { + const zoneId = this.getNodeParameter('zoneId', i) as string; + const certificateId = this.getNodeParameter('certificateId', i) as string; + + responseData = await cloudflareApiRequest.call( + this, + 'GET', + `/zones/${zoneId}/origin_tls_client_auth/${certificateId}`, + {}, + ); + responseData = responseData.result; + } + //https://api.cloudflare.com/#zone-level-authenticated-origin-pulls-list-certificates + if (operation === 'getMany') { + const zoneId = this.getNodeParameter('zoneId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i, {}) as IDataObject; + + Object.assign(qs, filters); + + if (returnAll) { + responseData = await cloudflareApiRequestAllItems.call( + this, + 'result', + 'GET', + `/zones/${zoneId}/origin_tls_client_auth`, + {}, + qs, + ); + } else { + const limit = this.getNodeParameter('limit', i) as number; + Object.assign(qs, { per_page: limit }); + responseData = await cloudflareApiRequest.call( + this, + 'GET', + `/zones/${zoneId}/origin_tls_client_auth`, + {}, + qs, + ); + responseData = responseData.result; + } + } + //https://api.cloudflare.com/#zone-level-authenticated-origin-pulls-upload-certificate + if (operation === 'upload') { + const zoneId = this.getNodeParameter('zoneId', i) as string; + const certificate = this.getNodeParameter('certificate', i) as string; + const privateKey = this.getNodeParameter('privateKey', i) as string; + + const body: IDataObject = { + certificate, + private_key: privateKey, + }; + + responseData = await cloudflareApiRequest.call( + this, + 'POST', + `/zones/${zoneId}/origin_tls_client_auth`, + body, + qs, + ); + + responseData = responseData.result; + } + } + + returnData.push( + ...this.helpers.constructExecutionMetaData(this.helpers.returnJsonArray(responseData), { + itemData: { item: i }, + }), + ); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ json: { error: error.message } }); + continue; + } + throw error; + } + } + + return [returnData as INodeExecutionData[]]; + } +} diff --git a/packages/nodes-base/nodes/Cloudflare/GenericFunctions.ts b/packages/nodes-base/nodes/Cloudflare/GenericFunctions.ts new file mode 100644 index 0000000000..1b4d7516a2 --- /dev/null +++ b/packages/nodes-base/nodes/Cloudflare/GenericFunctions.ts @@ -0,0 +1,65 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, + IPollFunctions, +} from 'n8n-core'; + +import { + IDataObject, NodeApiError, +} from 'n8n-workflow'; + +export async function cloudflareApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const options: OptionsWithUri = { + method, + body, + qs, + uri: `https://api.cloudflare.com/client/v4${resource}`, + json: true, + }; + + try { + if (Object.keys(headers).length !== 0) { + options.headers = Object.assign({}, options.headers, headers); + } + if (Object.keys(body).length === 0) { + delete options.body; + } + return await this.helpers.requestWithAuthentication.call(this, 'cloudflareApi', options); + } catch (error) { + throw new NodeApiError(this.getNode(), error); + } +} + +export async function cloudflareApiRequestAllItems( + this: IExecuteFunctions | ILoadOptionsFunctions, + propertyName: string, + method: string, + endpoint: string, + body: IDataObject = {}, + query: IDataObject = {}, + // tslint:disable-next-line:no-any +): Promise { + const returnData: IDataObject[] = []; + + let responseData; + query.page = 1; + + do { + responseData = await cloudflareApiRequest.call( + this, + method, + endpoint, + body, + query, + ); + query.page++; + returnData.push.apply(returnData, responseData[propertyName]); + } while (responseData.result_info.total_pages !== responseData.result_info.page); + return returnData; +} diff --git a/packages/nodes-base/nodes/Cloudflare/ZoneCertificateDescription.ts b/packages/nodes-base/nodes/Cloudflare/ZoneCertificateDescription.ts new file mode 100644 index 0000000000..32fd4e3def --- /dev/null +++ b/packages/nodes-base/nodes/Cloudflare/ZoneCertificateDescription.ts @@ -0,0 +1,220 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const zoneCertificateOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: [ + 'zoneCertificate', + ], + }, + }, + options: [ + { + name: 'Delete', + value: 'delete', + description: 'Delete a certificate', + action: 'Delete a certificate', + }, + { + name: 'Get', + value: 'get', + description: 'Get a certificate', + action: 'Get a certificate', + }, + { + name: 'Get Many', + value: 'getMany', + description: 'Get many certificates', + action: 'Get many certificates', + }, + { + name: 'Upload', + value: 'upload', + description: 'Upload a certificate', + action: 'Upload a certificate', + }, + ], + default: 'upload', + }, +]; + +export const zoneCertificateFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* certificate:upload */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Zone Name or ID', + name: 'zoneId', + type: 'options', + description: 'Choose from the list, or specify an ID using an expression', + typeOptions: { + loadOptionsMethod: 'getZones', + }, + required: true, + displayOptions: { + show: { + resource: [ + 'zoneCertificate', + ], + operation: [ + 'upload', + 'getMany', + 'get', + 'delete', + ], + }, + }, + default: '', + }, + { + displayName: 'Certificate Content', + name: 'certificate', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'zoneCertificate', + ], + operation: [ + 'upload', + ], + }, + }, + default: '', + description: 'The zone\'s leaf certificate', + }, + { + displayName: 'Private Key', + name: 'privateKey', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'zoneCertificate', + ], + operation: [ + 'upload', + ], + }, + }, + default: '', + }, + /* -------------------------------------------------------------------------- */ + /* certificate:getMany */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + description: 'Whether to return all results or only up to a given limit', + default: false, + displayOptions: { + show: { + resource: [ + 'zoneCertificate', + ], + operation: [ + 'getMany', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 25, + typeOptions: { + minValue: 1, + maxValue: 50, + }, + displayOptions: { + show: { + resource: [ + 'zoneCertificate', + ], + operation: [ + 'getMany', + ], + returnAll: [ + false, + ], + }, + }, + description: 'Max number of results to return', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'zoneCertificate', + ], + operation: [ + 'getMany', + ], + }, + }, + options: [ + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'Expired', + value: 'expired', + }, + { + name: 'Deleted', + value: 'deleted', + }, + { + name: 'Pending', + value: 'pending', + }, + ], + default: '', + description: 'Status of the zone\'s custom SSL', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* certificate:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Certificate ID', + name: 'certificateId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'zoneCertificate', + ], + operation: [ + 'get', + 'delete', + ], + }, + }, + default: '', + }, +]; diff --git a/packages/nodes-base/nodes/Cloudflare/cloudflare.svg b/packages/nodes-base/nodes/Cloudflare/cloudflare.svg new file mode 100644 index 0000000000..f50a496518 --- /dev/null +++ b/packages/nodes-base/nodes/Cloudflare/cloudflare.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 3ce56c11c7..e8c8042a5d 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -60,6 +60,7 @@ "dist/credentials/ChargebeeApi.credentials.js", "dist/credentials/CircleCiApi.credentials.js", "dist/credentials/CiscoWebexOAuth2Api.credentials.js", + "dist/credentials/CloudflareApi.credentials.js", "dist/credentials/ClearbitApi.credentials.js", "dist/credentials/ClickUpApi.credentials.js", "dist/credentials/ClickUpOAuth2Api.credentials.js", @@ -384,6 +385,7 @@ "dist/nodes/CircleCi/CircleCi.node.js", "dist/nodes/Cisco/Webex/CiscoWebex.node.js", "dist/nodes/Cisco/Webex/CiscoWebexTrigger.node.js", + "dist/nodes/Cloudflare/Cloudflare.node.js", "dist/nodes/Clearbit/Clearbit.node.js", "dist/nodes/ClickUp/ClickUp.node.js", "dist/nodes/ClickUp/ClickUpTrigger.node.js",