From 3c62f182ab714bc0eb3ffff9d64e0bf87f9fa99e Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Wed, 7 Dec 2022 23:41:46 +0100 Subject: [PATCH] feat(webhook): add webhook application --- .../src/apps/webhook/assets/favicon.svg | 8 +++++ .../backend/src/apps/webhook/auth/index.ts | 5 ++++ .../src/apps/webhook/auth/verify-webhook.ts | 7 +++++ packages/backend/src/apps/webhook/index.d.ts | 0 packages/backend/src/apps/webhook/index.ts | 16 ++++++++++ .../triggers/catch-raw-webhook/index.ts | 20 +++++++++++++ .../src/apps/webhook/triggers/index.ts | 3 ++ .../src/controllers/webhooks/handler.ts | 29 ++++++++++++++----- packages/backend/src/graphql/schema.graphql | 1 + .../backend/src/helpers/global-variable.ts | 1 + packages/backend/src/models/step.ts | 20 +++++++++++-- packages/backend/src/routes/webhooks.ts | 2 ++ packages/types/index.d.ts | 15 ++++------ 13 files changed, 107 insertions(+), 20 deletions(-) create mode 100644 packages/backend/src/apps/webhook/assets/favicon.svg create mode 100644 packages/backend/src/apps/webhook/auth/index.ts create mode 100644 packages/backend/src/apps/webhook/auth/verify-webhook.ts create mode 100644 packages/backend/src/apps/webhook/index.d.ts create mode 100644 packages/backend/src/apps/webhook/index.ts create mode 100644 packages/backend/src/apps/webhook/triggers/catch-raw-webhook/index.ts create mode 100644 packages/backend/src/apps/webhook/triggers/index.ts diff --git a/packages/backend/src/apps/webhook/assets/favicon.svg b/packages/backend/src/apps/webhook/assets/favicon.svg new file mode 100644 index 00000000..140ebd66 --- /dev/null +++ b/packages/backend/src/apps/webhook/assets/favicon.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/webhook/auth/index.ts b/packages/backend/src/apps/webhook/auth/index.ts new file mode 100644 index 00000000..ca3b207e --- /dev/null +++ b/packages/backend/src/apps/webhook/auth/index.ts @@ -0,0 +1,5 @@ +import verifyWebhook from './verify-webhook'; + +export default { + verifyWebhook, +}; diff --git a/packages/backend/src/apps/webhook/auth/verify-webhook.ts b/packages/backend/src/apps/webhook/auth/verify-webhook.ts new file mode 100644 index 00000000..22c2eaa9 --- /dev/null +++ b/packages/backend/src/apps/webhook/auth/verify-webhook.ts @@ -0,0 +1,7 @@ +import { IGlobalVariable } from '@automatisch/types'; + +const verifyWebhook = async ($: IGlobalVariable) => { + return true; +}; + +export default verifyWebhook; diff --git a/packages/backend/src/apps/webhook/index.d.ts b/packages/backend/src/apps/webhook/index.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/backend/src/apps/webhook/index.ts b/packages/backend/src/apps/webhook/index.ts new file mode 100644 index 00000000..fbd480b8 --- /dev/null +++ b/packages/backend/src/apps/webhook/index.ts @@ -0,0 +1,16 @@ +import defineApp from '../../helpers/define-app'; +import auth from './auth'; +import triggers from './triggers'; + +export default defineApp({ + name: 'Webhook', + key: 'webhook', + iconUrl: '{BASE_URL}/apps/webhook/assets/favicon.svg', + authDocUrl: 'https://automatisch.io/docs/apps/webhook/connection', + supportsConnections: false, + baseUrl: '', + apiBaseUrl: '', + primaryColor: '0059F7', + auth, + triggers, +}); diff --git a/packages/backend/src/apps/webhook/triggers/catch-raw-webhook/index.ts b/packages/backend/src/apps/webhook/triggers/catch-raw-webhook/index.ts new file mode 100644 index 00000000..88c52f9f --- /dev/null +++ b/packages/backend/src/apps/webhook/triggers/catch-raw-webhook/index.ts @@ -0,0 +1,20 @@ +import isEmpty from 'lodash/isEmpty'; +import defineTrigger from '../../../../helpers/define-trigger'; + +export default defineTrigger({ + name: 'Catch raw webhook', + key: 'catchRawWebhook', + type: 'webhook', + description: 'Triggers when the webhook receives a request.', + + async testRun($) { + if (!isEmpty($.lastExecutionStep?.dataOut)) { + $.pushTriggerItem({ + raw: $.lastExecutionStep.dataOut, + meta: { + internalId: '', + } + }); + } + }, +}); diff --git a/packages/backend/src/apps/webhook/triggers/index.ts b/packages/backend/src/apps/webhook/triggers/index.ts new file mode 100644 index 00000000..49159004 --- /dev/null +++ b/packages/backend/src/apps/webhook/triggers/index.ts @@ -0,0 +1,3 @@ +import catchRawWebhook from './catch-raw-webhook'; + +export default [catchRawWebhook]; diff --git a/packages/backend/src/controllers/webhooks/handler.ts b/packages/backend/src/controllers/webhooks/handler.ts index 72329adc..c8e51bb3 100644 --- a/packages/backend/src/controllers/webhooks/handler.ts +++ b/packages/backend/src/controllers/webhooks/handler.ts @@ -13,18 +13,19 @@ export default async (request: IRequest, response: Response) => { .findById(request.params.flowId) .throwIfNotFound(); - if (!flow.active) { - return response.send(404); - } - const triggerStep = await flow.getTriggerStep(); const triggerCommand = await triggerStep.getTriggerCommand(); + const app = await triggerStep.getApp(); + const isWebhookApp = app.key === 'webhook'; - if (triggerCommand.type !== 'webhook') { - return response.send(404); + if (!flow.active && !isWebhookApp) { + return response.sendStatus(404); + } + + if (triggerCommand.type !== 'webhook') { + return response.sendStatus(404); } - const app = await triggerStep.getApp(); if (app.auth.verifyWebhook) { const $ = await globalVariable({ @@ -42,8 +43,20 @@ export default async (request: IRequest, response: Response) => { } } + // in case trigger type is 'webhook' + let payload = request.body; + + // in case it's our built-in generic webhook trigger + if (isWebhookApp) { + payload = { + headers: request.headers, + body: request.body, + query: request.query, + } + } + const triggerItem: ITriggerItem = { - raw: request.body, + raw: payload, meta: { internalId: await bcrypt.hash(request.rawBody, 1), }, diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index bd0c39b9..3f9714a3 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -342,6 +342,7 @@ type Step { key: String appKey: String iconUrl: String + webhookUrl: String type: StepEnumType parameters: JSONObject connection: Connection diff --git a/packages/backend/src/helpers/global-variable.ts b/packages/backend/src/helpers/global-variable.ts index de9a09e8..6834b081 100644 --- a/packages/backend/src/helpers/global-variable.ts +++ b/packages/backend/src/helpers/global-variable.ts @@ -77,6 +77,7 @@ const globalVariable = async ( id: execution?.id, testRun, }, + lastExecutionStep: (await step?.getLastExecutionStep())?.toJSON(), triggerOutput: { data: [], }, diff --git a/packages/backend/src/models/step.ts b/packages/backend/src/models/step.ts index 719fbbf3..294671af 100644 --- a/packages/backend/src/models/step.ts +++ b/packages/backend/src/models/step.ts @@ -1,10 +1,11 @@ +import { URL } from 'node:url'; import { QueryContext, ModelOptions } from 'objection'; +import type { IJSONObject, IStep } from '@automatisch/types'; import Base from './base'; import App from './app'; import Flow from './flow'; import Connection from './connection'; import ExecutionStep from './execution-step'; -import type { IJSONObject, IStep } from '@automatisch/types'; import Telemetry from '../helpers/telemetry'; import appConfig from '../config/app'; @@ -46,7 +47,7 @@ class Step extends Base { }; static get virtualAttributes() { - return ['iconUrl']; + return ['iconUrl', 'webhookUrl']; } static relationMappings = () => ({ @@ -82,6 +83,13 @@ class Step extends Base { return `${appConfig.baseUrl}/apps/${this.appKey}/assets/favicon.svg`; } + get webhookUrl() { + if (this.appKey !== 'webhook') return null; + + const url = new URL(`/webhooks/${this.flowId}`, appConfig.webhookUrl); + return url.toString(); + } + async $afterInsert(queryContext: QueryContext) { await super.$afterInsert(queryContext); Telemetry.stepCreated(this); @@ -106,6 +114,14 @@ class Step extends Base { return await App.findOneByKey(this.appKey); } + async getLastExecutionStep() { + const lastExecutionStep = await this.$relatedQuery('executionSteps') + .orderBy('created_at', 'desc') + .first(); + + return lastExecutionStep; + } + async getNextStep() { const flow = await this.$relatedQuery('flow'); diff --git a/packages/backend/src/routes/webhooks.ts b/packages/backend/src/routes/webhooks.ts index c26f4add..ca040cc0 100644 --- a/packages/backend/src/routes/webhooks.ts +++ b/packages/backend/src/routes/webhooks.ts @@ -3,6 +3,8 @@ import webhookHandler from '../controllers/webhooks/handler'; const router = Router(); +router.get('/:flowId', webhookHandler); +router.put('/:flowId', webhookHandler); router.post('/:flowId', webhookHandler); export default router; diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index 0b2fb1f8..1386f962 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -54,6 +54,7 @@ export interface IStep { key?: string; appKey?: string; iconUrl: string; + webhookUrl: string; type: 'action' | 'trigger'; connectionId?: string; status: string; @@ -180,23 +181,16 @@ export interface IDynamicData { export interface IAuth { generateAuthUrl?($: IGlobalVariable): Promise; - verifyCredentials($: IGlobalVariable): Promise; - isStillVerified($: IGlobalVariable): Promise; + verifyCredentials?($: IGlobalVariable): Promise; + isStillVerified?($: IGlobalVariable): Promise; refreshToken?($: IGlobalVariable): Promise; verifyWebhook?($: IGlobalVariable): Promise; isRefreshTokenRequested?: boolean; - fields: IField[]; + fields?: IField[]; authenticationSteps?: IAuthenticationStep[]; reconnectionSteps?: IAuthenticationStep[]; } -export interface IService { - authenticationClient?: IAuthentication; - triggers?: any; - actions?: any; - data?: any; -} - export interface ITriggerOutput { data: ITriggerItem[]; error?: IJSONObject; @@ -300,6 +294,7 @@ export type IGlobalVariable = { id: string; testRun: boolean; }; + lastExecutionStep?: IExecutionStep; webhookUrl?: string; triggerOutput?: ITriggerOutput; actionOutput?: IActionOutput;