diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 0000000..568d245 --- /dev/null +++ b/.yarnrc @@ -0,0 +1 @@ +network-timeout 400000 diff --git a/README.md b/README.md index 47c9b75..f8d3017 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ There are other existing solutions in the market, like Zapier and Integromat, so you might be wondering why you should use Automatisch. -✅ The most significant advantage of having Automatisch is keeping your data on your own servers. Not all companies want to use an automation service in the cloud, and the current open-source or self-hosted solutions mainly focus on developers rather than a user without a technical background. +✅ One of the main benefits of using Automatisch is that it allows you to store your data on your own servers, which is essential for businesses that handle sensitive user information and cannot risk sharing it with external cloud services. This is especially relevant for industries such as healthcare and finance, as well as for European companies that must adhere to the General Data Protection Regulation (GDPR). 🤓 Your contributions are vital to the development of Automatisch. As an open-source software, anyone can have an impact on how it is being developed. @@ -32,10 +32,12 @@ git clone git@github.com:automatisch/automatisch.git cd automatisch # Start -docker compose -p automatisch up +docker compose up ``` -You can use `user@automatisch.io` email address and `sample` password to login to Automatisch. You can also change your email and password later on from the settings page. +You can use `user@automatisch.io` email address and `sample` password to login to Automatisch. Please do not forget to change your email and password from the settings page. + +For other installation types, you can check the [installation](https://automatisch.io/docs/guide/installation) guide. ## Community Links diff --git a/docker-compose.yml b/docker-compose.yml index fbbb0a1..c6fb050 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,6 +23,7 @@ services: - POSTGRES_USERNAME=automatisch_user - POSTGRES_PASSWORD=automatisch_password - ENCRYPTION_KEY + - WEBHOOK_SECRET_KEY - APP_SECRET_KEY volumes: - automatisch_storage:/automatisch/storage @@ -41,6 +42,7 @@ services: - POSTGRES_USERNAME=automatisch_user - POSTGRES_PASSWORD=automatisch_password - ENCRYPTION_KEY + - WEBHOOK_SECRET_KEY - APP_SECRET_KEY - WORKER=true volumes: diff --git a/docker/Dockerfile b/docker/Dockerfile index afff33c..12683b6 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -2,9 +2,13 @@ FROM node:16-alpine WORKDIR /automatisch +RUN apk --no-cache add --virtual build-dependencies python3 build-base + COPY ./entrypoint.sh /entrypoint.sh -RUN yarn global add @automatisch/cli@0.2.0 +RUN yarn global add @automatisch/cli@0.3.0 + +RUN apk del build-dependencies python3 build-base EXPOSE 3000 ENTRYPOINT ["sh", "/entrypoint.sh"] diff --git a/docker/Dockerfile.compose b/docker/Dockerfile.compose index 12e4bb9..285e7db 100644 --- a/docker/Dockerfile.compose +++ b/docker/Dockerfile.compose @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -FROM automatischio/automatisch:0.2.0 +FROM automatischio/automatisch:0.3.0 WORKDIR /automatisch RUN apk add --no-cache openssl dos2unix diff --git a/docker/compose-entrypoint.sh b/docker/compose-entrypoint.sh index 3a5448c..c02ae98 100755 --- a/docker/compose-entrypoint.sh +++ b/docker/compose-entrypoint.sh @@ -5,14 +5,22 @@ set -e if [ ! -f /automatisch/storage/.env ]; then >&2 echo "Saving environment variables" ENCRYPTION_KEY="${ENCRYPTION_KEY:-$(openssl rand -base64 36)}" + WEBHOOK_SECRET_KEY="${WEBHOOK_SECRET_KEY:-$(openssl rand -base64 36)}" APP_SECRET_KEY="${APP_SECRET_KEY:-$(openssl rand -base64 36)}" echo "ENCRYPTION_KEY=$ENCRYPTION_KEY" >> /automatisch/storage/.env + echo "WEBHOOK_SECRET_KEY=$WEBHOOK_SECRET_KEY" >> /automatisch/storage/.env echo "APP_SECRET_KEY=$APP_SECRET_KEY" >> /automatisch/storage/.env fi # initiate env. vars. from /automatisch/storage/.env file export $(grep -v '^#' /automatisch/storage/.env | xargs) +# migration for webhook secret key, will be removed in the future. +if [[ -z "${WEBHOOK_SECRET_KEY}" ]]; then + WEBHOOK_SECRET_KEY="$(openssl rand -base64 36)" + echo "WEBHOOK_SECRET_KEY=$WEBHOOK_SECRET_KEY" >> /automatisch/storage/.env +fi + echo "Environment variables have been set!" sh /entrypoint.sh diff --git a/lerna.json b/lerna.json index 4d1b27d..4c49a96 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "packages": [ "packages/*" ], - "version": "0.2.0", + "version": "0.3.0", "npmClient": "yarn", "useWorkspaces": true, "command": { diff --git a/packages/backend/.env-example b/packages/backend/.env-example index 924bd16..27ee434 100644 --- a/packages/backend/.env-example +++ b/packages/backend/.env-example @@ -11,6 +11,7 @@ POSTGRES_USERNAME=automatish_development_user POSTGRES_PASSWORD= POSTGRES_ENABLE_SSL=false ENCRYPTION_KEY=sample-encryption-key +WEBHOOK_SECRET_KEY=sample-webhook-key APP_SECRET_KEY=sample-app-secret-key REDIS_PORT=6379 REDIS_HOST=127.0.0.1 diff --git a/packages/backend/package.json b/packages/backend/package.json index ee1c692..96d3814 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "@automatisch/backend", - "version": "0.2.0", + "version": "0.3.0", "license": "AGPL-3.0", "description": "The open source Zapier alternative. Build workflow automation without spending time and money.", "scripts": { @@ -22,7 +22,7 @@ "prebuild": "rm -rf ./dist" }, "dependencies": { - "@automatisch/web": "^0.2.0", + "@automatisch/web": "^0.3.0", "@bull-board/express": "^3.10.1", "@graphql-tools/graphql-file-loader": "^7.3.4", "@graphql-tools/load": "^7.5.2", @@ -37,7 +37,7 @@ "crypto-js": "^4.1.1", "debug": "~2.6.9", "dotenv": "^10.0.0", - "express": "~4.16.1", + "express": "~4.17.3", "express-basic-auth": "^1.2.1", "express-graphql": "^0.12.0", "fast-xml-parser": "^4.0.11", @@ -93,7 +93,7 @@ "url": "https://github.com/automatisch/automatisch/issues" }, "devDependencies": { - "@automatisch/types": "^0.2.0", + "@automatisch/types": "^0.3.0", "@types/bcrypt": "^5.0.0", "@types/bull": "^3.15.8", "@types/cors": "^2.8.12", diff --git a/packages/backend/src/apps/delay/actions/delay-for/index.ts b/packages/backend/src/apps/delay/actions/delay-for/index.ts new file mode 100644 index 0000000..2b7af61 --- /dev/null +++ b/packages/backend/src/apps/delay/actions/delay-for/index.ts @@ -0,0 +1,56 @@ +import defineAction from '../../../../helpers/define-action'; + +export default defineAction({ + name: 'Delay For', + key: 'delayFor', + description: + 'Delays the execution of the next action by a specified amount of time.', + arguments: [ + { + label: 'Delay for unit', + key: 'delayForUnit', + type: 'dropdown' as const, + required: true, + value: null, + description: 'Delay for unit, e.g. minutes, hours, days, weeks.', + variables: false, + options: [ + { + label: 'Minutes', + value: 'minutes', + }, + { + label: 'Hours', + value: 'hours', + }, + { + label: 'Days', + value: 'days', + }, + { + label: 'Weeks', + value: 'weeks', + }, + ], + }, + { + label: 'Delay for value', + key: 'delayForValue', + type: 'string' as const, + required: true, + description: 'Delay for value, use a number, e.g. 1, 2, 3.', + variables: true, + }, + ], + + async run($) { + const { delayForUnit, delayForValue } = $.step.parameters; + + const dataItem = { + delayForUnit, + delayForValue, + }; + + $.setActionItem({ raw: dataItem }); + }, +}); diff --git a/packages/backend/src/apps/delay/actions/delay-until/index.ts b/packages/backend/src/apps/delay/actions/delay-until/index.ts new file mode 100644 index 0000000..4197704 --- /dev/null +++ b/packages/backend/src/apps/delay/actions/delay-until/index.ts @@ -0,0 +1,28 @@ +import defineAction from '../../../../helpers/define-action'; + +export default defineAction({ + name: 'Delay Until', + key: 'delayUntil', + description: + 'Delays the execution of the next action until a specified date.', + arguments: [ + { + label: 'Delay until (Date)', + key: 'delayUntil', + type: 'string' as const, + required: true, + description: 'Delay until the date. E.g. 2022-12-18', + variables: true, + }, + ], + + async run($) { + const { delayUntil } = $.step.parameters; + + const dataItem = { + delayUntil, + }; + + $.setActionItem({ raw: dataItem }); + }, +}); diff --git a/packages/backend/src/apps/delay/actions/index.ts b/packages/backend/src/apps/delay/actions/index.ts new file mode 100644 index 0000000..0779892 --- /dev/null +++ b/packages/backend/src/apps/delay/actions/index.ts @@ -0,0 +1,4 @@ +import delayFor from './delay-for'; +import delayUntil from './delay-until'; + +export default [delayFor, delayUntil]; diff --git a/packages/backend/src/apps/delay/assets/favicon.svg b/packages/backend/src/apps/delay/assets/favicon.svg new file mode 100644 index 0000000..af5da4d --- /dev/null +++ b/packages/backend/src/apps/delay/assets/favicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/backend/src/apps/delay/index.d.ts b/packages/backend/src/apps/delay/index.d.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/backend/src/apps/delay/index.ts b/packages/backend/src/apps/delay/index.ts new file mode 100644 index 0000000..7f25c1b --- /dev/null +++ b/packages/backend/src/apps/delay/index.ts @@ -0,0 +1,14 @@ +import defineApp from '../../helpers/define-app'; +import actions from './actions'; + +export default defineApp({ + name: 'Delay', + key: 'delay', + iconUrl: '{BASE_URL}/apps/delay/assets/favicon.svg', + authDocUrl: 'https://automatisch.io/docs/apps/delay/connection', + supportsConnections: false, + baseUrl: '', + apiBaseUrl: '', + primaryColor: '001F52', + actions, +}); diff --git a/packages/backend/src/apps/flowers-software/assets/favicon.svg b/packages/backend/src/apps/flowers-software/assets/favicon.svg new file mode 100644 index 0000000..55b8ed6 --- /dev/null +++ b/packages/backend/src/apps/flowers-software/assets/favicon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/flowers-software/auth/index.ts b/packages/backend/src/apps/flowers-software/auth/index.ts new file mode 100644 index 0000000..6dd4071 --- /dev/null +++ b/packages/backend/src/apps/flowers-software/auth/index.ts @@ -0,0 +1,43 @@ +import verifyCredentials from './verify-credentials'; +import isStillVerified from './is-still-verified'; + +export default { + fields: [ + { + key: 'username', + label: 'Username', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'password', + label: 'Password', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'apiKey', + label: 'API Key', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/flowers-software/auth/is-still-verified.ts b/packages/backend/src/apps/flowers-software/auth/is-still-verified.ts new file mode 100644 index 0000000..5a2fe2a --- /dev/null +++ b/packages/backend/src/apps/flowers-software/auth/is-still-verified.ts @@ -0,0 +1,10 @@ +import { IGlobalVariable } from '@automatisch/types'; +import verifyCredentials from './verify-credentials'; + +const isStillVerified = async ($: IGlobalVariable) => { + await verifyCredentials($); + + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/flowers-software/auth/verify-credentials.ts b/packages/backend/src/apps/flowers-software/auth/verify-credentials.ts new file mode 100644 index 0000000..4e7a4f6 --- /dev/null +++ b/packages/backend/src/apps/flowers-software/auth/verify-credentials.ts @@ -0,0 +1,20 @@ +import { IGlobalVariable } from '@automatisch/types'; +import getWebhooks from '../common/get-webhooks'; + +const verifyCredentials = async ($: IGlobalVariable) => { + const response = await getWebhooks($); + const successful = Array.isArray(response.data); + + if (!successful) { + throw new Error('Failed while authorizing!'); + } + + await $.auth.set({ + screenName: $.auth.data.username, + username: $.auth.data.username, + password: $.auth.data.password, + apiKey: $.auth.data.apiKey, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/flowers-software/common/add-auth-header.ts b/packages/backend/src/apps/flowers-software/common/add-auth-header.ts new file mode 100644 index 0000000..02f524f --- /dev/null +++ b/packages/backend/src/apps/flowers-software/common/add-auth-header.ts @@ -0,0 +1,18 @@ +import { TBeforeRequest } from '@automatisch/types'; + +const addAuthHeader: TBeforeRequest = ($, requestConfig) => { + const { data } = $.auth; + + if (data?.username && data.password && data.apiKey) { + requestConfig.headers['x-api-key'] = data.apiKey as string; + + requestConfig.auth = { + username: data.username as string, + password: data.password as string, + }; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/flowers-software/common/get-webhooks.ts b/packages/backend/src/apps/flowers-software/common/get-webhooks.ts new file mode 100644 index 0000000..bb529c2 --- /dev/null +++ b/packages/backend/src/apps/flowers-software/common/get-webhooks.ts @@ -0,0 +1,5 @@ +import type { IGlobalVariable } from "@automatisch/types"; + +export default async function getWebhooks($: IGlobalVariable) { + return await $.http.get('/v2/public/api/webhooks'); +} diff --git a/packages/backend/src/apps/flowers-software/common/webhook-filters.ts b/packages/backend/src/apps/flowers-software/common/webhook-filters.ts new file mode 100644 index 0000000..d8a22d1 --- /dev/null +++ b/packages/backend/src/apps/flowers-software/common/webhook-filters.ts @@ -0,0 +1,488 @@ +const webhookFilters = [ + { + label: "Contact Company Created", + value: "CONTACT_COMPANY_CREATED" + }, + { + label: "Contact Company Deleted", + value: "CONTACT_COMPANY_DELETED" + }, + { + label: "Contact Company Updated", + value: "CONTACT_COMPANY_UPDATED" + }, + { + label: "Contact Created", + value: "CONTACT_CREATED" + }, + { + label: "Contact Deleted", + value: "CONTACT_DELETED" + }, + { + label: "Contact Updated", + value: "CONTACT_UPDATED" + }, + { + label: "Customer Created", + value: "CUSTOMER_CREATED" + }, + { + label: "Customer Updated", + value: "CUSTOMER_UPDATED" + }, + { + label: "Document Deleted", + value: "DOCUMENT_DELETED" + }, + { + label: "Document Downloaded", + value: "DOCUMENT_DOWNLOADED" + }, + { + label: "Document Saved", + value: "DOCUMENT_SAVED" + }, + { + label: "Document Updated", + value: "DOCUMENT_UPDATED" + }, + { + label: "Flow Archived", + value: "FLOW_ARCHIVED" + }, + { + label: "Flow Created", + value: "FLOW_CREATED" + }, + { + label: "Flow Object Automation Action Created", + value: "FLOW_OBJECT_AUTOMATION_ACTION_CREATED" + }, + { + label: "Flow Object Automation Action Deleted", + value: "FLOW_OBJECT_AUTOMATION_ACTION_DELETED" + }, + { + label: "Flow Object Automation Created", + value: "FLOW_OBJECT_AUTOMATION_CREATED" + }, + { + label: "Flow Object Automation Deleted", + value: "FLOW_OBJECT_AUTOMATION_DELETED" + }, + { + label: "Flow Object Automation Updated", + value: "FLOW_OBJECT_AUTOMATION_UPDATED" + }, + { + label: "Flow Object Automation Webdav Created", + value: "FLOW_OBJECT_AUTOMATION_WEBDAV_CREATED" + }, + { + label: "Flow Object Automation Webdav Deleted", + value: "FLOW_OBJECT_AUTOMATION_WEBDAV_DELETED" + }, + { + label: "Flow Object Automation Webdav Updated", + value: "FLOW_OBJECT_AUTOMATION_WEBDAV_UPDATED" + }, + { + label: "Flow Object Created", + value: "FLOW_OBJECT_CREATED" + }, + { + label: "Flow Object Deleted", + value: "FLOW_OBJECT_DELETED" + }, + { + label: "Flow Object Document Added", + value: "FLOW_OBJECT_DOCUMENT_ADDED" + }, + { + label: "Flow Object Document Removed", + value: "FLOW_OBJECT_DOCUMENT_REMOVED" + }, + { + label: "Flow Object Resource Created", + value: "FLOW_OBJECT_RESOURCE_CREATED" + }, + { + label: "Flow Object Resource Deleted", + value: "FLOW_OBJECT_RESOURCE_DELETED" + }, + { + label: "Flow Object Resource Updated", + value: "FLOW_OBJECT_RESOURCE_UPDATED" + }, + { + label: "Flow Object Task Condition Created", + value: "FLOW_OBJECT_TASK_CONDITION_CREATED" + }, + { + label: "Flow Object Task Condition Deleted", + value: "FLOW_OBJECT_TASK_CONDITION_DELETED" + }, + { + label: "Flow Object Task Condition Updated", + value: "FLOW_OBJECT_TASK_CONDITION_UPDATED" + }, + { + label: "Flow Object Task Created", + value: "FLOW_OBJECT_TASK_CREATED" + }, + { + label: "Flow Object Task Deleted", + value: "FLOW_OBJECT_TASK_DELETED" + }, + { + label: "Flow Object Task Updated", + value: "FLOW_OBJECT_TASK_UPDATED" + }, + { + label: "Flow Object Updated", + value: "FLOW_OBJECT_UPDATED" + }, + { + label: "Flow Objects Connection Created", + value: "FLOW_OBJECTS_CONNECTION_CREATED" + }, + { + label: "Flow Objects Connection Deleted", + value: "FLOW_OBJECTS_CONNECTION_DELETED" + }, + { + label: "Flow Objects Connection Updated", + value: "FLOW_OBJECTS_CONNECTION_UPDATED" + }, + { + label: "Flow Objects External Connection Created", + value: "FLOW_OBJECTS_EXTERNAL_CONNECTION_CREATED" + }, + { + label: "Flow Objects External Connection Deleted", + value: "FLOW_OBJECTS_EXTERNAL_CONNECTION_DELETED" + }, + { + label: "Flow Objects External Connection Updated", + value: "FLOW_OBJECTS_EXTERNAL_CONNECTION_UPDATED" + }, + { + label: "Flow Objects External Connections Group Created", + value: "FLOW_OBJECTS_EXTERNAL_CONNECTIONS_GROUP_CREATED" + }, + { + label: "Flow Objects External Connections Group Deleted", + value: "FLOW_OBJECTS_EXTERNAL_CONNECTIONS_GROUP_DELETED" + }, + { + label: "Flow Objects External Connections Group Updated", + value: "FLOW_OBJECTS_EXTERNAL_CONNECTIONS_GROUP_UPDATED" + }, + { + label: "Flow Unarchived", + value: "FLOW_UNARCHIVED" + }, + { + label: "Flow Updated", + value: "FLOW_UPDATED" + }, + { + label: "Note Created", + value: "NOTE_CREATED" + }, + { + label: "Note Deleted", + value: "NOTE_DELETED" + }, + { + label: "Note Updated", + value: "NOTE_UPDATED" + }, + { + label: "Team Created", + value: "TEAM_CREATED" + }, + { + label: "Team Deleted", + value: "TEAM_DELETED" + }, + { + label: "Team Updated", + value: "TEAM_UPDATED" + }, + { + label: "User Added To Team", + value: "USER_ADDED_TO_TEAM" + }, + { + label: "User Added To Teamleads", + value: "USER_ADDED_TO_TEAMLEADS" + }, + { + label: "User Archived", + value: "USER_ARCHIVED" + }, + { + label: "User Changed Password", + value: "USER_CHANGED_PASSWORD" + }, + { + label: "User Created", + value: "USER_CREATED" + }, + { + label: "User Forgot Password", + value: "USER_FORGOT_PASSWORD" + }, + { + label: "User Invited", + value: "USER_INVITED" + }, + { + label: "User Logged In", + value: "USER_LOGGED_IN" + }, + { + label: "User Notification Settings Changed", + value: "USER_NOTIFICATION_SETTINGS_CHANGED" + }, + { + label: "User Profile Updated", + value: "USER_PROFILE_UPDATED" + }, + { + label: "User Removed From Team", + value: "USER_REMOVED_FROM_TEAM" + }, + { + label: "User Removed From Teamleads", + value: "USER_REMOVED_FROM_TEAMLEADS" + }, + { + label: "User Unarchived", + value: "USER_UNARCHIVED" + }, + { + label: "Workflow Archived", + value: "WORKFLOW_ARCHIVED" + }, + { + label: "Workflow Completed", + value: "WORKFLOW_COMPLETED" + }, + { + label: "Workflow Created", + value: "WORKFLOW_CREATED" + }, + { + label: "Workflow Creation Failed", + value: "WORKFLOW_CREATION_FAILED" + }, + { + label: "Workflow Object Automation Api Get Completed", + value: "WORKFLOW_OBJECT_AUTOMATION_API_GET_COMPLETED" + }, + { + label: "Workflow Object Automation Api Get Failed", + value: "WORKFLOW_OBJECT_AUTOMATION_API_GET_FAILED" + }, + { + label: "Workflow Object Automation Api Post Completed", + value: "WORKFLOW_OBJECT_AUTOMATION_API_POST_COMPLETED" + }, + { + label: "Workflow Object Automation Api Post Failed", + value: "WORKFLOW_OBJECT_AUTOMATION_API_POST_FAILED" + }, + { + label: "Workflow Object Automation Datev Completed", + value: "WORKFLOW_OBJECT_AUTOMATION_DATEV_COMPLETED" + }, + { + label: "Workflow Object Automation Datev Failed", + value: "WORKFLOW_OBJECT_AUTOMATION_DATEV_FAILED" + }, + { + label: "Workflow Object Automation Email Completed", + value: "WORKFLOW_OBJECT_AUTOMATION_EMAIL_COMPLETED" + }, + { + label: "Workflow Object Automation Email Failed", + value: "WORKFLOW_OBJECT_AUTOMATION_EMAIL_FAILED" + }, + { + label: "Workflow Object Automation Lexoffice Completed", + value: "WORKFLOW_OBJECT_AUTOMATION_LEXOFFICE_COMPLETED" + }, + { + label: "Workflow Object Automation Lexoffice Failed", + value: "WORKFLOW_OBJECT_AUTOMATION_LEXOFFICE_FAILED" + }, + { + label: "Workflow Object Automation Rejected", + value: "WORKFLOW_OBJECT_AUTOMATION_REJECTED" + }, + { + label: "Workflow Object Automation Retried", + value: "WORKFLOW_OBJECT_AUTOMATION_RETRIED" + }, + { + label: "Workflow Object Automation Sevdesk Completed", + value: "WORKFLOW_OBJECT_AUTOMATION_SEVDESK_COMPLETED" + }, + { + label: "Workflow Object Automation Sevdesk Failed", + value: "WORKFLOW_OBJECT_AUTOMATION_SEVDESK_FAILED" + }, + { + label: "Workflow Object Automation Stamp Completed", + value: "WORKFLOW_OBJECT_AUTOMATION_STAMP_COMPLETED" + }, + { + label: "Workflow Object Automation Stamp Failed", + value: "WORKFLOW_OBJECT_AUTOMATION_STAMP_FAILED" + }, + { + label: "Workflow Object Automation Task Completed", + value: "WORKFLOW_OBJECT_AUTOMATION_TASK_COMPLETED" + }, + { + label: "Workflow Object Automation Task Failed", + value: "WORKFLOW_OBJECT_AUTOMATION_TASK_FAILED" + }, + { + label: "Workflow Object Automation Template Completed", + value: "WORKFLOW_OBJECT_AUTOMATION_TEMPLATE_COMPLETED" + }, + { + label: "Workflow Object Automation Template Failed", + value: "WORKFLOW_OBJECT_AUTOMATION_TEMPLATE_FAILED" + }, + { + label: "Workflow Object Automation Webdav Document Uploaded", + value: "WORKFLOW_OBJECT_AUTOMATION_WEBDAV_DOCUMENT_UPLOADED" + }, + { + label: "Workflow Object Automation Zapier Completed", + value: "WORKFLOW_OBJECT_AUTOMATION_ZAPIER_COMPLETED" + }, + { + label: "Workflow Object Automation Zapier Failed", + value: "WORKFLOW_OBJECT_AUTOMATION_ZAPIER_FAILED" + }, + { + label: "Workflow Object Combination Task Group Created", + value: "WORKFLOW_OBJECT_COMBINATION_TASK_GROUP_CREATED" + }, + { + label: "Workflow Object Combination Task Group Deleted", + value: "WORKFLOW_OBJECT_COMBINATION_TASK_GROUP_DELETED" + }, + { + label: "Workflow Object Completed Automations Finished", + value: "WORKFLOW_OBJECT_COMPLETED_AUTOMATIONS_FINISHED" + }, + { + label: "Workflow Object Completed", + value: "WORKFLOW_OBJECT_COMPLETED" + }, + { + label: "Workflow Object Created", + value: "WORKFLOW_OBJECT_CREATED" + }, + { + label: "Workflow Object Document Added", + value: "WORKFLOW_OBJECT_DOCUMENT_ADDED" + }, + { + label: "Workflow Object Document Lock Added", + value: "WORKFLOW_OBJECT_DOCUMENT_LOCK_ADDED" + }, + { + label: "Workflow Object Document Lock Deleted", + value: "WORKFLOW_OBJECT_DOCUMENT_LOCK_DELETED" + }, + { + label: "Workflow Object Document Removed", + value: "WORKFLOW_OBJECT_DOCUMENT_REMOVED" + }, + { + label: "Workflow Object Email Added", + value: "WORKFLOW_OBJECT_EMAIL_ADDED" + }, + { + label: "Workflow Object Email Removed", + value: "WORKFLOW_OBJECT_EMAIL_REMOVED" + }, + { + label: "Workflow Object External User Created", + value: "WORKFLOW_OBJECT_EXTERNAL_USER_CREATED" + }, + { + label: "Workflow Object External User Deleted", + value: "WORKFLOW_OBJECT_EXTERNAL_USER_DELETED" + }, + { + label: "Workflow Object Note Added", + value: "WORKFLOW_OBJECT_NOTE_ADDED" + }, + { + label: "Workflow Object Note Removed", + value: "WORKFLOW_OBJECT_NOTE_REMOVED" + }, + { + label: "Workflow Object Resource Created", + value: "WORKFLOW_OBJECT_RESOURCE_CREATED" + }, + { + label: "Workflow Object Snapshot Created", + value: "WORKFLOW_OBJECT_SNAPSHOT_CREATED" + }, + { + label: "Workflow Object Task Condition Created", + value: "WORKFLOW_OBJECT_TASK_CONDITION_CREATED" + }, + { + label: "Workflow Object Task Created", + value: "WORKFLOW_OBJECT_TASK_CREATED" + }, + { + label: "Workflow Object Task Deleted", + value: "WORKFLOW_OBJECT_TASK_DELETED" + }, + { + label: "Workflow Object Task Snapshot Created", + value: "WORKFLOW_OBJECT_TASK_SNAPSHOT_CREATED" + }, + { + label: "Workflow Object Task Updated", + value: "WORKFLOW_OBJECT_TASK_UPDATED" + }, + { + label: "Workflow Object Updated", + value: "WORKFLOW_OBJECT_UPDATED" + }, + { + label: "Workflow Objects Connection Created", + value: "WORKFLOW_OBJECTS_CONNECTION_CREATED" + }, + { + label: "Workflow Objects External Connection Created", + value: "WORKFLOW_OBJECTS_EXTERNAL_CONNECTION_CREATED" + }, + { + label: "Workflow Objects External Connection Group Created", + value: "WORKFLOW_OBJECTS_EXTERNAL_CONNECTION_GROUP_CREATED" + }, + { + label: "Workflow Unarchived", + value: "WORKFLOW_UNARCHIVED" + }, + { + label: "Workflow Updated", + value: "WORKFLOW_UPDATED" + } +]; + +export default webhookFilters; \ No newline at end of file diff --git a/packages/backend/src/apps/flowers-software/index.d.ts b/packages/backend/src/apps/flowers-software/index.d.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/backend/src/apps/flowers-software/index.ts b/packages/backend/src/apps/flowers-software/index.ts new file mode 100644 index 0000000..a023486 --- /dev/null +++ b/packages/backend/src/apps/flowers-software/index.ts @@ -0,0 +1,18 @@ +import defineApp from '../../helpers/define-app'; +import addAuthHeader from './common/add-auth-header'; +import auth from './auth'; +import triggers from './triggers'; + +export default defineApp({ + name: 'Flowers Software', + key: 'flowers-software', + iconUrl: '{BASE_URL}/apps/flowers-software/assets/favicon.svg', + authDocUrl: 'https://automatisch.io/docs/apps/flowers-software/connection', + supportsConnections: true, + baseUrl: 'https://flowers-software.com', + apiBaseUrl: 'https://webapp.flowers-software.com/api', + primaryColor: '02AFC7', + beforeRequest: [addAuthHeader], + auth, + triggers, +}); diff --git a/packages/backend/src/apps/flowers-software/triggers/index.ts b/packages/backend/src/apps/flowers-software/triggers/index.ts new file mode 100644 index 0000000..5599f29 --- /dev/null +++ b/packages/backend/src/apps/flowers-software/triggers/index.ts @@ -0,0 +1,3 @@ +import newActivity from './new-activity'; + +export default [newActivity]; diff --git a/packages/backend/src/apps/flowers-software/triggers/new-activity/index.ts b/packages/backend/src/apps/flowers-software/triggers/new-activity/index.ts new file mode 100644 index 0000000..3cd2997 --- /dev/null +++ b/packages/backend/src/apps/flowers-software/triggers/new-activity/index.ts @@ -0,0 +1,54 @@ +import isEmpty from 'lodash/isEmpty'; +import defineTrigger from '../../../../helpers/define-trigger'; +import webhookFilters from '../../common/webhook-filters'; + +export default defineTrigger({ + name: 'New activity', + key: 'newActivity', + type: 'webhook', + description: 'Triggers when a new activity occurs.', + arguments: [ + { + label: 'Activity type', + key: 'filters', + type: 'dropdown' as const, + required: true, + description: 'Pick an activity type to receive events for.', + variables: false, + options: webhookFilters, + }, + ], + + async testRun($) { + if (!isEmpty($.lastExecutionStep?.dataOut)) { + $.pushTriggerItem({ + raw: $.lastExecutionStep.dataOut, + meta: { + internalId: '', + } + }); + } + }, + + async registerHook($) { + const payload = { + name: $.flow.id, + type: 'POST', + url: $.webhookUrl, + filters: [$.step.parameters.filters] + }; + + const { data } = await $.http.post( + `/v2/public/api/webhooks`, + payload + ); + + await $.flow.setRemoteWebhookId(data.id); + }, + + async unregisterHook($) { + await $.http.delete( + `/v2/public/api/webhooks/${$.flow.remoteWebhookId}` + ); + }, +}); diff --git a/packages/backend/src/apps/ntfy/actions/index.ts b/packages/backend/src/apps/ntfy/actions/index.ts new file mode 100644 index 0000000..37aeb33 --- /dev/null +++ b/packages/backend/src/apps/ntfy/actions/index.ts @@ -0,0 +1,3 @@ +import sendMessage from './send-message'; + +export default [sendMessage]; diff --git a/packages/backend/src/apps/ntfy/actions/send-message/index.ts b/packages/backend/src/apps/ntfy/actions/send-message/index.ts new file mode 100644 index 0000000..8cc9158 --- /dev/null +++ b/packages/backend/src/apps/ntfy/actions/send-message/index.ts @@ -0,0 +1,103 @@ +import qs from 'qs'; +import defineAction from '../../../../helpers/define-action'; + +export default defineAction({ + name: 'Send message', + key: 'sendMessage', + description: 'Sends a message to a topic you specify.', + arguments: [ + { + label: 'Topic', + key: 'topic', + type: 'string' as const, + required: true, + description: 'Target topic name.', + variables: true, + }, + { + label: 'Message body', + key: 'message', + type: 'string' as const, + required: true, + description: 'Message body to be sent, set to triggered if empty or not passed.', + variables: true, + }, + { + label: 'Title', + key: 'title', + type: 'string' as const, + required: false, + description: 'Message title.', + variables: true, + }, + { + label: 'Email', + key: 'email', + type: 'string' as const, + required: false, + description: 'E-mail address for e-mail notifications.', + variables: true, + }, + { + label: 'Click URL', + key: 'click', + type: 'string' as const, + required: false, + description: 'Website opened when notification is clicked.', + variables: true, + }, + { + label: 'Attach file by URL', + key: 'attach', + type: 'string' as const, + required: false, + description: 'URL of an attachment.', + variables: true, + }, + { + label: 'Filename', + key: 'filename', + type: 'string' as const, + required: false, + description: 'File name of the attachment.', + variables: true, + }, + { + label: 'Delay', + key: 'delay', + type: 'string' as const, + required: false, + description: 'Timestamp or duration for delayed delivery. For example, 30min or 9am.', + variables: true, + }, + ], + + async run($) { + const { + topic, + message, + title, + email, + click, + attach, + filename, + delay + } = $.step.parameters; + const payload = { + topic, + message, + title, + email, + click, + attach, + filename, + delay + }; + + const response = await $.http.post('/', payload); + + $.setActionItem({ + raw: response.data, + }); + }, +}); diff --git a/packages/backend/src/apps/ntfy/assets/favicon.svg b/packages/backend/src/apps/ntfy/assets/favicon.svg new file mode 100644 index 0000000..9e5b513 --- /dev/null +++ b/packages/backend/src/apps/ntfy/assets/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/backend/src/apps/ntfy/auth/index.ts b/packages/backend/src/apps/ntfy/auth/index.ts new file mode 100644 index 0000000..f783c90 --- /dev/null +++ b/packages/backend/src/apps/ntfy/auth/index.ts @@ -0,0 +1,41 @@ +import verifyCredentials from './verify-credentials'; +import isStillVerified from './is-still-verified'; + +export default { + fields: [ + { + key: 'serverUrl', + label: 'Server URL', + type: 'string' as const, + required: true, + readOnly: false, + value: 'https://ntfy.sh', + placeholder: null, + description: 'ntfy server to use.', + clickToCopy: false, + }, + { + key: 'username', + label: 'Username', + type: 'string' as const, + required: false, + readOnly: false, + placeholder: null, + clickToCopy: false, + description: 'You may need to provide your username if your installation requires authentication.', + }, + { + key: 'password', + label: 'Password', + type: 'string' as const, + required: false, + readOnly: false, + placeholder: null, + clickToCopy: false, + description: 'You may need to provide your password if your installation requires authentication.', + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/ntfy/auth/is-still-verified.ts b/packages/backend/src/apps/ntfy/auth/is-still-verified.ts new file mode 100644 index 0000000..66bb963 --- /dev/null +++ b/packages/backend/src/apps/ntfy/auth/is-still-verified.ts @@ -0,0 +1,9 @@ +import { IGlobalVariable } from '@automatisch/types'; +import verifyCredentials from './verify-credentials'; + +const isStillVerified = async ($: IGlobalVariable) => { + await verifyCredentials($); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/ntfy/auth/verify-credentials.ts b/packages/backend/src/apps/ntfy/auth/verify-credentials.ts new file mode 100644 index 0000000..84d1d62 --- /dev/null +++ b/packages/backend/src/apps/ntfy/auth/verify-credentials.ts @@ -0,0 +1,16 @@ +import { IGlobalVariable } from '@automatisch/types'; + +const verifyCredentials = async ($: IGlobalVariable) => { + await $.http.post('/', { topic: 'automatisch' }); + let screenName = $.auth.data.serverUrl; + + if ($.auth.data.username) { + screenName = `${$.auth.data.username} @ ${screenName}` + } + + await $.auth.set({ + screenName, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/ntfy/common/add-auth-header.ts b/packages/backend/src/apps/ntfy/common/add-auth-header.ts new file mode 100644 index 0000000..ff19448 --- /dev/null +++ b/packages/backend/src/apps/ntfy/common/add-auth-header.ts @@ -0,0 +1,18 @@ +import { TBeforeRequest } from '@automatisch/types'; + +const addAuthHeader: TBeforeRequest = ($, requestConfig) => { + if ($.auth.data.apiBaseUrl) { + requestConfig.baseURL = $.auth.data.apiBaseUrl as string; + } + + if ($.auth.data?.username && $.auth.data?.password) { + requestConfig.auth = { + username: $.auth.data.username as string, + password: $.auth.data.password as string, + } + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/ntfy/index.d.ts b/packages/backend/src/apps/ntfy/index.d.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/backend/src/apps/ntfy/index.ts b/packages/backend/src/apps/ntfy/index.ts new file mode 100644 index 0000000..1ba2396 --- /dev/null +++ b/packages/backend/src/apps/ntfy/index.ts @@ -0,0 +1,18 @@ +import defineApp from '../../helpers/define-app'; +import addAuthHeader from './common/add-auth-header'; +import auth from './auth'; +import actions from './actions'; + +export default defineApp({ + name: 'Ntfy', + key: 'ntfy', + iconUrl: '{BASE_URL}/apps/ntfy/assets/favicon.svg', + authDocUrl: 'https://automatisch.io/docs/apps/ntfy/connection', + supportsConnections: true, + baseUrl: 'https://ntfy.sh', + apiBaseUrl: 'https://ntfy.sh', + primaryColor: '56bda8', + beforeRequest: [addAuthHeader], + auth, + actions, +}); diff --git a/packages/backend/src/apps/telegram-bot/actions/index.ts b/packages/backend/src/apps/telegram-bot/actions/index.ts new file mode 100644 index 0000000..37aeb33 --- /dev/null +++ b/packages/backend/src/apps/telegram-bot/actions/index.ts @@ -0,0 +1,3 @@ +import sendMessage from './send-message'; + +export default [sendMessage]; diff --git a/packages/backend/src/apps/telegram-bot/actions/send-message/index.ts b/packages/backend/src/apps/telegram-bot/actions/send-message/index.ts new file mode 100644 index 0000000..5d884cb --- /dev/null +++ b/packages/backend/src/apps/telegram-bot/actions/send-message/index.ts @@ -0,0 +1,59 @@ +import qs from 'qs'; +import defineAction from '../../../../helpers/define-action'; + +export default defineAction({ + name: 'Send message', + key: 'sendMessage', + description: 'Sends a message to a chat you specify.', + arguments: [ + { + label: 'Chat ID', + key: 'chatId', + type: 'string' as const, + required: true, + description: 'Unique identifier for the target chat or username of the target channel (in the format @channelusername).', + variables: true, + }, + { + label: 'Message text', + key: 'text', + type: 'string' as const, + required: true, + description: 'Text of the message to be sent, 1-4096 characters.', + variables: true, + }, + { + label: 'Disable notification?', + key: 'disableNotification', + type: 'dropdown' as const, + required: false, + value: false, + description: 'Sends the message silently. Users will receive a notification with no sound.', + variables: false, + options: [ + { + label: 'Yes', + value: true, + }, + { + label: 'No', + value: false, + }, + ], + }, + ], + + async run($) { + const payload = { + chat_id: $.step.parameters.chatId, + text: $.step.parameters.text, + disable_notification: $.step.parameters.disableNotification, + }; + + const response = await $.http.post('/sendMessage', payload); + + $.setActionItem({ + raw: response.data, + }); + }, +}); diff --git a/packages/backend/src/apps/telegram-bot/assets/favicon.svg b/packages/backend/src/apps/telegram-bot/assets/favicon.svg new file mode 100644 index 0000000..8f16fb1 --- /dev/null +++ b/packages/backend/src/apps/telegram-bot/assets/favicon.svg @@ -0,0 +1,14 @@ + + + Telegram + + + + + + + + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/telegram-bot/auth/index.ts b/packages/backend/src/apps/telegram-bot/auth/index.ts new file mode 100644 index 0000000..01e68c2 --- /dev/null +++ b/packages/backend/src/apps/telegram-bot/auth/index.ts @@ -0,0 +1,21 @@ +import verifyCredentials from './verify-credentials'; +import isStillVerified from './is-still-verified'; + +export default { + fields: [ + { + key: 'token', + label: 'Bot token', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Bot token which should be retrieved from @botfather.', + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/telegram-bot/auth/is-still-verified.ts b/packages/backend/src/apps/telegram-bot/auth/is-still-verified.ts new file mode 100644 index 0000000..66bb963 --- /dev/null +++ b/packages/backend/src/apps/telegram-bot/auth/is-still-verified.ts @@ -0,0 +1,9 @@ +import { IGlobalVariable } from '@automatisch/types'; +import verifyCredentials from './verify-credentials'; + +const isStillVerified = async ($: IGlobalVariable) => { + await verifyCredentials($); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/telegram-bot/auth/verify-credentials.ts b/packages/backend/src/apps/telegram-bot/auth/verify-credentials.ts new file mode 100644 index 0000000..17bbfa9 --- /dev/null +++ b/packages/backend/src/apps/telegram-bot/auth/verify-credentials.ts @@ -0,0 +1,12 @@ +import { IGlobalVariable } from '@automatisch/types'; + +const verifyCredentials = async ($: IGlobalVariable) => { + const { data } = await $.http.get('/getMe'); + const { result: me } = data; + + await $.auth.set({ + screenName: me.first_name, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/telegram-bot/common/add-auth-header.ts b/packages/backend/src/apps/telegram-bot/common/add-auth-header.ts new file mode 100644 index 0000000..aa17b14 --- /dev/null +++ b/packages/backend/src/apps/telegram-bot/common/add-auth-header.ts @@ -0,0 +1,13 @@ +import { TBeforeRequest } from '@automatisch/types'; +import { URL } from 'node:url'; + +const addAuthHeader: TBeforeRequest = ($, requestConfig) => { + if ($.auth.data?.token) { + const token = $.auth.data.token as string; + requestConfig.baseURL = (new URL(`/bot${token}`, requestConfig.baseURL)).toString(); + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/telegram-bot/index.d.ts b/packages/backend/src/apps/telegram-bot/index.d.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/backend/src/apps/telegram-bot/index.ts b/packages/backend/src/apps/telegram-bot/index.ts new file mode 100644 index 0000000..364e01d --- /dev/null +++ b/packages/backend/src/apps/telegram-bot/index.ts @@ -0,0 +1,18 @@ +import defineApp from '../../helpers/define-app'; +import addAuthHeader from './common/add-auth-header'; +import auth from './auth'; +import actions from './actions'; + +export default defineApp({ + name: 'Telegram', + key: 'telegram-bot', + iconUrl: '{BASE_URL}/apps/telegram-bot/assets/favicon.svg', + authDocUrl: 'https://automatisch.io/docs/apps/telegram-bot/connection', + supportsConnections: true, + baseUrl: 'https://telegram.org', + apiBaseUrl: 'https://api.telegram.org', + primaryColor: '2AABEE', + beforeRequest: [addAuthHeader], + auth, + actions, +}); diff --git a/packages/backend/src/apps/typeform/auth/verify-webhook.ts b/packages/backend/src/apps/typeform/auth/verify-webhook.ts index f5ba0d4..19dd880 100644 --- a/packages/backend/src/apps/typeform/auth/verify-webhook.ts +++ b/packages/backend/src/apps/typeform/auth/verify-webhook.ts @@ -11,7 +11,7 @@ const verifyWebhook = async ($: IGlobalVariable) => { const verifySignature = function (receivedSignature: string, payload: string) { const hash = crypto - .createHmac('sha256', appConfig.appSecretKey) + .createHmac('sha256', appConfig.webhookSecretKey) .update(payload) .digest('base64'); return receivedSignature === `sha256=${hash}`; diff --git a/packages/backend/src/apps/typeform/triggers/new-entry/index.ts b/packages/backend/src/apps/typeform/triggers/new-entry/index.ts index 55eaf31..01b3c35 100644 --- a/packages/backend/src/apps/typeform/triggers/new-entry/index.ts +++ b/packages/backend/src/apps/typeform/triggers/new-entry/index.ts @@ -72,7 +72,7 @@ export default defineTrigger({ const subscriptionPayload = { enabled: true, url: $.webhookUrl, - secret: appConfig.appSecretKey, + secret: appConfig.webhookSecretKey, }; await $.http.put( 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 0000000..140ebd6 --- /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/index.d.ts b/packages/backend/src/apps/webhook/index.d.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/backend/src/apps/webhook/index.ts b/packages/backend/src/apps/webhook/index.ts new file mode 100644 index 0000000..107ee47 --- /dev/null +++ b/packages/backend/src/apps/webhook/index.ts @@ -0,0 +1,14 @@ +import defineApp from '../../helpers/define-app'; +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', + 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 0000000..88c52f9 --- /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 0000000..4915900 --- /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/config/app.ts b/packages/backend/src/config/app.ts index 7403421..6e00727 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -1,3 +1,4 @@ +import { URL } from 'node:url'; import * as dotenv from 'dotenv'; dotenv.config(); @@ -18,6 +19,7 @@ type AppConfig = { postgresEnableSsl: boolean; baseUrl: string; encryptionKey: string; + webhookSecretKey: string; appSecretKey: string; serveWebAppSeparately: boolean; redisHost: string; @@ -37,14 +39,23 @@ const port = process.env.PORT || '3000'; const serveWebAppSeparately = process.env.SERVE_WEB_APP_SEPARATELY === 'true' ? true : false; -let webAppUrl = `${protocol}://${host}:${port}`; -const webhookUrl = process.env.WEBHOOK_URL || webAppUrl; +let apiUrl = (new URL(`${protocol}://${host}:${port}`)).toString(); +apiUrl = apiUrl.substring(0, apiUrl.length - 1); -if (serveWebAppSeparately) { - webAppUrl = process.env.WEB_APP_URL || 'http://localhost:3001'; +// use apiUrl by default, which has less priority over the following cases +let webAppUrl = apiUrl; + +if (process.env.WEB_APP_URL) { + // use env. var. if provided + webAppUrl = (new URL(process.env.WEB_APP_URL)).toString(); + webAppUrl = webAppUrl.substring(0, webAppUrl.length - 1); +} else if (serveWebAppSeparately) { + // no env. var. and serving separately, sign of development + webAppUrl = 'http://localhost:3001' } -const baseUrl = `${protocol}://${host}:${port}`; +let webhookUrl = (new URL(process.env.WEBHOOK_URL || apiUrl)).toString(); +webhookUrl = webhookUrl.substring(0, webhookUrl.length - 1); const appEnv = process.env.APP_ENV || 'development'; @@ -63,6 +74,7 @@ const appConfig: AppConfig = { postgresPassword: process.env.POSTGRES_PASSWORD, postgresEnableSsl: process.env.POSTGRES_ENABLE_SSL === 'true', encryptionKey: process.env.ENCRYPTION_KEY || '', + webhookSecretKey: process.env.WEBHOOK_SECRET_KEY || '', appSecretKey: process.env.APP_SECRET_KEY || '', serveWebAppSeparately, redisHost: process.env.REDIS_HOST || '127.0.0.1', @@ -70,11 +82,10 @@ const appConfig: AppConfig = { redisUsername: process.env.REDIS_USERNAME, redisPassword: process.env.REDIS_PASSWORD, redisTls: process.env.REDIS_TLS === 'true', - enableBullMQDashboard: - process.env.ENABLE_BULLMQ_DASHBOARD === 'true', + enableBullMQDashboard: process.env.ENABLE_BULLMQ_DASHBOARD === 'true', bullMQDashboardUsername: process.env.BULLMQ_DASHBOARD_USERNAME, bullMQDashboardPassword: process.env.BULLMQ_DASHBOARD_PASSWORD, - baseUrl, + baseUrl: apiUrl, webAppUrl, webhookUrl, telemetryEnabled: process.env.TELEMETRY_ENABLED === 'false' ? false : true, @@ -84,4 +95,8 @@ if (!appConfig.encryptionKey) { throw new Error('ENCRYPTION_KEY environment variable needs to be set!'); } +if (!appConfig.webhookSecretKey) { + throw new Error('WEBHOOK_SECRET_KEY environment variable needs to be set!'); +} + export default appConfig; diff --git a/packages/backend/src/config/redis.ts b/packages/backend/src/config/redis.ts index fedb5be..ea56ccd 100644 --- a/packages/backend/src/config/redis.ts +++ b/packages/backend/src/config/redis.ts @@ -6,6 +6,7 @@ type TRedisConfig = { username?: string, password?: string, tls?: Record, + enableReadyCheck?: boolean, enableOfflineQueue: boolean, } @@ -15,6 +16,7 @@ const redisConfig: TRedisConfig = { username: appConfig.redisUsername, password: appConfig.redisPassword, enableOfflineQueue: false, + enableReadyCheck: true, }; if (appConfig.redisTls) { diff --git a/packages/backend/src/controllers/webhooks/handler.ts b/packages/backend/src/controllers/webhooks/handler.ts index 72329ad..ed8684d 100644 --- a/packages/backend/src/controllers/webhooks/handler.ts +++ b/packages/backend/src/controllers/webhooks/handler.ts @@ -13,20 +13,21 @@ export default async (request: IRequest, response: Response) => { .findById(request.params.flowId) .throwIfNotFound(); - if (!flow.active) { - return response.send(404); - } - + const testRun = !flow.active; 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 (testRun && !isWebhookApp) { + return response.sendStatus(404); } - const app = await triggerStep.getApp(); + if (triggerCommand.type !== 'webhook') { + return response.sendStatus(404); + } - if (app.auth.verifyWebhook) { + if (app.auth?.verifyWebhook) { const $ = await globalVariable({ flow, connection: await triggerStep.$relatedQuery('connection'), @@ -42,10 +43,25 @@ export default async (request: IRequest, response: Response) => { } } + // in case trigger type is 'webhook' + let payload = request.body; + let rawInternalId: string | Buffer = request.rawBody; + + // in case it's our built-in generic webhook trigger + if (isWebhookApp) { + payload = { + headers: request.headers, + body: request.body, + query: request.query, + } + + rawInternalId = JSON.stringify(payload); + } + const triggerItem: ITriggerItem = { - raw: request.body, + raw: payload, meta: { - internalId: await bcrypt.hash(request.rawBody, 1), + internalId: await bcrypt.hash(rawInternalId, 1), }, }; @@ -53,8 +69,13 @@ export default async (request: IRequest, response: Response) => { flowId: flow.id, stepId: triggerStep.id, triggerItem, + testRun }); + if (testRun) { + return response.sendStatus(200); + } + const nextStep = await triggerStep.getNextStep(); const jobName = `${executionId}-${nextStep.id}`; diff --git a/packages/backend/src/db/migrations/20221214184855_add_remote_webhook_id_in_flow.ts b/packages/backend/src/db/migrations/20221214184855_add_remote_webhook_id_in_flow.ts new file mode 100644 index 0000000..67a086a --- /dev/null +++ b/packages/backend/src/db/migrations/20221214184855_add_remote_webhook_id_in_flow.ts @@ -0,0 +1,13 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + return knex.schema.table('flows', (table) => { + table.string('remote_webhook_id'); + }); +} + +export async function down(knex: Knex): Promise { + return knex.schema.table('flows', (table) => { + table.dropColumn('remote_webhook_id'); + }); +} diff --git a/packages/backend/src/graphql/mutations/update-flow-status.ts b/packages/backend/src/graphql/mutations/update-flow-status.ts index 1cb80d5..99a504c 100644 --- a/packages/backend/src/graphql/mutations/update-flow-status.ts +++ b/packages/backend/src/graphql/mutations/update-flow-status.ts @@ -49,9 +49,9 @@ const updateFlowStatus = async ( testRun: false, }); - if (flow.active) { + if (flow.active && trigger.registerHook) { await trigger.registerHook($); - } else { + } else if (!flow.active && trigger.unregisterHook) { await trigger.unregisterHook($); } } else { diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index bd0c39b..a33d6f0 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 @@ -380,6 +381,7 @@ type Trigger { key: String description: String pollInterval: Int + type: String substeps: [TriggerSubstep] } diff --git a/packages/backend/src/helpers/check-worker-readiness.ts b/packages/backend/src/helpers/check-worker-readiness.ts new file mode 100644 index 0000000..8101192 --- /dev/null +++ b/packages/backend/src/helpers/check-worker-readiness.ts @@ -0,0 +1,11 @@ +import Redis from 'ioredis'; +import logger from './logger'; +import redisConfig from '../config/redis'; + +const redisClient = new Redis(redisConfig); + +redisClient.on('ready', () => { + logger.info(`Workers are ready!`); + + redisClient.disconnect(); +}); diff --git a/packages/backend/src/helpers/delay-as-milliseconds.ts b/packages/backend/src/helpers/delay-as-milliseconds.ts new file mode 100644 index 0000000..3b791a9 --- /dev/null +++ b/packages/backend/src/helpers/delay-as-milliseconds.ts @@ -0,0 +1,25 @@ +import Step from '../models/step'; +import delayForAsMilliseconds, { + TDelayForUnit, +} from './delay-for-as-milliseconds'; +import delayUntilAsMilliseconds from './delay-until-as-milliseconds'; + +const delayAsMilliseconds = (step: Step) => { + let delayDuration = 0; + + if (step.key === 'delayFor') { + const { delayForUnit, delayForValue } = step.parameters; + + delayDuration = delayForAsMilliseconds( + delayForUnit as TDelayForUnit, + Number(delayForValue) + ); + } else if (step.key === 'delayUntil') { + const { delayUntil } = step.parameters; + delayDuration = delayUntilAsMilliseconds(delayUntil as string); + } + + return delayDuration; +}; + +export default delayAsMilliseconds; diff --git a/packages/backend/src/helpers/delay-for-as-milliseconds.ts b/packages/backend/src/helpers/delay-for-as-milliseconds.ts new file mode 100644 index 0000000..582d1b1 --- /dev/null +++ b/packages/backend/src/helpers/delay-for-as-milliseconds.ts @@ -0,0 +1,21 @@ +export type TDelayForUnit = 'minutes' | 'hours' | 'days' | 'weeks'; + +const delayAsMilliseconds = ( + delayForUnit: TDelayForUnit, + delayForValue: number +) => { + switch (delayForUnit) { + case 'minutes': + return delayForValue * 60 * 1000; + case 'hours': + return delayForValue * 60 * 60 * 1000; + case 'days': + return delayForValue * 24 * 60 * 60 * 1000; + case 'weeks': + return delayForValue * 7 * 24 * 60 * 60 * 1000; + default: + return 0; + } +}; + +export default delayAsMilliseconds; diff --git a/packages/backend/src/helpers/delay-until-as-milliseconds.ts b/packages/backend/src/helpers/delay-until-as-milliseconds.ts new file mode 100644 index 0000000..4c1da96 --- /dev/null +++ b/packages/backend/src/helpers/delay-until-as-milliseconds.ts @@ -0,0 +1,8 @@ +const delayUntilAsMilliseconds = (delayUntil: string) => { + const delayUntilDate = new Date(delayUntil); + const now = new Date(); + + return delayUntilDate.getTime() - now.getTime(); +}; + +export default delayUntilAsMilliseconds; diff --git a/packages/backend/src/helpers/global-variable.ts b/packages/backend/src/helpers/global-variable.ts index de9a09e..ed4dd96 100644 --- a/packages/backend/src/helpers/global-variable.ts +++ b/packages/backend/src/helpers/global-variable.ts @@ -37,6 +37,7 @@ const globalVariable = async ( testRun = false, } = options; + const isTrigger = step?.isTrigger; const lastInternalId = testRun ? undefined : await flow?.lastInternalId(); const nextStep = await step?.getNextStep(); @@ -77,6 +78,7 @@ const globalVariable = async ( id: execution?.id, testRun, }, + lastExecutionStep: (await step?.getLastExecutionStep())?.toJSON(), triggerOutput: { data: [], }, @@ -122,6 +124,18 @@ const globalVariable = async ( $.webhookUrl = webhookUrl; } + if (isTrigger && (await step.getTriggerCommand()).type === 'webhook') { + $.flow.setRemoteWebhookId = async (remoteWebhookId) => { + await flow.$query().patchAndFetch({ + remoteWebhookId, + }); + + $.flow.remoteWebhookId = remoteWebhookId; + }; + + $.flow.remoteWebhookId = flow.remoteWebhookId; + } + const lastInternalIds = testRun || (flow && step.isAction) ? [] : await flow?.lastInternalIds(2000); diff --git a/packages/backend/src/models/flow.ts b/packages/backend/src/models/flow.ts index bb4015e..5577dd3 100644 --- a/packages/backend/src/models/flow.ts +++ b/packages/backend/src/models/flow.ts @@ -13,6 +13,7 @@ class Flow extends Base { active: boolean; steps: Step[]; published_at: string; + remoteWebhookId: string; executions?: Execution[]; static tableName = 'flows'; @@ -25,6 +26,7 @@ class Flow extends Base { id: { type: 'string', format: 'uuid' }, name: { type: 'string', minLength: 1 }, userId: { type: 'string', format: 'uuid' }, + remoteWebhookId: { type: 'string' }, active: { type: 'boolean' }, }, }; diff --git a/packages/backend/src/models/step.ts b/packages/backend/src/models/step.ts index 719fbbf..294671a 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 c26f4ad..e43aba1 100644 --- a/packages/backend/src/routes/webhooks.ts +++ b/packages/backend/src/routes/webhooks.ts @@ -3,6 +3,9 @@ import webhookHandler from '../controllers/webhooks/handler'; const router = Router(); +router.get('/:flowId', webhookHandler); +router.put('/:flowId', webhookHandler); +router.patch('/:flowId', webhookHandler); router.post('/:flowId', webhookHandler); export default router; diff --git a/packages/backend/src/worker.ts b/packages/backend/src/worker.ts index a69b069..6a6358e 100644 --- a/packages/backend/src/worker.ts +++ b/packages/backend/src/worker.ts @@ -1,4 +1,5 @@ import './config/orm'; +import './helpers/check-worker-readiness'; import './workers/flow'; import './workers/trigger'; import './workers/action'; diff --git a/packages/backend/src/workers/action.ts b/packages/backend/src/workers/action.ts index 80dd284..cf16d42 100644 --- a/packages/backend/src/workers/action.ts +++ b/packages/backend/src/workers/action.ts @@ -4,7 +4,11 @@ import logger from '../helpers/logger'; import Step from '../models/step'; import actionQueue from '../queues/action'; import { processAction } from '../services/action'; -import { REMOVE_AFTER_30_DAYS_OR_150_JOBS, REMOVE_AFTER_7_DAYS_OR_50_JOBS } from '../helpers/remove-job-configuration'; +import { + REMOVE_AFTER_30_DAYS_OR_150_JOBS, + REMOVE_AFTER_7_DAYS_OR_50_JOBS, +} from '../helpers/remove-job-configuration'; +import delayAsMilliseconds from '../helpers/delay-as-milliseconds'; type JobData = { flowId: string; @@ -12,6 +16,8 @@ type JobData = { stepId: string; }; +const DEFAULT_DELAY_DURATION = 0; + export const worker = new Worker( 'action', async (job) => { @@ -35,6 +41,11 @@ export const worker = new Worker( const jobOptions = { removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS, removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS, + delay: DEFAULT_DELAY_DURATION, + }; + + if (step.appKey === 'delay') { + jobOptions.delay = delayAsMilliseconds(step); } await actionQueue.add(jobName, jobPayload, jobOptions); diff --git a/packages/cli/package.json b/packages/cli/package.json index bb86f98..2917c69 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@automatisch/cli", - "version": "0.2.0", + "version": "0.3.0", "license": "AGPL-3.0", "description": "The open source Zapier alternative. Build workflow automation without spending time and money.", "contributors": [ @@ -33,7 +33,7 @@ "version": "oclif readme && git add README.md" }, "dependencies": { - "@automatisch/backend": "^0.2.0", + "@automatisch/backend": "^0.3.0", "@oclif/core": "^1", "@oclif/plugin-help": "^5", "@oclif/plugin-plugins": "^2.0.1", diff --git a/packages/cli/yarn.lock b/packages/cli/yarn.lock index 0e34a07..be07262 100644 --- a/packages/cli/yarn.lock +++ b/packages/cli/yarn.lock @@ -6463,10 +6463,10 @@ decimal.js@^10.2.1: resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.1.tgz#be75eeac4a2281aace80c1a8753587c27ef053e7" integrity sha512-F29o+vci4DodHYT9UrR5IEbfBw9pE5eSapIJdTqXK5+6hq+t8VRxwQyKlW2i+KDKFkkJQRvFyI/QXD83h8LyQw== -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og== +decode-uri-component@0.2.2, decode-uri-component@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== decompress-response@^6.0.0: version "6.0.0" diff --git a/packages/docs/package.json b/packages/docs/package.json index 959a683..85dab65 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -1,6 +1,6 @@ { "name": "@automatisch/docs", - "version": "0.2.0", + "version": "0.3.0", "license": "AGPL-3.0", "description": "The open source Zapier alternative. Build workflow automation without spending time and money.", "private": true, diff --git a/packages/docs/pages/.vitepress/config.js b/packages/docs/pages/.vitepress/config.js index 1841d88..b736150 100644 --- a/packages/docs/pages/.vitepress/config.js +++ b/packages/docs/pages/.vitepress/config.js @@ -41,6 +41,15 @@ export default defineConfig({ { text: 'Connection', link: '/apps/deepl/connection' }, ], }, + { + text: 'Delay', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/delay/actions' }, + { text: 'Connection', link: '/apps/delay/connection' }, + ], + }, { text: 'Discord', collapsible: true, @@ -69,6 +78,15 @@ export default defineConfig({ { text: 'Connection', link: '/apps/github/connection' }, ], }, + { + text: 'Ntfy', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/ntfy/actions' }, + { text: 'Connection', link: '/apps/ntfy/connection' }, + ], + }, { text: 'RSS', collapsible: true, @@ -123,6 +141,15 @@ export default defineConfig({ { text: 'Connection', link: '/apps/stripe/connection' }, ], }, + { + text: 'Telegram', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/telegram-bot/actions' }, + { text: 'Connection', link: '/apps/telegram-bot/connection' }, + ], + }, { text: 'Twilio', collapsible: true, @@ -152,6 +179,15 @@ export default defineConfig({ { text: 'Connection', link: '/apps/typeform/connection' }, ], }, + { + text: 'Webhooks', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/webhooks/triggers' }, + { text: 'Connection', link: '/apps/webhooks/connection' }, + ], + }, ], '/': [ { diff --git a/packages/docs/pages/.vitepress/theme/CustomLayout.vue b/packages/docs/pages/.vitepress/theme/CustomLayout.vue new file mode 100644 index 0000000..a2d3914 --- /dev/null +++ b/packages/docs/pages/.vitepress/theme/CustomLayout.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/packages/docs/pages/.vitepress/theme/custom.css b/packages/docs/pages/.vitepress/theme/custom.css index 98e5409..8ac3296 100644 --- a/packages/docs/pages/.vitepress/theme/custom.css +++ b/packages/docs/pages/.vitepress/theme/custom.css @@ -117,9 +117,33 @@ :root { overflow-y: scroll; + + --announcement-bar-height: 50px; } .VPTeamMembersItem .avatar-img { top: 50%; transform: translateY(-50%); } + +header.VPNav { + margin-top: 50px; +} + +.VPNavScreen.VPNavScreen { + top: calc(var(--announcement-bar-height) + var(--vp-nav-height-mobile)); +} + +.VPLocalNav.VPLocalNav { + top: 50px; +} + +aside.VPSidebar { + margin-top: 50px; +} + +@media (min-width: 960px) { + #VPContent { + margin-top: 50px; + } +} diff --git a/packages/docs/pages/.vitepress/theme/index.js b/packages/docs/pages/.vitepress/theme/index.js index d4dbe3d..75eaece 100644 --- a/packages/docs/pages/.vitepress/theme/index.js +++ b/packages/docs/pages/.vitepress/theme/index.js @@ -1,3 +1,8 @@ import DefaultTheme from 'vitepress/theme'; import './custom.css'; -export default DefaultTheme; +import CustomLayout from './CustomLayout.vue'; + +export default { + ...DefaultTheme, + Layout: CustomLayout, +}; diff --git a/packages/docs/pages/advanced/configuration.md b/packages/docs/pages/advanced/configuration.md index 88d90ba..c861f7c 100644 --- a/packages/docs/pages/advanced/configuration.md +++ b/packages/docs/pages/advanced/configuration.md @@ -11,28 +11,31 @@ The default values for some environment variables might be different in our deve ::: :::danger -Please be careful with the `ENCRYPTION_KEY` environment variable. It is used to encrypt your credentials from third-party services. If you change it, you will not be able to access your connections and thus, your existing flows and connections will be useless. +Please be careful with the `ENCRYPTION_KEY` and `WEBHOOK_SECRET_KEY` environment variables. They are used to encrypt your credentials from third-party services and verify webhook requests. If you change them, your existing connections and flows will not continue to work. ::: -| Variable Name | Type | Default Value | Description | -| --------------------------- | ------- | ------------------ | ----------------------------------- | -| `HOST` | string | `localhost` | HTTP Host | -| `PROTOCOL` | string | `http` | HTTP Protocol | -| `PORT` | string | `3000` | HTTP Port | -| `APP_ENV` | string | `production` | Automatisch Environment | -| `POSTGRES_DATABASE` | string | `automatisch` | Database Name | -| `POSTGRES_PORT` | number | `5432` | Database Port | -| `POSTGRES_HOST` | string | `postgres` | Database Host | -| `POSTGRES_USERNAME` | string | `automatisch_user` | Database User | -| `POSTGRES_PASSWORD` | string | | Password of Database User | -| `ENCRYPTION_KEY` | string | | Encryption Key to store credentials | -| `APP_SECRET_KEY` | string | | Secret Key to authenticate the user | -| `REDIS_HOST` | string | `redis` | Redis Host | -| `REDIS_PORT` | number | `6379` | Redis Port | -| `REDIS_USERNAME` | string | `` | Redis Username | -| `REDIS_PASSWORD` | string | `` | Redis Password | -| `REDIS_TLS` | boolean | `false` | Redis TLS | -| `TELEMETRY_ENABLED` | boolean | `true` | Enable/Disable Telemetry | -| `ENABLE_BULLMQ_DASHBOARD` | boolean | `false` | Enable BullMQ Dashboard | -| `BULLMQ_DASHBOARD_USERNAME` | string | | Username to login BullMQ Dashboard | -| `BULLMQ_DASHBOARD_PASSWORD` | string | | Password to login BullMQ Dashboard | +| Variable Name | Type | Default Value | Description | +| --------------------------- | ------- | ------------------ | ---------------------------------------------------- | +| `HOST` | string | `localhost` | HTTP Host | +| `PROTOCOL` | string | `http` | HTTP Protocol | +| `PORT` | string | `3000` | HTTP Port | +| `APP_ENV` | string | `production` | Automatisch Environment | +| `WEB_APP_URL` | string | | Can be used to override connection URLs and CORS URL | +| `WEBHOOK_URL` | string | | Can be used to override webhook URL | +| `POSTGRES_DATABASE` | string | `automatisch` | Database Name | +| `POSTGRES_PORT` | number | `5432` | Database Port | +| `POSTGRES_HOST` | string | `postgres` | Database Host | +| `POSTGRES_USERNAME` | string | `automatisch_user` | Database User | +| `POSTGRES_PASSWORD` | string | | Password of Database User | +| `ENCRYPTION_KEY` | string | | Encryption Key to store credentials | +| `WEBHOOK_SECRET_KEY` | string | | Webhook Secret Key to verify webhook requests | +| `APP_SECRET_KEY` | string | | Secret Key to authenticate the user | +| `REDIS_HOST` | string | `redis` | Redis Host | +| `REDIS_PORT` | number | `6379` | Redis Port | +| `REDIS_USERNAME` | string | | Redis Username | +| `REDIS_PASSWORD` | string | | Redis Password | +| `REDIS_TLS` | boolean | `false` | Redis TLS | +| `TELEMETRY_ENABLED` | boolean | `true` | Enable/Disable Telemetry | +| `ENABLE_BULLMQ_DASHBOARD` | boolean | `false` | Enable BullMQ Dashboard | +| `BULLMQ_DASHBOARD_USERNAME` | string | | Username to login BullMQ Dashboard | +| `BULLMQ_DASHBOARD_PASSWORD` | string | | Password to login BullMQ Dashboard | diff --git a/packages/docs/pages/advanced/credentials.md b/packages/docs/pages/advanced/credentials.md index f1223ac..e30a88a 100644 --- a/packages/docs/pages/advanced/credentials.md +++ b/packages/docs/pages/advanced/credentials.md @@ -5,5 +5,5 @@ We need to store your credentials in order to automatically communicate with thi Automatisch uses AES specification to encrypt and decrypt your credentials of third-party services. The Advanced Encryption Standard (AES) is a U.S. Federal Information Processing Standard (FIPS). It was selected after a 5-year process where 15 competing designs were evaluated. AES is now used worldwide to protect sensitive information. :::danger -Please be careful with the `ENCRYPTION_KEY` environment variable. It is used to encrypt your credentials from third-party services. If you change it, you will not be able to access your connections and thus, your existing flows and connections will be useless. +Please be careful with the `ENCRYPTION_KEY` and `WEBHOOK_SECRET_KEY` environment variables. They are used to encrypt your credentials from third-party services and verify webhook requests. If you change them, your existing connections and flows will not continue to work. ::: diff --git a/packages/docs/pages/apps/delay/actions.md b/packages/docs/pages/apps/delay/actions.md new file mode 100644 index 0000000..00e1495 --- /dev/null +++ b/packages/docs/pages/apps/delay/actions.md @@ -0,0 +1,14 @@ +--- +favicon: /favicons/delay.svg +items: + - name: Delay For + desc: Delays the execution of the next action by a specified amount of time. + - name: Delay Until + desc: Delays the execution of the next action until a specified date. +--- + + + + diff --git a/packages/docs/pages/apps/delay/connection.md b/packages/docs/pages/apps/delay/connection.md new file mode 100644 index 0000000..a2eec35 --- /dev/null +++ b/packages/docs/pages/apps/delay/connection.md @@ -0,0 +1,3 @@ +# Delay + +Delay is a built-in app shipped with Automatisch, and it doesn't need to talk with any other external service to run. So there are no additional steps to use the Delay app. It can be used only as an action and it delays the execution of the next action by a specified amount of time. diff --git a/packages/docs/pages/apps/ntfy/actions.md b/packages/docs/pages/apps/ntfy/actions.md new file mode 100644 index 0000000..efc1bfb --- /dev/null +++ b/packages/docs/pages/apps/ntfy/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/ntfy.svg +items: + - name: Send a message + desc: Sends a message to a topic you specify. +--- + + + + diff --git a/packages/docs/pages/apps/ntfy/connection.md b/packages/docs/pages/apps/ntfy/connection.md new file mode 100644 index 0000000..d2d81ac --- /dev/null +++ b/packages/docs/pages/apps/ntfy/connection.md @@ -0,0 +1,10 @@ +# Ntfy + +:::info +This page explains the steps you need to follow to set up the Ntfy +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +If you use ntfy.sh, the official public server for this service, you do not need to set up a connection with a custom configuration. It's enough to create one with the default server URL. + +However, if you have a ntfy installation, that's different than ntfy.sh, you need to specify your server URL on Automatisch while creating a connection. Additionally, you may need to provide your username and password if your installation requires authentication. diff --git a/packages/docs/pages/apps/telegram-bot/actions.md b/packages/docs/pages/apps/telegram-bot/actions.md new file mode 100644 index 0000000..261d241 --- /dev/null +++ b/packages/docs/pages/apps/telegram-bot/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/telegram-bot.svg +items: + - name: Send a message + desc: Sends a message to a chat you specify. +--- + + + + diff --git a/packages/docs/pages/apps/telegram-bot/connection.md b/packages/docs/pages/apps/telegram-bot/connection.md new file mode 100644 index 0000000..4a015cc --- /dev/null +++ b/packages/docs/pages/apps/telegram-bot/connection.md @@ -0,0 +1,14 @@ +# Telegram + +:::info +This page explains the steps you need to follow to set up the Telegram +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Start a chat with [Botfather](https://telegram.me/BotFather). +1. Enter `/newbot`. +1. Enter a name for your bot. +1. Enter a username for your bot. +1. Copy the **token** value from the answer to the **Bot token** field on Automatisch. +1. Click **Submit** button on Automatisch. +1. Congrats! Start using your new Telegram connection within the flows. diff --git a/packages/docs/pages/apps/webhooks/connection.md b/packages/docs/pages/apps/webhooks/connection.md new file mode 100644 index 0000000..a38b7e9 --- /dev/null +++ b/packages/docs/pages/apps/webhooks/connection.md @@ -0,0 +1,7 @@ +# Webhooks + +Webhooks is a built-in app shipped with Automatisch, and it doesn't need to authenticate with any other external service to run. + +## How to use + +You will be given a webhook URL in the test substep on the editor page, and you can use it to send a GET, POST, PUT, or PATCH request to Automatisch to trigger the flow. diff --git a/packages/docs/pages/apps/webhooks/triggers.md b/packages/docs/pages/apps/webhooks/triggers.md new file mode 100644 index 0000000..9dda3c3 --- /dev/null +++ b/packages/docs/pages/apps/webhooks/triggers.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/webhooks.svg +items: + - name: Catch raw webhook + desc: Triggers when the webhook receives a request. +--- + + + + diff --git a/packages/docs/pages/build-integrations/examples.md b/packages/docs/pages/build-integrations/examples.md index 4a957a1..308be03 100644 --- a/packages/docs/pages/build-integrations/examples.md +++ b/packages/docs/pages/build-integrations/examples.md @@ -49,6 +49,10 @@ The build integrations section is best understood when read from beginning to en ### Webhook-based triggers +:::warning +If you are developing a webhook-based trigger, you need to ensure that the webhook is publicly accessible. You can use [ngrok](https://ngrok.com) for this purpose and override the webhook URL by setting the **WEBHOOK_URL** environment variable. +::: + - [New entry - Typeform](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/typeform/triggers/new-entry/index.ts) ### Pagination with descending order diff --git a/packages/docs/pages/guide/available-apps.md b/packages/docs/pages/guide/available-apps.md index 5697771..2edd2db 100644 --- a/packages/docs/pages/guide/available-apps.md +++ b/packages/docs/pages/guide/available-apps.md @@ -7,14 +7,19 @@ We just have a few available integrations at the moment and we also know that wo Following integrations are currently supported by Automatisch. - [DeepL](/apps/deepl/actions) +- [Delay](/apps/delay/actions) - [Discord](/apps/discord/actions) - [Flickr](/apps/flickr/triggers) - [Github](/apps/github/triggers) - [RSS](/apps/rss/triggers) +- [Ntfy](/apps/ntfy/triggers) - [Salesforce](/apps/salesforce/triggers) - [Scheduler](/apps/scheduler/triggers) - [Slack](/apps/slack/actions) - [SMTP](/apps/smtp/actions) - [Stripe](/apps/stripe/triggers) +- [Telegram](/apps/telegram-bot/actions) - [Twilio](/apps/twilio/triggers) - [Twitter](/apps/twitter/triggers) +- [Typeform](/apps/typeform/triggers) +- [Webhooks](/apps/webhooks/triggers) diff --git a/packages/docs/pages/guide/create-flow.md b/packages/docs/pages/guide/create-flow.md index a099036..4f7e849 100644 --- a/packages/docs/pages/guide/create-flow.md +++ b/packages/docs/pages/guide/create-flow.md @@ -1 +1,53 @@ -TBD +# Create Flow + +To understand how we can create a flow, it's better to start with a real use case. Let's say we want to create a flow that will fetch new submissions from Typeform and then send them to a Slack channel. To do that, we will use [Typeform](/apps/typeform/triggers) and [Slack](/apps/slack/actions) apps. Let's start with creating connections for these apps. + +## Typeform connection + +- Go to the **My Apps** page in Automatisch and click on **Add connection** button. +- Select the **Typeform** app from the list. +- It will ask you `Client ID` and `Client Secret` from Typeform and there is an information box above the fields. +- Click on **our documentation** link in the information box and follow the instructions to get the `Client ID` and `Client Secret` from Typeform. + +:::tip +Whenever you want to create a connection for an app, you can click on **our documentation** link in the information box to learn how to create a connection for that specific app. +::: + +- After you get the `Client ID` and `Client Secret` from Typeform, you can paste them to the fields in Automatisch and click on **Submit** button. + +## Slack connection + +- Go to the **My Apps** page in Automatisch and click on **Add connection** button. +- Select the **Slack** app from the list. +- It will ask you `API Key` and `API Secret` values from Slack and there is an information box above the fields. +- Click on **our documentation** link in the information box and follow the instructions to get the `API Key` and `API Secret` from Slack. +- After you get the `API Key` and `API Secret` from Slack, you can paste them into the fields in Automatisch and click on **Submit** button. + +## Build the flow + +### Trigger step + +- Go to the **Flows** page in Automatisch and click on **Create flow** button. +- It will give you empty trigger and action steps. +- For the trigger step (1st step), select the **Typeform** app from `Choose an app` dropdown. +- Select the **New entry** as the trigger event and click on the **Continue** button. +- It will ask you to select the connection you created for the Typeform app. Select the connection you have just created and click on the **Continue** button. +- Select the form you want to get the new entries from and click on the **Continue** button. +- Click on **Test & Continue** button to test the trigger step. If you see the data that reflects the recent submission in the form, you can continue to the next (action) step. + +### Action step + +- For the action step (2nd step), select the **Slack** app from `Choose an app` dropdown. +- Select the **Send a message to channel** as the action event and click on the **Continue** button. +- It will ask you to select the connection you created for the Slack app. Select the connection you have just created and click on the **Continue** button. +- Select the channel you want to send the message to. +- Write the message you want to send to the channel. You can use variables in the message from the trigger step. +- Select `Yes` for the `Send as a bot` option. +- Give a name for the bot and click on the **Continue** button. +- Click on **Test & Continue** button to test the action step. If you see the message in the Slack channel you selected, we can say that the flow is working as expected and is ready to be published. + +### Publish the flow + +- Click on the **Publish** button to publish the flow. +- Published flows will be executed automatically when the trigger event happens or at intervals of 15 minutes depending on the trigger type. +- You can not change the flow after it's published. If you want to change the flow, you need to unpublish it first and then make the changes. diff --git a/packages/docs/pages/guide/installation.md b/packages/docs/pages/guide/installation.md index 376a71b..91fa423 100644 --- a/packages/docs/pages/guide/installation.md +++ b/packages/docs/pages/guide/installation.md @@ -1,6 +1,20 @@ # Installation -You can install Automatisch by using docker compose. +:::info +We have installation guides for docker compose and docker setup at the moment, but if you need another installation type, let us know by [creating a GitHub issue](https://github.com/automatisch/automatisch/issues/new). +::: + +:::tip + +You can use `user@automatisch.io` email address and `sample` password to login to Automatisch. Please do not forget to change your email and password from the settings page. + +::: + +:::danger +Please be careful with the `ENCRYPTION_KEY` and `WEBHOOK_SECRET_KEY` environment variables. They are used to encrypt your credentials from third-party services and verify webhook requests. If you change them, your existing connections and flows will not continue to work. +::: + +## Docker Compose ```bash # Clone the repository @@ -10,17 +24,72 @@ git clone git@github.com:automatisch/automatisch.git cd automatisch # Start -docker compose -p automatisch up +docker compose up ``` -## Let's discover! +✌️ That's it; you have Automatisch running. Let's check it out by browsing [http://localhost:3000](https://localhost:3000) -✌️ That's it; you have Automatisch running. Let's check it out by browsing `http://localhost:3000` +## Docker -:::tip +Automatisch comes with two services which are `main` and `worker`. They both use the same image and need to have the same environment variables except for the `WORKER` environment variable which is set to `true` for the worker service. -You can use `user@automatisch.io` email address and `sample` password to login to Automatisch. You can also change your email and password later on from the settings page. +::: warning +We give the sample environment variable files for the setup but you should adjust them to include your own values. +::: + +To run the main: + +```bash +docker run --env-file=./.env automatischio/automatisch +``` + +To run the worker: + +```bash +docker run --env-file=./.env -e WORKER=true automatischio/automatisch +``` + +::: details .env + +```bash +APP_ENV=production +HOST= +PROTOCOL= +PORT= +ENCRYPTION_KEY= +WEBHOOK_SECRET_KEY= +APP_SECRET_KEY= +POSTGRES_HOST= +POSTGRES_PORT= +POSTGRES_DATABASE= +POSTGRES_USERNAME= +POSTGRES_PASSWORD= +POSTGRES_ENABLE_SSL= +REDIS_HOST= +REDIS_PORT= +REDIS_USERNAME= +REDIS_PASSWORD= +REDIS_TLS= +``` ::: +## Render + + + Deploy to Render + + +:::info + +We use default values of render plans with the `render.yaml` file, if you want to use the free plan or change the plan, you can change the `render.yaml` file in your fork and use your repository URL while creating a blueprint in Render. + +::: + +## Production setup + +If you need to change any other environment variables for your production setup, let's check out the [environment variables](/advanced/configuration#environment-variables) section of the configuration page. + +## Let's discover! + If you see any problems while installing Automatisch, let us know via [github issues](https://github.com/automatisch/automatisch/issues) or our [discord server](https://discord.gg/dJSah9CVrC). diff --git a/packages/docs/pages/index.md b/packages/docs/pages/index.md index 437ff62..864a34f 100644 --- a/packages/docs/pages/index.md +++ b/packages/docs/pages/index.md @@ -28,7 +28,7 @@ You need to prepare the workflow once, and it will run continuously until you st There are other existing solutions in the market, like Zapier and Integromat, so you might be wondering why you should use Automatisch. -✅ The most significant advantage of having Automatisch is **keeping your data on your own servers**. Not all companies want to use an automation service in the cloud, and the current open-source or self-hosted solutions mainly focus on developers rather than a user without a technical background. +✅ One of the main benefits of using Automatisch is that it allows you to **store your data on your own servers**, which is essential for businesses that handle sensitive user information and cannot risk sharing it with external cloud services. This is especially relevant for industries such as healthcare and finance, as well as for European companies that must adhere to the General Data Protection Regulation (GDPR). 🤓 Your contributions are vital to the development of Automatisch. As an **open-source software**, anyone can have an impact on how it is being developed. diff --git a/packages/docs/pages/public/favicons/delay.svg b/packages/docs/pages/public/favicons/delay.svg new file mode 100644 index 0000000..af5da4d --- /dev/null +++ b/packages/docs/pages/public/favicons/delay.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/docs/pages/public/favicons/ntfy.svg b/packages/docs/pages/public/favicons/ntfy.svg new file mode 100644 index 0000000..9e5b513 --- /dev/null +++ b/packages/docs/pages/public/favicons/ntfy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/telegram-bot.svg b/packages/docs/pages/public/favicons/telegram-bot.svg new file mode 100644 index 0000000..8f16fb1 --- /dev/null +++ b/packages/docs/pages/public/favicons/telegram-bot.svg @@ -0,0 +1,14 @@ + + + Telegram + + + + + + + + + + + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/webhooks.svg b/packages/docs/pages/public/favicons/webhooks.svg new file mode 100644 index 0000000..894b13e --- /dev/null +++ b/packages/docs/pages/public/favicons/webhooks.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json index dbdee1c..10c1faf 100644 --- a/packages/e2e-tests/package.json +++ b/packages/e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "@automatisch/e2e-tests", - "version": "0.2.0", + "version": "0.3.0", "license": "AGPL-3.0", "private": true, "description": "The open source Zapier alternative. Build workflow automation without spending time and money.", diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index 0b2fb1f..7f56d0e 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; @@ -75,6 +76,7 @@ export interface IFlow { steps: IStep[]; createdAt: string; updatedAt: string; + remoteWebhookId: string; lastInternalId: () => Promise; } @@ -180,23 +182,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; @@ -285,6 +280,8 @@ export type IGlobalVariable = { id: string; lastInternalId: string; isAlreadyProcessed?: (internalId: string) => boolean; + remoteWebhookId?: string; + setRemoteWebhookId?: (remoteWebhookId: string) => Promise; }; step?: { id: string; @@ -300,6 +297,7 @@ export type IGlobalVariable = { id: string; testRun: boolean; }; + lastExecutionStep?: IExecutionStep; webhookUrl?: string; triggerOutput?: ITriggerOutput; actionOutput?: IActionOutput; diff --git a/packages/types/package.json b/packages/types/package.json index 0936b68..225f6eb 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@automatisch/types", - "version": "0.2.0", + "version": "0.3.0", "license": "AGPL-3.0", "description": "Type definitions for automatisch", "homepage": "https://github.com/automatisch/automatisch", diff --git a/packages/web/package.json b/packages/web/package.json index b7e91f7..2e7e780 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,11 +1,11 @@ { "name": "@automatisch/web", - "version": "0.2.0", + "version": "0.3.0", "license": "AGPL-3.0", "description": "The open source Zapier alternative. Build workflow automation without spending time and money.", "dependencies": { "@apollo/client": "^3.6.9", - "@automatisch/types": "^0.2.0", + "@automatisch/types": "^0.3.0", "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", "@hookform/resolvers": "^2.8.8", diff --git a/packages/web/src/components/AddAppConnection/index.tsx b/packages/web/src/components/AddAppConnection/index.tsx index 7b54e0f..e1581b3 100644 --- a/packages/web/src/components/AddAppConnection/index.tsx +++ b/packages/web/src/components/AddAppConnection/index.tsx @@ -6,22 +6,15 @@ import DialogContentText from '@mui/material/DialogContentText'; import Dialog from '@mui/material/Dialog'; import LoadingButton from '@mui/lab/LoadingButton'; import { FieldValues, SubmitHandler } from 'react-hook-form'; -import { IJSONObject } from '@automatisch/types'; +import type { IApp, IJSONObject, IField } from '@automatisch/types'; import useFormatMessage from 'hooks/useFormatMessage'; import computeAuthStepVariables from 'helpers/computeAuthStepVariables'; import { processStep } from 'helpers/authenticationSteps'; import InputCreator from 'components/InputCreator'; -import type { IApp, IField } from '@automatisch/types'; +import { generateExternalLink } from '../../helpers/translation-values'; import { Form } from './style'; -const generateDocsLink = (link: string) => (str: string) => - ( - - {str} - - ); - type AddAppConnectionProps = { onClose: (response: Record) => void; application: IApp; @@ -112,7 +105,7 @@ export default function AddAppConnection( {formatMessage('addAppConnection.callToDocs', { appName: name, - docsLink: generateDocsLink(authDocUrl), + docsLink: generateExternalLink(authDocUrl), })} )} @@ -123,7 +116,7 @@ export default function AddAppConnection( sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }} > {error.message} -
{JSON.stringify(error.details, null, 2)}
+ {error.details &&
{JSON.stringify(error.details, null, 2)}
} )} diff --git a/packages/web/src/components/AddNewAppConnection/index.tsx b/packages/web/src/components/AddNewAppConnection/index.tsx index 8b6cd30..bbd95d8 100644 --- a/packages/web/src/components/AddNewAppConnection/index.tsx +++ b/packages/web/src/components/AddNewAppConnection/index.tsx @@ -18,6 +18,7 @@ import ListItemText from '@mui/material/ListItemText'; import InputLabel from '@mui/material/InputLabel'; import OutlinedInput from '@mui/material/OutlinedInput'; import FormControl from '@mui/material/FormControl'; +import Box from '@mui/material/Box'; import type { IApp } from '@automatisch/types'; import * as URLS from 'config/urls'; @@ -76,7 +77,7 @@ export default function AddNewAppConnection( {formatMessage('apps.addNewAppConnection')} - + + + {loading && ( diff --git a/packages/web/src/components/AppRow/index.tsx b/packages/web/src/components/AppRow/index.tsx index f059c6c..0ac728d 100644 --- a/packages/web/src/components/AppRow/index.tsx +++ b/packages/web/src/components/AppRow/index.tsx @@ -25,11 +25,11 @@ const countTranslation = (value: React.ReactNode) => ( function AppRow(props: AppRowProps): React.ReactElement { const formatMessage = useFormatMessage(); - const { name, primaryColor, iconUrl, connectionCount, flowCount } = + const { name, key, primaryColor, iconUrl, connectionCount, flowCount } = props.application; return ( - + diff --git a/packages/web/src/components/ChooseAppAndEventSubstep/index.tsx b/packages/web/src/components/ChooseAppAndEventSubstep/index.tsx index fc964bb..534e2f1 100644 --- a/packages/web/src/components/ChooseAppAndEventSubstep/index.tsx +++ b/packages/web/src/components/ChooseAppAndEventSubstep/index.tsx @@ -7,6 +7,7 @@ import Collapse from '@mui/material/Collapse'; import ListItem from '@mui/material/ListItem'; import TextField from '@mui/material/TextField'; import Autocomplete from '@mui/material/Autocomplete'; +import Chip from '@mui/material/Chip'; import useFormatMessage from 'hooks/useFormatMessage'; import { EditorContext } from 'contexts/Editor'; @@ -38,8 +39,20 @@ const optionGenerator = (app: { value: app.key as string, }); -const getOption = (options: Record[], appKey?: IApp['key']) => - options.find((option) => option.value === (appKey as string)) || null; +const eventOptionGenerator = (app: { + name: string; + key: string; + type?: string; +}): { label: string; value: string; type: string } => ({ + label: app.name as string, + value: app.key as string, + type: app?.type as string, +}); + +const getOption = ( + options: T[], + selectedOptionValue?: string +) => options.find((option) => option.value === selectedOptionValue); function ChooseAppAndEventSubstep( props: ChooseAppAndEventSubstepProps @@ -72,14 +85,17 @@ function ChooseAppAndEventSubstep( ); const actionsOrTriggers: Array = (isTrigger ? app?.triggers : app?.actions) || []; - const actionOptions = React.useMemo( - () => actionsOrTriggers.map((trigger) => optionGenerator(trigger)), + const actionOrTriggerOptions = React.useMemo( + () => actionsOrTriggers.map((trigger) => eventOptionGenerator(trigger)), [app?.key] ); const selectedActionOrTrigger = actionsOrTriggers.find( (actionOrTrigger: IAction | ITrigger) => actionOrTrigger.key === step?.key ); + const isWebhook = + isTrigger && (selectedActionOrTrigger as ITrigger)?.type === 'webhook'; + const { name } = substep; const valid: boolean = !!step.key && !!step.appKey; @@ -177,14 +193,49 @@ function ChooseAppAndEventSubstep( disablePortal disableClearable disabled={editorContext.readOnly} - options={actionOptions} + options={actionOrTriggerOptions} renderInput={(params) => ( + {isWebhook && ( + + )} + + {params.InputProps.endAdornment} + + ), + }} /> )} - value={getOption(actionOptions, step.key)} + renderOption={(optionProps, option) => ( +
  • + {option.label} + + {option.type === 'webhook' && ( + + )} +
  • + )} + value={getOption(actionOrTriggerOptions, step.key)} onChange={onEventChange} data-test="choose-event-autocomplete" /> diff --git a/packages/web/src/components/ControlledAutocomplete/index.tsx b/packages/web/src/components/ControlledAutocomplete/index.tsx index d3f2905..c209afa 100644 --- a/packages/web/src/components/ControlledAutocomplete/index.tsx +++ b/packages/web/src/components/ControlledAutocomplete/index.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; -import FormHelperText from '@mui/material/FormHelperText'; import { Controller, useFormContext } from 'react-hook-form'; +import FormHelperText from '@mui/material/FormHelperText'; import Autocomplete, { AutocompleteProps } from '@mui/material/Autocomplete'; +import Typography from '@mui/material/Typography'; import type { IFieldDropdownOption } from '@automatisch/types'; interface ControlledAutocompleteProps @@ -9,6 +10,7 @@ interface ControlledAutocompleteProps shouldUnregister?: boolean; name: string; required?: boolean; + showOptionValue?: boolean; description?: string; dependsOn?: string[]; } @@ -31,6 +33,7 @@ function ControlledAutocomplete( description, options = [], dependsOn = [], + showOptionValue, ...autocompleteProps } = props; @@ -96,6 +99,19 @@ function ControlledAutocomplete( }} ref={ref} data-test={`${name}-autocomplete`} + renderOption={(optionProps, option) => ( +
  • + {option.label} + + {showOptionValue && ( + {option.value} + )} +
  • + )} /> {flow?.steps?.map((step, index, steps) => ( - + ))} diff --git a/packages/web/src/components/InputCreator/index.tsx b/packages/web/src/components/InputCreator/index.tsx index 0115816..f54d331 100644 --- a/packages/web/src/components/InputCreator/index.tsx +++ b/packages/web/src/components/InputCreator/index.tsx @@ -14,6 +14,7 @@ type InputCreatorProps = { namePrefix?: string; stepId?: string; disabled?: boolean; + showOptionValue?: boolean; }; type RawOption = { @@ -27,7 +28,15 @@ const optionGenerator = (options: RawOption[]): IFieldDropdownOption[] => export default function InputCreator( props: InputCreatorProps ): React.ReactElement { - const { onChange, onBlur, schema, namePrefix, stepId, disabled } = props; + const { + onChange, + onBlur, + schema, + namePrefix, + stepId, + disabled, + showOptionValue, + } = props; const { key: name, @@ -62,6 +71,7 @@ export default function InputCreator( description={description} loading={loading} disabled={disabled} + showOptionValue={showOptionValue} /> ); } diff --git a/packages/web/src/components/TestSubstep/index.tsx b/packages/web/src/components/TestSubstep/index.tsx index f2b895c..b7c3c76 100644 --- a/packages/web/src/components/TestSubstep/index.tsx +++ b/packages/web/src/components/TestSubstep/index.tsx @@ -9,8 +9,9 @@ import LoadingButton from '@mui/lab/LoadingButton'; import { EditorContext } from 'contexts/Editor'; import useFormatMessage from 'hooks/useFormatMessage'; -import JSONViewer from 'components/JSONViewer'; import { EXECUTE_FLOW } from 'graphql/mutations/execute-flow'; +import JSONViewer from 'components/JSONViewer'; +import WebhookUrlInfo from 'components/WebhookUrlInfo'; import FlowSubstepTitle from 'components/FlowSubstepTitle'; import type { IStep, ISubstep } from '@automatisch/types'; @@ -107,7 +108,7 @@ function TestSubstep(props: TestSubstepProps): React.ReactElement { {!!error?.graphQLErrors?.length && ( {serializeErrors(error.graphQLErrors).map((error: any) => (
    {error.message}
    @@ -115,6 +116,10 @@ function TestSubstep(props: TestSubstepProps): React.ReactElement {
    )} + {step.webhookUrl && ( + + )} + {hasNoOutput && ( diff --git a/packages/web/src/components/WebhookUrlInfo/index.tsx b/packages/web/src/components/WebhookUrlInfo/index.tsx new file mode 100644 index 0000000..82a1a3c --- /dev/null +++ b/packages/web/src/components/WebhookUrlInfo/index.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import Typography from '@mui/material/Typography'; +import type { AlertProps } from '@mui/material/Alert'; + +import { generateExternalLink } from '../../helpers/translation-values'; +import { WEBHOOK_DOCS } from '../../config/urls'; +import TextField from '../TextField'; +import { Alert } from './style'; + +type WebhookUrlInfoProps = { + webhookUrl: string; +} & AlertProps; + +function WebhookUrlInfo(props: WebhookUrlInfoProps): React.ReactElement { + const { webhookUrl, ...alertProps } = props; + + return ( + + + + + + + + + + + } + /> + + ); +} + +export default WebhookUrlInfo; diff --git a/packages/web/src/components/WebhookUrlInfo/style.ts b/packages/web/src/components/WebhookUrlInfo/style.ts new file mode 100644 index 0000000..b74a383 --- /dev/null +++ b/packages/web/src/components/WebhookUrlInfo/style.ts @@ -0,0 +1,14 @@ +import { styled } from '@mui/material/styles'; +import MuiAlert, { alertClasses } from '@mui/material/Alert'; + +export const Alert = styled(MuiAlert)(({ theme }) => ({ + [`&.${alertClasses.root}`]: { + fontWeight: 300, + width: '100%', + display: 'flex', + flexDirection: 'column' + }, + [`& .${alertClasses.message}`]: { + width: '100%' + } +})); diff --git a/packages/web/src/config/urls.ts b/packages/web/src/config/urls.ts index 27214fe..aeca5e8 100644 --- a/packages/web/src/config/urls.ts +++ b/packages/web/src/config/urls.ts @@ -65,3 +65,7 @@ export const UPDATES = '/updates'; export const SETTINGS_PROFILE = `${SETTINGS}/${PROFILE}`; export const DASHBOARD = FLOWS; + +// External links +export const WEBHOOK_DOCS = + 'https://automatisch.io/docs/apps/webhooks/connection'; diff --git a/packages/web/src/graphql/queries/get-app.ts b/packages/web/src/graphql/queries/get-app.ts index b0f49df..f9009c7 100644 --- a/packages/web/src/graphql/queries/get-app.ts +++ b/packages/web/src/graphql/queries/get-app.ts @@ -59,6 +59,7 @@ export const GET_APP = gql` triggers { name key + type pollInterval description substeps { diff --git a/packages/web/src/graphql/queries/get-apps.ts b/packages/web/src/graphql/queries/get-apps.ts index e643845..c6d1968 100644 --- a/packages/web/src/graphql/queries/get-apps.ts +++ b/packages/web/src/graphql/queries/get-apps.ts @@ -66,6 +66,7 @@ export const GET_APPS = gql` triggers { name key + type pollInterval description substeps { diff --git a/packages/web/src/graphql/queries/get-flow.ts b/packages/web/src/graphql/queries/get-flow.ts index 3434598..f3e8644 100644 --- a/packages/web/src/graphql/queries/get-flow.ts +++ b/packages/web/src/graphql/queries/get-flow.ts @@ -11,6 +11,8 @@ export const GET_FLOW = gql` type key appKey + iconUrl + webhookUrl status position connection { diff --git a/packages/web/src/helpers/translation-values.tsx b/packages/web/src/helpers/translation-values.tsx new file mode 100644 index 0000000..86e84ce --- /dev/null +++ b/packages/web/src/helpers/translation-values.tsx @@ -0,0 +1,6 @@ +export const generateExternalLink = (link: string) => (str: string) => + ( + + {str} + + ); diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index 4d79ee2..4512cfb 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -57,6 +57,7 @@ "flowEditor.pollIntervalValue": "Every {minutes} minutes", "flowEditor.triggerEvent": "Trigger event", "flowEditor.actionEvent": "Action event", + "flowEditor.instantTriggerType": "Instant", "chooseConnectionSubstep.continue": "Continue", "chooseConnectionSubstep.addNewConnection": "Add new connection", "chooseConnectionSubstep.chooseConnection": "Choose connection", @@ -87,5 +88,9 @@ "profileSettings.updatedPassword": "Your password has been updated.", "profileSettings.updatePassword": "Update password", "notifications.title": "Notifications", - "notification.releasedAt": "Released {relativeDate}" + "notification.releasedAt": "Released {relativeDate}", + "webhookUrlInfo.title": "Your webhook URL", + "webhookUrlInfo.description": "You'll need to configure your application with this webhook URL.", + "webhookUrlInfo.helperText": "We've generated a custom webhook URL for you to send requests to. Learn more about webhooks.", + "webhookUrlInfo.copy": "Copy" } \ No newline at end of file diff --git a/packages/web/yarn.lock b/packages/web/yarn.lock index f8265d2..38be911 100644 --- a/packages/web/yarn.lock +++ b/packages/web/yarn.lock @@ -4697,10 +4697,10 @@ decimal.js@^10.2.1: resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= +decode-uri-component@0.2.2, decode-uri-component@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== dedent@^0.7.0: version "0.7.0" diff --git a/render.yaml b/render.yaml new file mode 100644 index 0000000..ec7e421 --- /dev/null +++ b/render.yaml @@ -0,0 +1,113 @@ +services: + - type: web + name: automatisch-main + env: docker + dockerfilePath: ./docker/Dockerfile + dockerContext: ./docker + repo: https://github.com/automatisch/automatisch + autoDeploy: false + envVars: + - key: HOST + fromService: + name: automatisch-main + type: web + envVarKey: RENDER_EXTERNAL_HOSTNAME + - key: POSTGRES_HOST + fromDatabase: + name: automatisch-database + property: host + - key: POSTGRES_PORT + fromDatabase: + name: automatisch-database + property: port + - key: POSTGRES_DATABASE + fromDatabase: + name: automatisch-database + property: database + - key: POSTGRES_USERNAME + fromDatabase: + name: automatisch-database + property: user + - key: POSTGRES_PASSWORD + fromDatabase: + name: automatisch-database + property: password + - key: REDIS_HOST + fromService: + type: redis + name: automatisch-redis + property: host + - key: REDIS_PORT + fromService: + type: redis + name: automatisch-redis + property: port + - fromGroup: common-env-vars + - type: worker + name: automatisch-worker + env: docker + dockerfilePath: ./docker/Dockerfile + dockerContext: ./docker + repo: https://github.com/automatisch/automatisch + autoDeploy: false + envVars: + - key: WORKER + value: true + - key: HOST + fromService: + name: automatisch-main + type: web + envVarKey: RENDER_EXTERNAL_HOSTNAME + - key: POSTGRES_HOST + fromDatabase: + name: automatisch-database + property: host + - key: POSTGRES_PORT + fromDatabase: + name: automatisch-database + property: port + - key: POSTGRES_DATABASE + fromDatabase: + name: automatisch-database + property: database + - key: POSTGRES_USERNAME + fromDatabase: + name: automatisch-database + property: user + - key: POSTGRES_PASSWORD + fromDatabase: + name: automatisch-database + property: password + - key: REDIS_HOST + fromService: + type: redis + name: automatisch-redis + property: host + - key: REDIS_PORT + fromService: + type: redis + name: automatisch-redis + property: port + - fromGroup: common-env-vars + - type: redis + name: automatisch-redis + ipAllowList: [] # allow only internal connections + maxmemoryPolicy: noeviction +databases: + - name: automatisch-database + databaseName: automatisch +envVarGroups: + - name: common-env-vars + envVars: + - key: APP_ENV + value: production + - key: PROTOCOL + value: https + - key: PORT + value: 443 + - key: ENCRYPTION_KEY + generateValue: true + - key: WEBHOOK_SECRET_KEY + generateValue: true + - key: APP_SECRET_KEY + generateValue: true diff --git a/yarn.lock b/yarn.lock index deece01..0f83b38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4774,15 +4774,7 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -accepts@^1.3.7, accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: - version "1.3.7" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" - integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== - dependencies: - mime-types "~2.1.24" - negotiator "0.6.2" - -accepts@~1.3.8: +accepts@^1.3.7, accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== @@ -5661,38 +5653,6 @@ blueimp-md5@^2.10.0: resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.19.0.tgz#b53feea5498dcb53dc6ec4b823adb84b729c4af0" integrity sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w== -body-parser@1.18.3: - version "1.18.3" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4" - integrity sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ= - dependencies: - bytes "3.0.0" - content-type "~1.0.4" - debug "2.6.9" - depd "~1.1.2" - http-errors "~1.6.3" - iconv-lite "0.4.23" - on-finished "~2.3.0" - qs "6.5.2" - raw-body "2.3.3" - type-is "~1.6.16" - -body-parser@1.19.1: - version "1.19.1" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.1.tgz#1499abbaa9274af3ecc9f6f10396c995943e31d4" - integrity sha512-8ljfQi5eBk8EJfECMrgqNGWPEY5jWP+1IzkzkGdFFEwFQZZyaZ21UqdaHktgiMlH0xLHqIFtE/u2OYE5dOtViA== - dependencies: - bytes "3.1.1" - content-type "~1.0.4" - debug "2.6.9" - depd "~1.1.2" - http-errors "1.8.1" - iconv-lite "0.4.24" - on-finished "~2.3.0" - qs "6.9.6" - raw-body "2.4.2" - type-is "~1.6.18" - body-parser@1.19.2: version "1.19.2" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.2.tgz#4714ccd9c157d44797b8b5607d72c0b89952f26e" @@ -6602,11 +6562,6 @@ console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control- resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= -content-disposition@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" - integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ= - content-disposition@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -6718,16 +6673,6 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= -cookie@0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" - integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= - -cookie@0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" - integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== - cookie@0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" @@ -7194,10 +7139,10 @@ debug@2.6.9, debug@^2.6.0, debug@^2.6.9, debug@~2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3: - version "4.3.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" - integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" @@ -7215,13 +7160,6 @@ debug@^3.1.0, debug@^3.1.1, debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - debuglog@^1.0.0, debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -7246,9 +7184,9 @@ decimal.js@^10.2.1: integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og== + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== decompress-response@^3.3.0: version "3.3.0" @@ -8550,7 +8488,7 @@ express-graphql@^0.12.0: http-errors "1.8.0" raw-body "^2.4.1" -express@4.17.3: +express@4.17.3, express@^4.17.1, express@~4.17.3: version "4.17.3" resolved "https://registry.yarnpkg.com/express/-/express-4.17.3.tgz#f6c7302194a4fb54271b73a1fe7a06478c8f85a1" integrity sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg== @@ -8586,78 +8524,6 @@ express@4.17.3: utils-merge "1.0.1" vary "~1.1.2" -express@^4.17.1: - version "4.17.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.17.2.tgz#c18369f265297319beed4e5558753cc8c1364cb3" - integrity sha512-oxlxJxcQlYwqPWKVJJtvQiwHgosH/LrLSPA+H4UxpyvSS6jC5aH+5MoHFM+KABgTOt0APue4w66Ha8jCUo9QGg== - dependencies: - accepts "~1.3.7" - array-flatten "1.1.1" - body-parser "1.19.1" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.4.1" - cookie-signature "1.0.6" - debug "2.6.9" - depd "~1.1.2" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "~1.1.2" - fresh "0.5.2" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.7" - qs "6.9.6" - range-parser "~1.2.1" - safe-buffer "5.2.1" - send "0.17.2" - serve-static "1.14.2" - setprototypeof "1.2.0" - statuses "~1.5.0" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -express@~4.16.1: - version "4.16.4" - resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e" - integrity sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg== - dependencies: - accepts "~1.3.5" - array-flatten "1.1.1" - body-parser "1.18.3" - content-disposition "0.5.2" - content-type "~1.0.4" - cookie "0.3.1" - cookie-signature "1.0.6" - debug "2.6.9" - depd "~1.1.2" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.1.1" - fresh "0.5.2" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.2" - path-to-regexp "0.1.7" - proxy-addr "~2.0.4" - qs "6.5.2" - range-parser "~1.2.0" - safe-buffer "5.1.2" - send "0.16.2" - serve-static "1.13.2" - setprototypeof "1.1.0" - statuses "~1.4.0" - type-is "~1.6.16" - utils-merge "1.0.1" - vary "~1.1.2" - extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" @@ -8841,19 +8707,6 @@ filter-obj@^1.1.0: resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ== -finalhandler@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" - integrity sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg== - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.2" - statuses "~1.4.0" - unpipe "~1.0.0" - finalhandler@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" @@ -9714,16 +9567,6 @@ http-deceiver@^1.2.7: resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= -http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3: - version "1.6.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" - integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - http-errors@1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.0.tgz#75d1bbe497e1044f51e4ee9e704a62f28d336507" @@ -9746,6 +9589,16 @@ http-errors@1.8.1: statuses ">= 1.5.0 < 2" toidentifier "1.0.1" +http-errors@~1.6.2, http-errors@~1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + http-parser-js@>=0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.5.tgz#d7c30d5d3c90d865b4a2e870181f9d6f22ac7ac5" @@ -9837,13 +9690,6 @@ hyperlinker@^1.0.0: resolved "https://registry.yarnpkg.com/hyperlinker/-/hyperlinker-1.0.0.tgz#23dc9e38a206b208ee49bc2d6c8ef47027df0c0e" integrity sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ== -iconv-lite@0.4.23: - version "0.4.23" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" - integrity sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -11997,11 +11843,6 @@ mime-types@~2.1.34: dependencies: mime-db "1.52.0" -mime@1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" - integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== - mime@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" @@ -12356,16 +12197,16 @@ natural-orderby@^2.0.3: resolved "https://registry.yarnpkg.com/natural-orderby/-/natural-orderby-2.0.3.tgz#8623bc518ba162f8ff1cdb8941d74deb0fdcc016" integrity sha512-p7KTHxU0CUrcOXe62Zfrb5Z13nLvPhSWR/so3kFulUQU0sgUll2Z0LwpsLN351eOOD+hRGu/F1g+6xDfPeD++Q== -negotiator@0.6.2, negotiator@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" - integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== - negotiator@0.6.3, negotiator@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +negotiator@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + neo-async@^2.6.0, neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" @@ -14300,7 +14141,7 @@ protocols@^2.0.1: resolved "https://registry.yarnpkg.com/protocols/-/protocols-2.0.1.tgz#8f155da3fc0f32644e83c5782c8e8212ccf70a86" integrity sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q== -proxy-addr@~2.0.4, proxy-addr@~2.0.7: +proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== @@ -14372,16 +14213,6 @@ qqjs@^0.3.11: tmp "^0.1.0" write-json-file "^4.1.1" -qs@6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== - -qs@6.9.6: - version "6.9.6" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee" - integrity sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ== - qs@6.9.7: version "6.9.7" resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe" @@ -14453,31 +14284,11 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" -range-parser@^1.2.1, range-parser@~1.2.0, range-parser@~1.2.1: +range-parser@^1.2.1, range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" - integrity sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw== - dependencies: - bytes "3.0.0" - http-errors "1.6.3" - iconv-lite "0.4.23" - unpipe "1.0.0" - -raw-body@2.4.2, raw-body@^2.4.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.2.tgz#baf3e9c21eebced59dd6533ac872b71f7b61cb32" - integrity sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ== - dependencies: - bytes "3.1.1" - http-errors "1.8.1" - iconv-lite "0.4.24" - unpipe "1.0.0" - raw-body@2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.3.tgz#8f80305d11c2a0a545c2d9d89d7a0286fcead43c" @@ -14488,6 +14299,16 @@ raw-body@2.4.3: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.2.tgz#baf3e9c21eebced59dd6533ac872b71f7b61cb32" + integrity sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ== + dependencies: + bytes "3.1.1" + http-errors "1.8.1" + iconv-lite "0.4.24" + unpipe "1.0.0" + rc@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -15420,25 +15241,6 @@ semver@^7.3.7: dependencies: lru-cache "^6.0.0" -send@0.16.2: - version "0.16.2" - resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" - integrity sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw== - dependencies: - debug "2.6.9" - depd "~1.1.2" - destroy "~1.0.4" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "~1.6.2" - mime "1.4.1" - ms "2.0.0" - on-finished "~2.3.0" - range-parser "~1.2.0" - statuses "~1.4.0" - send@0.17.2: version "0.17.2" resolved "https://registry.yarnpkg.com/send/-/send-0.17.2.tgz#926622f76601c41808012c8bf1688fe3906f7820" @@ -15492,16 +15294,6 @@ serve-index@^1.9.1: mime-types "~2.1.17" parseurl "~1.3.2" -serve-static@1.13.2: - version "1.13.2" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" - integrity sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.2" - send "0.16.2" - serve-static@1.14.2: version "1.14.2" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.2.tgz#722d6294b1d62626d41b43a013ece4598d292bfa" @@ -15970,11 +15762,6 @@ standard-as-callback@^2.1.0: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= -statuses@~1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" - integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew== - stdout-stderr@^0.1.9: version "0.1.13" resolved "https://registry.yarnpkg.com/stdout-stderr/-/stdout-stderr-0.1.13.tgz#54e3450f3d4c54086a49c0c7f8786a44d1844b6f" @@ -16874,7 +16661,7 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-is@~1.6.16, type-is@~1.6.18: +type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==