diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index fdc9e94e4e..4fe1ef70d7 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -913,7 +913,7 @@ class App { // Verify and store app code. Generate access tokens and store for respective credential. this.app.get('/rest/oauth2-credential/callback', async (req: express.Request, res: express.Response) => { - const {code, state: stateEncoded} = req.query; + const {code, state: stateEncoded } = req.query; if (code === undefined || stateEncoded === undefined) { throw new Error('Insufficient parameters for OAuth2 callback'); @@ -953,6 +953,17 @@ class App { return ResponseHelper.sendErrorResponse(res, errorResponse); } + let options = {}; + + if (_.get(oauthCredentials, 'authentication', 'header') as string === 'body') { + options = { + body: { + client_id: _.get(oauthCredentials, 'clientId') as string, + client_secret: _.get(oauthCredentials, 'clientSecret', '') as string, + }, + }; + } + const oAuthObj = new clientOAuth2({ clientId: _.get(oauthCredentials, 'clientId') as string, clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string, @@ -962,7 +973,7 @@ class App { scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ',') }); - const oauthToken = await oAuthObj.code.getToken(req.originalUrl); + const oauthToken = await oAuthObj.code.getToken(req.originalUrl, options); if (oauthToken === undefined) { const errorResponse = new ResponseHelper.ResponseError('Unable to get access tokens!', undefined, 404); diff --git a/packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts index 9466ce447b..afca85f57b 100644 --- a/packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts @@ -37,5 +37,11 @@ export class GithubOAuth2Api implements ICredentialType { type: 'hidden' as NodePropertyTypes, default: '', }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'header', + }, ]; } diff --git a/packages/nodes-base/credentials/OAuth2Api.credentials.ts b/packages/nodes-base/credentials/OAuth2Api.credentials.ts index efa5127e6e..ce2a248b62 100644 --- a/packages/nodes-base/credentials/OAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/OAuth2Api.credentials.ts @@ -53,5 +53,24 @@ export class OAuth2Api implements ICredentialType { description: 'For some services additional query parameters have to be set which can be defined here.', placeholder: 'access_type=offline', }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'options' as NodePropertyTypes, + options: [ + { + name: 'Body', + value: 'body', + description: 'Send credentials in body', + }, + { + name: 'Header', + value: 'header', + description: 'Send credentials as Basic Auth header', + }, + ], + default: 'header', + description: 'Resource to consume.', + }, ]; } diff --git a/packages/nodes-base/credentials/ZohoOAuth2Api.credentials.ts b/packages/nodes-base/credentials/ZohoOAuth2Api.credentials.ts new file mode 100644 index 0000000000..801b164ffd --- /dev/null +++ b/packages/nodes-base/credentials/ZohoOAuth2Api.credentials.ts @@ -0,0 +1,80 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class ZohoOAuth2Api implements ICredentialType { + name = 'zohoOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Zoho OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'options' as NodePropertyTypes, + options: [ + { + name: 'https://accounts.zoho.com/oauth/v2/auth', + value: 'https://accounts.zoho.com/oauth/v2/auth', + description: 'For the EU, AU, and IN domains', + }, + { + name: 'https://accounts.zoho.com.cn/oauth/v2/auth', + value: 'https://accounts.zoho.com.cn/oauth/v2/auth', + description: 'For the CN domain', + }, + ], + default: 'https://accounts.zoho.com/oauth/v2/auth', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'options' as NodePropertyTypes, + options: [ + { + name: 'US - https://accounts.zoho.com/oauth/v2/token', + value: 'https://accounts.zoho.com/oauth/v2/token', + }, + { + name: 'AU - https://accounts.zoho.com.au/oauth/v2/token', + value: 'https://accounts.zoho.com.au/oauth/v2/token', + }, + { + name: 'EU - https://accounts.zoho.eu/oauth/v2/token', + value: 'https://accounts.zoho.eu/oauth/v2/token', + }, + { + name: 'IN - https://accounts.zoho.in/oauth/v2/token', + value: 'https://accounts.zoho.in/oauth/v2/token', + }, + { + name: 'CN - https://accounts.zoho.com.cn/oauth/v2/token', + value: ' https://accounts.zoho.com.cn/oauth/v2/token', + }, + ], + default: 'https://accounts.zoho.com/oauth/v2/token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: 'ZohoCRM.modules.ALL', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: 'access_type=offline', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Zoho/GenericFunctions.ts b/packages/nodes-base/nodes/Zoho/GenericFunctions.ts new file mode 100644 index 0000000000..04fe204858 --- /dev/null +++ b/packages/nodes-base/nodes/Zoho/GenericFunctions.ts @@ -0,0 +1,55 @@ +import { OptionsWithUri } from 'request'; +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; +import { + IDataObject +} from 'n8n-workflow'; + +export async function zohoApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body: { + data: [ + body, + ], + }, + qs, + uri: uri || `https://www.zohoapis.com/crm/v2${resource}`, + json: true + }; + try { + //@ts-ignore + return await this.helpers.requestOAuth.call(this, 'zohoOAuth2Api', options); + } catch (error) { + if (error.response && error.response.body && error.response.body.message) { + // Try to return the error prettier + throw new Error(`Zoho error response [${error.statusCode}]: ${error.response.body.message}`); + } + throw error; + } +} + +export async function zohoApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + let uri: string | undefined; + + do { + responseData = await zohoApiRequest.call(this, method, endpoint, body, query, uri); + uri = responseData.nextRecordsUrl; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.nextRecordsUrl !== undefined && + responseData.nextRecordsUrl !== null + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Zoho/LeadDescription.ts b/packages/nodes-base/nodes/Zoho/LeadDescription.ts new file mode 100644 index 0000000000..198abbe472 --- /dev/null +++ b/packages/nodes-base/nodes/Zoho/LeadDescription.ts @@ -0,0 +1,171 @@ +import { INodeProperties } from "n8n-workflow"; + +export const leadOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'lead', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new lead', + }, + { + name: 'Get', + value: 'get', + description: 'Get data of a lead', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get data of all leads', + }, + { + name: 'Update', + value: 'update', + description: 'Update new lead', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a lead', + } + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const leadFields = [ + +/* -------------------------------------------------------------------------- */ +/* lead:create */ +/* -------------------------------------------------------------------------- */ + + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'lead', + ], + operation: [ + 'create', + ], + }, + }, + description: `User's last name`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'lead', + ], + }, + }, + options: [ + { + displayName: 'Avatar', + name: 'avatar', + type: 'string', + default: '', + description: 'An avatar image URL. note: the image url needs to be https.', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the user', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'The phone number of the user', + }, + { + displayName: 'Unsubscribed From Emails', + name: 'unsubscribedFromEmails', + type: 'boolean', + default: false, + description: 'Whether the Lead is unsubscribed from emails', + }, + { + displayName: 'Update Last Request At', + name: 'updateLastRequestAt', + type: 'boolean', + default: false, + description: 'A boolean value, which if true, instructs Intercom to update the
users last_request_at value to the current API service time in
UTC. default value if not sent is false.', + }, + { + displayName: 'Companies', + name: 'companies', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getCompanies', + }, + default: [], + description: 'Identifies the companies this user belongs to.', + }, + { + displayName: 'UTM Source', + name: 'utmSource', + type: 'string', + default: '', + description: 'An avatar image URL. note: the image url needs to be https.', + }, + { + displayName: 'UTM Medium', + name: 'utmMedium', + type: 'string', + default: '', + description: 'Identifies what type of link was used', + }, + { + displayName: 'UTM Campaign', + name: 'utmCampaign', + type: 'string', + default: '', + description: 'Identifies a specific product promotion or strategic campaign', + }, + { + displayName: 'UTM Term', + name: 'utmTerm', + type: 'string', + default: '', + description: 'Identifies search terms', + }, + { + displayName: 'UTM Content', + name: 'utmContent', + type: 'string', + default: '', + description: 'Identifies what specifically was clicked to bring the user to the site', + }, + ] + }, + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts b/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts new file mode 100644 index 0000000000..f1d397c2dc --- /dev/null +++ b/packages/nodes-base/nodes/Zoho/ZohoCrm.node.ts @@ -0,0 +1,98 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeTypeDescription, + INodeType, +} from 'n8n-workflow'; + +import { + zohoApiRequest, + zohoApiRequestAllItems, +} from './GenericFunctions'; + +import { + leadOperations, + leadFields, +} from './LeadDescription'; + +export class ZohoCrm implements INodeType { + description: INodeTypeDescription = { + displayName: 'Zoho CRM', + name: 'zohoCrm', + icon: 'file:zohoCrm.png', + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + group: ['input'], + version: 1, + description: 'Consume Zoho CRM API.', + defaults: { + name: 'Zoho CRM', + color: '#CE2232', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'zohoOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Lead', + value: 'lead', + }, + ], + default: 'lead', + description: 'The resource to operate on.', + }, + ...leadOperations, + ...leadFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + let responseData; + for (let i = 0; i < length; i++) { + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + if (resource === 'lead') { + if (operation === 'create') { + const lastName = this.getNodeParameter('lastName', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body = { + Last_Name: lastName, + }; + // if (additionalFields.email) { + // // @ts-ignore + // body.email = additionalFields.email as string; + // } + responseData = await zohoApiRequest.call(this, 'POST', '/leads', body); + responseData = responseData.data; + } else { + throw new Error(`The operation "${operation}" is not known!`); + } + } else { + throw new Error(`The resource "${resource}" is not known!`); + } + + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Zoho/zohoCrm.png b/packages/nodes-base/nodes/Zoho/zohoCrm.png new file mode 100644 index 0000000000..8d541a4514 Binary files /dev/null and b/packages/nodes-base/nodes/Zoho/zohoCrm.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index ec149d9910..746f91f723 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -80,7 +80,8 @@ "dist/credentials/TypeformApi.credentials.js", "dist/credentials/TogglApi.credentials.js", "dist/credentials/VeroApi.credentials.js", - "dist/credentials/WordpressApi.credentials.js" + "dist/credentials/WordpressApi.credentials.js", + "dist/credentials/ZohoOAuth2Api.credentials.js" ], "nodes": [ "dist/nodes/ActiveCampaign/ActiveCampaign.node.js", @@ -174,7 +175,8 @@ "dist/nodes/WriteBinaryFile.node.js", "dist/nodes/Webhook.node.js", "dist/nodes/Wordpress/Wordpress.node.js", - "dist/nodes/Xml.node.js" + "dist/nodes/Xml.node.js", + "dist/nodes/Zoho/ZohoCrm.node.js" ] }, "devDependencies": {