mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-02 17:57:06 +02:00
Merge branch 'master' of github.com:n8n-io/n8n into enhancement/automatic-credential-selection
This commit is contained in:
commit
f6920c2e99
|
|
@ -1,2 +1,3 @@
|
|||
packages/editor-ui
|
||||
packages/design-system
|
||||
packages/cli/scripts/build.mjs
|
||||
|
|
|
|||
44
.eslintrc.js
44
.eslintrc.js
|
|
@ -370,55 +370,99 @@ module.exports = {
|
|||
'import/prefer-default-export': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['./packages/nodes-base/credentials/*.credentials.ts'],
|
||||
plugins: ['eslint-plugin-n8n-nodes-base'],
|
||||
rules: {
|
||||
'n8n-nodes-base/cred-class-field-display-name-missing-oauth2': 'error',
|
||||
'n8n-nodes-base/cred-class-field-name-missing-oauth2': 'error',
|
||||
'n8n-nodes-base/cred-class-field-name-unsuffixed': 'error',
|
||||
'n8n-nodes-base/cred-class-field-name-uppercase-first-char': 'error',
|
||||
'n8n-nodes-base/cred-class-name-missing-oauth2-suffix': 'error',
|
||||
'n8n-nodes-base/cred-class-name-unsuffixed': 'error',
|
||||
'n8n-nodes-base/cred-filename-against-convention': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['./packages/nodes-base/nodes/**/*.ts'],
|
||||
plugins: ['eslint-plugin-n8n-nodes-base'],
|
||||
rules: {
|
||||
'n8n-nodes-base/node-class-description-credentials-name-unsuffixed': 'error',
|
||||
'n8n-nodes-base/node-class-description-display-name-unsuffixed-trigger-node': 'error',
|
||||
'n8n-nodes-base/node-class-description-empty-string': 'error',
|
||||
'n8n-nodes-base/node-class-description-icon-not-svg': 'error',
|
||||
'n8n-nodes-base/node-class-description-inputs-wrong-regular-node': 'error',
|
||||
'n8n-nodes-base/node-class-description-inputs-wrong-trigger-node': 'error',
|
||||
'n8n-nodes-base/node-class-description-missing-subtitle': 'error',
|
||||
'n8n-nodes-base/node-class-description-name-unsuffixed-trigger-node': 'error',
|
||||
'n8n-nodes-base/node-class-description-outputs-wrong': 'error',
|
||||
'n8n-nodes-base/node-dirname-against-convention': 'error',
|
||||
'n8n-nodes-base/node-execute-block-double-assertion-for-items': 'error',
|
||||
'n8n-nodes-base/node-execute-block-wrong-error-thrown': 'error',
|
||||
'n8n-nodes-base/node-filename-against-convention': 'error',
|
||||
'n8n-nodes-base/node-param-array-type-assertion': 'error',
|
||||
'n8n-nodes-base/node-param-collection-type-unsorted-items': 'error',
|
||||
'n8n-nodes-base/node-param-color-type-unused': 'error',
|
||||
'n8n-nodes-base/node-param-default-missing': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-boolean': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-collection': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-fixed-collection': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-fixed-collection': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-multi-options': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-number': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-simplify': 'error',
|
||||
'n8n-nodes-base/node-param-default-wrong-for-string': 'error',
|
||||
'n8n-nodes-base/node-param-description-boolean-without-whether': 'error',
|
||||
'n8n-nodes-base/node-param-description-comma-separated-hyphen': 'error',
|
||||
'n8n-nodes-base/node-param-description-empty-string': 'error',
|
||||
'n8n-nodes-base/node-param-description-excess-final-period': 'error',
|
||||
'n8n-nodes-base/node-param-description-excess-inner-whitespace': 'error',
|
||||
'n8n-nodes-base/node-param-description-identical-to-display-name': 'error',
|
||||
'n8n-nodes-base/node-param-description-line-break-html-tag': 'error',
|
||||
'n8n-nodes-base/node-param-description-lowercase-first-char': 'error',
|
||||
'n8n-nodes-base/node-param-description-miscased-id': 'error',
|
||||
'n8n-nodes-base/node-param-description-miscased-json': 'error',
|
||||
'n8n-nodes-base/node-param-description-miscased-url': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-final-period': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-for-ignore-ssl-issues': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-for-return-all': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-for-simplify': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-from-dynamic-multi-options': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-from-dynamic-options': 'error',
|
||||
'n8n-nodes-base/node-param-description-missing-from-limit': 'error',
|
||||
'n8n-nodes-base/node-param-description-unencoded-angle-brackets': 'error',
|
||||
'n8n-nodes-base/node-param-description-unneeded-backticks': 'error',
|
||||
'n8n-nodes-base/node-param-description-untrimmed': 'error',
|
||||
'n8n-nodes-base/node-param-description-url-missing-protocol': 'error',
|
||||
'n8n-nodes-base/node-param-description-weak': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-dynamic-multi-options': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-dynamic-options': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-ignore-ssl-issues': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-limit': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-return-all': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-simplify': 'error',
|
||||
'n8n-nodes-base/node-param-description-wrong-for-upsert': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-excess-inner-whitespace': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-miscased': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-miscased-id': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-untrimmed': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-wrong-for-simplify': 'error',
|
||||
'n8n-nodes-base/node-param-display-name-wrong-for-update-fields': 'error',
|
||||
'n8n-nodes-base/node-param-min-value-wrong-for-limit': 'error',
|
||||
'n8n-nodes-base/node-param-multi-options-type-unsorted-items': 'error',
|
||||
'n8n-nodes-base/node-param-operation-without-no-data-expression': 'error',
|
||||
'n8n-nodes-base/node-param-operation-option-without-action': 'error',
|
||||
'n8n-nodes-base/node-param-option-description-identical-to-name': 'error',
|
||||
'n8n-nodes-base/node-param-option-name-containing-star': 'error',
|
||||
'n8n-nodes-base/node-param-option-name-duplicate': 'error',
|
||||
'n8n-nodes-base/node-param-option-name-wrong-for-get-all': 'error',
|
||||
'n8n-nodes-base/node-param-option-name-wrong-for-upsert': 'error',
|
||||
'n8n-nodes-base/node-param-option-value-duplicate': 'error',
|
||||
'n8n-nodes-base/node-param-options-type-unsorted-items': 'error',
|
||||
'n8n-nodes-base/node-param-placeholder-miscased-id': 'error',
|
||||
'n8n-nodes-base/node-param-placeholder-missing-email': 'error',
|
||||
'n8n-nodes-base/node-param-required-false': 'error',
|
||||
'n8n-nodes-base/node-param-resource-with-plural-option': 'error',
|
||||
'n8n-nodes-base/node-param-resource-without-no-data-expression': 'error',
|
||||
|
|
|
|||
1
.github/workflows/docker-images-nightly.yml
vendored
1
.github/workflows/docker-images-nightly.yml
vendored
|
|
@ -58,6 +58,7 @@ jobs:
|
|||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: n8nio/n8n:${{ github.event.inputs.tag || 'nightly' }}
|
||||
no-cache: true
|
||||
- name: Call Success URL - optionally
|
||||
run: |
|
||||
[[ "${{github.event.inputs.success-url}}" != "" ]] && curl -v ${{github.event.inputs.success-url}} || echo ""
|
||||
|
|
|
|||
572
.prettierignore
572
.prettierignore
|
|
@ -1,4 +1,574 @@
|
|||
packages/nodes-base
|
||||
packages/editor-ui
|
||||
packages/design-system
|
||||
*package.json
|
||||
|
||||
!packages/nodes-base/src
|
||||
!packages/nodes-base/test
|
||||
!packages/nodes-base/nodes
|
||||
!packages/nodes-base/credentials
|
||||
|
||||
packages/nodes-base/nodes/ActionNetwork
|
||||
packages/nodes-base/nodes/ActiveCampaign
|
||||
packages/nodes-base/nodes/AcuityScheduling
|
||||
packages/nodes-base/nodes/Affinity
|
||||
packages/nodes-base/nodes/AgileCrm
|
||||
packages/nodes-base/nodes/Airtable
|
||||
packages/nodes-base/nodes/Amqp
|
||||
packages/nodes-base/nodes/ApiTemplateIo
|
||||
packages/nodes-base/nodes/Asana
|
||||
packages/nodes-base/nodes/Automizy
|
||||
packages/nodes-base/nodes/Autopilot
|
||||
packages/nodes-base/nodes/Aws
|
||||
packages/nodes-base/nodes/BambooHr
|
||||
packages/nodes-base/nodes/Bannerbear
|
||||
packages/nodes-base/nodes/Baserow
|
||||
packages/nodes-base/nodes/Beeminder
|
||||
packages/nodes-base/nodes/Bitbucket
|
||||
packages/nodes-base/nodes/Bitly
|
||||
packages/nodes-base/nodes/Bitwarden
|
||||
packages/nodes-base/nodes/Box
|
||||
packages/nodes-base/nodes/Brandfetch
|
||||
packages/nodes-base/nodes/Bubble
|
||||
packages/nodes-base/nodes/Cal
|
||||
packages/nodes-base/nodes/Calendly
|
||||
packages/nodes-base/nodes/Chargebee
|
||||
packages/nodes-base/nodes/CircleCi
|
||||
packages/nodes-base/nodes/Cisco
|
||||
packages/nodes-base/nodes/Clearbit
|
||||
packages/nodes-base/nodes/ClickUp
|
||||
packages/nodes-base/nodes/Clockify
|
||||
packages/nodes-base/nodes/Cockpit
|
||||
packages/nodes-base/nodes/Coda
|
||||
packages/nodes-base/nodes/CoinGecko
|
||||
packages/nodes-base/nodes/Compression
|
||||
packages/nodes-base/nodes/Contentful
|
||||
packages/nodes-base/nodes/ConvertKit
|
||||
packages/nodes-base/nodes/Copper
|
||||
packages/nodes-base/nodes/Cortex
|
||||
packages/nodes-base/nodes/CrateDb
|
||||
packages/nodes-base/nodes/Cron
|
||||
packages/nodes-base/nodes/Crypto
|
||||
packages/nodes-base/nodes/CustomerIo
|
||||
packages/nodes-base/nodes/DateTime
|
||||
packages/nodes-base/nodes/DeepL
|
||||
packages/nodes-base/nodes/Demio
|
||||
packages/nodes-base/nodes/Dhl
|
||||
packages/nodes-base/nodes/Discord
|
||||
packages/nodes-base/nodes/Discourse
|
||||
packages/nodes-base/nodes/Disqus
|
||||
packages/nodes-base/nodes/Drift
|
||||
packages/nodes-base/nodes/Dropbox
|
||||
packages/nodes-base/nodes/Dropcontact
|
||||
packages/nodes-base/nodes/EditImage
|
||||
packages/nodes-base/nodes/Egoi
|
||||
packages/nodes-base/nodes/Elastic
|
||||
packages/nodes-base/nodes/EmailReadImap
|
||||
packages/nodes-base/nodes/EmailSend
|
||||
packages/nodes-base/nodes/Emelia
|
||||
packages/nodes-base/nodes/ERPNext
|
||||
packages/nodes-base/nodes/ErrorTrigger
|
||||
packages/nodes-base/nodes/Eventbrite
|
||||
packages/nodes-base/nodes/ExecuteCommand
|
||||
packages/nodes-base/nodes/ExecuteWorkflow
|
||||
packages/nodes-base/nodes/Facebook
|
||||
packages/nodes-base/nodes/Figma
|
||||
packages/nodes-base/nodes/FileMaker
|
||||
packages/nodes-base/nodes/Flow
|
||||
packages/nodes-base/nodes/FormIo
|
||||
packages/nodes-base/nodes/Formstack
|
||||
packages/nodes-base/nodes/Freshdesk
|
||||
packages/nodes-base/nodes/Freshservice
|
||||
packages/nodes-base/nodes/FreshworksCrm
|
||||
packages/nodes-base/nodes/Ftp
|
||||
packages/nodes-base/nodes/Function
|
||||
packages/nodes-base/nodes/FunctionItem
|
||||
packages/nodes-base/nodes/GetResponse
|
||||
packages/nodes-base/nodes/Ghost
|
||||
packages/nodes-base/nodes/Git
|
||||
packages/nodes-base/nodes/Github
|
||||
packages/nodes-base/nodes/Gitlab
|
||||
packages/nodes-base/nodes/Google
|
||||
packages/nodes-base/nodes/Gotify
|
||||
packages/nodes-base/nodes/GoToWebinar
|
||||
packages/nodes-base/nodes/Grafana
|
||||
packages/nodes-base/nodes/GraphQL
|
||||
packages/nodes-base/nodes/Grist
|
||||
packages/nodes-base/nodes/Gumroad
|
||||
packages/nodes-base/nodes/HackerNews
|
||||
packages/nodes-base/nodes/HaloPSA
|
||||
packages/nodes-base/nodes/Harvest
|
||||
packages/nodes-base/nodes/HelpScout
|
||||
packages/nodes-base/nodes/HomeAssistant
|
||||
packages/nodes-base/nodes/HtmlExtract
|
||||
packages/nodes-base/nodes/HttpRequest
|
||||
packages/nodes-base/nodes/Hubspot
|
||||
packages/nodes-base/nodes/HumanticAI
|
||||
packages/nodes-base/nodes/Hunter
|
||||
packages/nodes-base/nodes/ICalendar
|
||||
packages/nodes-base/nodes/If
|
||||
packages/nodes-base/nodes/Intercom
|
||||
packages/nodes-base/nodes/Interval
|
||||
packages/nodes-base/nodes/InvoiceNinja
|
||||
packages/nodes-base/nodes/ItemLists
|
||||
packages/nodes-base/nodes/Iterable
|
||||
packages/nodes-base/nodes/Jenkins
|
||||
packages/nodes-base/nodes/Jira
|
||||
packages/nodes-base/nodes/JotForm
|
||||
packages/nodes-base/nodes/Kafka
|
||||
packages/nodes-base/nodes/Keap
|
||||
packages/nodes-base/nodes/Kitemaker
|
||||
packages/nodes-base/nodes/KoBoToolbox
|
||||
packages/nodes-base/nodes/Lemlist
|
||||
packages/nodes-base/nodes/Line
|
||||
packages/nodes-base/nodes/Linear
|
||||
packages/nodes-base/nodes/LingvaNex
|
||||
packages/nodes-base/nodes/LinkedIn
|
||||
packages/nodes-base/nodes/LocalFileTrigger
|
||||
packages/nodes-base/nodes/Magento
|
||||
packages/nodes-base/nodes/Mailcheck
|
||||
packages/nodes-base/nodes/Mailchimp
|
||||
packages/nodes-base/nodes/MailerLite
|
||||
packages/nodes-base/nodes/Mailgun
|
||||
packages/nodes-base/nodes/Mailjet
|
||||
packages/nodes-base/nodes/Mandrill
|
||||
packages/nodes-base/nodes/Markdown
|
||||
packages/nodes-base/nodes/Marketstack
|
||||
packages/nodes-base/nodes/Matrix
|
||||
packages/nodes-base/nodes/Mattermost
|
||||
packages/nodes-base/nodes/Mautic
|
||||
packages/nodes-base/nodes/Medium
|
||||
packages/nodes-base/nodes/Merge
|
||||
packages/nodes-base/nodes/MessageBird
|
||||
packages/nodes-base/nodes/Microsoft
|
||||
packages/nodes-base/nodes/Mindee
|
||||
packages/nodes-base/nodes/Misp
|
||||
packages/nodes-base/nodes/Mocean
|
||||
packages/nodes-base/nodes/MondayCom
|
||||
packages/nodes-base/nodes/MongoDb
|
||||
packages/nodes-base/nodes/MonicaCrm
|
||||
packages/nodes-base/nodes/MoveBinaryData
|
||||
packages/nodes-base/nodes/MQTT
|
||||
packages/nodes-base/nodes/Msg91
|
||||
packages/nodes-base/nodes/MySql
|
||||
packages/nodes-base/nodes/N8nTrainingCustomerDatastore
|
||||
packages/nodes-base/nodes/N8nTrainingCustomerMessenger
|
||||
packages/nodes-base/nodes/N8nTrigger
|
||||
packages/nodes-base/nodes/Nasa
|
||||
packages/nodes-base/nodes/Netlify
|
||||
packages/nodes-base/nodes/NextCloud
|
||||
packages/nodes-base/nodes/NocoDB
|
||||
packages/nodes-base/nodes/NoOp
|
||||
packages/nodes-base/nodes/Notion
|
||||
packages/nodes-base/nodes/Odoo
|
||||
packages/nodes-base/nodes/OneSimpleApi
|
||||
packages/nodes-base/nodes/Onfleet
|
||||
packages/nodes-base/nodes/OpenThesaurus
|
||||
packages/nodes-base/nodes/OpenWeatherMap
|
||||
packages/nodes-base/nodes/Orbit
|
||||
packages/nodes-base/nodes/Oura
|
||||
packages/nodes-base/nodes/Paddle
|
||||
packages/nodes-base/nodes/PagerDuty
|
||||
packages/nodes-base/nodes/PayPal
|
||||
packages/nodes-base/nodes/Peekalink
|
||||
packages/nodes-base/nodes/Phantombuster
|
||||
packages/nodes-base/nodes/PhilipsHue
|
||||
packages/nodes-base/nodes/Pipedrive
|
||||
packages/nodes-base/nodes/Plivo
|
||||
packages/nodes-base/nodes/PostBin
|
||||
packages/nodes-base/nodes/Postgres
|
||||
packages/nodes-base/nodes/PostHog
|
||||
packages/nodes-base/nodes/Postmark
|
||||
packages/nodes-base/nodes/ProfitWell
|
||||
packages/nodes-base/nodes/Pushbullet
|
||||
packages/nodes-base/nodes/Pushcut
|
||||
packages/nodes-base/nodes/Pushover
|
||||
packages/nodes-base/nodes/QuestDb
|
||||
packages/nodes-base/nodes/QuickBase
|
||||
packages/nodes-base/nodes/QuickBooks
|
||||
packages/nodes-base/nodes/RabbitMQ
|
||||
packages/nodes-base/nodes/Raindrop
|
||||
packages/nodes-base/nodes/ReadBinaryFile
|
||||
packages/nodes-base/nodes/ReadBinaryFiles
|
||||
packages/nodes-base/nodes/ReadPdf
|
||||
packages/nodes-base/nodes/Reddit
|
||||
packages/nodes-base/nodes/Redis
|
||||
packages/nodes-base/nodes/RenameKeys
|
||||
packages/nodes-base/nodes/RespondToWebhook
|
||||
packages/nodes-base/nodes/Rocketchat
|
||||
packages/nodes-base/nodes/RssFeedRead
|
||||
packages/nodes-base/nodes/Rundeck
|
||||
packages/nodes-base/nodes/S3
|
||||
packages/nodes-base/nodes/Salesforce
|
||||
packages/nodes-base/nodes/Salesmate
|
||||
packages/nodes-base/nodes/SeaTable
|
||||
packages/nodes-base/nodes/SecurityScorecard
|
||||
packages/nodes-base/nodes/Segment
|
||||
packages/nodes-base/nodes/SendGrid
|
||||
packages/nodes-base/nodes/Sendy
|
||||
packages/nodes-base/nodes/SentryIo
|
||||
packages/nodes-base/nodes/ServiceNow
|
||||
packages/nodes-base/nodes/Set
|
||||
packages/nodes-base/nodes/Shopify
|
||||
packages/nodes-base/nodes/Signl4
|
||||
packages/nodes-base/nodes/Slack
|
||||
packages/nodes-base/nodes/Sms77
|
||||
packages/nodes-base/nodes/Snowflake
|
||||
packages/nodes-base/nodes/SplitInBatches
|
||||
packages/nodes-base/nodes/Splunk
|
||||
packages/nodes-base/nodes/Spontit
|
||||
packages/nodes-base/nodes/Spotify
|
||||
packages/nodes-base/nodes/SpreadsheetFile
|
||||
packages/nodes-base/nodes/SseTrigger
|
||||
packages/nodes-base/nodes/Ssh
|
||||
packages/nodes-base/nodes/Stackby
|
||||
packages/nodes-base/nodes/Start
|
||||
packages/nodes-base/nodes/StickyNote
|
||||
packages/nodes-base/nodes/StopAndError
|
||||
packages/nodes-base/nodes/Storyblok
|
||||
packages/nodes-base/nodes/Strapi
|
||||
packages/nodes-base/nodes/Strava
|
||||
packages/nodes-base/nodes/Stripe
|
||||
packages/nodes-base/nodes/Supabase
|
||||
packages/nodes-base/nodes/SurveyMonkey
|
||||
packages/nodes-base/nodes/Switch
|
||||
packages/nodes-base/nodes/SyncroMSP
|
||||
packages/nodes-base/nodes/Taiga
|
||||
packages/nodes-base/nodes/Tapfiliate
|
||||
packages/nodes-base/nodes/Telegram
|
||||
packages/nodes-base/nodes/TheHive
|
||||
packages/nodes-base/nodes/TimescaleDb
|
||||
packages/nodes-base/nodes/Todoist
|
||||
packages/nodes-base/nodes/Toggl
|
||||
packages/nodes-base/nodes/TravisCi
|
||||
packages/nodes-base/nodes/Trello
|
||||
packages/nodes-base/nodes/Twake
|
||||
packages/nodes-base/nodes/Twilio
|
||||
packages/nodes-base/nodes/Twist
|
||||
packages/nodes-base/nodes/Twitter
|
||||
packages/nodes-base/nodes/Typeform
|
||||
packages/nodes-base/nodes/UnleashedSoftware
|
||||
packages/nodes-base/nodes/Uplead
|
||||
packages/nodes-base/nodes/UProc
|
||||
packages/nodes-base/nodes/UptimeRobot
|
||||
packages/nodes-base/nodes/UrlScanIo
|
||||
packages/nodes-base/nodes/utils
|
||||
packages/nodes-base/nodes/Vero
|
||||
packages/nodes-base/nodes/Vonage
|
||||
packages/nodes-base/nodes/Wait
|
||||
packages/nodes-base/nodes/Webflow
|
||||
packages/nodes-base/nodes/Webhook
|
||||
packages/nodes-base/nodes/Wekan
|
||||
packages/nodes-base/nodes/Wise
|
||||
packages/nodes-base/nodes/WooCommerce
|
||||
packages/nodes-base/nodes/Wordpress
|
||||
packages/nodes-base/nodes/Workable
|
||||
packages/nodes-base/nodes/WorkflowTrigger
|
||||
packages/nodes-base/nodes/WriteBinaryFile
|
||||
packages/nodes-base/nodes/Wufoo
|
||||
packages/nodes-base/nodes/Xero
|
||||
packages/nodes-base/nodes/Xml
|
||||
packages/nodes-base/nodes/Yourls
|
||||
packages/nodes-base/nodes/Zammad
|
||||
packages/nodes-base/nodes/Zendesk
|
||||
packages/nodes-base/nodes/Zoho
|
||||
packages/nodes-base/nodes/Zoom
|
||||
packages/nodes-base/nodes/Zulip
|
||||
|
||||
packages/nodes-base/credentials/ActionNetworkApi.credentials.ts
|
||||
packages/nodes-base/credentials/ActiveCampaignApi.credentials.ts
|
||||
packages/nodes-base/credentials/AcuitySchedulingApi.credentials.ts
|
||||
packages/nodes-base/credentials/AcuitySchedulingOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/AffinityApi.credentials.ts
|
||||
packages/nodes-base/credentials/AgileCrmApi.credentials.ts
|
||||
packages/nodes-base/credentials/AirtableApi.credentials.ts
|
||||
packages/nodes-base/credentials/Amqp.credentials.ts
|
||||
packages/nodes-base/credentials/ApiTemplateIoApi.credentials.ts
|
||||
packages/nodes-base/credentials/AsanaApi.credentials.ts
|
||||
packages/nodes-base/credentials/AsanaOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/AutomizyApi.credentials.ts
|
||||
packages/nodes-base/credentials/AutopilotApi.credentials.ts
|
||||
packages/nodes-base/credentials/Aws.credentials.ts
|
||||
packages/nodes-base/credentials/AWS.svg
|
||||
packages/nodes-base/credentials/BambooHrApi.credentials.ts
|
||||
packages/nodes-base/credentials/BannerbearApi.credentials.ts
|
||||
packages/nodes-base/credentials/BaserowApi.credentials.ts
|
||||
packages/nodes-base/credentials/BeeminderApi.credentials.ts
|
||||
packages/nodes-base/credentials/BitbucketApi.credentials.ts
|
||||
packages/nodes-base/credentials/BitlyApi.credentials.ts
|
||||
packages/nodes-base/credentials/BitlyOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/BitwardenApi.credentials.ts
|
||||
packages/nodes-base/credentials/BoxOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/BrandfetchApi.credentials.ts
|
||||
packages/nodes-base/credentials/BubbleApi.credentials.ts
|
||||
packages/nodes-base/credentials/CalApi.credentials.ts
|
||||
packages/nodes-base/credentials/CalendlyApi.credentials.ts
|
||||
packages/nodes-base/credentials/ChargebeeApi.credentials.ts
|
||||
packages/nodes-base/credentials/CircleCiApi.credentials.ts
|
||||
packages/nodes-base/credentials/CiscoWebexOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/ClearbitApi.credentials.ts
|
||||
packages/nodes-base/credentials/ClickUpApi.credentials.ts
|
||||
packages/nodes-base/credentials/ClickUpOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/ClockifyApi.credentials.ts
|
||||
packages/nodes-base/credentials/CockpitApi.credentials.ts
|
||||
packages/nodes-base/credentials/CodaApi.credentials.ts
|
||||
packages/nodes-base/credentials/ContentfulApi.credentials.ts
|
||||
packages/nodes-base/credentials/ConvertKitApi.credentials.ts
|
||||
packages/nodes-base/credentials/CopperApi.credentials.ts
|
||||
packages/nodes-base/credentials/CortexApi.credentials.ts
|
||||
packages/nodes-base/credentials/CrateDb.credentials.ts
|
||||
packages/nodes-base/credentials/CustomerIoApi.credentials.ts
|
||||
packages/nodes-base/credentials/DeepLApi.credentials.ts
|
||||
packages/nodes-base/credentials/DemioApi.credentials.ts
|
||||
packages/nodes-base/credentials/DhlApi.credentials.ts
|
||||
packages/nodes-base/credentials/DiscourseApi.credentials.ts
|
||||
packages/nodes-base/credentials/DisqusApi.credentials.ts
|
||||
packages/nodes-base/credentials/DriftApi.credentials.ts
|
||||
packages/nodes-base/credentials/DriftOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/DropboxApi.credentials.ts
|
||||
packages/nodes-base/credentials/DropboxOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/DropcontactApi.credentials.ts
|
||||
packages/nodes-base/credentials/EgoiApi.credentials.ts
|
||||
packages/nodes-base/credentials/ElasticsearchApi.credentials.ts
|
||||
packages/nodes-base/credentials/ElasticSecurityApi.credentials.ts
|
||||
packages/nodes-base/credentials/EmeliaApi.credentials.ts
|
||||
packages/nodes-base/credentials/ERPNextApi.credentials.ts
|
||||
packages/nodes-base/credentials/EventbriteApi.credentials.ts
|
||||
packages/nodes-base/credentials/EventbriteOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/FacebookGraphApi.credentials.ts
|
||||
packages/nodes-base/credentials/FacebookGraphAppApi.credentials.ts
|
||||
packages/nodes-base/credentials/FigmaApi.credentials.ts
|
||||
packages/nodes-base/credentials/FileMaker.credentials.ts
|
||||
packages/nodes-base/credentials/FlowApi.credentials.ts
|
||||
packages/nodes-base/credentials/FormIoApi.credentials.ts
|
||||
packages/nodes-base/credentials/FormstackApi.credentials.ts
|
||||
packages/nodes-base/credentials/FormstackOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/FreshdeskApi.credentials.ts
|
||||
packages/nodes-base/credentials/FreshserviceApi.credentials.ts
|
||||
packages/nodes-base/credentials/FreshworksCrmApi.credentials.ts
|
||||
packages/nodes-base/credentials/Ftp.credentials.ts
|
||||
packages/nodes-base/credentials/GetResponseApi.credentials.ts
|
||||
packages/nodes-base/credentials/GetResponseOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/GhostAdminApi.credentials.ts
|
||||
packages/nodes-base/credentials/GhostContentApi.credentials.ts
|
||||
packages/nodes-base/credentials/GithubApi.credentials.ts
|
||||
packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/GitlabApi.credentials.ts
|
||||
packages/nodes-base/credentials/GitlabOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/GitPassword.credentials.ts
|
||||
packages/nodes-base/credentials/GmailOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/Google.svg
|
||||
packages/nodes-base/credentials/GoogleAnalyticsOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/GoogleApi.credentials.ts
|
||||
packages/nodes-base/credentials/GoogleBigQueryOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/GoogleBooksOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/GoogleCalendarOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/GoogleCloudNaturalLanguageOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/GoogleContactsOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/GoogleDocsOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/GoogleDriveOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/GoogleFirebaseCloudFirestoreOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/GoogleFirebaseRealtimeDatabaseOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/GoogleOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/GooglePerspectiveOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/GoogleSheetsOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/GoogleSlidesOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/GoogleTasksOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/GoogleTranslateOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/GotifyApi.credentials.ts
|
||||
packages/nodes-base/credentials/GoToWebinarOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/GrafanaApi.credentials.ts
|
||||
packages/nodes-base/credentials/GristApi.credentials.ts
|
||||
packages/nodes-base/credentials/GSuiteAdminOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/GumroadApi.credentials.ts
|
||||
packages/nodes-base/credentials/HaloPSAApi.credentials.ts
|
||||
packages/nodes-base/credentials/HarvestApi.credentials.ts
|
||||
packages/nodes-base/credentials/HarvestOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/HelpScoutOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/HomeAssistantApi.credentials.ts
|
||||
packages/nodes-base/credentials/HttpBasicAuth.credentials.ts
|
||||
packages/nodes-base/credentials/HttpDigestAuth.credentials.ts
|
||||
packages/nodes-base/credentials/HttpHeaderAuth.credentials.ts
|
||||
packages/nodes-base/credentials/HttpQueryAuth.credentials.ts
|
||||
packages/nodes-base/credentials/HubspotApi.credentials.ts
|
||||
packages/nodes-base/credentials/HubspotAppToken.credentials.ts
|
||||
packages/nodes-base/credentials/HubspotDeveloperApi.credentials.ts
|
||||
packages/nodes-base/credentials/HubspotOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/HumanticAiApi.credentials.ts
|
||||
packages/nodes-base/credentials/HunterApi.credentials.ts
|
||||
packages/nodes-base/credentials/Imap.credentials.ts
|
||||
packages/nodes-base/credentials/IntercomApi.credentials.ts
|
||||
packages/nodes-base/credentials/InvoiceNinjaApi.credentials.ts
|
||||
packages/nodes-base/credentials/IterableApi.credentials.ts
|
||||
packages/nodes-base/credentials/JenkinsApi.credentials.ts
|
||||
packages/nodes-base/credentials/JiraSoftwareCloudApi.credentials.ts
|
||||
packages/nodes-base/credentials/JiraSoftwareServerApi.credentials.ts
|
||||
packages/nodes-base/credentials/JotFormApi.credentials.ts
|
||||
packages/nodes-base/credentials/Kafka.credentials.ts
|
||||
packages/nodes-base/credentials/KeapOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/KitemakerApi.credentials.ts
|
||||
packages/nodes-base/credentials/KoBoToolboxApi.credentials.ts
|
||||
packages/nodes-base/credentials/LemlistApi.credentials.ts
|
||||
packages/nodes-base/credentials/LinearApi.credentials.ts
|
||||
packages/nodes-base/credentials/LineNotifyOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/LingvaNexApi.credentials.ts
|
||||
packages/nodes-base/credentials/LinkedInOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/Magento2Api.credentials.ts
|
||||
packages/nodes-base/credentials/MailcheckApi.credentials.ts
|
||||
packages/nodes-base/credentials/MailchimpApi.credentials.ts
|
||||
packages/nodes-base/credentials/MailchimpOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/MailerLiteApi.credentials.ts
|
||||
packages/nodes-base/credentials/MailgunApi.credentials.ts
|
||||
packages/nodes-base/credentials/MailjetEmailApi.credentials.ts
|
||||
packages/nodes-base/credentials/MailjetSmsApi.credentials.ts
|
||||
packages/nodes-base/credentials/MandrillApi.credentials.ts
|
||||
packages/nodes-base/credentials/MarketstackApi.credentials.ts
|
||||
packages/nodes-base/credentials/MatrixApi.credentials.ts
|
||||
packages/nodes-base/credentials/MattermostApi.credentials.ts
|
||||
packages/nodes-base/credentials/MauticApi.credentials.ts
|
||||
packages/nodes-base/credentials/MauticOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/MediumApi.credentials.ts
|
||||
packages/nodes-base/credentials/MediumOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/MessageBirdApi.credentials.ts
|
||||
packages/nodes-base/credentials/Microsoft.svg
|
||||
packages/nodes-base/credentials/MicrosoftDynamicsOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/MicrosoftExcelOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/MicrosoftGraphSecurityOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/MicrosoftOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/MicrosoftOneDriveOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/MicrosoftOutlookOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/MicrosoftSql.credentials.ts
|
||||
packages/nodes-base/credentials/MicrosoftTeamsOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/MicrosoftToDoOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/MindeeInvoiceApi.credentials.ts
|
||||
packages/nodes-base/credentials/MindeeReceiptApi.credentials.ts
|
||||
packages/nodes-base/credentials/MispApi.credentials.ts
|
||||
packages/nodes-base/credentials/MoceanApi.credentials.ts
|
||||
packages/nodes-base/credentials/MondayComApi.credentials.ts
|
||||
packages/nodes-base/credentials/MondayComOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/MongoDb.credentials.ts
|
||||
packages/nodes-base/credentials/MonicaCrmApi.credentials.ts
|
||||
packages/nodes-base/credentials/Mqtt.credentials.ts
|
||||
packages/nodes-base/credentials/Msg91Api.credentials.ts
|
||||
packages/nodes-base/credentials/MySql.credentials.ts
|
||||
packages/nodes-base/credentials/NasaApi.credentials.ts
|
||||
packages/nodes-base/credentials/NetlifyApi.credentials.ts
|
||||
packages/nodes-base/credentials/NetlifyOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/NextCloudApi.credentials.ts
|
||||
packages/nodes-base/credentials/NextCloudOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/NocoDb.credentials.ts
|
||||
packages/nodes-base/credentials/NotionApi.credentials.ts
|
||||
packages/nodes-base/credentials/NotionOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/OAuth1Api.credentials.ts
|
||||
packages/nodes-base/credentials/OAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/OdooApi.credentials.ts
|
||||
packages/nodes-base/credentials/OneSimpleApi.credentials.ts
|
||||
packages/nodes-base/credentials/OnfleetApi.credentials.ts
|
||||
packages/nodes-base/credentials/OpenWeatherMapApi.credentials.ts
|
||||
packages/nodes-base/credentials/OrbitApi.credentials.ts
|
||||
packages/nodes-base/credentials/OuraApi.credentials.ts
|
||||
packages/nodes-base/credentials/PaddleApi.credentials.ts
|
||||
packages/nodes-base/credentials/PagerDutyApi.credentials.ts
|
||||
packages/nodes-base/credentials/PagerDutyOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/PayPalApi.credentials.ts
|
||||
packages/nodes-base/credentials/PeekalinkApi.credentials.ts
|
||||
packages/nodes-base/credentials/PhantombusterApi.credentials.ts
|
||||
packages/nodes-base/credentials/PhilipsHueOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/PipedriveApi.credentials.ts
|
||||
packages/nodes-base/credentials/PipedriveOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/PlivoApi.credentials.ts
|
||||
packages/nodes-base/credentials/Postgres.credentials.ts
|
||||
packages/nodes-base/credentials/PostHogApi.credentials.ts
|
||||
packages/nodes-base/credentials/PostmarkApi.credentials.ts
|
||||
packages/nodes-base/credentials/ProfitWellApi.credentials.ts
|
||||
packages/nodes-base/credentials/PushbulletOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/PushcutApi.credentials.ts
|
||||
packages/nodes-base/credentials/PushoverApi.credentials.ts
|
||||
packages/nodes-base/credentials/QuestDb.credentials.ts
|
||||
packages/nodes-base/credentials/QuickBaseApi.credentials.ts
|
||||
packages/nodes-base/credentials/QuickBooksOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/RabbitMQ.credentials.ts
|
||||
packages/nodes-base/credentials/RaindropOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/RedditOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/Redis.credentials.ts
|
||||
packages/nodes-base/credentials/RocketchatApi.credentials.ts
|
||||
packages/nodes-base/credentials/RundeckApi.credentials.ts
|
||||
packages/nodes-base/credentials/S3.credentials.ts
|
||||
packages/nodes-base/credentials/SalesforceJwtApi.credentials.ts
|
||||
packages/nodes-base/credentials/SalesforceOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/SalesmateApi.credentials.ts
|
||||
packages/nodes-base/credentials/SeaTableApi.credentials.ts
|
||||
packages/nodes-base/credentials/SecurityScorecardApi.credentials.ts
|
||||
packages/nodes-base/credentials/SegmentApi.credentials.ts
|
||||
packages/nodes-base/credentials/SendGridApi.credentials.ts
|
||||
packages/nodes-base/credentials/SendyApi.credentials.ts
|
||||
packages/nodes-base/credentials/SentryIoApi.credentials.ts
|
||||
packages/nodes-base/credentials/SentryIoOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/SentryIoServerApi.credentials.ts
|
||||
packages/nodes-base/credentials/ServiceNowBasicApi.credentials.ts
|
||||
packages/nodes-base/credentials/ServiceNowOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/Sftp.credentials.ts
|
||||
packages/nodes-base/credentials/ShopifyApi.credentials.ts
|
||||
packages/nodes-base/credentials/Signl4Api.credentials.ts
|
||||
packages/nodes-base/credentials/SlackApi.credentials.ts
|
||||
packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/Sms77Api.credentials.ts
|
||||
packages/nodes-base/credentials/Smtp.credentials.ts
|
||||
packages/nodes-base/credentials/Snowflake.credentials.ts
|
||||
packages/nodes-base/credentials/SplunkApi.credentials.ts
|
||||
packages/nodes-base/credentials/SpontitApi.credentials.ts
|
||||
packages/nodes-base/credentials/SpotifyOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/SshPassword.credentials.ts
|
||||
packages/nodes-base/credentials/SshPrivateKey.credentials.ts
|
||||
packages/nodes-base/credentials/StackbyApi.credentials.ts
|
||||
packages/nodes-base/credentials/StoryblokContentApi.credentials.ts
|
||||
packages/nodes-base/credentials/StoryblokManagementApi.credentials.ts
|
||||
packages/nodes-base/credentials/StrapiApi.credentials.ts
|
||||
packages/nodes-base/credentials/StravaOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/StripeApi.credentials.ts
|
||||
packages/nodes-base/credentials/SupabaseApi.credentials.ts
|
||||
packages/nodes-base/credentials/SurveyMonkeyApi.credentials.ts
|
||||
packages/nodes-base/credentials/SurveyMonkeyOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/SyncroMspApi.credentials.ts
|
||||
packages/nodes-base/credentials/TaigaApi.credentials.ts
|
||||
packages/nodes-base/credentials/TapfiliateApi.credentials.ts
|
||||
packages/nodes-base/credentials/TelegramApi.credentials.ts
|
||||
packages/nodes-base/credentials/TheHiveApi.credentials.ts
|
||||
packages/nodes-base/credentials/TimescaleDb.credentials.ts
|
||||
packages/nodes-base/credentials/TodoistApi.credentials.ts
|
||||
packages/nodes-base/credentials/TodoistOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/TogglApi.credentials.ts
|
||||
packages/nodes-base/credentials/TravisCiApi.credentials.ts
|
||||
packages/nodes-base/credentials/TrelloApi.credentials.ts
|
||||
packages/nodes-base/credentials/TwakeCloudApi.credentials.ts
|
||||
packages/nodes-base/credentials/TwakeServerApi.credentials.ts
|
||||
packages/nodes-base/credentials/TwilioApi.credentials.ts
|
||||
packages/nodes-base/credentials/TwistOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/TwitterOAuth1Api.credentials.ts
|
||||
packages/nodes-base/credentials/TypeformApi.credentials.ts
|
||||
packages/nodes-base/credentials/TypeformOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/UnleashedSoftwareApi.credentials.ts
|
||||
packages/nodes-base/credentials/UpleadApi.credentials.ts
|
||||
packages/nodes-base/credentials/UProcApi.credentials.ts
|
||||
packages/nodes-base/credentials/UptimeRobotApi.credentials.ts
|
||||
packages/nodes-base/credentials/UrlScanIoApi.credentials.ts
|
||||
packages/nodes-base/credentials/VeroApi.credentials.ts
|
||||
packages/nodes-base/credentials/VonageApi.credentials.ts
|
||||
packages/nodes-base/credentials/WebflowApi.credentials.ts
|
||||
packages/nodes-base/credentials/WebflowOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/WekanApi.credentials.ts
|
||||
packages/nodes-base/credentials/WiseApi.credentials.ts
|
||||
packages/nodes-base/credentials/WooCommerceApi.credentials.ts
|
||||
packages/nodes-base/credentials/WordpressApi.credentials.ts
|
||||
packages/nodes-base/credentials/WorkableApi.credentials.ts
|
||||
packages/nodes-base/credentials/WufooApi.credentials.ts
|
||||
packages/nodes-base/credentials/XeroOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/YourlsApi.credentials.ts
|
||||
packages/nodes-base/credentials/YouTubeOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/ZammadBasicAuthApi.credentials.ts
|
||||
packages/nodes-base/credentials/ZammadTokenAuthApi.credentials.ts
|
||||
packages/nodes-base/credentials/ZendeskApi.credentials.ts
|
||||
packages/nodes-base/credentials/ZendeskOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/ZohoOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/ZoomApi.credentials.ts
|
||||
packages/nodes-base/credentials/ZoomOAuth2Api.credentials.ts
|
||||
packages/nodes-base/credentials/ZulipApi.credentials.ts
|
||||
|
|
|
|||
48
.vscode/DEBUGGER.md
vendored
Normal file
48
.vscode/DEBUGGER.md
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# How to debug n8n
|
||||
|
||||
When developing nodes or making changes to the core, debugging is an important tool.
|
||||
|
||||
The process outlined below does not cover front end debugging; right now only back end is covered.
|
||||
|
||||
We are based on the premise that you're using VSCode since the configurations provided are tailored for this IDE.
|
||||
|
||||
## What is debugging
|
||||
|
||||
Debugging is the act of inspecting the code. It can be used to find bugs and, hopefully, fix them.
|
||||
|
||||
The act of debugging consists in executing the code while adding "Breakpoints". As the name implies, Breakpoints are points of the code that you want to inspect, by checking variable values and inspecting behavior.
|
||||
|
||||
Adding breakpoints is as easy as clicking the row number inside VSCode on the file you wish to debug.
|
||||
|
||||
Breakpoints are noted with a red dot in front of the line, meaning that whenever your code reaches that point, the code will stop executing and your IDE will focus the line where the breakpoint was placed, allowing you to inspect variable values, proceed the code or even stop the execution entirely.
|
||||
|
||||
## What if I change the code?
|
||||
|
||||
You might need to restart the debugger if you make changes to your code, since the running process will be executing an oudated version of the code.
|
||||
|
||||
In order to make this process easier you can simply run `npm run watch` in another terminal window, so you don't have to fully build the project. Please note that restarting n8n is still required, but this is much faster.
|
||||
|
||||
## Debugging options
|
||||
|
||||
Docker debugging is currently not functional. We offer 2 other methods:
|
||||
|
||||
1) Launch n8n from inside VSCode:
|
||||
From the "Run and Debug" section in VSCode you can choose the option named "Launch n8n with debug".
|
||||
This will start n8n to run as normal, but with debugger attached.
|
||||
2) Another possibility is if n8n is already running, say, in your terminal.
|
||||
You can attach the debugger to it.
|
||||
This is done by choosing the option "Attach to running n8n".
|
||||
VSCode will present you with a prompt to select the n8n process. It usually is displayed with `node ./n8n`
|
||||
|
||||
## What can be debugged?
|
||||
|
||||
With the debugger you can actually debug any Javascript (derived from Typescript) files in the following packages:
|
||||
|
||||
- cli
|
||||
- core
|
||||
- workflow
|
||||
- nodes-base
|
||||
|
||||
## Further reading
|
||||
|
||||
Please check [VSCode's docs about debugging](https://code.visualstudio.com/docs/editor/debugging) for more information.
|
||||
44
.vscode/launch.json
vendored
Normal file
44
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Attach to running n8n",
|
||||
"processId": "${command:PickProcess}",
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "node"
|
||||
},
|
||||
{
|
||||
"name": "Launch n8n with debug",
|
||||
"program": "${workspaceFolder}/packages/cli/bin/n8n",
|
||||
"request": "launch",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "node",
|
||||
"env": {
|
||||
// "N8N_PORT": "5679",
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
How this works:
|
||||
|
||||
This file gives VS Code the ability to start and debug n8n.
|
||||
The editor is not debuggable from here.
|
||||
|
||||
The "Run and Debug" tab of your editor should display the "Launch n8n with debug" option.
|
||||
This should start n8n and open a debug console. You can add breakpoints to
|
||||
Parts of the code residing inside `cli`, `core`, `workflow` and `nodes-base` packages
|
||||
|
||||
You can also choose to "Attach to running n8n". This is useful if you
|
||||
have n8n running in another terminal window and want to debug it.
|
||||
Once you click to Debug, VS Code will prompt you to select a process to attach to.
|
||||
*/
|
||||
}
|
||||
238
CHANGELOG.md
238
CHANGELOG.md
|
|
@ -1,3 +1,241 @@
|
|||
## [0.186.1](https://github.com/n8n-io/n8n/compare/n8n@0.186.0...n8n@0.186.1) (2022-07-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Airtable Node:** Fix authentication issue ([#3709](https://github.com/n8n-io/n8n/issues/3709)) ([33d8042](https://github.com/n8n-io/n8n/commit/33d804284ae02140749ab94eecfca1699e13afee))
|
||||
|
||||
|
||||
|
||||
# [0.186.0](https://github.com/n8n-io/n8n/compare/n8n@0.185.0...n8n@0.186.0) (2022-07-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **editor:** Fix error after multiple executions ([#3697](https://github.com/n8n-io/n8n/issues/3697)) ([d200661](https://github.com/n8n-io/n8n/commit/d200661b84c36b3f04d812cf022bb338f9664392))
|
||||
* **EmailReadImap Node:** Improve handling of network problems ([#3406](https://github.com/n8n-io/n8n/issues/3406)) ([6f5809e](https://github.com/n8n-io/n8n/commit/6f5809edb3f9cac0c29d448300b37ab9b6e74c08))
|
||||
* **Google Drive Node:** Process all input items with List operation ([#3525](https://github.com/n8n-io/n8n/issues/3525)) ([ece1836](https://github.com/n8n-io/n8n/commit/ece1836c45707d349330f742eb3b83fa1f4eaebb))
|
||||
* **Telegram Node:** Fix sending binaryData media (photo, document, video etc.) ([#3408](https://github.com/n8n-io/n8n/issues/3408)) ([af45a07](https://github.com/n8n-io/n8n/commit/af45a07f21d8448bad5c12ed702b7aa983017a2b))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add item information to more node errors ([#3681](https://github.com/n8n-io/n8n/issues/3681)) ([2a8043c](https://github.com/n8n-io/n8n/commit/2a8043cd27968b92b1857135d130e3ee54aae779))
|
||||
* **AWS DynamoDB Node:** Improve error handling + add optional GetAll Scan FilterExpression ([#3318](https://github.com/n8n-io/n8n/issues/3318)) ([732c8fc](https://github.com/n8n-io/n8n/commit/732c8fcf8488fc35839855499f75202436fc4c9a))
|
||||
* **Customer.io Node:** Add support for tracking API region selection ([#3378](https://github.com/n8n-io/n8n/issues/3378)) ([82a254a](https://github.com/n8n-io/n8n/commit/82a254a8d9295901e42ec999432a7f5b40f38281))
|
||||
* **Elasticsearch Node:** Add 'Source Excludes' and 'Source Includes' options on 'Document: getAll' operation ([#3660](https://github.com/n8n-io/n8n/issues/3660)) ([8999403](https://github.com/n8n-io/n8n/commit/899940322831612bdf6e59db7f696c34f96cd496))
|
||||
* **Elasticsearch Node:** Add credential tests, index pipelines and index refresh ([#2420](https://github.com/n8n-io/n8n/issues/2420))
|
||||
* **Freshworks CRM Node:** Add Search + Lookup functionality ([#3131](https://github.com/n8n-io/n8n/issues/3131)) ([dbc0280](https://github.com/n8n-io/n8n/commit/dbc02803db5351d759b1420e94b14f2c7c8b1bef))
|
||||
* **Jira Trigger Node:** Add optional query auth for security ([#3172](https://github.com/n8n-io/n8n/issues/3172)) ([25093b6](https://github.com/n8n-io/n8n/commit/25093b64e693a33a76efd1bd12f00ce0d4cc0f3c))
|
||||
* **Postgres Node:** Improvement handling of large numbers ([#3360](https://github.com/n8n-io/n8n/issues/3360)) ([9f908e7](https://github.com/n8n-io/n8n/commit/9f908e7405d687bf57391e503ad724d58caaac07))
|
||||
* **Redis Node:** Add push and pop operations ([#3127](https://github.com/n8n-io/n8n/issues/3127)) ([32c68eb](https://github.com/n8n-io/n8n/commit/32c68eb126f8411d1a3261dc8a900c109b99da6f))
|
||||
* **Rename Node:** Add regex replace ([#2576](https://github.com/n8n-io/n8n/issues/2576)) ([eae9a60](https://github.com/n8n-io/n8n/commit/eae9a60a431bc08fb58016e3249328abb90716b0))
|
||||
* **SpreadsheetFile Node:** Allow skipping headers when writing spreadsheets ([#3234](https://github.com/n8n-io/n8n/issues/3234)) ([dbfb8d5](https://github.com/n8n-io/n8n/commit/dbfb8d56dc6290837701dea5957d4e73db418892))
|
||||
* Updated multiple credentials with tests and allow to be used on HTTP Request Node ([#3670](https://github.com/n8n-io/n8n/issues/3670)) ([d5d4dd3](https://github.com/n8n-io/n8n/commit/d5d4dd38450b788ee0ce3ed8ad0eb714c86977d2))
|
||||
|
||||
|
||||
|
||||
# [0.185.0](https://github.com/n8n-io/n8n/compare/n8n@0.184.0...n8n@0.185.0) (2022-07-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Hubspot Node:** Fix search endpoints ([#3640](https://github.com/n8n-io/n8n/issues/3640)) ([16b9926](https://github.com/n8n-io/n8n/commit/16b9926cd25abf4a2ae4c9eba494340eab58082f))
|
||||
* **KoboToolbox Node:** Improve attachment matching logic and GeoJSON Polygon format ([#3535](https://github.com/n8n-io/n8n/issues/3535)) ([637e815](https://github.com/n8n-io/n8n/commit/637e81552f86788058567342cf69e2784e3d6b2f))
|
||||
* **Odoo Node:** Prevent possible issues with some custom fields ([#3496](https://github.com/n8n-io/n8n/issues/3496)) ([7d968ec](https://github.com/n8n-io/n8n/commit/7d968ec202ceccc6a009ec150747cc927273f841))
|
||||
* **Sticky Node:** Fix main header hiding ([#3654](https://github.com/n8n-io/n8n/issues/3654)) ([88486bc](https://github.com/n8n-io/n8n/commit/88486bc778786d4a47ef1bb5c743c9fb206aee01))
|
||||
* **Todoist Node:** Fix multiple item support ([#3614](https://github.com/n8n-io/n8n/issues/3614)) ([7ba85c4](https://github.com/n8n-io/n8n/commit/7ba85c4ab910ed02696078ece12c88f2141cccad))
|
||||
|
||||
|
||||
### Features
|
||||
* **core:** Add `action` to `INodePropertyOptions` ([#3610](https://github.com/n8n-io/n8n/issues/3610)) ([3c65968](https://github.com/n8n-io/n8n/commit/3c659682e94cdd01fd6f267a468a031b028cf690))
|
||||
* **DeepL Node:** Add support for longer texts + Credential tests ([#3651](https://github.com/n8n-io/n8n/issues/3651)) ([88d6cfc](https://github.com/n8n-io/n8n/commit/88d6cfc07bfd2be64a39f285d235e22aae8c1522))
|
||||
* **Facebook Node:** Add support for Facebook Graph API versions 14 ([#3656](https://github.com/n8n-io/n8n/issues/3656)) ([174d063](https://github.com/n8n-io/n8n/commit/174d06383191e6e70ba27bc3e6e46527731c80b5))
|
||||
* **Google Ads Node:** Add new node ([#3526](https://github.com/n8n-io/n8n/issues/3526)) ([088daf9](https://github.com/n8n-io/n8n/commit/088daf952ea7340a3101362bce18668147b8431f))
|
||||
* **Jira Node:** Use Jira rendered fields with simplify option ([#3323](https://github.com/n8n-io/n8n/issues/3323)) ([07b6cff](https://github.com/n8n-io/n8n/commit/07b6cffdba55a48bfed629a1faec8cf88bee88bc))
|
||||
* **Webflow Trigger Node:** Reduce chance of webhook duplication and add credential test ([#3594](https://github.com/n8n-io/n8n/issues/3594)) ([224e008](https://github.com/n8n-io/n8n/commit/224e008fb64dabef99998508eb4385e1b872c5ad))
|
||||
* **Wordpress Node:** Add post template option ([#3139](https://github.com/n8n-io/n8n/issues/3139)) ([02bc3da](https://github.com/n8n-io/n8n/commit/02bc3da78545de4771edf6fdc68720b0e7d596b9))
|
||||
|
||||
|
||||
|
||||
# [0.184.0](https://github.com/n8n-io/n8n/compare/n8n@0.183.0...n8n@0.184.0) (2022-06-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** Fix logger error when logging circular json ([#3583](https://github.com/n8n-io/n8n/issues/3583)) ([3cb693d](https://github.com/n8n-io/n8n/commit/3cb693d5d4b8aaf800df70e62c1b2ca2ff208c4d))
|
||||
* Correct misfix from `node-param-display-name-wrong-for-dynamic-multi-options` ([#3575](https://github.com/n8n-io/n8n/issues/3575)) ([2ccc7fb](https://github.com/n8n-io/n8n/commit/2ccc7fbc9d1df3f044cf42fe1af72bc7352caa9f))
|
||||
* **Cortex Node:** Fix issue that not all Analyzers got returned ([#3606](https://github.com/n8n-io/n8n/issues/3606)) ([6e595c7](https://github.com/n8n-io/n8n/commit/6e595c72760f47107f67c1fd2bdbe76c31af4a8b))
|
||||
* **editor:** Display full text of long error messages ([#3561](https://github.com/n8n-io/n8n/issues/3561)) ([8db4405](https://github.com/n8n-io/n8n/commit/8db44057f2101698ef4869fca436862e4dd39fc1))
|
||||
* **editor:** Fix credentials rendering when the node has no parameters ([#3563](https://github.com/n8n-io/n8n/issues/3563)) ([55bab19](https://github.com/n8n-io/n8n/commit/55bab19eb440ed9d58137f4334a37d5f731afe0f))
|
||||
* Fix issue with required optional parameters ([#3577](https://github.com/n8n-io/n8n/issues/3577)) ([42d2959](https://github.com/n8n-io/n8n/commit/42d2959f47f33defda4239a4d2fbba6927d98617))
|
||||
* Fix issue with required optional parameters ([#3597](https://github.com/n8n-io/n8n/issues/3597)) ([848fcfd](https://github.com/n8n-io/n8n/commit/848fcfde5d95d952170e9a3d51b629971a13b832))
|
||||
* **HTTP Request Node:** Make all OAuth2 credentials work with HTTP Request Node ([#3503](https://github.com/n8n-io/n8n/issues/3503)) ([acdb4d9](https://github.com/n8n-io/n8n/commit/acdb4d92c8ef95646e69694b2451a9111a81c52f))
|
||||
* **LinkedIn Node:** Fix LinkedIn image preview ([#3528](https://github.com/n8n-io/n8n/issues/3528)) ([32f245d](https://github.com/n8n-io/n8n/commit/32f245da53c186a03172dbb23761a05b5e301532))
|
||||
* **Salesforce Node:** Fix issue with lead status not using name on update ([#3599](https://github.com/n8n-io/n8n/issues/3599)) ([7ccae7c](https://github.com/n8n-io/n8n/commit/7ccae7c9b22f2848a8aa357227d145241801ba82))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Clockify Node:** Add more resources and improvements ([#3411](https://github.com/n8n-io/n8n/issues/3411)) ([447d190](https://github.com/n8n-io/n8n/commit/447d19024c512eea8e290d8ebc6c3ce82a53f002))
|
||||
* **core:** Expose item index being processed ([#3590](https://github.com/n8n-io/n8n/issues/3590)) ([1e4fd9e](https://github.com/n8n-io/n8n/commit/1e4fd9e4df524fdee8195de7be244ff03d97f917))
|
||||
* **core:** Give access to getBinaryDataBuffer in preSend method ([#3588](https://github.com/n8n-io/n8n/issues/3588)) ([522b31a](https://github.com/n8n-io/n8n/commit/522b31a47b4f4e9990e07dcc504ef2821a1fd0a5))
|
||||
* Migrated to npm release of riot-tmpl fork ([#3581](https://github.com/n8n-io/n8n/issues/3581)) ([891844e](https://github.com/n8n-io/n8n/commit/891844ea8b3248195355f736d7331fd967ee99e1))
|
||||
|
||||
|
||||
|
||||
# [0.183.0](https://github.com/n8n-io/n8n/compare/n8n@0.182.1...n8n@0.183.0) (2022-06-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** Do allow OPTIONS requests from any source ([#3555](https://github.com/n8n-io/n8n/issues/3555)) ([74e6b06](https://github.com/n8n-io/n8n/commit/74e6b06467f8d0059c8cc45154e2d2822dc9b0c5))
|
||||
* **core:** Fix issue that GET /workflows/:id does not return tags ([#3522](https://github.com/n8n-io/n8n/issues/3522)) ([f75f5d7](https://github.com/n8n-io/n8n/commit/f75f5d711f886892a1afcebff722ab476390f4f0))
|
||||
* **core:** Fix issue that some predefined credentials do not show up on HTTP Request Node ([#3556](https://github.com/n8n-io/n8n/issues/3556)) ([d417ea7](https://github.com/n8n-io/n8n/commit/d417ea7ffad9e2210f3b2b5e7122ffbe70f2ba27))
|
||||
* **core:** Return correct error message if Axios error ([#3478](https://github.com/n8n-io/n8n/issues/3478)) ([1bef4df](https://github.com/n8n-io/n8n/commit/1bef4df75f999ac2e413b6c179baab3321c52fa2))
|
||||
* **core:** Updated expressions allowlist and denylist. ([#3424](https://github.com/n8n-io/n8n/issues/3424)) ([d18a29d](https://github.com/n8n-io/n8n/commit/d18a29d5882fb8f4475258189f6badcd0a573b34))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **editor:** Improve trigger panel ([#3509](https://github.com/n8n-io/n8n/issues/3509)) ([a2f6289](https://github.com/n8n-io/n8n/commit/a2f628927dff7ea6741ef8e4a60bcafd95dac7bf))
|
||||
* **Hubspot Node:** Allow to set Stage on Ticket Update ([#3317](https://github.com/n8n-io/n8n/issues/3317)) ([0ac9e3f](https://github.com/n8n-io/n8n/commit/0ac9e3f975b73e88acabb66de8b8565f881f64ec))
|
||||
* **Todoist Node:** Make it possible to move tasks between sections ([#3074](https://github.com/n8n-io/n8n/issues/3074)) ([049e454](https://github.com/n8n-io/n8n/commit/049e4544d9ccc0acce2a596aced06ec86992e09a))
|
||||
* **Twake Node:** Update icon, add cred test and custom operation support ([#3431](https://github.com/n8n-io/n8n/issues/3431)) ([6d64e84](https://github.com/n8n-io/n8n/commit/6d64e84f5e19d5f6d83ccc0a55cdcbd256e5804f))
|
||||
|
||||
|
||||
|
||||
## [0.182.1](https://github.com/n8n-io/n8n/compare/n8n@0.182.0...n8n@0.182.1) (2022-06-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** Fix issue with restarting waiting executions ([#3531](https://github.com/n8n-io/n8n/issues/3531)) ([c9273bc](https://github.com/n8n-io/n8n/commit/c9273bcd3862217b4918ac8abb37fae9c2e64622))
|
||||
|
||||
|
||||
|
||||
# [0.182.0](https://github.com/n8n-io/n8n/compare/n8n@0.181.2...n8n@0.182.0) (2022-06-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** Fix issue that parameters got lost in some edge cases ([04f0bf5](https://github.com/n8n-io/n8n/commit/04f0bf5b65c8224a4fdfd3c9d2c896f63dfbcc1d))
|
||||
* **core:** Fix issue with combined expression not resolving if one is invalid ([#3506](https://github.com/n8n-io/n8n/issues/3506)) ([9ff5762](https://github.com/n8n-io/n8n/commit/9ff57629c5afb2f0fd4aee84cda79c9a6f7962d0))
|
||||
* **core:** Fix Public API failing to build on Windows ([#3499](https://github.com/n8n-io/n8n/issues/3499)) ([c121952](https://github.com/n8n-io/n8n/commit/c121952324619434e8a7be540970c167df715b13))
|
||||
* **editor:** Fix issue that some errors did not show up correctly ([#3507](https://github.com/n8n-io/n8n/issues/3507)) ([955db0a](https://github.com/n8n-io/n8n/commit/955db0ab101feb17efffe760c79ec2820e1d4c3b))
|
||||
* **HTTP Request Node:** Fix issue with requests that return null ([#3498](https://github.com/n8n-io/n8n/issues/3498)) ([7346da0](https://github.com/n8n-io/n8n/commit/7346da0b34b5fdf7ab630ccc5cda102cf80c8036))
|
||||
* **Pipedrive Node:** Fix limit issue with Lead -> GetAll ([#3436](https://github.com/n8n-io/n8n/issues/3436)) ([34e891c](https://github.com/n8n-io/n8n/commit/34e891c0f8c987c9be9cff463422b9972f02269f))
|
||||
* **PostBin Node:** Fix issue with it throwing unnecessary error ([#3494](https://github.com/n8n-io/n8n/issues/3494)) ([9df3e30](https://github.com/n8n-io/n8n/commit/9df3e30d36104d8e31972c773cb71f4cc82f6970))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **core:** Add "Client Credentials" grant type to OAuth2 ([#3489](https://github.com/n8n-io/n8n/issues/3489)) ([e29c597](https://github.com/n8n-io/n8n/commit/e29c5975e1f1ad089167df46021203e9f67c8ef1))
|
||||
* **Twilio Node:** Add ability to make a voice call using TTS ([#3467](https://github.com/n8n-io/n8n/issues/3467)) ([eff97e8](https://github.com/n8n-io/n8n/commit/eff97e8d67cd3f0342bbb9648503b351f4691f46))
|
||||
* **Wise Node:** Add Support to download statements as JSON, CSV or PDF ([#3468](https://github.com/n8n-io/n8n/issues/3468)) ([51663c1](https://github.com/n8n-io/n8n/commit/51663c1fcbe879e29790af942b73318e95065d8f))
|
||||
|
||||
|
||||
|
||||
## [0.181.2](https://github.com/n8n-io/n8n/compare/n8n@0.181.1...n8n@0.181.2) (2022-06-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** Fix issue when a node does not return data ([5eea3cd](https://github.com/n8n-io/n8n/commit/5eea3cd6d0b59963dc7c7a9e1ca597137cf3ce98))
|
||||
|
||||
|
||||
|
||||
## [0.181.1](https://github.com/n8n-io/n8n/compare/n8n@0.181.0...n8n@0.181.1) (2022-06-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** Fix another possible issue with multi input nodes ([e88fab5](https://github.com/n8n-io/n8n/commit/e88fab5ee2b82665c3d68c52894a5479ce6eccf6))
|
||||
* **core:** Fix issue with multi input nodes ([f79675d](https://github.com/n8n-io/n8n/commit/f79675d5c7876875065fc29504eb0590678d67d3))
|
||||
|
||||
|
||||
|
||||
# [0.181.0](https://github.com/n8n-io/n8n/compare/n8n@0.180.0...n8n@0.181.0) (2022-06-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** Properly resolve expressions in declarative node design ([1999f4b](https://github.com/n8n-io/n8n/commit/1999f4b066784cc1dd6a962f51d7c11641577a8b))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add n8n Public API ([#3064](https://github.com/n8n-io/n8n/issues/3064)) ([a18081d](https://github.com/n8n-io/n8n/commit/a18081d749c51d497645d43614fdccb220344607))
|
||||
* **core:** Make it possible to block access to environment variables ([ddb3baa](https://github.com/n8n-io/n8n/commit/ddb3baa4eddeb85e2f7abe4465ac4ff4058e1ece))
|
||||
|
||||
|
||||
|
||||
# [0.180.0](https://github.com/n8n-io/n8n/compare/n8n@0.179.0...n8n@0.180.0) (2022-06-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **core:** Allow "window" again in expressions ([#3474](https://github.com/n8n-io/n8n/issues/3474)) ([ca92ff7](https://github.com/n8n-io/n8n/commit/ca92ff70d7789e7d0af812cff7c7351e499ddfa2))
|
||||
- **core:** Fix `user-management:reset` command ([#3403](https://github.com/n8n-io/n8n/issues/3403)) ([58ecadf](https://github.com/n8n-io/n8n/commit/58ecadf53c35ee0dc897eb7eb29f345f8e797b2b))
|
||||
- **core:** Fix crashes in queue mode ([#3397](https://github.com/n8n-io/n8n/issues/3397)) ([042b8da](https://github.com/n8n-io/n8n/commit/042b8daf1cd16822e2da03cf18a69091477d4451))
|
||||
- **editor:** Fix delete button hover spacing ([#3412](https://github.com/n8n-io/n8n/issues/3412)) ([50ff75e](https://github.com/n8n-io/n8n/commit/50ff75ecb2b42dfdb00e9c086cf604f1ca699360))
|
||||
- **editor:** Fix stuck loading states ([#3428](https://github.com/n8n-io/n8n/issues/3428)) ([450a9aa](https://github.com/n8n-io/n8n/commit/450a9aafea0e44c5d6e6541a9e0872a9d3ac7dee))
|
||||
- **EmailReadImap Node:** Improve error handling ([#3465](https://github.com/n8n-io/n8n/issues/3465)) ([3806d63](https://github.com/n8n-io/n8n/commit/3806d6355d4af4ad1222bac20cb36f5ef586501a))
|
||||
- **Hubspot Node:** Fix loading of Contacts ([#3426](https://github.com/n8n-io/n8n/issues/3426)) ([f02421b](https://github.com/n8n-io/n8n/commit/f02421b5f3c4946aac6257bbd806d72d0031313f))
|
||||
|
||||
### Features
|
||||
|
||||
- **Cal Trigger Node:** Add cal.com Trigger Node ([#3439](https://github.com/n8n-io/n8n/issues/3439)) ([1fa445e](https://github.com/n8n-io/n8n/commit/1fa445e0e74462dd28fc81329230b868137dcbd5))
|
||||
- **core:** Add support for pairedItem (beta) ([#3012](https://github.com/n8n-io/n8n/issues/3012)) ([bdb8413](https://github.com/n8n-io/n8n/commit/bdb84130d687811d65337ff6b025e7cb0eae8256))
|
||||
- **core:** Add support to import/export tags ([#3130](https://github.com/n8n-io/n8n/issues/3130)) ([15a20d2](https://github.com/n8n-io/n8n/commit/15a20d257d7b6b35224c0a654f0f1988081d06d2))
|
||||
- **core:** Run Error Workflow also on trigger activation error ([#3470](https://github.com/n8n-io/n8n/issues/3470)) ([b5535e4](https://github.com/n8n-io/n8n/commit/b5535e4a6233d397060308ad1b8c254b28a2d57e))
|
||||
- **editor:** Display Credential-Selector after Authentication Type-Selector ([#3461](https://github.com/n8n-io/n8n/issues/3461)) ([59a59e0](https://github.com/n8n-io/n8n/commit/59a59e0c5f1a5207a7124655b5768ac9fededcdc))
|
||||
- **editor:** Display node specific settings above general ones ([50ca9c4](https://github.com/n8n-io/n8n/commit/50ca9c4c7e39e1ba176724c4b20dbbab12695cc4))
|
||||
- **GitHub Node:**: Add Organization -> Get All operation ([#3247](https://github.com/n8n-io/n8n/pull/3247))
|
||||
- **QuickBooks Node:** Add optional Tax item field ([#3404](https://github.com/n8n-io/n8n/issues/3404)) ([c341b45](https://github.com/n8n-io/n8n/commit/c341b45396c7282da087046ade16265c99c8d9dd))
|
||||
|
||||
|
||||
# [0.179.0](https://github.com/n8n-io/n8n/compare/n8n@0.178.2...n8n@0.179.0) (2022-05-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **core:** Fix issue that "closeFunction" got called twice ([1910299](https://github.com/n8n-io/n8n/commit/1910299a884e8d4d80d4aa6656eb4892b0fcb713))
|
||||
- **core:** Fix migrations on non-public Postgres schema ([#3356](https://github.com/n8n-io/n8n/issues/3356)) ([b49d493](https://github.com/n8n-io/n8n/commit/b49d49365398f06376e53d86e6c8c5dc15f67e57))
|
||||
- **core:** Fix problem with uploading large files ([#3370](https://github.com/n8n-io/n8n/issues/3370)) ([d3cecfc](https://github.com/n8n-io/n8n/commit/d3cecfc55baf547b2a2bedddebf7e890510187e0))
|
||||
- **core:** Prevent expressions XSS ([#3366](https://github.com/n8n-io/n8n/issues/3366)) ([993554f](https://github.com/n8n-io/n8n/commit/993554f22a575d68f7b4424fbcf7d5e0dd8a7186))
|
||||
- **Discord Node:** Fix broken rate limit handling ([#3311](https://github.com/n8n-io/n8n/issues/3311)) ([b687ba1](https://github.com/n8n-io/n8n/commit/b687ba11ccac0dfb5a5c61e5db2604ffa8b5dec0))
|
||||
- **editor:** Fix component in executions list failing custom validator ([#3284](https://github.com/n8n-io/n8n/issues/3284)) ([d719678](https://github.com/n8n-io/n8n/commit/d71967878f0ef43b8a464aa9e3703f80f5a08ed7))
|
||||
- **editor:** Fix conflicting hover states between sticky button and node view ([#3368](https://github.com/n8n-io/n8n/issues/3368)) ([96a109a](https://github.com/n8n-io/n8n/commit/96a109a57c808943f3ab6121ff3e830b12e82d96))
|
||||
- **editor:** Fix credential display bug ([#3372](https://github.com/n8n-io/n8n/issues/3372)) ([ed69c3c](https://github.com/n8n-io/n8n/commit/ed69c3cc18d47f906f8cf5c2a6784ee20ae390bd))
|
||||
- **Gmail Node:** Fix sending attachments when filesystem mode is used ([#3396](https://github.com/n8n-io/n8n/issues/3396)) ([3a09da9](https://github.com/n8n-io/n8n/commit/3a09da92be556df1b840c0e16e020b0618ce7643))
|
||||
- **Google Sheet Node:** Fix issue with null values and "Use Header Names as JSON Paths" option ([#3395](https://github.com/n8n-io/n8n/issues/3395)) ([fbf6019](https://github.com/n8n-io/n8n/commit/fbf60199d95d2448f9f34d0175da316fc18a80b7))
|
||||
- **NextCloud Node:** Fix folder list with Nextcloud v24 ([#3386](https://github.com/n8n-io/n8n/issues/3386)) ([5f3bed3](https://github.com/n8n-io/n8n/commit/5f3bed3d4e9134b96d6964ca28b5b5dfb1adc1c3))
|
||||
|
||||
### Features
|
||||
|
||||
- **PostBin Node:** Add PostBin node ([#3236](https://github.com/n8n-io/n8n/issues/3236)) ([06c407d](https://github.com/n8n-io/n8n/commit/06c407def88e5872b2478ac240430006055a2a22))
|
||||
- **RabbitMQ Trigger Node:** Make message acknowledgement and parallel processing configurable ([#3385](https://github.com/n8n-io/n8n/issues/3385)) ([b851289](https://github.com/n8n-io/n8n/commit/b85128900187b1709a7bf13eb5c5d5c4a5528fde))
|
||||
- **ServiceNow Node:** Add attachment functionality ([#3137](https://github.com/n8n-io/n8n/issues/3137)) ([c38f6af](https://github.com/n8n-io/n8n/commit/c38f6af4993cd695888ff18b3f95e0d900e65711))
|
||||
- **Todoist Node:** Add support for specifying the parent task when adding and listing tasks ([#3161](https://github.com/n8n-io/n8n/issues/3161)) ([dc77594](https://github.com/n8n-io/n8n/commit/dc77594a1eaec73fa34ed09c52d108482002ffff))
|
||||
|
||||
|
||||
## [0.178.2](https://github.com/n8n-io/n8n/compare/n8n@0.178.1...n8n@0.178.2) (2022-05-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **editor:** Fix parameter loading bug ([#3374](https://github.com/n8n-io/n8n/issues/3374)) ([c7c2061](https://github.com/n8n-io/n8n/commit/c7c2061590493a1b24a8ab4e2615d6d9eb2641e1))
|
||||
|
||||
|
||||
|
||||
## [0.178.1](https://github.com/n8n-io/n8n/compare/n8n@0.178.0...n8n@0.178.1) (2022-05-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **editor:** Fix problem with HTTP Request Node 1 credentials to be set ([#3371](https://github.com/n8n-io/n8n/issues/3371)) ([c5fc3bc](https://github.com/n8n-io/n8n/commit/c5fc3bc45e80eec47f4c06b950ab8b3ddaf66f2f))
|
||||
|
||||
|
||||
|
||||
# [0.178.0](https://github.com/n8n-io/n8n/compare/n8n@0.177.0...n8n@0.178.0) (2022-05-24)
|
||||
|
||||
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -1,6 +1,6 @@
|
|||
# n8n - Workflow Automation Tool
|
||||

|
||||
|
||||

|
||||
# n8n - Workflow automation tool
|
||||
|
||||
n8n is an extendable workflow automation tool. With a [fair-code](http://faircode.io) distribution model, n8n will always have visible source code, be available to self-host, and allow you to add your own custom functions, logic and apps. n8n's node-based approach makes it highly versatile, enabling you to connect anything to everything.
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ n8n is an extendable workflow automation tool. With a [fair-code](http://faircod
|
|||
|
||||
## Available integrations
|
||||
|
||||
n8n has 200+ different nodes to automate workflows. The list can be found on: [https://n8n.io/nodes](https://n8n.io/nodes)
|
||||
n8n has 200+ different nodes to automate workflows. The list can be found on: [https://n8n.io/integrations](https://n8n.io/integrations)
|
||||
|
||||
## Documentation
|
||||
|
||||
|
|
@ -31,11 +31,11 @@ The changelog can be found [here](https://docs.n8n.io/reference/changelog.html)
|
|||
|
||||
Execute: `npm run start`
|
||||
|
||||
## n8n.cloud
|
||||
## n8n cloud
|
||||
|
||||
Sign-up for an [n8n.cloud](https://www.n8n.cloud/) account.
|
||||
Sign-up for an [n8n cloud](https://www.n8n.io/cloud/) account.
|
||||
|
||||
While n8n.cloud and n8n are the same in terms of features, n8n.cloud provides certain conveniences such as:
|
||||
While n8n cloud and n8n are the same in terms of features, n8 cloud provides certain conveniences such as:
|
||||
|
||||
- Not having to set up and maintain your n8n instance
|
||||
- Managed OAuth for authentication
|
||||
|
|
@ -66,7 +66,7 @@ However, I did not like how long the name was and I could not imagine writing
|
|||
something that long every time in the CLI. That is when I then ended up on
|
||||
'n8n'." - **Jan Oberhauser, Founder and CEO, n8n.io**
|
||||
|
||||
## Development Setup
|
||||
## Development setup
|
||||
|
||||
Have you found a bug :bug: ? Or maybe you have a nice feature :sparkles: to contribute ? The [CONTRIBUTING guide](https://github.com/n8n-io/n8n/blob/master/CONTRIBUTING.md) will help you get your development environment ready in minutes.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# n8n - Workflow Automation
|
||||

|
||||
|
||||

|
||||
# n8n - Workflow automation tool
|
||||
|
||||
n8n is a free and open [fair-code](http://faircode.io) distributed node based Workflow Automation Tool. It can be self-hosted, easily extended, and so also used with internal tools.
|
||||
n8n is an extendable workflow automation tool. With a [fair-code](http://faircode.io) distribution model, n8n will always have visible source code, be available to self-host, and allow you to add your own custom functions, logic and apps. n8n's node-based approach makes it highly versatile, enabling you to connect anything to everything.
|
||||
|
||||
<a href="https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-screenshot.png"><img src="https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-screenshot.png" alt="n8n.io - Screenshot"></a>
|
||||
|
||||
|
|
|
|||
24196
package-lock.json
generated
24196
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n",
|
||||
"version": "0.178.0",
|
||||
"version": "0.186.1",
|
||||
"private": true,
|
||||
"homepage": "https://n8n.io",
|
||||
"scripts": {
|
||||
|
|
|
|||
57
packages/cli/commands/BaseCommand.ts
Normal file
57
packages/cli/commands/BaseCommand.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { Command } from '@oclif/core';
|
||||
import { LoggerProxy } from 'n8n-workflow';
|
||||
import { getLogger, Logger } from '../src/Logger';
|
||||
import { User } from '../src/databases/entities/User';
|
||||
import { Db } from '../src';
|
||||
|
||||
export abstract class BaseCommand extends Command {
|
||||
logger: Logger;
|
||||
|
||||
/**
|
||||
* Lifecycle methods
|
||||
*/
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.logger = getLogger();
|
||||
LoggerProxy.init(this.logger);
|
||||
|
||||
await Db.init();
|
||||
}
|
||||
|
||||
async finally(): Promise<void> {
|
||||
if (process.env.NODE_ENV === 'test') return;
|
||||
|
||||
this.exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* User Management utils
|
||||
*/
|
||||
|
||||
defaultUserProps = {
|
||||
firstName: null,
|
||||
lastName: null,
|
||||
email: null,
|
||||
password: null,
|
||||
resetPasswordToken: null,
|
||||
};
|
||||
|
||||
async getInstanceOwner(): Promise<User> {
|
||||
const globalRole = await Db.collections.Role.findOneOrFail({
|
||||
name: 'owner',
|
||||
scope: 'global',
|
||||
});
|
||||
|
||||
const owner = await Db.collections.User.findOne({ globalRole });
|
||||
|
||||
if (owner) return owner;
|
||||
|
||||
const user = new User();
|
||||
|
||||
Object.assign(user, { ...this.defaultUserProps, globalRole });
|
||||
|
||||
await Db.collections.User.save(user);
|
||||
|
||||
return Db.collections.User.findOneOrFail({ globalRole });
|
||||
}
|
||||
}
|
||||
|
|
@ -110,8 +110,10 @@ export class ExportWorkflowsCommand extends Command {
|
|||
findQuery.id = flags.id;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const workflows = await Db.collections.Workflow.find(findQuery);
|
||||
const workflows = await Db.collections.Workflow.find({
|
||||
where: findQuery,
|
||||
relations: ['tags'],
|
||||
});
|
||||
|
||||
if (workflows.length === 0) {
|
||||
throw new Error('No workflows found with specified filters.');
|
||||
|
|
|
|||
|
|
@ -17,15 +17,34 @@ import glob from 'fast-glob';
|
|||
import { UserSettings } from 'n8n-core';
|
||||
import { EntityManager, getConnection } from 'typeorm';
|
||||
import { getLogger } from '../../src/Logger';
|
||||
import { Db, ICredentialsDb } from '../../src';
|
||||
import { Db, ICredentialsDb, IWorkflowToImport } from '../../src';
|
||||
import { SharedWorkflow } from '../../src/databases/entities/SharedWorkflow';
|
||||
import { WorkflowEntity } from '../../src/databases/entities/WorkflowEntity';
|
||||
import { Role } from '../../src/databases/entities/Role';
|
||||
import { User } from '../../src/databases/entities/User';
|
||||
import { setTagsForImport } from '../../src/TagHelpers';
|
||||
|
||||
const FIX_INSTRUCTION =
|
||||
'Please fix the database by running ./packages/cli/bin/n8n user-management:reset';
|
||||
|
||||
function assertHasWorkflowsToImport(workflows: unknown): asserts workflows is IWorkflowToImport[] {
|
||||
if (!Array.isArray(workflows)) {
|
||||
throw new Error(
|
||||
'File does not seem to contain workflows. Make sure the workflows are contained in an array.',
|
||||
);
|
||||
}
|
||||
|
||||
for (const workflow of workflows) {
|
||||
if (
|
||||
typeof workflow !== 'object' ||
|
||||
!Object.prototype.hasOwnProperty.call(workflow, 'nodes') ||
|
||||
!Object.prototype.hasOwnProperty.call(workflow, 'connections')
|
||||
) {
|
||||
throw new Error('File does not seem to contain valid workflows.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ImportWorkflowsCommand extends Command {
|
||||
static description = 'Import workflows';
|
||||
|
||||
|
|
@ -82,7 +101,8 @@ export class ImportWorkflowsCommand extends Command {
|
|||
|
||||
// Make sure the settings exist
|
||||
await UserSettings.prepareUserSettings();
|
||||
const credentials = (await Db.collections.Credentials.find()) ?? [];
|
||||
const credentials = await Db.collections.Credentials.find();
|
||||
const tags = await Db.collections.Tag.find();
|
||||
|
||||
let totalImported = 0;
|
||||
|
||||
|
|
@ -111,6 +131,10 @@ export class ImportWorkflowsCommand extends Command {
|
|||
});
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(workflow, 'tags')) {
|
||||
await setTagsForImport(transactionManager, workflow, tags);
|
||||
}
|
||||
|
||||
await this.storeWorkflow(workflow, user);
|
||||
}
|
||||
});
|
||||
|
|
@ -121,13 +145,9 @@ export class ImportWorkflowsCommand extends Command {
|
|||
|
||||
const workflows = JSON.parse(fs.readFileSync(flags.input, { encoding: 'utf8' }));
|
||||
|
||||
totalImported = workflows.length;
|
||||
assertHasWorkflowsToImport(workflows);
|
||||
|
||||
if (!Array.isArray(workflows)) {
|
||||
throw new Error(
|
||||
'File does not seem to contain workflows. Make sure the workflows are contained in an array.',
|
||||
);
|
||||
}
|
||||
totalImported = workflows.length;
|
||||
|
||||
await getConnection().transaction(async (transactionManager) => {
|
||||
this.transactionManager = transactionManager;
|
||||
|
|
@ -139,6 +159,10 @@ export class ImportWorkflowsCommand extends Command {
|
|||
});
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(workflow, 'tags')) {
|
||||
await setTagsForImport(transactionManager, workflow, tags);
|
||||
}
|
||||
|
||||
await this.storeWorkflow(workflow, user);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,85 +1,51 @@
|
|||
/* eslint-disable no-console */
|
||||
|
||||
import Command from '@oclif/command';
|
||||
import { Not } from 'typeorm';
|
||||
import { LoggerProxy } from 'n8n-workflow';
|
||||
import { Db } from '../../src';
|
||||
import { User } from '../../src/databases/entities/User';
|
||||
import { getLogger } from '../../src/Logger';
|
||||
import { BaseCommand } from '../BaseCommand';
|
||||
|
||||
export class Reset extends Command {
|
||||
export class Reset extends BaseCommand {
|
||||
static description = '\nResets the database to the default user state';
|
||||
|
||||
private defaultUserProps = {
|
||||
firstName: null,
|
||||
lastName: null,
|
||||
email: null,
|
||||
password: null,
|
||||
resetPasswordToken: null,
|
||||
};
|
||||
|
||||
async run(): Promise<void> {
|
||||
const logger = getLogger();
|
||||
LoggerProxy.init(logger);
|
||||
await Db.init();
|
||||
const owner = await this.getInstanceOwner();
|
||||
|
||||
try {
|
||||
const owner = await this.getInstanceOwner();
|
||||
|
||||
const ownerWorkflowRole = await Db.collections.Role.findOneOrFail({
|
||||
name: 'owner',
|
||||
scope: 'workflow',
|
||||
});
|
||||
|
||||
const ownerCredentialRole = await Db.collections.Role.findOneOrFail({
|
||||
name: 'owner',
|
||||
scope: 'credential',
|
||||
});
|
||||
|
||||
await Db.collections.SharedWorkflow.update(
|
||||
{ user: { id: Not(owner.id) }, role: ownerWorkflowRole },
|
||||
{ user: owner },
|
||||
);
|
||||
|
||||
await Db.collections.SharedCredentials.update(
|
||||
{ user: { id: Not(owner.id) }, role: ownerCredentialRole },
|
||||
{ user: owner },
|
||||
);
|
||||
await Db.collections.User.delete({ id: Not(owner.id) });
|
||||
await Db.collections.User.save(Object.assign(owner, this.defaultUserProps));
|
||||
|
||||
await Db.collections.Settings.update(
|
||||
{ key: 'userManagement.isInstanceOwnerSetUp' },
|
||||
{ value: 'false' },
|
||||
);
|
||||
await Db.collections.Settings.update(
|
||||
{ key: 'userManagement.skipInstanceOwnerSetup' },
|
||||
{ value: 'false' },
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error resetting database. See log messages for details.');
|
||||
if (error instanceof Error) logger.error(error.message);
|
||||
this.exit(1);
|
||||
}
|
||||
|
||||
console.info('Successfully reset the database to default user state.');
|
||||
this.exit();
|
||||
}
|
||||
|
||||
private async getInstanceOwner(): Promise<User> {
|
||||
const globalRole = await Db.collections.Role.findOneOrFail({
|
||||
const ownerWorkflowRole = await Db.collections.Role.findOneOrFail({
|
||||
name: 'owner',
|
||||
scope: 'global',
|
||||
scope: 'workflow',
|
||||
});
|
||||
|
||||
const owner = await Db.collections.User.findOne({ globalRole });
|
||||
const ownerCredentialRole = await Db.collections.Role.findOneOrFail({
|
||||
name: 'owner',
|
||||
scope: 'credential',
|
||||
});
|
||||
|
||||
if (owner) return owner;
|
||||
await Db.collections.SharedWorkflow.update(
|
||||
{ user: { id: Not(owner.id) }, role: ownerWorkflowRole },
|
||||
{ user: owner },
|
||||
);
|
||||
|
||||
const user = new User();
|
||||
await Db.collections.SharedCredentials.update(
|
||||
{ user: { id: Not(owner.id) }, role: ownerCredentialRole },
|
||||
{ user: owner },
|
||||
);
|
||||
|
||||
await Db.collections.User.save(Object.assign(user, { ...this.defaultUserProps, globalRole }));
|
||||
await Db.collections.User.delete({ id: Not(owner.id) });
|
||||
await Db.collections.User.save(Object.assign(owner, this.defaultUserProps));
|
||||
|
||||
return Db.collections.User.findOneOrFail({ globalRole });
|
||||
await Db.collections.Settings.update(
|
||||
{ key: 'userManagement.isInstanceOwnerSetUp' },
|
||||
{ value: 'false' },
|
||||
);
|
||||
await Db.collections.Settings.update(
|
||||
{ key: 'userManagement.skipInstanceOwnerSetup' },
|
||||
{ value: 'false' },
|
||||
);
|
||||
|
||||
this.logger.info('Successfully reset the database to default user state.');
|
||||
}
|
||||
|
||||
async catch(error: Error): Promise<void> {
|
||||
this.logger.error('Error resetting database. See log messages for details.');
|
||||
this.logger.error(error.message);
|
||||
this.exit(1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,10 +122,15 @@ export class Worker extends Command {
|
|||
const executionDb = await Db.collections.Execution.findOne(jobData.executionId);
|
||||
|
||||
if (!executionDb) {
|
||||
LoggerProxy.error('Worker failed to find execution data in database. Cannot continue.', {
|
||||
executionId: jobData.executionId,
|
||||
});
|
||||
throw new Error('Unable to find execution data in database. Aborting execution.');
|
||||
LoggerProxy.error(
|
||||
`Worker failed to find data of execution "${jobData.executionId}" in database. Cannot continue.`,
|
||||
{
|
||||
executionId: jobData.executionId,
|
||||
},
|
||||
);
|
||||
throw new Error(
|
||||
`Unable to find data of execution "${jobData.executionId}" in database. Aborting execution.`,
|
||||
);
|
||||
}
|
||||
const currentExecutionDb = ResponseHelper.unflattenExecutionData(executionDb);
|
||||
LoggerProxy.info(
|
||||
|
|
@ -349,6 +354,7 @@ export class Worker extends Command {
|
|||
process.exit(2);
|
||||
} else {
|
||||
logger.error('Error from queue: ', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ config.getEnv = config.get;
|
|||
// optional configuration files
|
||||
if (process.env.N8N_CONFIG_FILES !== undefined) {
|
||||
const configFiles = process.env.N8N_CONFIG_FILES.split(',');
|
||||
console.log(`\nLoading configuration overwrites from:\n - ${configFiles.join('\n - ')}\n`);
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
console.log(`\nLoading configuration overwrites from:\n - ${configFiles.join('\n - ')}\n`);
|
||||
}
|
||||
|
||||
config.loadFile(configFiles);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -582,6 +582,21 @@ export const schema = {
|
|||
},
|
||||
},
|
||||
|
||||
publicApi: {
|
||||
disabled: {
|
||||
format: Boolean,
|
||||
default: false,
|
||||
env: 'N8N_PUBLIC_API_DISABLED',
|
||||
doc: 'Whether to disable the Public API',
|
||||
},
|
||||
path: {
|
||||
format: String,
|
||||
default: 'api',
|
||||
env: 'N8N_PUBLIC_API_ENDPOINT',
|
||||
doc: 'Path for the public api endpoints',
|
||||
},
|
||||
},
|
||||
|
||||
workflowTagsDisabled: {
|
||||
format: Boolean,
|
||||
default: false,
|
||||
|
|
@ -772,7 +787,7 @@ export const schema = {
|
|||
endpoint: {
|
||||
doc: 'Endpoint to retrieve version information from.',
|
||||
format: String,
|
||||
default: 'https://api.n8n.io/versions/',
|
||||
default: 'https://api.n8n.io/api/versions/',
|
||||
env: 'N8N_VERSION_NOTIFICATIONS_ENDPOINT',
|
||||
},
|
||||
infoUrl: {
|
||||
|
|
@ -793,7 +808,7 @@ export const schema = {
|
|||
host: {
|
||||
doc: 'Endpoint host to retrieve workflow templates from endpoints.',
|
||||
format: String,
|
||||
default: 'https://api.n8n.io/',
|
||||
default: 'https://api.n8n.io/api/',
|
||||
env: 'N8N_TEMPLATES_HOST',
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
import path from 'path';
|
||||
import { UserSettings } from 'n8n-core';
|
||||
import { entities } from '../src/databases/entities';
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
name: 'sqlite',
|
||||
type: 'sqlite',
|
||||
logging: true,
|
||||
entities: Object.values(entities),
|
||||
database: path.join(UserSettings.getUserN8nFolderPath(), 'database.sqlite'),
|
||||
migrations: ['./src/databases/sqlite/migrations/index.ts'],
|
||||
subscribers: ['./src/databases/sqlite/subscribers/*.ts'],
|
||||
cli: {
|
||||
entitiesDir: './src/databases/entities',
|
||||
migrationsDir: './src/databases/sqlite/migrations',
|
||||
subscribersDir: './src/databases/sqlite/subscribers',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'postgres',
|
||||
type: 'postgres',
|
||||
logging: false,
|
||||
host: 'localhost',
|
||||
username: 'postgres',
|
||||
password: '',
|
||||
port: 5432,
|
||||
database: 'n8n',
|
||||
schema: 'public',
|
||||
entities: Object.values(entities),
|
||||
migrations: ['./src/databases/postgresdb/migrations/index.ts'],
|
||||
subscribers: ['src/subscriber/**/*.ts'],
|
||||
cli: {
|
||||
entitiesDir: './src/databases/entities',
|
||||
migrationsDir: './src/databases/postgresdb/migrations',
|
||||
subscribersDir: './src/databases/postgresdb/subscribers',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mysql',
|
||||
type: 'mysql',
|
||||
database: 'n8n',
|
||||
username: 'root',
|
||||
password: 'password',
|
||||
host: 'localhost',
|
||||
port: '3306',
|
||||
logging: false,
|
||||
entities: Object.values(entities),
|
||||
migrations: ['./src/databases/mysqldb/migrations/index.ts'],
|
||||
subscribers: ['src/subscriber/**/*.ts'],
|
||||
cli: {
|
||||
entitiesDir: './src/databases/entities',
|
||||
migrationsDir: './src/databases/mysqldb/migrations',
|
||||
subscribersDir: './src/databases/mysqldb/Subscribers',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mariadb',
|
||||
type: 'mariadb',
|
||||
database: 'n8n',
|
||||
username: 'root',
|
||||
password: 'password',
|
||||
host: 'localhost',
|
||||
port: '3306',
|
||||
logging: false,
|
||||
entities: Object.values(entities),
|
||||
migrations: ['./src/databases/mysqldb/migrations/*.ts'],
|
||||
subscribers: ['src/subscriber/**/*.ts'],
|
||||
cli: {
|
||||
entitiesDir: './src/databases/entities',
|
||||
migrationsDir: './src/databases/mysqldb/migrations',
|
||||
subscribersDir: './src/databases/mysqldb/Subscribers',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n",
|
||||
"version": "0.178.0",
|
||||
"version": "0.186.1",
|
||||
"description": "n8n Workflow Automation Tool",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
|
@ -19,9 +19,7 @@
|
|||
"bin": "n8n"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "run-script-os",
|
||||
"build:default": "tsc && cp -r ./src/UserManagement/email/templates ./dist/src/UserManagement/email",
|
||||
"build:windows": "tsc && xcopy /E /I src\\UserManagement\\email\\templates dist\\src\\UserManagement\\email\\templates",
|
||||
"build": "node scripts/build.mjs",
|
||||
"dev": "concurrently -k -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold\" \"npm run watch\" \"nodemon\"",
|
||||
"format": "cd ../.. && node_modules/prettier/bin-prettier.js packages/cli/**/**.ts --write",
|
||||
"lint": "cd ../.. && node_modules/eslint/bin/eslint.js packages/cli",
|
||||
|
|
@ -32,11 +30,12 @@
|
|||
"start:default": "cd bin && ./n8n",
|
||||
"start:windows": "cd bin && n8n",
|
||||
"test": "npm run test:sqlite",
|
||||
"test:sqlite": "export N8N_LOG_LEVEL='silent'; export DB_TYPE=sqlite; jest",
|
||||
"test:postgres": "export N8N_LOG_LEVEL='silent'; export DB_TYPE=postgresdb && jest",
|
||||
"test:mysql": "export N8N_LOG_LEVEL='silent'; export DB_TYPE=mysqldb && jest",
|
||||
"test:sqlite": "export N8N_LOG_LEVEL=silent && export DB_TYPE=sqlite && jest --forceExit",
|
||||
"test:postgres": "export N8N_LOG_LEVEL=silent && export DB_TYPE=postgresdb && jest",
|
||||
"test:postgres:alt-schema": "export DB_POSTGRESDB_SCHEMA=alt_schema && npm run test:postgres",
|
||||
"test:mysql": "export N8N_LOG_LEVEL=silent && export DB_TYPE=mysqldb && jest",
|
||||
"watch": "tsc --watch",
|
||||
"typeorm": "ts-node ../../node_modules/typeorm/cli.js"
|
||||
"typeorm": "ts-node -T ../../node_modules/typeorm/cli.js"
|
||||
},
|
||||
"bin": {
|
||||
"n8n": "./bin/n8n"
|
||||
|
|
@ -73,10 +72,15 @@
|
|||
"@types/localtunnel": "^1.9.0",
|
||||
"@types/lodash.get": "^4.4.6",
|
||||
"@types/lodash.merge": "^4.6.6",
|
||||
"@types/node": "14.17.27",
|
||||
"@types/lodash.omit": "^4.5.7",
|
||||
"@types/lodash.set": "^4.3.6",
|
||||
"@types/lodash.split": "^4.4.7",
|
||||
"@types/lodash.unset": "^4.5.7",
|
||||
"@types/node": "^16.11.22",
|
||||
"@types/open": "^6.1.0",
|
||||
"@types/parseurl": "^1.3.1",
|
||||
"@types/passport-jwt": "^3.0.6",
|
||||
"@types/psl": "^1.1.0",
|
||||
"@types/request-promise-native": "~1.0.15",
|
||||
"@types/superagent": "4.1.13",
|
||||
"@types/supertest": "^2.0.11",
|
||||
|
|
@ -94,11 +98,17 @@
|
|||
"typescript": "~4.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@oclif/core": "^1.9.3",
|
||||
"@apidevtools/swagger-cli": "4.0.0",
|
||||
"@oclif/command": "^1.5.18",
|
||||
"@oclif/errors": "^1.2.2",
|
||||
"@rudderstack/rudder-sdk-node": "1.0.6",
|
||||
"@types/json-diff": "^0.5.1",
|
||||
"@types/jsonwebtoken": "^8.5.2",
|
||||
"@types/lodash.intersection": "^4.4.7",
|
||||
"@types/shelljs": "^0.8.11",
|
||||
"@types/swagger-ui-express": "^4.1.3",
|
||||
"@types/yamljs": "^0.2.31",
|
||||
"basic-auth": "^2.0.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"body-parser": "^1.18.3",
|
||||
|
|
@ -116,37 +126,49 @@
|
|||
"csrf": "^3.1.0",
|
||||
"dotenv": "^8.0.0",
|
||||
"express": "^4.16.4",
|
||||
"express-openapi-validator": "^4.13.6",
|
||||
"fast-glob": "^3.2.5",
|
||||
"flatted": "^3.2.4",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"inquirer": "^7.0.1",
|
||||
"json-diff": "^0.5.4",
|
||||
"jsonschema": "^1.4.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jwks-rsa": "~1.12.1",
|
||||
"localtunnel": "^2.0.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.intersection": "^4.4.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"lodash.omit": "^4.5.0",
|
||||
"lodash.set": "^4.3.2",
|
||||
"lodash.split": "^4.4.2",
|
||||
"lodash.unset": "^4.5.2",
|
||||
"mysql2": "~2.3.0",
|
||||
"n8n-core": "~0.118.0",
|
||||
"n8n-editor-ui": "~0.144.0",
|
||||
"n8n-nodes-base": "~0.176.0",
|
||||
"n8n-workflow": "~0.100.0",
|
||||
"n8n-core": "~0.126.0",
|
||||
"n8n-editor-ui": "~0.152.0",
|
||||
"n8n-nodes-base": "~0.184.1",
|
||||
"n8n-workflow": "~0.108.0",
|
||||
"nodemailer": "^6.7.1",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"open": "^7.0.0",
|
||||
"openapi-types": "^10.0.0",
|
||||
"p-cancelable": "^2.0.0",
|
||||
"passport": "^0.5.0",
|
||||
"passport-cookie": "^1.0.9",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"pg": "^8.3.0",
|
||||
"prom-client": "^13.1.0",
|
||||
"psl": "^1.8.0",
|
||||
"request-promise-native": "^1.0.7",
|
||||
"shelljs": "^0.8.5",
|
||||
"sqlite3": "^5.0.2",
|
||||
"sse-channel": "^3.1.1",
|
||||
"swagger-ui-express": "^4.3.0",
|
||||
"tslib": "1.14.1",
|
||||
"typeorm": "0.2.30",
|
||||
"typeorm": "0.2.45",
|
||||
"uuid": "^8.3.0",
|
||||
"validator": "13.7.0",
|
||||
"winston": "^3.3.3"
|
||||
"winston": "^3.3.3",
|
||||
"yamljs": "^0.3.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
59
packages/cli/scripts/build.mjs
Normal file
59
packages/cli/scripts/build.mjs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import shell from 'shelljs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const ROOT_DIR = path.resolve(__dirname, '..');
|
||||
const SPEC_FILENAME = 'openapi.yml';
|
||||
const SPEC_THEME_FILENAME = 'swaggerTheme.css';
|
||||
|
||||
const userManagementEnabled = process.env.N8N_USER_MANAGEMENT_DISABLED !== 'true';
|
||||
const publicApiEnabled = process.env.N8N_PUBLIC_API_DISABLED !== 'true';
|
||||
|
||||
shell.rm('-rf', path.resolve(ROOT_DIR, 'dist'));
|
||||
|
||||
shell.exec('tsc');
|
||||
|
||||
if (userManagementEnabled) {
|
||||
copyUserManagementEmailTemplates();
|
||||
}
|
||||
|
||||
if (publicApiEnabled) {
|
||||
copySwaggerTheme();
|
||||
bundleOpenApiSpecs();
|
||||
}
|
||||
|
||||
function copyUserManagementEmailTemplates(rootDir = ROOT_DIR) {
|
||||
const templates = {
|
||||
source: path.resolve(rootDir, 'src', 'UserManagement', 'email', 'templates'),
|
||||
destination: path.resolve(rootDir, 'dist', 'src', 'UserManagement', 'email'),
|
||||
};
|
||||
|
||||
shell.cp('-r', templates.source, templates.destination);
|
||||
}
|
||||
|
||||
function copySwaggerTheme(rootDir = ROOT_DIR, themeFilename = SPEC_THEME_FILENAME) {
|
||||
const swaggerTheme = {
|
||||
source: path.resolve(rootDir, 'src', 'PublicApi', themeFilename),
|
||||
destination: path.resolve(rootDir, 'dist', 'src', 'PublicApi'),
|
||||
};
|
||||
|
||||
shell.cp('-r', swaggerTheme.source, swaggerTheme.destination);
|
||||
}
|
||||
|
||||
function bundleOpenApiSpecs(rootDir = ROOT_DIR, specFileName = SPEC_FILENAME) {
|
||||
const publicApiDir = path.resolve(rootDir, 'src', 'PublicApi');
|
||||
|
||||
shell
|
||||
.find(publicApiDir)
|
||||
.reduce((acc, cur) => {
|
||||
return cur.endsWith(specFileName) ? [...acc, path.relative('.', cur)] : acc;
|
||||
}, [])
|
||||
.forEach((specPath) => {
|
||||
const distSpecPath = path.resolve(rootDir, 'dist', specPath);
|
||||
const command = `swagger-cli bundle ${specPath} --type yaml --outfile ${distSpecPath}`;
|
||||
shell.exec(command, { silent: true });
|
||||
});
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
import { ActiveWorkflows, NodeExecuteFunctions } from 'n8n-core';
|
||||
|
||||
import {
|
||||
ExecutionError,
|
||||
IDeferredPromise,
|
||||
IExecuteData,
|
||||
IExecuteResponsePromiseData,
|
||||
|
|
@ -20,12 +21,15 @@ import {
|
|||
IGetExecuteTriggerFunctions,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
IRun,
|
||||
IRunExecutionData,
|
||||
IWorkflowBase,
|
||||
IWorkflowExecuteAdditionalData as IWorkflowExecuteAdditionalDataWorkflow,
|
||||
NodeHelpers,
|
||||
WebhookHttpMethod,
|
||||
Workflow,
|
||||
WorkflowActivateMode,
|
||||
WorkflowActivationError,
|
||||
WorkflowExecuteMode,
|
||||
LoggerProxy as Logger,
|
||||
} from 'n8n-workflow';
|
||||
|
|
@ -52,6 +56,9 @@ import config from '../config';
|
|||
import { User } from './databases/entities/User';
|
||||
import { whereClause } from './WorkflowHelpers';
|
||||
import { WorkflowEntity } from './databases/entities/WorkflowEntity';
|
||||
import * as ActiveExecutions from './ActiveExecutions';
|
||||
|
||||
const activeExecutions = ActiveExecutions.getInstance();
|
||||
|
||||
const WEBHOOK_PROD_UNREGISTERED_HINT = `The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)`;
|
||||
|
||||
|
|
@ -114,6 +121,7 @@ export class ActiveWorkflowRunner {
|
|||
workflowName: workflowData.name,
|
||||
workflowId: workflowData.id,
|
||||
});
|
||||
this.executeErrorWorkflow(error, workflowData, 'internal');
|
||||
}
|
||||
}
|
||||
Logger.verbose('Finished initializing active workflows (startup)');
|
||||
|
|
@ -134,22 +142,24 @@ export class ActiveWorkflowRunner {
|
|||
* @memberof ActiveWorkflowRunner
|
||||
*/
|
||||
async removeAll(): Promise<void> {
|
||||
const activeWorkflowId: string[] = [];
|
||||
let activeWorkflowIds: string[] = [];
|
||||
Logger.verbose('Call to remove all active workflows received (removeAll)');
|
||||
|
||||
if (this.activeWorkflows !== null) {
|
||||
// TODO: This should be renamed!
|
||||
activeWorkflowId.push.apply(activeWorkflowId, this.activeWorkflows.allActiveWorkflows());
|
||||
activeWorkflowIds.push.apply(activeWorkflowIds, this.activeWorkflows.allActiveWorkflows());
|
||||
}
|
||||
|
||||
const activeWorkflows = await this.getActiveWorkflows();
|
||||
activeWorkflowId.push.apply(
|
||||
activeWorkflowId,
|
||||
activeWorkflows.map((workflow) => workflow.id),
|
||||
);
|
||||
activeWorkflowIds = [
|
||||
...activeWorkflowIds,
|
||||
...activeWorkflows.map((workflow) => workflow.id.toString()),
|
||||
];
|
||||
|
||||
// Make sure IDs are unique
|
||||
activeWorkflowIds = Array.from(new Set(activeWorkflowIds));
|
||||
|
||||
const removePromises = [];
|
||||
for (const workflowId of activeWorkflowId) {
|
||||
for (const workflowId of activeWorkflowIds) {
|
||||
removePromises.push(this.remove(workflowId));
|
||||
}
|
||||
|
||||
|
|
@ -585,6 +595,7 @@ export class ActiveWorkflowRunner {
|
|||
data: {
|
||||
main: data,
|
||||
},
|
||||
source: null,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -597,6 +608,7 @@ export class ActiveWorkflowRunner {
|
|||
contextData: {},
|
||||
nodeExecutionStack,
|
||||
waitingExecution: {},
|
||||
waitingExecutionSource: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -673,14 +685,31 @@ export class ActiveWorkflowRunner {
|
|||
returnFunctions.emit = (
|
||||
data: INodeExecutionData[][],
|
||||
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
|
||||
donePromise?: IDeferredPromise<IRun | undefined>,
|
||||
): void => {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
Logger.debug(`Received trigger for workflow "${workflow.name}"`);
|
||||
WorkflowHelpers.saveStaticData(workflow);
|
||||
// eslint-disable-next-line id-denylist
|
||||
this.runWorkflow(workflowData, node, data, additionalData, mode, responsePromise).catch(
|
||||
(error) => console.error(error),
|
||||
const executePromise = this.runWorkflow(
|
||||
workflowData,
|
||||
node,
|
||||
data,
|
||||
additionalData,
|
||||
mode,
|
||||
responsePromise,
|
||||
);
|
||||
|
||||
if (donePromise) {
|
||||
executePromise.then((executionId) => {
|
||||
activeExecutions
|
||||
.getPostExecutePromise(executionId)
|
||||
.then(donePromise.resolve)
|
||||
.catch(donePromise.reject);
|
||||
});
|
||||
} else {
|
||||
executePromise.catch(console.error);
|
||||
}
|
||||
};
|
||||
returnFunctions.emitError = async (error: Error): Promise<void> => {
|
||||
await this.activeWorkflows?.remove(workflowData.id.toString());
|
||||
|
|
@ -690,11 +719,39 @@ export class ActiveWorkflowRunner {
|
|||
message: error.message,
|
||||
},
|
||||
};
|
||||
const activationError = new WorkflowActivationError(
|
||||
'There was a problem with the trigger, for that reason did the workflow had to be deactivated',
|
||||
error,
|
||||
node,
|
||||
);
|
||||
|
||||
this.executeErrorWorkflow(activationError, workflowData, mode);
|
||||
};
|
||||
return returnFunctions;
|
||||
};
|
||||
}
|
||||
|
||||
executeErrorWorkflow(
|
||||
error: ExecutionError,
|
||||
workflowData: IWorkflowBase,
|
||||
mode: WorkflowExecuteMode,
|
||||
): void {
|
||||
const fullRunData: IRun = {
|
||||
data: {
|
||||
resultData: {
|
||||
error,
|
||||
runData: {},
|
||||
},
|
||||
},
|
||||
finished: false,
|
||||
mode,
|
||||
startedAt: new Date(),
|
||||
stoppedAt: new Date(),
|
||||
};
|
||||
|
||||
WorkflowExecuteAdditionalData.executeErrorWorkflow(workflowData, fullRunData, mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a workflow active
|
||||
*
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import {
|
|||
WorkflowExecuteMode,
|
||||
ITaskDataConnections,
|
||||
LoggerProxy as Logger,
|
||||
IHttpRequestHelper,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
|
|
@ -51,6 +52,8 @@ import {
|
|||
} from '.';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { User } from './databases/entities/User';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { CredentialsEntity } from './databases/entities/CredentialsEntity';
|
||||
|
||||
const mockNodeTypes: INodeTypes = {
|
||||
nodeTypes: {} as INodeTypeData,
|
||||
|
|
@ -96,73 +99,41 @@ export class CredentialsHelper extends ICredentialsHelper {
|
|||
if (typeof credentialType.authenticate === 'object') {
|
||||
// Predefined authentication method
|
||||
|
||||
let keyResolved: string;
|
||||
let valueResolved: string;
|
||||
const { authenticate } = credentialType;
|
||||
if (requestOptions.headers === undefined) {
|
||||
requestOptions.headers = {};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (authenticate.type === 'bearer') {
|
||||
const tokenPropertyName: string =
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
authenticate.properties.tokenPropertyName ?? 'accessToken';
|
||||
requestOptions.headers.Authorization = `Bearer ${
|
||||
credentials[tokenPropertyName] as string
|
||||
}`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
} else if (authenticate.type === 'basicAuth') {
|
||||
const userPropertyName: string =
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
authenticate.properties.userPropertyName ?? 'user';
|
||||
const passwordPropertyName: string =
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
authenticate.properties.passwordPropertyName ?? 'password';
|
||||
if (authenticate.type === 'generic') {
|
||||
Object.entries(authenticate.properties).forEach(([outerKey, outerValue]) => {
|
||||
Object.entries(outerValue).forEach(([key, value]) => {
|
||||
keyResolved = this.resolveValue(
|
||||
key,
|
||||
{ $credentials: credentials },
|
||||
workflow,
|
||||
node,
|
||||
defaultTimezone,
|
||||
);
|
||||
|
||||
requestOptions.auth = {
|
||||
username: credentials[userPropertyName] as string,
|
||||
password: credentials[passwordPropertyName] as string,
|
||||
};
|
||||
} else if (authenticate.type === 'headerAuth') {
|
||||
const key = this.resolveValue(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
authenticate.properties.name,
|
||||
{ $credentials: credentials },
|
||||
workflow,
|
||||
node,
|
||||
defaultTimezone,
|
||||
);
|
||||
valueResolved = this.resolveValue(
|
||||
value as string,
|
||||
{ $credentials: credentials },
|
||||
workflow,
|
||||
node,
|
||||
defaultTimezone,
|
||||
);
|
||||
|
||||
const value = this.resolveValue(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
authenticate.properties.value,
|
||||
{ $credentials: credentials },
|
||||
workflow,
|
||||
node,
|
||||
defaultTimezone,
|
||||
);
|
||||
requestOptions.headers[key] = value;
|
||||
} else if (authenticate.type === 'queryAuth') {
|
||||
const key = this.resolveValue(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
authenticate.properties.key,
|
||||
{ $credentials: credentials },
|
||||
workflow,
|
||||
node,
|
||||
defaultTimezone,
|
||||
);
|
||||
|
||||
const value = this.resolveValue(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
authenticate.properties.value,
|
||||
{ $credentials: credentials },
|
||||
workflow,
|
||||
node,
|
||||
defaultTimezone,
|
||||
);
|
||||
if (!requestOptions.qs) {
|
||||
requestOptions.qs = {};
|
||||
}
|
||||
requestOptions.qs[key] = value;
|
||||
// @ts-ignore
|
||||
if (!requestOptions[outerKey]) {
|
||||
// @ts-ignore
|
||||
requestOptions[outerKey] = {};
|
||||
}
|
||||
// @ts-ignore
|
||||
requestOptions[outerKey][keyResolved] = valueResolved;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -170,6 +141,61 @@ export class CredentialsHelper extends ICredentialsHelper {
|
|||
return requestOptions as IHttpRequestOptions;
|
||||
}
|
||||
|
||||
async preAuthentication(
|
||||
helpers: IHttpRequestHelper,
|
||||
credentials: ICredentialDataDecryptedObject,
|
||||
typeName: string,
|
||||
node: INode,
|
||||
credentialsExpired: boolean,
|
||||
): Promise<ICredentialDataDecryptedObject | undefined> {
|
||||
const credentialType = this.credentialTypes.getByName(typeName);
|
||||
|
||||
const expirableProperty = credentialType.properties.find(
|
||||
(property) => property.type === 'hidden' && property?.typeOptions?.expirable === true,
|
||||
);
|
||||
|
||||
if (expirableProperty === undefined || expirableProperty.name === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// check if the node is the mockup node used for testing
|
||||
// if so, it means this is a credential test and not normal node execution
|
||||
const isTestingCredentials =
|
||||
node?.parameters?.temp === '' && node?.type === 'n8n-nodes-base.noOp';
|
||||
|
||||
if (credentialType.preAuthentication) {
|
||||
if (typeof credentialType.preAuthentication === 'function') {
|
||||
// if the expirable property is empty in the credentials
|
||||
// or are expired, call pre authentication method
|
||||
// or the credentials are being tested
|
||||
if (
|
||||
credentials[expirableProperty?.name] === '' ||
|
||||
credentialsExpired ||
|
||||
isTestingCredentials
|
||||
) {
|
||||
const output = await credentialType.preAuthentication.call(helpers, credentials);
|
||||
|
||||
// if there is data in the output, make sure the returned
|
||||
// property is the expirable property
|
||||
// else the database will not get updated
|
||||
if (output[expirableProperty.name] === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (node.credentials) {
|
||||
await this.updateCredentials(
|
||||
node.credentials[credentialType.name],
|
||||
credentialType.name,
|
||||
Object.assign(credentials, output),
|
||||
);
|
||||
return Object.assign(credentials, output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the given value in case it is an expression
|
||||
*/
|
||||
|
|
@ -190,6 +216,7 @@ export class CredentialsHelper extends ICredentialsHelper {
|
|||
'internal',
|
||||
defaultTimezone,
|
||||
additionalKeys,
|
||||
undefined,
|
||||
'',
|
||||
);
|
||||
|
||||
|
|
@ -366,6 +393,7 @@ export class CredentialsHelper extends ICredentialsHelper {
|
|||
mode,
|
||||
timezone,
|
||||
{},
|
||||
undefined,
|
||||
false,
|
||||
decryptedData,
|
||||
) as ICredentialDataDecryptedObject;
|
||||
|
|
@ -398,6 +426,7 @@ export class CredentialsHelper extends ICredentialsHelper {
|
|||
defaultTimezone,
|
||||
{},
|
||||
undefined,
|
||||
undefined,
|
||||
decryptedData,
|
||||
) as ICredentialDataDecryptedObject;
|
||||
}
|
||||
|
|
@ -565,6 +594,12 @@ export class CredentialsHelper extends ICredentialsHelper {
|
|||
? nodeType.description.version.slice(-1)[0]
|
||||
: nodeType.description.version,
|
||||
position: [0, 0],
|
||||
credentials: {
|
||||
[credentialType]: {
|
||||
id: credentialsDecrypted.id.toString(),
|
||||
name: credentialsDecrypted.name,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const workflowData = {
|
||||
|
|
@ -642,13 +677,14 @@ export class CredentialsHelper extends ICredentialsHelper {
|
|||
inputData,
|
||||
runIndex,
|
||||
nodeTypeCopy,
|
||||
{ node, data: {}, source: null },
|
||||
NodeExecuteFunctions,
|
||||
credentialsDecrypted,
|
||||
);
|
||||
} catch (error) {
|
||||
// Do not fail any requests to allow custom error messages and
|
||||
// make logic easier
|
||||
if (error.cause.response) {
|
||||
if (error.cause?.response) {
|
||||
const errorResponseData = {
|
||||
statusCode: error.cause.response.status,
|
||||
statusMessage: error.cause.response.statusText,
|
||||
|
|
@ -764,3 +800,14 @@ export async function getCredentialWithoutUser(
|
|||
const credential = await Db.collections.Credentials.findOne(credentialId);
|
||||
return credential;
|
||||
}
|
||||
|
||||
export function createCredentiasFromCredentialsEntity(
|
||||
credential: CredentialsEntity,
|
||||
encrypt = false,
|
||||
): Credentials {
|
||||
const { id, name, type, nodesAccess, data } = credential;
|
||||
if (encrypt) {
|
||||
return new Credentials({ id: null, name }, type, nodesAccess);
|
||||
}
|
||||
return new Credentials({ id: id.toString(), name }, type, nodesAccess, data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@ import config from '../config';
|
|||
// eslint-disable-next-line import/no-cycle
|
||||
import { entities } from './databases/entities';
|
||||
|
||||
import { postgresMigrations } from './databases/postgresdb/migrations';
|
||||
import { mysqlMigrations } from './databases/mysqldb/migrations';
|
||||
import { sqliteMigrations } from './databases/sqlite/migrations';
|
||||
import { postgresMigrations } from './databases/migrations/postgresdb';
|
||||
import { mysqlMigrations } from './databases/migrations/mysqldb';
|
||||
import { sqliteMigrations } from './databases/migrations/sqlite';
|
||||
|
||||
export let isInitialized = false;
|
||||
export const collections = {} as IDatabaseCollections;
|
||||
|
|
@ -45,6 +45,8 @@ export function linkRepository<Entity>(entityClass: EntityTarget<Entity>): Repos
|
|||
export async function init(
|
||||
testConnectionOptions?: ConnectionOptions,
|
||||
): Promise<IDatabaseCollections> {
|
||||
if (isInitialized) return collections;
|
||||
|
||||
const dbType = (await GenericHelpers.getConfigValue('database.type')) as DatabaseType;
|
||||
const n8nFolder = UserSettings.getUserN8nFolderPath();
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
IRunExecutionData,
|
||||
ITaskData,
|
||||
ITelemetrySettings,
|
||||
ITelemetryTrackProperties,
|
||||
IWorkflowBase as IWorkflowBaseWorkflow,
|
||||
Workflow,
|
||||
WorkflowExecuteMode,
|
||||
|
|
@ -114,6 +115,13 @@ export interface ITagDb {
|
|||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface ITagToImport {
|
||||
id: string | number;
|
||||
name: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export type UsageCount = {
|
||||
usageCount: number;
|
||||
};
|
||||
|
|
@ -134,6 +142,10 @@ export interface IWorkflowDb extends IWorkflowBase {
|
|||
tags: ITagDb[];
|
||||
}
|
||||
|
||||
export interface IWorkflowToImport extends IWorkflowBase {
|
||||
tags: ITagToImport[];
|
||||
}
|
||||
|
||||
export interface IWorkflowResponse extends IWorkflowBase {
|
||||
id: string;
|
||||
}
|
||||
|
|
@ -218,6 +230,19 @@ export interface IExecutionFlattedResponse extends IExecutionFlatted {
|
|||
retryOf?: string;
|
||||
}
|
||||
|
||||
export interface IExecutionResponseApi {
|
||||
id: number | string;
|
||||
mode: WorkflowExecuteMode;
|
||||
startedAt: Date;
|
||||
stoppedAt?: Date;
|
||||
workflowId?: string;
|
||||
finished: boolean;
|
||||
retryOf?: number | string;
|
||||
retrySuccessId?: number | string;
|
||||
data?: object;
|
||||
waitTill?: Date | null;
|
||||
workflowData: IWorkflowBase;
|
||||
}
|
||||
export interface IExecutionsListResponse {
|
||||
count: number;
|
||||
// results: IExecutionShortResponse[];
|
||||
|
|
@ -352,16 +377,20 @@ export interface IInternalHooksClass {
|
|||
firstWorkflowCreatedAt?: Date,
|
||||
): Promise<unknown[]>;
|
||||
onPersonalizationSurveySubmitted(userId: string, answers: Record<string, string>): Promise<void>;
|
||||
onWorkflowCreated(userId: string, workflow: IWorkflowBase): Promise<void>;
|
||||
onWorkflowDeleted(userId: string, workflowId: string): Promise<void>;
|
||||
onWorkflowSaved(userId: string, workflow: IWorkflowBase): Promise<void>;
|
||||
onWorkflowCreated(userId: string, workflow: IWorkflowBase, publicApi: boolean): Promise<void>;
|
||||
onWorkflowDeleted(userId: string, workflowId: string, publicApi: boolean): Promise<void>;
|
||||
onWorkflowSaved(userId: string, workflow: IWorkflowBase, publicApi: boolean): Promise<void>;
|
||||
onWorkflowPostExecute(
|
||||
executionId: string,
|
||||
workflow: IWorkflowBase,
|
||||
runData?: IRun,
|
||||
userId?: string,
|
||||
): Promise<void>;
|
||||
onUserDeletion(userId: string, userDeletionData: ITelemetryUserDeletionData): Promise<void>;
|
||||
onUserDeletion(
|
||||
userId: string,
|
||||
userDeletionData: ITelemetryUserDeletionData,
|
||||
publicApi: boolean,
|
||||
): Promise<void>;
|
||||
onUserInvite(userInviteData: { user_id: string; target_user_id: string[] }): Promise<void>;
|
||||
onUserReinvite(userReinviteData: { user_id: string; target_user_id: string }): Promise<void>;
|
||||
onUserUpdate(userUpdateData: { user_id: string; fields_changed: string[] }): Promise<void>;
|
||||
|
|
@ -457,6 +486,7 @@ export interface IN8nUISettings {
|
|||
personalizationSurveyEnabled: boolean;
|
||||
defaultLocale: string;
|
||||
userManagement: IUserManagementSettings;
|
||||
publicApi: IPublicApiSettings;
|
||||
workflowTagsDisabled: boolean;
|
||||
logLevel: 'info' | 'debug' | 'warn' | 'error' | 'verbose' | 'silent';
|
||||
hiringBannerEnabled: boolean;
|
||||
|
|
@ -484,6 +514,11 @@ export interface IUserManagementSettings {
|
|||
showSetupOnFirstLoad?: boolean;
|
||||
smtpSetup: boolean;
|
||||
}
|
||||
export interface IPublicApiSettings {
|
||||
enabled: boolean;
|
||||
latestVersion: number;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface IPackageVersions {
|
||||
cli: string;
|
||||
|
|
@ -580,13 +615,20 @@ export interface ITransferNodeTypes {
|
|||
}
|
||||
|
||||
export interface IWorkflowErrorData {
|
||||
[key: string]: IDataObject | string | number | ExecutionError;
|
||||
execution: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[key: string]: any;
|
||||
execution?: {
|
||||
id?: string;
|
||||
url?: string;
|
||||
retryOf?: string;
|
||||
error: ExecutionError;
|
||||
lastNodeExecuted: string;
|
||||
mode: WorkflowExecuteMode;
|
||||
};
|
||||
trigger?: {
|
||||
error: ExecutionError;
|
||||
mode: WorkflowExecuteMode;
|
||||
};
|
||||
workflow: {
|
||||
id?: string;
|
||||
name: string;
|
||||
|
|
@ -626,3 +668,14 @@ export interface IWorkflowExecuteProcess {
|
|||
}
|
||||
|
||||
export type WhereClause = Record<string, { id: string }>;
|
||||
|
||||
// ----------------------------------
|
||||
// telemetry
|
||||
// ----------------------------------
|
||||
|
||||
export interface IExecutionTrackProperties extends ITelemetryTrackProperties {
|
||||
workflow_id: string;
|
||||
success: boolean;
|
||||
error_node_type?: string;
|
||||
is_manual: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
/* eslint-disable import/no-cycle */
|
||||
import { get as pslGet } from 'psl';
|
||||
import { BinaryDataManager } from 'n8n-core';
|
||||
import { IDataObject, INodeTypes, IRun, TelemetryHelpers } from 'n8n-workflow';
|
||||
import {
|
||||
INodesGraphResult,
|
||||
INodeTypes,
|
||||
IRun,
|
||||
ITelemetryTrackProperties,
|
||||
TelemetryHelpers,
|
||||
} from 'n8n-workflow';
|
||||
import { snakeCase } from 'change-case';
|
||||
import {
|
||||
IDiagnosticInfo,
|
||||
|
|
@ -10,6 +17,7 @@ import {
|
|||
IWorkflowDb,
|
||||
} from '.';
|
||||
import { Telemetry } from './telemetry';
|
||||
import { IExecutionTrackProperties } from './Interfaces';
|
||||
|
||||
export class InternalHooksClass implements IInternalHooksClass {
|
||||
private versionCli: string;
|
||||
|
|
@ -48,6 +56,10 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
]);
|
||||
}
|
||||
|
||||
async onFrontendSettingsAPI(sessionId?: string): Promise<void> {
|
||||
return this.telemetry.track('Session started', { session_id: sessionId });
|
||||
}
|
||||
|
||||
async onPersonalizationSurveySubmitted(
|
||||
userId: string,
|
||||
answers: Record<string, string>,
|
||||
|
|
@ -64,24 +76,29 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
);
|
||||
}
|
||||
|
||||
async onWorkflowCreated(userId: string, workflow: IWorkflowBase): Promise<void> {
|
||||
async onWorkflowCreated(
|
||||
userId: string,
|
||||
workflow: IWorkflowBase,
|
||||
publicApi: boolean,
|
||||
): Promise<void> {
|
||||
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
||||
return this.telemetry.track('User created workflow', {
|
||||
user_id: userId,
|
||||
workflow_id: workflow.id,
|
||||
node_graph: nodeGraph,
|
||||
node_graph_string: JSON.stringify(nodeGraph),
|
||||
public_api: publicApi,
|
||||
});
|
||||
}
|
||||
|
||||
async onWorkflowDeleted(userId: string, workflowId: string): Promise<void> {
|
||||
async onWorkflowDeleted(userId: string, workflowId: string, publicApi: boolean): Promise<void> {
|
||||
return this.telemetry.track('User deleted workflow', {
|
||||
user_id: userId,
|
||||
workflow_id: workflowId,
|
||||
public_api: publicApi,
|
||||
});
|
||||
}
|
||||
|
||||
async onWorkflowSaved(userId: string, workflow: IWorkflowDb): Promise<void> {
|
||||
async onWorkflowSaved(userId: string, workflow: IWorkflowDb, publicApi: boolean): Promise<void> {
|
||||
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
||||
|
||||
const notesCount = Object.keys(nodeGraph.notes).length;
|
||||
|
|
@ -92,12 +109,12 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
return this.telemetry.track('User saved workflow', {
|
||||
user_id: userId,
|
||||
workflow_id: workflow.id,
|
||||
node_graph: nodeGraph,
|
||||
node_graph_string: JSON.stringify(nodeGraph),
|
||||
notes_count_overlapping: overlappingCount,
|
||||
notes_count_non_overlapping: notesCount - overlappingCount,
|
||||
version_cli: this.versionCli,
|
||||
num_tags: workflow.tags?.length ?? 0,
|
||||
public_api: publicApi,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -108,10 +125,16 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
userId?: string,
|
||||
): Promise<void> {
|
||||
const promises = [Promise.resolve()];
|
||||
const properties: IDataObject = {
|
||||
workflow_id: workflow.id,
|
||||
|
||||
if (!workflow.id) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const properties: IExecutionTrackProperties = {
|
||||
workflow_id: workflow.id.toString(),
|
||||
is_manual: false,
|
||||
version_cli: this.versionCli,
|
||||
success: false,
|
||||
};
|
||||
|
||||
if (userId) {
|
||||
|
|
@ -123,7 +146,7 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
properties.success = !!runData.finished;
|
||||
properties.is_manual = runData.mode === 'manual';
|
||||
|
||||
let nodeGraphResult;
|
||||
let nodeGraphResult: INodesGraphResult | null = null;
|
||||
|
||||
if (!properties.success && runData?.data.resultData.error) {
|
||||
properties.error_message = runData?.data.resultData.error.message;
|
||||
|
|
@ -158,22 +181,19 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
||||
}
|
||||
|
||||
const manualExecEventProperties = {
|
||||
workflow_id: workflow.id,
|
||||
const manualExecEventProperties: ITelemetryTrackProperties = {
|
||||
workflow_id: workflow.id.toString(),
|
||||
status: properties.success ? 'success' : 'failed',
|
||||
error_message: properties.error_message,
|
||||
error_message: properties.error_message as string,
|
||||
error_node_type: properties.error_node_type,
|
||||
node_graph: properties.node_graph,
|
||||
node_graph_string: properties.node_graph_string,
|
||||
error_node_id: properties.error_node_id,
|
||||
node_graph_string: properties.node_graph_string as string,
|
||||
error_node_id: properties.error_node_id as string,
|
||||
webhook_domain: null,
|
||||
};
|
||||
|
||||
if (!manualExecEventProperties.node_graph) {
|
||||
if (!manualExecEventProperties.node_graph_string) {
|
||||
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
||||
manualExecEventProperties.node_graph = nodeGraphResult.nodeGraph;
|
||||
manualExecEventProperties.node_graph_string = JSON.stringify(
|
||||
manualExecEventProperties.node_graph,
|
||||
);
|
||||
manualExecEventProperties.node_graph_string = JSON.stringify(nodeGraphResult.nodeGraph);
|
||||
}
|
||||
|
||||
if (runData.data.startData?.destinationNode) {
|
||||
|
|
@ -188,6 +208,16 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
}),
|
||||
);
|
||||
} else {
|
||||
nodeGraphResult.webhookNodeNames.forEach((name: string) => {
|
||||
const execJson = runData.data.resultData.runData[name]?.[0]?.data?.main?.[0]?.[0]
|
||||
?.json as { headers?: { origin?: string } };
|
||||
if (execJson?.headers?.origin && execJson.headers.origin !== '') {
|
||||
manualExecEventProperties.webhook_domain = pslGet(
|
||||
execJson.headers.origin.replace(/^https?:\/\//, ''),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
promises.push(
|
||||
this.telemetry.track('Manual workflow exec finished', manualExecEventProperties),
|
||||
);
|
||||
|
|
@ -215,21 +245,73 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
async onUserDeletion(
|
||||
userId: string,
|
||||
userDeletionData: ITelemetryUserDeletionData,
|
||||
publicApi: boolean,
|
||||
): Promise<void> {
|
||||
return this.telemetry.track('User deleted user', { ...userDeletionData, user_id: userId });
|
||||
return this.telemetry.track('User deleted user', {
|
||||
...userDeletionData,
|
||||
user_id: userId,
|
||||
public_api: publicApi,
|
||||
});
|
||||
}
|
||||
|
||||
async onUserInvite(userInviteData: { user_id: string; target_user_id: string[] }): Promise<void> {
|
||||
async onUserInvite(userInviteData: {
|
||||
user_id: string;
|
||||
target_user_id: string[];
|
||||
public_api: boolean;
|
||||
}): Promise<void> {
|
||||
return this.telemetry.track('User invited new user', userInviteData);
|
||||
}
|
||||
|
||||
async onUserReinvite(userReinviteData: {
|
||||
user_id: string;
|
||||
target_user_id: string;
|
||||
public_api: boolean;
|
||||
}): Promise<void> {
|
||||
return this.telemetry.track('User resent new user invite email', userReinviteData);
|
||||
}
|
||||
|
||||
async onUserRetrievedUser(userRetrievedData: {
|
||||
user_id: string;
|
||||
public_api: boolean;
|
||||
}): Promise<void> {
|
||||
return this.telemetry.track('User retrieved user', userRetrievedData);
|
||||
}
|
||||
|
||||
async onUserRetrievedAllUsers(userRetrievedData: {
|
||||
user_id: string;
|
||||
public_api: boolean;
|
||||
}): Promise<void> {
|
||||
return this.telemetry.track('User retrieved all users', userRetrievedData);
|
||||
}
|
||||
|
||||
async onUserRetrievedExecution(userRetrievedData: {
|
||||
user_id: string;
|
||||
public_api: boolean;
|
||||
}): Promise<void> {
|
||||
return this.telemetry.track('User retrieved execution', userRetrievedData);
|
||||
}
|
||||
|
||||
async onUserRetrievedAllExecutions(userRetrievedData: {
|
||||
user_id: string;
|
||||
public_api: boolean;
|
||||
}): Promise<void> {
|
||||
return this.telemetry.track('User retrieved all executions', userRetrievedData);
|
||||
}
|
||||
|
||||
async onUserRetrievedWorkflow(userRetrievedData: {
|
||||
user_id: string;
|
||||
public_api: boolean;
|
||||
}): Promise<void> {
|
||||
return this.telemetry.track('User retrieved workflow', userRetrievedData);
|
||||
}
|
||||
|
||||
async onUserRetrievedAllWorkflows(userRetrievedData: {
|
||||
user_id: string;
|
||||
public_api: boolean;
|
||||
}): Promise<void> {
|
||||
return this.telemetry.track('User retrieved all workflows', userRetrievedData);
|
||||
}
|
||||
|
||||
async onUserUpdate(userUpdateData: { user_id: string; fields_changed: string[] }): Promise<void> {
|
||||
return this.telemetry.track('User changed personal settings', userUpdateData);
|
||||
}
|
||||
|
|
@ -248,13 +330,37 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
async onUserTransactionalEmail(userTransactionalEmailData: {
|
||||
user_id: string;
|
||||
message_type: 'Reset password' | 'New user invite' | 'Resend invite';
|
||||
public_api: boolean;
|
||||
}): Promise<void> {
|
||||
return this.telemetry.track(
|
||||
'Instance sent transactional email to user',
|
||||
'Instance sent transacptional email to user',
|
||||
userTransactionalEmailData,
|
||||
);
|
||||
}
|
||||
|
||||
async onUserInvokedApi(userInvokedApiData: {
|
||||
user_id: string;
|
||||
path: string;
|
||||
method: string;
|
||||
api_version: string;
|
||||
}): Promise<void> {
|
||||
return this.telemetry.track('User invoked API', userInvokedApiData);
|
||||
}
|
||||
|
||||
async onApiKeyDeleted(apiKeyDeletedData: {
|
||||
user_id: string;
|
||||
public_api: boolean;
|
||||
}): Promise<void> {
|
||||
return this.telemetry.track('API key deleted', apiKeyDeletedData);
|
||||
}
|
||||
|
||||
async onApiKeyCreated(apiKeyCreatedData: {
|
||||
user_id: string;
|
||||
public_api: boolean;
|
||||
}): Promise<void> {
|
||||
return this.telemetry.track('API key created', apiKeyCreatedData);
|
||||
}
|
||||
|
||||
async onUserPasswordResetRequestClick(userPasswordResetData: { user_id: string }): Promise<void> {
|
||||
return this.telemetry.track(
|
||||
'User requested password reset while logged out',
|
||||
|
|
@ -273,6 +379,7 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
async onEmailFailed(failedEmailData: {
|
||||
user_id: string;
|
||||
message_type: 'Reset password' | 'New user invite' | 'Resend invite';
|
||||
public_api: boolean;
|
||||
}): Promise<void> {
|
||||
return this.telemetry.track(
|
||||
'Instance failed to send transactional email to user',
|
||||
|
|
|
|||
|
|
@ -150,6 +150,19 @@ class LoadNodesAndCredentialsClass {
|
|||
|
||||
let tempCredential: ICredentialType;
|
||||
try {
|
||||
// Add serializer method "toJSON" to the class so that authenticate method (if defined)
|
||||
// gets mapped to the authenticate attribute before it is sent to the client.
|
||||
// The authenticate property is used by the client to decide whether or not to
|
||||
// include the credential type in the predifined credentials (HTTP node)
|
||||
// eslint-disable-next-line func-names
|
||||
tempModule[credentialName].prototype.toJSON = function () {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return {
|
||||
...this,
|
||||
authenticate: typeof this.authenticate === 'function' ? {} : this.authenticate,
|
||||
};
|
||||
};
|
||||
|
||||
tempCredential = new tempModule[credentialName]() as ICredentialType;
|
||||
|
||||
if (tempCredential.icon && tempCredential.icon.startsWith('file:')) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-shadow */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
|
||||
import { inspect } from 'util';
|
||||
import winston from 'winston';
|
||||
|
||||
import { IDataObject, ILogger, LogTypes } from 'n8n-workflow';
|
||||
|
|
@ -8,7 +12,7 @@ import callsites from 'callsites';
|
|||
import { basename } from 'path';
|
||||
import config from '../config';
|
||||
|
||||
class Logger implements ILogger {
|
||||
export class Logger implements ILogger {
|
||||
private logger: winston.Logger;
|
||||
|
||||
constructor() {
|
||||
|
|
@ -36,12 +40,12 @@ class Logger implements ILogger {
|
|||
winston.format.printf(({ level, message, timestamp, metadata }) => {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
return `${timestamp} | ${level.padEnd(18)} | ${message}${
|
||||
Object.keys(metadata).length ? ` ${JSON.stringify(metadata)}` : ''
|
||||
Object.keys(metadata).length ? ` ${JSON.stringify(inspect(metadata))}` : ''
|
||||
}`;
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
format = winston.format.printf(({ message }) => message);
|
||||
format = winston.format.printf(({ message }: { message: string }) => message);
|
||||
}
|
||||
|
||||
this.logger.add(
|
||||
|
|
@ -68,7 +72,7 @@ class Logger implements ILogger {
|
|||
}
|
||||
}
|
||||
|
||||
log(type: LogTypes, message: string, meta: object = {}) {
|
||||
log(type: LogTypes, message: string, meta: object = {}): void {
|
||||
const callsite = callsites();
|
||||
// We are using the third array element as the structure is as follows:
|
||||
// [0]: this file
|
||||
|
|
@ -90,23 +94,23 @@ class Logger implements ILogger {
|
|||
|
||||
// Convenience methods below
|
||||
|
||||
debug(message: string, meta: object = {}) {
|
||||
debug(message: string, meta: object = {}): void {
|
||||
this.log('debug', message, meta);
|
||||
}
|
||||
|
||||
info(message: string, meta: object = {}) {
|
||||
info(message: string, meta: object = {}): void {
|
||||
this.log('info', message, meta);
|
||||
}
|
||||
|
||||
error(message: string, meta: object = {}) {
|
||||
error(message: string, meta: object = {}): void {
|
||||
this.log('error', message, meta);
|
||||
}
|
||||
|
||||
verbose(message: string, meta: object = {}) {
|
||||
verbose(message: string, meta: object = {}): void {
|
||||
this.log('verbose', message, meta);
|
||||
}
|
||||
|
||||
warn(message: string, meta: object = {}) {
|
||||
warn(message: string, meta: object = {}): void {
|
||||
this.log('warn', message, meta);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
143
packages/cli/src/PublicApi/index.ts
Normal file
143
packages/cli/src/PublicApi/index.ts
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable import/no-cycle */
|
||||
import express, { Router } from 'express';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
import * as OpenApiValidator from 'express-openapi-validator';
|
||||
import { HttpError } from 'express-openapi-validator/dist/framework/types';
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
import swaggerUi from 'swagger-ui-express';
|
||||
import validator from 'validator';
|
||||
import YAML from 'yamljs';
|
||||
|
||||
import config from '../../config';
|
||||
import { Db, InternalHooksManager } from '..';
|
||||
import { getInstanceBaseUrl } from '../UserManagement/UserManagementHelper';
|
||||
|
||||
function createApiRouter(
|
||||
version: string,
|
||||
openApiSpecPath: string,
|
||||
handlersDirectory: string,
|
||||
swaggerThemeCss: string,
|
||||
publicApiEndpoint: string,
|
||||
): Router {
|
||||
const n8nPath = config.getEnv('path');
|
||||
const swaggerDocument = YAML.load(openApiSpecPath) as swaggerUi.JsonObject;
|
||||
// add the server depeding on the config so the user can interact with the API
|
||||
// from the Swagger UI
|
||||
swaggerDocument.server = [
|
||||
{
|
||||
url: `${getInstanceBaseUrl()}/${publicApiEndpoint}/${version}}`,
|
||||
},
|
||||
];
|
||||
const apiController = express.Router();
|
||||
|
||||
apiController.use(
|
||||
`/${publicApiEndpoint}/${version}/docs`,
|
||||
swaggerUi.serveFiles(swaggerDocument),
|
||||
swaggerUi.setup(swaggerDocument, {
|
||||
customCss: swaggerThemeCss,
|
||||
customSiteTitle: 'n8n Public API UI',
|
||||
customfavIcon: `${n8nPath}favicon.ico`,
|
||||
}),
|
||||
);
|
||||
|
||||
apiController.use(`/${publicApiEndpoint}/${version}`, express.json());
|
||||
|
||||
apiController.use(
|
||||
`/${publicApiEndpoint}/${version}`,
|
||||
OpenApiValidator.middleware({
|
||||
apiSpec: openApiSpecPath,
|
||||
operationHandlers: handlersDirectory,
|
||||
validateRequests: true,
|
||||
validateApiSpec: true,
|
||||
formats: [
|
||||
{
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
validate: (email: string) => validator.isEmail(email),
|
||||
},
|
||||
{
|
||||
name: 'identifier',
|
||||
type: 'string',
|
||||
validate: (identifier: string) =>
|
||||
validator.isUUID(identifier) || validator.isEmail(identifier),
|
||||
},
|
||||
{
|
||||
name: 'jsonString',
|
||||
validate: (data: string) => {
|
||||
try {
|
||||
JSON.parse(data);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
validateSecurity: {
|
||||
handlers: {
|
||||
ApiKeyAuth: async (
|
||||
req: express.Request,
|
||||
_scopes: unknown,
|
||||
schema: OpenAPIV3.ApiKeySecurityScheme,
|
||||
): Promise<boolean> => {
|
||||
const apiKey = req.headers[schema.name.toLowerCase()];
|
||||
const user = await Db.collections.User.findOne({
|
||||
where: { apiKey },
|
||||
relations: ['globalRole'],
|
||||
});
|
||||
|
||||
if (!user) return false;
|
||||
|
||||
void InternalHooksManager.getInstance().onUserInvokedApi({
|
||||
user_id: user.id,
|
||||
path: req.path,
|
||||
method: req.method,
|
||||
api_version: version,
|
||||
});
|
||||
|
||||
req.user = user;
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
apiController.use(
|
||||
(
|
||||
error: HttpError,
|
||||
_req: express.Request,
|
||||
res: express.Response,
|
||||
_next: express.NextFunction,
|
||||
) => {
|
||||
return res.status(error.status || 400).json({
|
||||
message: error.message,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return apiController;
|
||||
}
|
||||
|
||||
export const loadPublicApiVersions = async (
|
||||
publicApiEndpoint: string,
|
||||
): Promise<{ apiRouters: express.Router[]; apiLatestVersion: number }> => {
|
||||
const swaggerThemePath = path.join(__dirname, 'swaggerTheme.css');
|
||||
const folders = await fs.readdir(__dirname);
|
||||
const css = (await fs.readFile(swaggerThemePath)).toString();
|
||||
const versions = folders.filter((folderName) => folderName.startsWith('v'));
|
||||
|
||||
const apiRouters = versions.map((version) => {
|
||||
const openApiPath = path.join(__dirname, version, 'openapi.yml');
|
||||
return createApiRouter(version, openApiPath, __dirname, css, publicApiEndpoint);
|
||||
});
|
||||
|
||||
return {
|
||||
apiRouters,
|
||||
apiLatestVersion: Number(versions.pop()?.charAt(1)) ?? 1,
|
||||
};
|
||||
};
|
||||
26
packages/cli/src/PublicApi/swaggerTheme.css
Normal file
26
packages/cli/src/PublicApi/swaggerTheme.css
Normal file
File diff suppressed because one or more lines are too long
165
packages/cli/src/PublicApi/types.d.ts
vendored
Normal file
165
packages/cli/src/PublicApi/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
/* eslint-disable import/no-cycle */
|
||||
import express from 'express';
|
||||
import { IDataObject } from 'n8n-workflow';
|
||||
|
||||
import type { User } from '../databases/entities/User';
|
||||
|
||||
import type { Role } from '../databases/entities/Role';
|
||||
|
||||
import type { WorkflowEntity } from '../databases/entities/WorkflowEntity';
|
||||
|
||||
import * as UserManagementMailer from '../UserManagement/email/UserManagementMailer';
|
||||
|
||||
export type ExecutionStatus = 'error' | 'running' | 'success' | 'waiting' | null;
|
||||
|
||||
export type AuthlessRequest<
|
||||
RouteParams = {},
|
||||
ResponseBody = {},
|
||||
RequestBody = {},
|
||||
RequestQuery = {},
|
||||
> = express.Request<RouteParams, ResponseBody, RequestBody, RequestQuery>;
|
||||
|
||||
export type AuthenticatedRequest<
|
||||
RouteParams = {},
|
||||
ResponseBody = {},
|
||||
RequestBody = {},
|
||||
RequestQuery = {},
|
||||
> = express.Request<RouteParams, ResponseBody, RequestBody, RequestQuery> & {
|
||||
user: User;
|
||||
globalMemberRole?: Role;
|
||||
mailer?: UserManagementMailer.UserManagementMailer;
|
||||
};
|
||||
|
||||
export type PaginatatedRequest = AuthenticatedRequest<
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
offset?: number;
|
||||
lastId?: number;
|
||||
}
|
||||
>;
|
||||
export declare namespace ExecutionRequest {
|
||||
type GetAll = AuthenticatedRequest<
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{
|
||||
status?: ExecutionStatus;
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
offset?: number;
|
||||
includeData?: boolean;
|
||||
workflowId?: number;
|
||||
lastId?: number;
|
||||
}
|
||||
>;
|
||||
|
||||
type Get = AuthenticatedRequest<{ id: number }, {}, {}, { includeData?: boolean }>;
|
||||
type Delete = Get;
|
||||
}
|
||||
|
||||
export declare namespace CredentialTypeRequest {
|
||||
type Get = AuthenticatedRequest<{ credentialTypeName: string }, {}, {}, {}>;
|
||||
}
|
||||
|
||||
export declare namespace WorkflowRequest {
|
||||
type GetAll = AuthenticatedRequest<
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{
|
||||
tags?: string;
|
||||
status?: ExecutionStatus;
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
offset?: number;
|
||||
workflowId?: number;
|
||||
active: boolean;
|
||||
}
|
||||
>;
|
||||
|
||||
type Create = AuthenticatedRequest<{}, {}, WorkflowEntity, {}>;
|
||||
type Get = AuthenticatedRequest<{ id: number }, {}, {}, {}>;
|
||||
type Delete = Get;
|
||||
type Update = AuthenticatedRequest<{ id: number }, {}, WorkflowEntity, {}>;
|
||||
type Activate = Get;
|
||||
}
|
||||
|
||||
export declare namespace UserRequest {
|
||||
export type Invite = AuthenticatedRequest<{}, {}, Array<{ email: string }>>;
|
||||
|
||||
export type ResolveSignUp = AuthlessRequest<
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{ inviterId?: string; inviteeId?: string }
|
||||
>;
|
||||
|
||||
export type SignUp = AuthenticatedRequest<
|
||||
{ id: string },
|
||||
{ inviterId?: string; inviteeId?: string }
|
||||
>;
|
||||
|
||||
export type Delete = AuthenticatedRequest<
|
||||
{ id: string; email: string },
|
||||
{},
|
||||
{},
|
||||
{ transferId?: string; includeRole: boolean }
|
||||
>;
|
||||
|
||||
export type Get = AuthenticatedRequest<
|
||||
{ id: string; email: string },
|
||||
{},
|
||||
{},
|
||||
{ limit?: number; offset?: number; cursor?: string; includeRole?: boolean }
|
||||
>;
|
||||
|
||||
export type Reinvite = AuthenticatedRequest<{ id: string }>;
|
||||
|
||||
export type Update = AuthlessRequest<
|
||||
{ id: string },
|
||||
{},
|
||||
{
|
||||
inviterId: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
password: string;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
export declare namespace CredentialRequest {
|
||||
type Create = AuthenticatedRequest<{}, {}, { type: string; name: string; data: IDataObject }, {}>;
|
||||
}
|
||||
|
||||
export type OperationID = 'getUsers' | 'getUser';
|
||||
|
||||
type PaginationBase = { limit: number };
|
||||
|
||||
type PaginationOffsetDecoded = PaginationBase & { offset: number };
|
||||
|
||||
type PaginationCursorDecoded = PaginationBase & { lastId: number };
|
||||
|
||||
type OffsetPagination = PaginationBase & { offset: number; numberOfTotalRecords: number };
|
||||
|
||||
type CursorPagination = PaginationBase & { lastId: number; numberOfNextRecords: number };
|
||||
export interface IRequired {
|
||||
required?: string[];
|
||||
not?: { required?: string[] };
|
||||
}
|
||||
export interface IDependency {
|
||||
if?: { properties: {} };
|
||||
then?: { oneOf: IRequired[] };
|
||||
else?: { allOf: IRequired[] };
|
||||
}
|
||||
|
||||
export interface IJsonSchema {
|
||||
additionalProperties: boolean;
|
||||
type: 'object';
|
||||
properties: { [key: string]: { type: string } };
|
||||
allOf?: IDependency[];
|
||||
required: string[];
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
import express from 'express';
|
||||
|
||||
import { CredentialsHelper } from '../../../../CredentialsHelper';
|
||||
import { CredentialTypes } from '../../../../CredentialTypes';
|
||||
import { CredentialsEntity } from '../../../../databases/entities/CredentialsEntity';
|
||||
import { CredentialRequest } from '../../../../requests';
|
||||
import { CredentialTypeRequest } from '../../../types';
|
||||
import { authorize } from '../../shared/middlewares/global.middleware';
|
||||
import { validCredentialsProperties, validCredentialType } from './credentials.middleware';
|
||||
|
||||
import {
|
||||
createCredential,
|
||||
encryptCredential,
|
||||
getCredentials,
|
||||
getSharedCredentials,
|
||||
removeCredential,
|
||||
sanitizeCredentials,
|
||||
saveCredential,
|
||||
toJsonSchema,
|
||||
} from './credentials.service';
|
||||
|
||||
export = {
|
||||
createCredential: [
|
||||
authorize(['owner', 'member']),
|
||||
validCredentialType,
|
||||
validCredentialsProperties,
|
||||
async (
|
||||
req: CredentialRequest.Create,
|
||||
res: express.Response,
|
||||
): Promise<express.Response<Partial<CredentialsEntity>>> => {
|
||||
try {
|
||||
const newCredential = await createCredential(req.body);
|
||||
|
||||
const encryptedData = await encryptCredential(newCredential);
|
||||
|
||||
Object.assign(newCredential, encryptedData);
|
||||
|
||||
const savedCredential = await saveCredential(newCredential, req.user, encryptedData);
|
||||
|
||||
// LoggerProxy.verbose('New credential created', {
|
||||
// credentialId: newCredential.id,
|
||||
// ownerId: req.user.id,
|
||||
// });
|
||||
|
||||
return res.json(sanitizeCredentials(savedCredential));
|
||||
} catch ({ message, httpStatusCode }) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
return res.status(httpStatusCode ?? 500).json({ message });
|
||||
}
|
||||
},
|
||||
],
|
||||
deleteCredential: [
|
||||
authorize(['owner', 'member']),
|
||||
async (
|
||||
req: CredentialRequest.Delete,
|
||||
res: express.Response,
|
||||
): Promise<express.Response<Partial<CredentialsEntity>>> => {
|
||||
const { id: credentialId } = req.params;
|
||||
let credential: CredentialsEntity | undefined;
|
||||
|
||||
if (req.user.globalRole.name !== 'owner') {
|
||||
const shared = await getSharedCredentials(req.user.id, credentialId, [
|
||||
'credentials',
|
||||
'role',
|
||||
]);
|
||||
|
||||
if (shared?.role.name === 'owner') {
|
||||
credential = shared.credentials;
|
||||
}
|
||||
} else {
|
||||
credential = (await getCredentials(credentialId)) as CredentialsEntity;
|
||||
}
|
||||
|
||||
if (!credential) {
|
||||
return res.status(404).json({ message: 'Not Found' });
|
||||
}
|
||||
|
||||
await removeCredential(credential);
|
||||
credential.id = Number(credentialId);
|
||||
|
||||
return res.json(sanitizeCredentials(credential));
|
||||
},
|
||||
],
|
||||
|
||||
getCredentialType: [
|
||||
authorize(['owner', 'member']),
|
||||
async (req: CredentialTypeRequest.Get, res: express.Response): Promise<express.Response> => {
|
||||
const { credentialTypeName } = req.params;
|
||||
|
||||
try {
|
||||
CredentialTypes().getByName(credentialTypeName);
|
||||
} catch (error) {
|
||||
return res.status(404).json({ message: 'Not Found' });
|
||||
}
|
||||
|
||||
const schema = new CredentialsHelper('')
|
||||
.getCredentialsProperties(credentialTypeName)
|
||||
.filter((property) => property.type !== 'hidden');
|
||||
|
||||
return res.json(toJsonSchema(schema));
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/* eslint-disable @typescript-eslint/no-invalid-void-type */
|
||||
|
||||
import express from 'express';
|
||||
import { validate } from 'jsonschema';
|
||||
|
||||
import { CredentialsHelper, CredentialTypes } from '../../../..';
|
||||
import { CredentialRequest } from '../../../types';
|
||||
import { toJsonSchema } from './credentials.service';
|
||||
|
||||
export const validCredentialType = (
|
||||
req: CredentialRequest.Create,
|
||||
res: express.Response,
|
||||
next: express.NextFunction,
|
||||
): express.Response | void => {
|
||||
try {
|
||||
CredentialTypes().getByName(req.body.type);
|
||||
} catch (_) {
|
||||
return res.status(400).json({ message: 'req.body.type is not a known type' });
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
export const validCredentialsProperties = (
|
||||
req: CredentialRequest.Create,
|
||||
res: express.Response,
|
||||
next: express.NextFunction,
|
||||
): express.Response | void => {
|
||||
const { type, data } = req.body;
|
||||
|
||||
const properties = new CredentialsHelper('')
|
||||
.getCredentialsProperties(type)
|
||||
.filter((property) => property.type !== 'hidden');
|
||||
|
||||
const schema = toJsonSchema(properties);
|
||||
|
||||
const { valid, errors } = validate(data, schema, { nestedErrors: true });
|
||||
|
||||
if (!valid) {
|
||||
return res.status(400).json({
|
||||
message: errors.map((error) => `request.body.data ${error.message}`).join(','),
|
||||
});
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
import { FindOneOptions } from 'typeorm';
|
||||
import { UserSettings, Credentials } from 'n8n-core';
|
||||
import { IDataObject, INodeProperties, INodePropertyOptions } from 'n8n-workflow';
|
||||
import { Db, ICredentialsDb } from '../../../..';
|
||||
import { CredentialsEntity } from '../../../../databases/entities/CredentialsEntity';
|
||||
import { SharedCredentials } from '../../../../databases/entities/SharedCredentials';
|
||||
import { User } from '../../../../databases/entities/User';
|
||||
import { externalHooks } from '../../../../Server';
|
||||
import { IDependency, IJsonSchema } from '../../../types';
|
||||
import { CredentialRequest } from '../../../../requests';
|
||||
|
||||
export async function getCredentials(
|
||||
credentialId: number | string,
|
||||
): Promise<ICredentialsDb | undefined> {
|
||||
return Db.collections.Credentials.findOne(credentialId);
|
||||
}
|
||||
|
||||
export async function getSharedCredentials(
|
||||
userId: string,
|
||||
credentialId: number | string,
|
||||
relations?: string[],
|
||||
): Promise<SharedCredentials | undefined> {
|
||||
const options: FindOneOptions = {
|
||||
where: {
|
||||
user: { id: userId },
|
||||
credentials: { id: credentialId },
|
||||
},
|
||||
};
|
||||
|
||||
if (relations) {
|
||||
options.relations = relations;
|
||||
}
|
||||
|
||||
return Db.collections.SharedCredentials.findOne(options);
|
||||
}
|
||||
|
||||
export async function createCredential(
|
||||
properties: CredentialRequest.CredentialProperties,
|
||||
): Promise<CredentialsEntity> {
|
||||
const newCredential = new CredentialsEntity();
|
||||
|
||||
Object.assign(newCredential, properties);
|
||||
|
||||
if (!newCredential.nodesAccess || newCredential.nodesAccess.length === 0) {
|
||||
newCredential.nodesAccess = [
|
||||
{
|
||||
nodeType: `n8n-nodes-base.${properties.type?.toLowerCase() ?? 'unknown'}`,
|
||||
date: new Date(),
|
||||
},
|
||||
];
|
||||
} else {
|
||||
// Add the added date for node access permissions
|
||||
newCredential.nodesAccess.forEach((nodeAccess) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
nodeAccess.date = new Date();
|
||||
});
|
||||
}
|
||||
|
||||
return newCredential;
|
||||
}
|
||||
|
||||
export async function saveCredential(
|
||||
credential: CredentialsEntity,
|
||||
user: User,
|
||||
encryptedData: ICredentialsDb,
|
||||
): Promise<CredentialsEntity> {
|
||||
const role = await Db.collections.Role.findOneOrFail({
|
||||
name: 'owner',
|
||||
scope: 'credential',
|
||||
});
|
||||
|
||||
await externalHooks.run('credentials.create', [encryptedData]);
|
||||
|
||||
return Db.transaction(async (transactionManager) => {
|
||||
const savedCredential = await transactionManager.save<CredentialsEntity>(credential);
|
||||
|
||||
savedCredential.data = credential.data;
|
||||
|
||||
const newSharedCredential = new SharedCredentials();
|
||||
|
||||
Object.assign(newSharedCredential, {
|
||||
role,
|
||||
user,
|
||||
credentials: savedCredential,
|
||||
});
|
||||
|
||||
await transactionManager.save<SharedCredentials>(newSharedCredential);
|
||||
|
||||
return savedCredential;
|
||||
});
|
||||
}
|
||||
|
||||
export async function removeCredential(credentials: CredentialsEntity): Promise<ICredentialsDb> {
|
||||
await externalHooks.run('credentials.delete', [credentials.id]);
|
||||
return Db.collections.Credentials.remove(credentials);
|
||||
}
|
||||
|
||||
export async function encryptCredential(credential: CredentialsEntity): Promise<ICredentialsDb> {
|
||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
||||
|
||||
// Encrypt the data
|
||||
const coreCredential = new Credentials(
|
||||
{ id: null, name: credential.name },
|
||||
credential.type,
|
||||
credential.nodesAccess,
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
coreCredential.setData(credential.data, encryptionKey);
|
||||
|
||||
return coreCredential.getDataToSave() as ICredentialsDb;
|
||||
}
|
||||
|
||||
export function sanitizeCredentials(credentials: CredentialsEntity): Partial<CredentialsEntity>;
|
||||
export function sanitizeCredentials(
|
||||
credentials: CredentialsEntity[],
|
||||
): Array<Partial<CredentialsEntity>>;
|
||||
|
||||
export function sanitizeCredentials(
|
||||
credentials: CredentialsEntity | CredentialsEntity[],
|
||||
): Partial<CredentialsEntity> | Array<Partial<CredentialsEntity>> {
|
||||
const argIsArray = Array.isArray(credentials);
|
||||
const credentialsList = argIsArray ? credentials : [credentials];
|
||||
|
||||
const sanitizedCredentials = credentialsList.map((credential) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { data, nodesAccess, shared, ...rest } = credential;
|
||||
return rest;
|
||||
});
|
||||
|
||||
return argIsArray ? sanitizedCredentials : sanitizedCredentials[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* toJsonSchema
|
||||
* Take an array of crendentials parameter and map it
|
||||
* to a JSON Schema (see https://json-schema.org/). With
|
||||
* the JSON Schema defintion we can validate the credential's shape
|
||||
* @param properties - Credentials properties
|
||||
* @returns The credentials schema definition.
|
||||
*/
|
||||
export function toJsonSchema(properties: INodeProperties[]): IDataObject {
|
||||
const jsonSchema: IJsonSchema = {
|
||||
additionalProperties: false,
|
||||
type: 'object',
|
||||
properties: {},
|
||||
allOf: [],
|
||||
required: [],
|
||||
};
|
||||
|
||||
const optionsValues: { [key: string]: string[] } = {};
|
||||
const resolveProperties: string[] = [];
|
||||
|
||||
// get all posible values of properties type "options"
|
||||
// so we can later resolve the displayOptions dependencies
|
||||
properties
|
||||
.filter((property) => property.type === 'options')
|
||||
.forEach((property) => {
|
||||
Object.assign(optionsValues, {
|
||||
[property.name]: property.options?.map((option: INodePropertyOptions) => option.value),
|
||||
});
|
||||
});
|
||||
|
||||
let requiredFields: string[] = [];
|
||||
|
||||
const propertyRequiredDependencies: { [key: string]: IDependency } = {};
|
||||
|
||||
// add all credential's properties to the properties
|
||||
// object in the JSON Schema definition. This allows us
|
||||
// to later validate that only this properties are set in
|
||||
// the credentials sent in the API call.
|
||||
properties.forEach((property) => {
|
||||
requiredFields.push(property.name);
|
||||
if (property.type === 'options') {
|
||||
// if the property is type options,
|
||||
// include all possible values in the anum property.
|
||||
Object.assign(jsonSchema.properties, {
|
||||
[property.name]: {
|
||||
type: 'string',
|
||||
enum: property.options?.map((data: INodePropertyOptions) => data.value),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
Object.assign(jsonSchema.properties, {
|
||||
[property.name]: {
|
||||
type: property.type,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// if the credential property has a dependency
|
||||
// then add a JSON Schema condition that satisfy each property value
|
||||
// e.x: If A has value X then required B, else required C
|
||||
// see https://json-schema.org/understanding-json-schema/reference/conditionals.html#if-then-else
|
||||
if (property.displayOptions?.show) {
|
||||
const dependantName = Object.keys(property.displayOptions?.show)[0] || '';
|
||||
const displayOptionsValues = property.displayOptions.show[dependantName];
|
||||
let dependantValue: string | number | boolean = '';
|
||||
|
||||
if (displayOptionsValues && Array.isArray(displayOptionsValues) && displayOptionsValues[0]) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
dependantValue = displayOptionsValues[0];
|
||||
}
|
||||
|
||||
if (propertyRequiredDependencies[dependantName] === undefined) {
|
||||
propertyRequiredDependencies[dependantName] = {};
|
||||
}
|
||||
|
||||
if (!resolveProperties.includes(dependantName)) {
|
||||
propertyRequiredDependencies[dependantName] = {
|
||||
if: {
|
||||
properties: {
|
||||
[dependantName]: {
|
||||
enum: [dependantValue],
|
||||
},
|
||||
},
|
||||
},
|
||||
then: {
|
||||
oneOf: [],
|
||||
},
|
||||
else: {
|
||||
allOf: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
propertyRequiredDependencies[dependantName].then?.oneOf.push({ required: [property.name] });
|
||||
propertyRequiredDependencies[dependantName].else?.allOf.push({
|
||||
not: { required: [property.name] },
|
||||
});
|
||||
|
||||
resolveProperties.push(dependantName);
|
||||
// remove global required
|
||||
requiredFields = requiredFields.filter((field) => field !== property.name);
|
||||
}
|
||||
});
|
||||
Object.assign(jsonSchema, { required: requiredFields });
|
||||
|
||||
jsonSchema.allOf = Object.values(propertyRequiredDependencies);
|
||||
|
||||
if (!jsonSchema.allOf.length) {
|
||||
delete jsonSchema.allOf;
|
||||
}
|
||||
|
||||
return jsonSchema as unknown as IDataObject;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
delete:
|
||||
x-eov-operation-id: deleteCredential
|
||||
x-eov-operation-handler: v1/handlers/credentials/credentials.handler
|
||||
tags:
|
||||
- Credential
|
||||
summary: Delete credential by ID
|
||||
description: Deletes a credential from your instance. You must be the owner of the credentials
|
||||
operationId: deleteCredential
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
description: The credential ID that needs to be deleted
|
||||
required: true
|
||||
schema:
|
||||
type: number
|
||||
responses:
|
||||
'200':
|
||||
description: Operation successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/credential.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
'404':
|
||||
$ref: '../../../../shared/spec/responses/notFound.yml'
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
get:
|
||||
x-eov-operation-id: getCredentialType
|
||||
x-eov-operation-handler: v1/handlers/credentials/credentials.handler
|
||||
tags:
|
||||
- Credential
|
||||
summary: Show credential data schema
|
||||
parameters:
|
||||
- name: credentialTypeName
|
||||
in: path
|
||||
description: The credential type name that you want to get the schema for
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Operation successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
examples:
|
||||
freshdeskApi:
|
||||
value:
|
||||
additionalProperties: false
|
||||
type: 'object'
|
||||
properties: { apiKey: { type: 'string' }, domain: { type: 'string' } }
|
||||
required: ['apiKey', 'domain']
|
||||
slackOAuth2Api:
|
||||
value:
|
||||
additionalProperties: false
|
||||
type: 'object'
|
||||
properties: { clientId: { type: 'string' }, clientSecret: { type: 'string' } }
|
||||
required: ['clientId', 'clientSecret']
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
'404':
|
||||
$ref: '../../../../shared/spec/responses/notFound.yml'
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
post:
|
||||
x-eov-operation-id: createCredential
|
||||
x-eov-operation-handler: v1/handlers/credentials/credentials.handler
|
||||
tags:
|
||||
- Credential
|
||||
summary: Create a credential
|
||||
description: Creates a credential that can be used by nodes of the specified type.
|
||||
requestBody:
|
||||
description: Credential to be created.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/credential.yml'
|
||||
responses:
|
||||
'200':
|
||||
description: Operation successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/credential.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
'415':
|
||||
description: Unsupported media type.
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
required:
|
||||
- name
|
||||
- type
|
||||
- data
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: number
|
||||
readOnly: true
|
||||
example: 42
|
||||
name:
|
||||
type: string
|
||||
example: Joe's Github Credentials
|
||||
type:
|
||||
type: string
|
||||
example: github
|
||||
data:
|
||||
type: object
|
||||
writeOnly: true
|
||||
example: { token: 'ada612vad6fa5df4adf5a5dsf4389adsf76da7s' }
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
example: '2022-04-29T11:02:29.842Z'
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
example: '2022-04-29T11:02:29.842Z'
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
type: object
|
||||
properties:
|
||||
displayName:
|
||||
type: string
|
||||
readOnly: true
|
||||
example: Email
|
||||
name:
|
||||
type: string
|
||||
readOnly: true
|
||||
example: email
|
||||
type:
|
||||
type: string
|
||||
readOnly: true
|
||||
example: string
|
||||
default:
|
||||
type: string
|
||||
readOnly: true
|
||||
example: string
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
import express from 'express';
|
||||
|
||||
import { BinaryDataManager } from 'n8n-core';
|
||||
|
||||
import {
|
||||
getExecutions,
|
||||
getExecutionInWorkflows,
|
||||
deleteExecution,
|
||||
getExecutionsCount,
|
||||
} from './executions.service';
|
||||
import { ActiveExecutions } from '../../../..';
|
||||
import { authorize, validCursor } from '../../shared/middlewares/global.middleware';
|
||||
import { ExecutionRequest } from '../../../types';
|
||||
import { getSharedWorkflowIds } from '../workflows/workflows.service';
|
||||
import { encodeNextCursor } from '../../shared/services/pagination.service';
|
||||
import { InternalHooksManager } from '../../../../InternalHooksManager';
|
||||
|
||||
export = {
|
||||
deleteExecution: [
|
||||
authorize(['owner', 'member']),
|
||||
async (req: ExecutionRequest.Delete, res: express.Response): Promise<express.Response> => {
|
||||
const sharedWorkflowsIds = await getSharedWorkflowIds(req.user);
|
||||
|
||||
// user does not have workflows hence no executions
|
||||
// or the execution he is trying to access belongs to a workflow he does not own
|
||||
if (!sharedWorkflowsIds.length) {
|
||||
return res.status(404).json({ message: 'Not Found' });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
|
||||
// look for the execution on the workflow the user owns
|
||||
const execution = await getExecutionInWorkflows(id, sharedWorkflowsIds, false);
|
||||
|
||||
if (!execution) {
|
||||
return res.status(404).json({ message: 'Not Found' });
|
||||
}
|
||||
|
||||
await BinaryDataManager.getInstance().deleteBinaryDataByExecutionId(execution.id.toString());
|
||||
|
||||
await deleteExecution(execution);
|
||||
|
||||
execution.id = id;
|
||||
|
||||
return res.json(execution);
|
||||
},
|
||||
],
|
||||
getExecution: [
|
||||
authorize(['owner', 'member']),
|
||||
async (req: ExecutionRequest.Get, res: express.Response): Promise<express.Response> => {
|
||||
const sharedWorkflowsIds = await getSharedWorkflowIds(req.user);
|
||||
|
||||
// user does not have workflows hence no executions
|
||||
// or the execution he is trying to access belongs to a workflow he does not own
|
||||
if (!sharedWorkflowsIds.length) {
|
||||
return res.status(404).json({ message: 'Not Found' });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const { includeData = false } = req.query;
|
||||
|
||||
// look for the execution on the workflow the user owns
|
||||
const execution = await getExecutionInWorkflows(id, sharedWorkflowsIds, includeData);
|
||||
|
||||
if (!execution) {
|
||||
return res.status(404).json({ message: 'Not Found' });
|
||||
}
|
||||
|
||||
void InternalHooksManager.getInstance().onUserRetrievedExecution({
|
||||
user_id: req.user.id,
|
||||
public_api: true,
|
||||
});
|
||||
|
||||
return res.json(execution);
|
||||
},
|
||||
],
|
||||
getExecutions: [
|
||||
authorize(['owner', 'member']),
|
||||
validCursor,
|
||||
async (req: ExecutionRequest.GetAll, res: express.Response): Promise<express.Response> => {
|
||||
const {
|
||||
lastId = undefined,
|
||||
limit = 100,
|
||||
status = undefined,
|
||||
includeData = false,
|
||||
workflowId = undefined,
|
||||
} = req.query;
|
||||
|
||||
const sharedWorkflowsIds = await getSharedWorkflowIds(req.user);
|
||||
|
||||
// user does not have workflows hence no executions
|
||||
// or the execution he is trying to access belongs to a workflow he does not own
|
||||
if (!sharedWorkflowsIds.length) {
|
||||
return res.status(200).json({ data: [], nextCursor: null });
|
||||
}
|
||||
|
||||
// get running workflows so we exclude them from the result
|
||||
const runningExecutionsIds = ActiveExecutions.getInstance()
|
||||
.getActiveExecutions()
|
||||
.map(({ id }) => Number(id));
|
||||
|
||||
const filters = {
|
||||
status,
|
||||
limit,
|
||||
lastId,
|
||||
includeData,
|
||||
...(workflowId && { workflowIds: [workflowId] }),
|
||||
excludedExecutionsIds: runningExecutionsIds,
|
||||
};
|
||||
|
||||
const executions = await getExecutions(filters);
|
||||
|
||||
const newLastId = !executions.length ? 0 : (executions.slice(-1)[0].id as number);
|
||||
|
||||
filters.lastId = newLastId;
|
||||
|
||||
const count = await getExecutionsCount(filters);
|
||||
|
||||
void InternalHooksManager.getInstance().onUserRetrievedAllExecutions({
|
||||
user_id: req.user.id,
|
||||
public_api: true,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
data: executions,
|
||||
nextCursor: encodeNextCursor({
|
||||
lastId: newLastId,
|
||||
limit,
|
||||
numberOfNextRecords: count,
|
||||
}),
|
||||
});
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
import { parse } from 'flatted';
|
||||
import { In, Not, ObjectLiteral, LessThan, IsNull } from 'typeorm';
|
||||
|
||||
import { Db, IExecutionFlattedDb, IExecutionResponseApi } from '../../../..';
|
||||
import { ExecutionStatus } from '../../../types';
|
||||
|
||||
function prepareExecutionData(
|
||||
execution: IExecutionFlattedDb | undefined,
|
||||
): IExecutionResponseApi | undefined {
|
||||
if (!execution) return undefined;
|
||||
|
||||
// @ts-ignore
|
||||
if (!execution.data) return execution;
|
||||
|
||||
return {
|
||||
...execution,
|
||||
data: parse(execution.data) as object,
|
||||
};
|
||||
}
|
||||
|
||||
function getStatusCondition(status: ExecutionStatus): ObjectLiteral {
|
||||
const condition: ObjectLiteral = {};
|
||||
|
||||
if (status === 'success') {
|
||||
condition.finished = true;
|
||||
} else if (status === 'waiting') {
|
||||
condition.waitTill = Not(IsNull());
|
||||
} else if (status === 'error') {
|
||||
condition.stoppedAt = Not(IsNull());
|
||||
condition.finished = false;
|
||||
}
|
||||
|
||||
return condition;
|
||||
}
|
||||
|
||||
function getExecutionSelectableProperties(includeData?: boolean): Array<keyof IExecutionFlattedDb> {
|
||||
const selectFields: Array<keyof IExecutionFlattedDb> = [
|
||||
'id',
|
||||
'mode',
|
||||
'retryOf',
|
||||
'retrySuccessId',
|
||||
'startedAt',
|
||||
'stoppedAt',
|
||||
'workflowId',
|
||||
'waitTill',
|
||||
'finished',
|
||||
];
|
||||
|
||||
if (includeData) selectFields.push('data');
|
||||
|
||||
return selectFields;
|
||||
}
|
||||
|
||||
export async function getExecutions(data: {
|
||||
limit: number;
|
||||
includeData?: boolean;
|
||||
lastId?: number;
|
||||
workflowIds?: number[];
|
||||
status?: ExecutionStatus;
|
||||
excludedExecutionsIds?: number[];
|
||||
}): Promise<IExecutionResponseApi[]> {
|
||||
const executions = await Db.collections.Execution.find({
|
||||
select: getExecutionSelectableProperties(data.includeData),
|
||||
where: {
|
||||
...(data.lastId && { id: LessThan(data.lastId) }),
|
||||
...(data.status && { ...getStatusCondition(data.status) }),
|
||||
...(data.workflowIds && { workflowId: In(data.workflowIds.map(String)) }),
|
||||
...(data.excludedExecutionsIds && { id: Not(In(data.excludedExecutionsIds)) }),
|
||||
},
|
||||
order: { id: 'DESC' },
|
||||
take: data.limit,
|
||||
});
|
||||
|
||||
return executions.map((execution) => prepareExecutionData(execution)) as IExecutionResponseApi[];
|
||||
}
|
||||
|
||||
export async function getExecutionsCount(data: {
|
||||
limit: number;
|
||||
lastId?: number;
|
||||
workflowIds?: number[];
|
||||
status?: ExecutionStatus;
|
||||
excludedWorkflowIds?: number[];
|
||||
}): Promise<number> {
|
||||
const executions = await Db.collections.Execution.count({
|
||||
where: {
|
||||
...(data.lastId && { id: LessThan(data.lastId) }),
|
||||
...(data.status && { ...getStatusCondition(data.status) }),
|
||||
...(data.workflowIds && { workflowId: In(data.workflowIds) }),
|
||||
...(data.excludedWorkflowIds && { workflowId: Not(In(data.excludedWorkflowIds)) }),
|
||||
},
|
||||
take: data.limit,
|
||||
});
|
||||
|
||||
return executions;
|
||||
}
|
||||
|
||||
export async function getExecutionInWorkflows(
|
||||
id: number,
|
||||
workflows: number[],
|
||||
includeData?: boolean,
|
||||
): Promise<IExecutionResponseApi | undefined> {
|
||||
const execution = await Db.collections.Execution.findOne({
|
||||
select: getExecutionSelectableProperties(includeData),
|
||||
where: {
|
||||
id,
|
||||
workflowId: In(workflows),
|
||||
},
|
||||
});
|
||||
|
||||
return prepareExecutionData(execution);
|
||||
}
|
||||
|
||||
export async function deleteExecution(
|
||||
execution: IExecutionResponseApi | undefined,
|
||||
): Promise<IExecutionFlattedDb> {
|
||||
// @ts-ignore
|
||||
return Db.collections.Execution.remove(execution);
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
get:
|
||||
x-eov-operation-id: getExecution
|
||||
x-eov-operation-handler: v1/handlers/executions/executions.handler
|
||||
tags:
|
||||
- Execution
|
||||
summary: Retrieve an execution
|
||||
description: Retrieve an execution from you instance.
|
||||
parameters:
|
||||
- $ref: '../schemas/parameters/executionId.yml'
|
||||
- $ref: '../schemas/parameters/includeData.yml'
|
||||
responses:
|
||||
'200':
|
||||
description: Operation successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/execution.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
'404':
|
||||
$ref: '../../../../shared/spec/responses/notFound.yml'
|
||||
delete:
|
||||
x-eov-operation-id: deleteExecution
|
||||
x-eov-operation-handler: v1/handlers/executions/executions.handler
|
||||
tags:
|
||||
- Execution
|
||||
summary: Delete an execution
|
||||
description: Deletes an execution from your instance.
|
||||
parameters:
|
||||
- $ref: '../schemas/parameters/executionId.yml'
|
||||
responses:
|
||||
'200':
|
||||
description: Operation successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/execution.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
'404':
|
||||
$ref: '../../../../shared/spec/responses/notFound.yml'
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
get:
|
||||
x-eov-operation-id: getExecutions
|
||||
x-eov-operation-handler: v1/handlers/executions/executions.handler
|
||||
tags:
|
||||
- Execution
|
||||
summary: Retrieve all executions
|
||||
description: Retrieve all executions from your instance.
|
||||
parameters:
|
||||
- $ref: '../schemas/parameters/includeData.yml'
|
||||
- name: status
|
||||
in: query
|
||||
description: Status to filter the executions by.
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum: ['error', 'success', 'waiting']
|
||||
- name: workflowId
|
||||
in: query
|
||||
description: Workflow to filter the executions by.
|
||||
required: false
|
||||
schema:
|
||||
type: number
|
||||
example: 1000
|
||||
- $ref: '../../../../shared/spec/parameters/limit.yml'
|
||||
- $ref: '../../../../shared/spec/parameters/cursor.yml'
|
||||
responses:
|
||||
'200':
|
||||
description: Operation successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/executionList.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
'404':
|
||||
$ref: '../../../../shared/spec/responses/notFound.yml'
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: number
|
||||
example: 1000
|
||||
data:
|
||||
type: object
|
||||
finished:
|
||||
type: boolean
|
||||
example: true
|
||||
mode:
|
||||
type: string
|
||||
enum: ['cli', 'error', 'integrated', 'internal', 'manual', 'retry', 'trigger', 'webhook']
|
||||
retryOf:
|
||||
type: string
|
||||
nullable: true
|
||||
retrySuccessId:
|
||||
type: string
|
||||
nullable: true
|
||||
example: 2
|
||||
startedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
stoppedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
workflowId:
|
||||
type: string
|
||||
example: 1000
|
||||
waitTill:
|
||||
type: string
|
||||
nullable: true
|
||||
format: date-time
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: './execution.yml'
|
||||
nextCursor:
|
||||
type: string
|
||||
description: Paginate through executions by setting the cursor parameter to a nextCursor attribute returned by a previous request. Default value fetches the first "page" of the collection.
|
||||
nullable: true
|
||||
example: MTIzZTQ1NjctZTg5Yi0xMmQzLWE0NTYtNDI2NjE0MTc0MDA
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
name: id
|
||||
in: path
|
||||
description: The ID of the execution.
|
||||
required: true
|
||||
schema:
|
||||
type: number
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
name: includeData
|
||||
in: query
|
||||
description: Whether or not to include the execution's detailed data.
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { Db } from '../../../..';
|
||||
import { Role } from '../../../../databases/entities/Role';
|
||||
import { User } from '../../../../databases/entities/User';
|
||||
|
||||
export function isInstanceOwner(user: User): boolean {
|
||||
return user.globalRole.name === 'owner';
|
||||
}
|
||||
|
||||
export async function getWorkflowOwnerRole(): Promise<Role> {
|
||||
return Db.collections.Role.findOneOrFail({
|
||||
name: 'owner',
|
||||
scope: 'workflow',
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
post:
|
||||
x-eov-operation-id: activateWorkflow
|
||||
x-eov-operation-handler: v1/handlers/workflows/workflows.handler
|
||||
tags:
|
||||
- Workflow
|
||||
summary: Activate a workflow
|
||||
description: Active a workflow.
|
||||
parameters:
|
||||
- $ref: '../schemas/parameters/workflowId.yml'
|
||||
responses:
|
||||
'200':
|
||||
description: Workflow object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/workflow.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
'404':
|
||||
$ref: '../../../../shared/spec/responses/notFound.yml'
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
post:
|
||||
x-eov-operation-id: deactivateWorkflow
|
||||
x-eov-operation-handler: v1/handlers/workflows/workflows.handler
|
||||
tags:
|
||||
- Workflow
|
||||
summary: Deactivate a workflow
|
||||
description: Deactivate a workflow.
|
||||
parameters:
|
||||
- $ref: '../schemas/parameters/workflowId.yml'
|
||||
responses:
|
||||
'200':
|
||||
description: Workflow object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/workflow.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
'404':
|
||||
$ref: '../../../../shared/spec/responses/notFound.yml'
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
get:
|
||||
x-eov-operation-id: getWorkflow
|
||||
x-eov-operation-handler: v1/handlers/workflows/workflows.handler
|
||||
tags:
|
||||
- Workflow
|
||||
summary: Retrieves a workflow
|
||||
description: Retrieves a workflow.
|
||||
parameters:
|
||||
- $ref: '../schemas/parameters/workflowId.yml'
|
||||
responses:
|
||||
'200':
|
||||
description: Operation successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/workflow.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
'404':
|
||||
$ref: '../../../../shared/spec/responses/notFound.yml'
|
||||
delete:
|
||||
x-eov-operation-id: deleteWorkflow
|
||||
x-eov-operation-handler: v1/handlers/workflows/workflows.handler
|
||||
tags:
|
||||
- Workflow
|
||||
summary: Delete a workflow
|
||||
description: Deletes a workflow.
|
||||
parameters:
|
||||
- $ref: '../schemas/parameters/workflowId.yml'
|
||||
responses:
|
||||
'200':
|
||||
description: Operation successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/workflow.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
'404':
|
||||
$ref: '../../../../shared/spec/responses/notFound.yml'
|
||||
put:
|
||||
x-eov-operation-id: updateWorkflow
|
||||
x-eov-operation-handler: v1/handlers/workflows/workflows.handler
|
||||
tags:
|
||||
- Workflow
|
||||
summary: Update a workflow
|
||||
description: Update a workflow.
|
||||
parameters:
|
||||
- $ref: '../schemas/parameters/workflowId.yml'
|
||||
requestBody:
|
||||
description: Updated workflow object.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/workflow.yml'
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: Workflow object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/workflow.yml'
|
||||
'400':
|
||||
$ref: '../../../../shared/spec/responses/badRequest.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
'404':
|
||||
$ref: '../../../../shared/spec/responses/notFound.yml'
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
post:
|
||||
x-eov-operation-id: createWorkflow
|
||||
x-eov-operation-handler: v1/handlers/workflows/workflows.handler
|
||||
tags:
|
||||
- Workflow
|
||||
summary: Create a workflow
|
||||
description: Create a workflow in your instance.
|
||||
requestBody:
|
||||
description: Created workflow object.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/workflow.yml'
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: A workflow object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/workflow.yml'
|
||||
'400':
|
||||
$ref: '../../../../shared/spec/responses/badRequest.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
get:
|
||||
x-eov-operation-id: getWorkflows
|
||||
x-eov-operation-handler: v1/handlers/workflows/workflows.handler
|
||||
tags:
|
||||
- Workflow
|
||||
summary: Retrieve all workflows
|
||||
description: Retrieve all workflows from your instance.
|
||||
parameters:
|
||||
- name: active
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
example: true
|
||||
- name: tags
|
||||
in: query
|
||||
required: false
|
||||
explode: false
|
||||
allowReserved: true
|
||||
schema:
|
||||
type: string
|
||||
example: test,production
|
||||
- $ref: '../../../../shared/spec/parameters/limit.yml'
|
||||
- $ref: '../../../../shared/spec/parameters/cursor.yml'
|
||||
responses:
|
||||
'200':
|
||||
description: Operation successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/workflowList.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
example: Jira
|
||||
webhookId:
|
||||
type: string
|
||||
disabled:
|
||||
type: boolean
|
||||
notesInFlow:
|
||||
type: boolean
|
||||
notes:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
example: n8n-nodes-base.Jira
|
||||
typeVersion:
|
||||
type: number
|
||||
example: 1
|
||||
executeOnce:
|
||||
type: boolean
|
||||
example: false
|
||||
alwaysOutputData:
|
||||
type: boolean
|
||||
example: false
|
||||
retryOnFail:
|
||||
type: boolean
|
||||
example: false
|
||||
maxTries:
|
||||
type: number
|
||||
waitBetweenTries:
|
||||
type: number
|
||||
continueOnFail:
|
||||
type: boolean
|
||||
example: false
|
||||
position:
|
||||
type: array
|
||||
items:
|
||||
type: number
|
||||
example: [-100, 80]
|
||||
parameters:
|
||||
type: object
|
||||
example: { additionalProperties: {} }
|
||||
credentials:
|
||||
type: object
|
||||
example: { jiraSoftwareCloudApi: { id: "35", name: "jiraApi"} }
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
name: id
|
||||
in: path
|
||||
description: The ID of the workflow.
|
||||
required: true
|
||||
schema:
|
||||
type: number
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
example: 12
|
||||
name:
|
||||
type: string
|
||||
example: Production
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
type: object
|
||||
required:
|
||||
- name
|
||||
- nodes
|
||||
- connections
|
||||
- settings
|
||||
properties:
|
||||
id:
|
||||
type: number
|
||||
readOnly: true
|
||||
example: 1
|
||||
name:
|
||||
type: string
|
||||
example: Workflow 1
|
||||
active:
|
||||
type: boolean
|
||||
readOnly: true
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
nodes:
|
||||
type: array
|
||||
items:
|
||||
$ref: './node.yml'
|
||||
connections:
|
||||
type: object
|
||||
example: { main: [{ node: 'Jira', type: 'main', index: 0 }] }
|
||||
settings:
|
||||
$ref: './workflowSettings.yml'
|
||||
staticData:
|
||||
example: { lastId: 1 }
|
||||
nullable: true
|
||||
anyOf:
|
||||
- type: string
|
||||
format: 'jsonString'
|
||||
nullable: true
|
||||
- type: object
|
||||
nullable: true
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
$ref: './tag.yml'
|
||||
readOnly: true
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: './workflow.yml'
|
||||
nextCursor:
|
||||
type: string
|
||||
description: Paginate through workflows by setting the cursor parameter to a nextCursor attribute returned by a previous request. Default value fetches the first "page" of the collection.
|
||||
nullable: true
|
||||
example: MTIzZTQ1NjctZTg5Yi0xMmQzLWE0NTYtNDI2NjE0MTc0MDA
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
saveExecutionProgress:
|
||||
type: boolean
|
||||
saveManualExecutions:
|
||||
type: boolean
|
||||
saveDataErrorExecution:
|
||||
type: string
|
||||
enum: ['all', 'none']
|
||||
saveDataSuccessExecution:
|
||||
type: string
|
||||
enum: ['all', 'none']
|
||||
executionTimeout:
|
||||
type: number
|
||||
example: 3600
|
||||
maxLength: 3600
|
||||
errorWorkflow:
|
||||
type: string
|
||||
example: 10
|
||||
description: The ID of the workflow that contains the error trigger node.
|
||||
timezone:
|
||||
type: string
|
||||
example: America/New_York
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
import express from 'express';
|
||||
|
||||
import { FindManyOptions, In } from 'typeorm';
|
||||
|
||||
import { ActiveWorkflowRunner, Db } from '../../../..';
|
||||
import config = require('../../../../../config');
|
||||
import { WorkflowEntity } from '../../../../databases/entities/WorkflowEntity';
|
||||
import { InternalHooksManager } from '../../../../InternalHooksManager';
|
||||
import { externalHooks } from '../../../../Server';
|
||||
import { replaceInvalidCredentials } from '../../../../WorkflowHelpers';
|
||||
import { WorkflowRequest } from '../../../types';
|
||||
import { authorize, validCursor } from '../../shared/middlewares/global.middleware';
|
||||
import { encodeNextCursor } from '../../shared/services/pagination.service';
|
||||
import { getWorkflowOwnerRole, isInstanceOwner } from '../users/users.service';
|
||||
import {
|
||||
getWorkflowById,
|
||||
getSharedWorkflow,
|
||||
setWorkflowAsActive,
|
||||
setWorkflowAsInactive,
|
||||
updateWorkflow,
|
||||
hasStartNode,
|
||||
getStartNode,
|
||||
getWorkflows,
|
||||
getSharedWorkflows,
|
||||
getWorkflowsCount,
|
||||
createWorkflow,
|
||||
getWorkflowIdsViaTags,
|
||||
parseTagNames,
|
||||
} from './workflows.service';
|
||||
|
||||
export = {
|
||||
createWorkflow: [
|
||||
authorize(['owner', 'member']),
|
||||
async (req: WorkflowRequest.Create, res: express.Response): Promise<express.Response> => {
|
||||
const workflow = req.body;
|
||||
|
||||
workflow.active = false;
|
||||
|
||||
if (!hasStartNode(workflow)) {
|
||||
workflow.nodes.push(getStartNode());
|
||||
}
|
||||
|
||||
await replaceInvalidCredentials(workflow);
|
||||
|
||||
const role = await getWorkflowOwnerRole();
|
||||
|
||||
const createdWorkflow = await createWorkflow(workflow, req.user, role);
|
||||
|
||||
await externalHooks.run('workflow.afterCreate', [createdWorkflow]);
|
||||
void InternalHooksManager.getInstance().onWorkflowCreated(req.user.id, createdWorkflow, true);
|
||||
|
||||
return res.json(createdWorkflow);
|
||||
},
|
||||
],
|
||||
deleteWorkflow: [
|
||||
authorize(['owner', 'member']),
|
||||
async (req: WorkflowRequest.Get, res: express.Response): Promise<express.Response> => {
|
||||
const { id } = req.params;
|
||||
|
||||
const sharedWorkflow = await getSharedWorkflow(req.user, id.toString());
|
||||
|
||||
if (!sharedWorkflow) {
|
||||
// user trying to access a workflow he does not own
|
||||
// or workflow does not exist
|
||||
return res.status(404).json({ message: 'Not Found' });
|
||||
}
|
||||
|
||||
if (sharedWorkflow.workflow.active) {
|
||||
// deactivate before deleting
|
||||
await ActiveWorkflowRunner.getInstance().remove(id.toString());
|
||||
}
|
||||
|
||||
await Db.collections.Workflow.delete(id);
|
||||
|
||||
void InternalHooksManager.getInstance().onWorkflowDeleted(req.user.id, id.toString(), true);
|
||||
await externalHooks.run('workflow.afterDelete', [id.toString()]);
|
||||
|
||||
return res.json(sharedWorkflow.workflow);
|
||||
},
|
||||
],
|
||||
getWorkflow: [
|
||||
authorize(['owner', 'member']),
|
||||
async (req: WorkflowRequest.Get, res: express.Response): Promise<express.Response> => {
|
||||
const { id } = req.params;
|
||||
|
||||
const sharedWorkflow = await getSharedWorkflow(req.user, id.toString());
|
||||
|
||||
if (!sharedWorkflow) {
|
||||
// user trying to access a workflow he does not own
|
||||
// or workflow does not exist
|
||||
return res.status(404).json({ message: 'Not Found' });
|
||||
}
|
||||
|
||||
void InternalHooksManager.getInstance().onUserRetrievedWorkflow({
|
||||
user_id: req.user.id,
|
||||
public_api: true,
|
||||
});
|
||||
|
||||
return res.json(sharedWorkflow.workflow);
|
||||
},
|
||||
],
|
||||
getWorkflows: [
|
||||
authorize(['owner', 'member']),
|
||||
validCursor,
|
||||
async (req: WorkflowRequest.GetAll, res: express.Response): Promise<express.Response> => {
|
||||
const { offset = 0, limit = 100, active = undefined, tags = undefined } = req.query;
|
||||
|
||||
let workflows: WorkflowEntity[];
|
||||
let count: number;
|
||||
|
||||
const query: FindManyOptions<WorkflowEntity> = {
|
||||
skip: offset,
|
||||
take: limit,
|
||||
where: {
|
||||
...(active !== undefined && { active }),
|
||||
},
|
||||
...(!config.getEnv('workflowTagsDisabled') && { relations: ['tags'] }),
|
||||
};
|
||||
|
||||
if (isInstanceOwner(req.user)) {
|
||||
if (tags) {
|
||||
const workflowIds = await getWorkflowIdsViaTags(parseTagNames(tags));
|
||||
Object.assign(query.where, { id: In(workflowIds) });
|
||||
}
|
||||
|
||||
workflows = await getWorkflows(query);
|
||||
|
||||
count = await getWorkflowsCount(query);
|
||||
} else {
|
||||
const options: { workflowIds?: number[] } = {};
|
||||
|
||||
if (tags) {
|
||||
options.workflowIds = await getWorkflowIdsViaTags(parseTagNames(tags));
|
||||
}
|
||||
|
||||
const sharedWorkflows = await getSharedWorkflows(req.user, options);
|
||||
|
||||
if (!sharedWorkflows.length) {
|
||||
return res.status(200).json({
|
||||
data: [],
|
||||
nextCursor: null,
|
||||
});
|
||||
}
|
||||
|
||||
const workflowsIds = sharedWorkflows.map((shareWorkflow) => shareWorkflow.workflowId);
|
||||
|
||||
Object.assign(query.where, { id: In(workflowsIds) });
|
||||
|
||||
workflows = await getWorkflows(query);
|
||||
|
||||
count = await getWorkflowsCount(query);
|
||||
}
|
||||
|
||||
void InternalHooksManager.getInstance().onUserRetrievedAllWorkflows({
|
||||
user_id: req.user.id,
|
||||
public_api: true,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
data: workflows,
|
||||
nextCursor: encodeNextCursor({
|
||||
offset,
|
||||
limit,
|
||||
numberOfTotalRecords: count,
|
||||
}),
|
||||
});
|
||||
},
|
||||
],
|
||||
updateWorkflow: [
|
||||
authorize(['owner', 'member']),
|
||||
async (req: WorkflowRequest.Update, res: express.Response): Promise<express.Response> => {
|
||||
const { id } = req.params;
|
||||
const updateData = new WorkflowEntity();
|
||||
Object.assign(updateData, req.body);
|
||||
|
||||
const sharedWorkflow = await getSharedWorkflow(req.user, id.toString());
|
||||
|
||||
if (!sharedWorkflow) {
|
||||
// user trying to access a workflow he does not own
|
||||
// or workflow does not exist
|
||||
return res.status(404).json({ message: 'Not Found' });
|
||||
}
|
||||
|
||||
if (!hasStartNode(updateData)) {
|
||||
updateData.nodes.push(getStartNode());
|
||||
}
|
||||
|
||||
await replaceInvalidCredentials(updateData);
|
||||
|
||||
const workflowRunner = ActiveWorkflowRunner.getInstance();
|
||||
|
||||
if (sharedWorkflow.workflow.active) {
|
||||
// When workflow gets saved always remove it as the triggers could have been
|
||||
// changed and so the changes would not take effect
|
||||
await workflowRunner.remove(id.toString());
|
||||
}
|
||||
|
||||
await updateWorkflow(sharedWorkflow.workflowId, updateData);
|
||||
|
||||
if (sharedWorkflow.workflow.active) {
|
||||
try {
|
||||
await workflowRunner.add(sharedWorkflow.workflowId.toString(), 'update');
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.status(400).json({ message: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updatedWorkflow = await getWorkflowById(sharedWorkflow.workflowId);
|
||||
|
||||
await externalHooks.run('workflow.afterUpdate', [updateData]);
|
||||
void InternalHooksManager.getInstance().onWorkflowSaved(req.user.id, updateData, true);
|
||||
|
||||
return res.json(updatedWorkflow);
|
||||
},
|
||||
],
|
||||
activateWorkflow: [
|
||||
authorize(['owner', 'member']),
|
||||
async (req: WorkflowRequest.Activate, res: express.Response): Promise<express.Response> => {
|
||||
const { id } = req.params;
|
||||
|
||||
const sharedWorkflow = await getSharedWorkflow(req.user, id.toString());
|
||||
|
||||
if (!sharedWorkflow) {
|
||||
// user trying to access a workflow he does not own
|
||||
// or workflow does not exist
|
||||
return res.status(404).json({ message: 'Not Found' });
|
||||
}
|
||||
|
||||
if (!sharedWorkflow.workflow.active) {
|
||||
try {
|
||||
await ActiveWorkflowRunner.getInstance().add(
|
||||
sharedWorkflow.workflowId.toString(),
|
||||
'activate',
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.status(400).json({ message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// change the status to active in the DB
|
||||
await setWorkflowAsActive(sharedWorkflow.workflow);
|
||||
|
||||
sharedWorkflow.workflow.active = true;
|
||||
|
||||
return res.json(sharedWorkflow.workflow);
|
||||
}
|
||||
|
||||
// nothing to do as the wokflow is already active
|
||||
return res.json(sharedWorkflow.workflow);
|
||||
},
|
||||
],
|
||||
deactivateWorkflow: [
|
||||
authorize(['owner', 'member']),
|
||||
async (req: WorkflowRequest.Activate, res: express.Response): Promise<express.Response> => {
|
||||
const { id } = req.params;
|
||||
|
||||
const sharedWorkflow = await getSharedWorkflow(req.user, id.toString());
|
||||
|
||||
if (!sharedWorkflow) {
|
||||
// user trying to access a workflow he does not own
|
||||
// or workflow does not exist
|
||||
return res.status(404).json({ message: 'Not Found' });
|
||||
}
|
||||
|
||||
const workflowRunner = ActiveWorkflowRunner.getInstance();
|
||||
|
||||
if (sharedWorkflow.workflow.active) {
|
||||
await workflowRunner.remove(sharedWorkflow.workflowId.toString());
|
||||
|
||||
await setWorkflowAsInactive(sharedWorkflow.workflow);
|
||||
|
||||
sharedWorkflow.workflow.active = false;
|
||||
|
||||
return res.json(sharedWorkflow.workflow);
|
||||
}
|
||||
|
||||
// nothing to do as the wokflow is already inactive
|
||||
return res.json(sharedWorkflow.workflow);
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
import { FindManyOptions, In, UpdateResult } from 'typeorm';
|
||||
import intersection from 'lodash.intersection';
|
||||
import type { INode } from 'n8n-workflow';
|
||||
|
||||
import { Db } from '../../../..';
|
||||
import { User } from '../../../../databases/entities/User';
|
||||
import { WorkflowEntity } from '../../../../databases/entities/WorkflowEntity';
|
||||
import { SharedWorkflow } from '../../../../databases/entities/SharedWorkflow';
|
||||
import { isInstanceOwner } from '../users/users.service';
|
||||
import { Role } from '../../../../databases/entities/Role';
|
||||
import config from '../../../../../config';
|
||||
|
||||
function insertIf(condition: boolean, elements: string[]): string[] {
|
||||
return condition ? elements : [];
|
||||
}
|
||||
|
||||
export async function getSharedWorkflowIds(user: User): Promise<number[]> {
|
||||
const sharedWorkflows = await Db.collections.SharedWorkflow.find({
|
||||
where: { user },
|
||||
});
|
||||
|
||||
return sharedWorkflows.map((workflow) => workflow.workflowId);
|
||||
}
|
||||
|
||||
export async function getSharedWorkflow(
|
||||
user: User,
|
||||
workflowId?: string | undefined,
|
||||
): Promise<SharedWorkflow | undefined> {
|
||||
return Db.collections.SharedWorkflow.findOne({
|
||||
where: {
|
||||
...(!isInstanceOwner(user) && { user }),
|
||||
...(workflowId && { workflow: { id: workflowId } }),
|
||||
},
|
||||
relations: [...insertIf(!config.getEnv('workflowTagsDisabled'), ['workflow.tags']), 'workflow'],
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSharedWorkflows(
|
||||
user: User,
|
||||
options: {
|
||||
relations?: string[];
|
||||
workflowIds?: number[];
|
||||
},
|
||||
): Promise<SharedWorkflow[]> {
|
||||
return Db.collections.SharedWorkflow.find({
|
||||
where: {
|
||||
...(!isInstanceOwner(user) && { user }),
|
||||
...(options.workflowIds && { workflow: { id: In(options.workflowIds) } }),
|
||||
},
|
||||
...(options.relations && { relations: options.relations }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getWorkflowById(id: number): Promise<WorkflowEntity | undefined> {
|
||||
return Db.collections.Workflow.findOne({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the workflow IDs that have certain tags.
|
||||
* Intersection! e.g. workflow needs to have all provided tags.
|
||||
*/
|
||||
export async function getWorkflowIdsViaTags(tags: string[]): Promise<number[]> {
|
||||
const dbTags = await Db.collections.Tag.find({
|
||||
where: { name: In(tags) },
|
||||
relations: ['workflows'],
|
||||
});
|
||||
|
||||
const workflowIdsPerTag = dbTags.map((tag) => tag.workflows.map((workflow) => workflow.id));
|
||||
|
||||
return intersection(...workflowIdsPerTag);
|
||||
}
|
||||
|
||||
export async function createWorkflow(
|
||||
workflow: WorkflowEntity,
|
||||
user: User,
|
||||
role: Role,
|
||||
): Promise<WorkflowEntity> {
|
||||
return Db.transaction(async (transactionManager) => {
|
||||
const newWorkflow = new WorkflowEntity();
|
||||
Object.assign(newWorkflow, workflow);
|
||||
const savedWorkflow = await transactionManager.save<WorkflowEntity>(newWorkflow);
|
||||
|
||||
const newSharedWorkflow = new SharedWorkflow();
|
||||
Object.assign(newSharedWorkflow, {
|
||||
role,
|
||||
user,
|
||||
workflow: savedWorkflow,
|
||||
});
|
||||
await transactionManager.save<SharedWorkflow>(newSharedWorkflow);
|
||||
|
||||
return savedWorkflow;
|
||||
});
|
||||
}
|
||||
|
||||
export async function setWorkflowAsActive(workflow: WorkflowEntity): Promise<UpdateResult> {
|
||||
return Db.collections.Workflow.update(workflow.id, { active: true, updatedAt: new Date() });
|
||||
}
|
||||
|
||||
export async function setWorkflowAsInactive(workflow: WorkflowEntity): Promise<UpdateResult> {
|
||||
return Db.collections.Workflow.update(workflow.id, { active: false, updatedAt: new Date() });
|
||||
}
|
||||
|
||||
export async function deleteWorkflow(workflow: WorkflowEntity): Promise<WorkflowEntity> {
|
||||
return Db.collections.Workflow.remove(workflow);
|
||||
}
|
||||
|
||||
export async function getWorkflows(
|
||||
options: FindManyOptions<WorkflowEntity>,
|
||||
): Promise<WorkflowEntity[]> {
|
||||
return Db.collections.Workflow.find(options);
|
||||
}
|
||||
|
||||
export async function getWorkflowsCount(options: FindManyOptions<WorkflowEntity>): Promise<number> {
|
||||
return Db.collections.Workflow.count(options);
|
||||
}
|
||||
|
||||
export async function updateWorkflow(
|
||||
workflowId: number,
|
||||
updateData: WorkflowEntity,
|
||||
): Promise<UpdateResult> {
|
||||
return Db.collections.Workflow.update(workflowId, updateData);
|
||||
}
|
||||
|
||||
export function hasStartNode(workflow: WorkflowEntity): boolean {
|
||||
if (!workflow.nodes.length) return false;
|
||||
|
||||
const found = workflow.nodes.find((node) => node.type === 'n8n-nodes-base.start');
|
||||
|
||||
return Boolean(found);
|
||||
}
|
||||
|
||||
export function getStartNode(): INode {
|
||||
return {
|
||||
parameters: {},
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.start',
|
||||
typeVersion: 1,
|
||||
position: [240, 300],
|
||||
};
|
||||
}
|
||||
|
||||
export function parseTagNames(tags: string): string[] {
|
||||
return tags.split(',').map((tag) => tag.trim());
|
||||
}
|
||||
59
packages/cli/src/PublicApi/v1/openapi.yml
Normal file
59
packages/cli/src/PublicApi/v1/openapi.yml
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: n8n Public API
|
||||
description: n8n Public API
|
||||
termsOfService: https://n8n.io/legal/terms
|
||||
contact:
|
||||
email: hello@n8n.io
|
||||
license:
|
||||
name: Sustainable Use License
|
||||
url: https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md
|
||||
version: 1.0.0
|
||||
externalDocs:
|
||||
description: n8n API documentation
|
||||
url: https://docs.n8n.io/api/
|
||||
servers:
|
||||
- url: /api/v1
|
||||
tags:
|
||||
- name: Execution
|
||||
description: Operations about executions
|
||||
- name: Workflow
|
||||
description: Operations about workflows
|
||||
- name: Credential
|
||||
description: Operations about credentials
|
||||
|
||||
paths:
|
||||
/credentials:
|
||||
$ref: './handlers/credentials/spec/paths/credentials.yml'
|
||||
/credentials/{id}:
|
||||
$ref: './handlers/credentials/spec/paths/credentials.id.yml'
|
||||
/credentials/schema/{credentialTypeName}:
|
||||
$ref: './handlers/credentials/spec/paths/credentials.schema.id.yml'
|
||||
/executions:
|
||||
$ref: './handlers/executions/spec/paths/executions.yml'
|
||||
/executions/{id}:
|
||||
$ref: './handlers/executions/spec/paths/executions.id.yml'
|
||||
/workflows:
|
||||
$ref: './handlers/workflows/spec/paths/workflows.yml'
|
||||
/workflows/{id}:
|
||||
$ref: './handlers/workflows/spec/paths/workflows.id.yml'
|
||||
/workflows/{id}/activate:
|
||||
$ref: './handlers/workflows/spec/paths/workflows.id.activate.yml'
|
||||
/workflows/{id}/deactivate:
|
||||
$ref: './handlers/workflows/spec/paths/workflows.id.deactivate.yml'
|
||||
components:
|
||||
schemas:
|
||||
$ref: './shared/spec/schemas/_index.yml'
|
||||
responses:
|
||||
$ref: './shared/spec/responses/_index.yml'
|
||||
parameters:
|
||||
$ref: './shared/spec/parameters/_index.yml'
|
||||
securitySchemes:
|
||||
ApiKeyAuth:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: X-N8N-API-KEY
|
||||
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/* eslint-disable @typescript-eslint/no-invalid-void-type */
|
||||
|
||||
import express from 'express';
|
||||
|
||||
import { AuthenticatedRequest, PaginatatedRequest } from '../../../types';
|
||||
import { decodeCursor } from '../services/pagination.service';
|
||||
|
||||
export const authorize =
|
||||
(authorizedRoles: readonly string[]) =>
|
||||
(
|
||||
req: AuthenticatedRequest,
|
||||
res: express.Response,
|
||||
next: express.NextFunction,
|
||||
): express.Response | void => {
|
||||
const { name } = req.user.globalRole;
|
||||
|
||||
if (!authorizedRoles.includes(name)) {
|
||||
return res.status(403).json({ message: 'Forbidden' });
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
export const validCursor = (
|
||||
req: PaginatatedRequest,
|
||||
res: express.Response,
|
||||
next: express.NextFunction,
|
||||
): express.Response | void => {
|
||||
if (req.query.cursor) {
|
||||
const { cursor } = req.query;
|
||||
try {
|
||||
const paginationData = decodeCursor(cursor);
|
||||
if ('offset' in paginationData) {
|
||||
req.query.offset = paginationData.offset;
|
||||
req.query.limit = paginationData.limit;
|
||||
} else {
|
||||
req.query.lastId = paginationData.lastId;
|
||||
req.query.limit = paginationData.limit;
|
||||
}
|
||||
} catch (error) {
|
||||
return res.status(400).json({
|
||||
message: 'An invalid cursor was provided',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import {
|
||||
CursorPagination,
|
||||
OffsetPagination,
|
||||
PaginationCursorDecoded,
|
||||
PaginationOffsetDecoded,
|
||||
} from '../../../types';
|
||||
|
||||
export const decodeCursor = (cursor: string): PaginationOffsetDecoded | PaginationCursorDecoded => {
|
||||
return JSON.parse(Buffer.from(cursor, 'base64').toString()) as
|
||||
| PaginationCursorDecoded
|
||||
| PaginationOffsetDecoded;
|
||||
};
|
||||
|
||||
const encodeOffSetPagination = (pagination: OffsetPagination): string | null => {
|
||||
if (pagination.numberOfTotalRecords > pagination.offset + pagination.limit) {
|
||||
return Buffer.from(
|
||||
JSON.stringify({
|
||||
limit: pagination.limit,
|
||||
offset: pagination.offset + pagination.limit,
|
||||
}),
|
||||
).toString('base64');
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const encodeCursorPagination = (pagination: CursorPagination): string | null => {
|
||||
if (pagination.numberOfNextRecords) {
|
||||
return Buffer.from(
|
||||
JSON.stringify({
|
||||
lastId: pagination.lastId,
|
||||
limit: pagination.limit,
|
||||
}),
|
||||
).toString('base64');
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const encodeNextCursor = (
|
||||
pagination: OffsetPagination | CursorPagination,
|
||||
): string | null => {
|
||||
if ('offset' in pagination) {
|
||||
return encodeOffSetPagination(pagination);
|
||||
}
|
||||
return encodeCursorPagination(pagination);
|
||||
};
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
Cursor:
|
||||
$ref: './cursor.yml'
|
||||
Limit:
|
||||
$ref: './limit.yml'
|
||||
ExecutionId:
|
||||
$ref: '../../../handlers/executions/spec/schemas/parameters/executionId.yml'
|
||||
WorkflowId:
|
||||
$ref: '../../../handlers/workflows/spec/schemas/parameters/workflowId.yml'
|
||||
IncludeData:
|
||||
$ref: '../../../handlers/executions/spec/schemas/parameters/includeData.yml'
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
name: cursor
|
||||
in: query
|
||||
description: Paginate through users by setting the cursor parameter to a nextCursor attribute returned by a previous request's response. Default value fetches the first "page" of the collection. See pagination for more detail.
|
||||
required: false
|
||||
style: form
|
||||
schema:
|
||||
type: string
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
name: limit
|
||||
in: query
|
||||
description: The maximum number of items to return.
|
||||
required: false
|
||||
schema:
|
||||
type: number
|
||||
example: 100
|
||||
default: 100
|
||||
maximum: 250
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
NotFound:
|
||||
$ref: './notFound.yml'
|
||||
Unauthorized:
|
||||
$ref: './unauthorized.yml'
|
||||
BadRequest:
|
||||
$ref: './badRequest.yml'
|
||||
|
|
@ -0,0 +1 @@
|
|||
description: The request is invalid or provides malformed data.
|
||||
|
|
@ -0,0 +1 @@
|
|||
description: The specified resource was not found.
|
||||
|
|
@ -0,0 +1 @@
|
|||
description: Unauthorized
|
||||
20
packages/cli/src/PublicApi/v1/shared/spec/schemas/_index.yml
Normal file
20
packages/cli/src/PublicApi/v1/shared/spec/schemas/_index.yml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
Error:
|
||||
$ref: './error.yml'
|
||||
Execution:
|
||||
$ref: './../../../handlers/executions/spec/schemas/execution.yml'
|
||||
Node:
|
||||
$ref: './../../../handlers/workflows/spec/schemas/node.yml'
|
||||
Tag:
|
||||
$ref: './../../../handlers/workflows/spec/schemas/tag.yml'
|
||||
Workflow:
|
||||
$ref: './../../../handlers/workflows/spec/schemas/workflow.yml'
|
||||
WorkflowSettings:
|
||||
$ref: './../../../handlers/workflows/spec/schemas/workflowSettings.yml'
|
||||
ExecutionList:
|
||||
$ref: './../../../handlers/executions/spec/schemas/executionList.yml'
|
||||
WorkflowList:
|
||||
$ref: './../../../handlers/workflows/spec/schemas/workflowList.yml'
|
||||
Credential:
|
||||
$ref: './../../../handlers/credentials/spec/schemas/credential.yml'
|
||||
CredentialType:
|
||||
$ref: './../../../handlers/credentials/spec/schemas/credentialType.yml'
|
||||
10
packages/cli/src/PublicApi/v1/shared/spec/schemas/error.yml
Normal file
10
packages/cli/src/PublicApi/v1/shared/spec/schemas/error.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
required:
|
||||
- message
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
|
|
@ -130,7 +130,6 @@ export function sendErrorResponse(res: Response, error: ResponseError) {
|
|||
// @ts-ignore
|
||||
response.stack = error.stack;
|
||||
}
|
||||
|
||||
res.status(httpStatusCode).json(response);
|
||||
}
|
||||
|
||||
|
|
@ -147,12 +146,13 @@ const isUniqueConstraintError = (error: Error) =>
|
|||
* @returns
|
||||
*/
|
||||
|
||||
export function send(processFunction: (req: Request, res: Response) => Promise<any>) {
|
||||
export function send(processFunction: (req: Request, res: Response) => Promise<any>, raw = false) {
|
||||
// eslint-disable-next-line consistent-return
|
||||
return async (req: Request, res: Response) => {
|
||||
try {
|
||||
const data = await processFunction(req, res);
|
||||
|
||||
sendSuccessResponse(res, data);
|
||||
sendSuccessResponse(res, data, raw);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && isUniqueConstraintError(error)) {
|
||||
error.message = 'There is already an entry with this name';
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ import clientOAuth2 from 'client-oauth2';
|
|||
import clientOAuth1, { RequestOptions } from 'oauth-1.0a';
|
||||
import csrf from 'csrf';
|
||||
import requestPromise, { OptionsWithUrl } from 'request-promise-native';
|
||||
import { createHmac } from 'crypto';
|
||||
import { createHmac, randomBytes } from 'crypto';
|
||||
// IMPORTANT! Do not switch to anther bcrypt library unless really necessary and
|
||||
// tested with all possible systems like Windows, Alpine on ARM, FreeBSD, ...
|
||||
import { compare } from 'bcryptjs';
|
||||
|
|
@ -102,6 +102,7 @@ import {
|
|||
ICredentialsDb,
|
||||
ICredentialsOverwrite,
|
||||
ICustomRequest,
|
||||
IDiagnosticInfo,
|
||||
IExecutionFlattedDb,
|
||||
IExecutionFlattedResponse,
|
||||
IExecutionPushResponse,
|
||||
|
|
@ -110,7 +111,6 @@ import {
|
|||
IExecutionsStopData,
|
||||
IExecutionsSummary,
|
||||
IExternalHooksClass,
|
||||
IDiagnosticInfo,
|
||||
IN8nUISettings,
|
||||
IPackageVersions,
|
||||
ITagWithCountDb,
|
||||
|
|
@ -146,22 +146,26 @@ import { userManagementRouter } from './UserManagement';
|
|||
import { resolveJwt } from './UserManagement/auth/jwt';
|
||||
import { User } from './databases/entities/User';
|
||||
import type {
|
||||
AuthenticatedRequest,
|
||||
CredentialRequest,
|
||||
ExecutionRequest,
|
||||
WorkflowRequest,
|
||||
NodeParameterOptionsRequest,
|
||||
OAuthRequest,
|
||||
TagsRequest,
|
||||
WorkflowRequest,
|
||||
} from './requests';
|
||||
import { DEFAULT_EXECUTIONS_GET_ALL_LIMIT, validateEntity } from './GenericHelpers';
|
||||
import { ExecutionEntity } from './databases/entities/ExecutionEntity';
|
||||
import { SharedWorkflow } from './databases/entities/SharedWorkflow';
|
||||
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from './constants';
|
||||
import { credentialsController } from './api/credentials.api';
|
||||
import { oauth2CredentialController } from './api/oauth2Credential.api';
|
||||
import {
|
||||
getInstanceBaseUrl,
|
||||
isEmailSetUp,
|
||||
isUserManagementEnabled,
|
||||
} from './UserManagement/UserManagementHelper';
|
||||
import { loadPublicApiVersions } from './PublicApi';
|
||||
|
||||
require('body-parser-xml')(bodyParser);
|
||||
|
||||
|
|
@ -210,6 +214,8 @@ class App {
|
|||
|
||||
restEndpoint: string;
|
||||
|
||||
publicApiEndpoint: string;
|
||||
|
||||
frontendSettings: IN8nUISettings;
|
||||
|
||||
protocol: string;
|
||||
|
|
@ -234,14 +240,15 @@ class App {
|
|||
this.defaultWorkflowName = config.getEnv('workflows.defaultName');
|
||||
this.defaultCredentialsName = config.getEnv('credentials.defaultName');
|
||||
|
||||
this.saveDataErrorExecution = config.getEnv('executions.saveDataOnError');
|
||||
this.saveDataSuccessExecution = config.getEnv('executions.saveDataOnSuccess');
|
||||
this.saveManualExecutions = config.getEnv('executions.saveDataManualExecutions');
|
||||
this.executionTimeout = config.getEnv('executions.timeout');
|
||||
this.maxExecutionTimeout = config.getEnv('executions.maxTimeout');
|
||||
this.payloadSizeMax = config.getEnv('endpoints.payloadSizeMax');
|
||||
this.timezone = config.getEnv('generic.timezone');
|
||||
this.restEndpoint = config.getEnv('endpoints.rest');
|
||||
this.saveDataErrorExecution = config.get('executions.saveDataOnError');
|
||||
this.saveDataSuccessExecution = config.get('executions.saveDataOnSuccess');
|
||||
this.saveManualExecutions = config.get('executions.saveDataManualExecutions');
|
||||
this.executionTimeout = config.get('executions.timeout');
|
||||
this.maxExecutionTimeout = config.get('executions.maxTimeout');
|
||||
this.payloadSizeMax = config.get('endpoints.payloadSizeMax');
|
||||
this.timezone = config.get('generic.timezone');
|
||||
this.restEndpoint = config.get('endpoints.rest');
|
||||
this.publicApiEndpoint = config.get('publicApi.path');
|
||||
|
||||
this.activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
|
||||
this.testWebhooks = TestWebhooks.getInstance();
|
||||
|
|
@ -310,6 +317,11 @@ class App {
|
|||
config.getEnv('userManagement.skipInstanceOwnerSetup') === false,
|
||||
smtpSetup: isEmailSetUp(),
|
||||
},
|
||||
publicApi: {
|
||||
enabled: config.getEnv('publicApi.disabled') === false,
|
||||
latestVersion: 1,
|
||||
path: config.getEnv('publicApi.path'),
|
||||
},
|
||||
workflowTagsDisabled: config.getEnv('workflowTagsDisabled'),
|
||||
logLevel: config.getEnv('logs.level'),
|
||||
hiringBannerEnabled: config.getEnv('hiringBanner.enabled'),
|
||||
|
|
@ -373,6 +385,9 @@ class App {
|
|||
this.endpointWebhookTest,
|
||||
this.endpointPresetCredentials,
|
||||
];
|
||||
if (!config.getEnv('publicApi.disabled')) {
|
||||
ignoredEndpoints.push(this.publicApiEndpoint);
|
||||
}
|
||||
// eslint-disable-next-line prefer-spread
|
||||
ignoredEndpoints.push.apply(ignoredEndpoints, excludeEndpoints.split(':'));
|
||||
|
||||
|
|
@ -484,8 +499,9 @@ class App {
|
|||
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
function isTenantAllowed(decodedToken: object): boolean {
|
||||
if (jwtNamespace === '' || jwtAllowedTenantKey === '' || jwtAllowedTenant === '')
|
||||
if (jwtNamespace === '' || jwtAllowedTenantKey === '' || jwtAllowedTenant === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(decodedToken)) {
|
||||
if (k === jwtNamespace) {
|
||||
|
|
@ -543,6 +559,15 @@ class App {
|
|||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// Public API
|
||||
// ----------------------------------------
|
||||
|
||||
if (!config.getEnv('publicApi.disabled')) {
|
||||
const { apiRouters, apiLatestVersion } = await loadPublicApiVersions(this.publicApiEndpoint);
|
||||
this.app.use(...apiRouters);
|
||||
this.frontendSettings.publicApi.latestVersion = apiLatestVersion;
|
||||
}
|
||||
// Parse cookies for easier access
|
||||
this.app.use(cookieParser());
|
||||
|
||||
|
|
@ -786,7 +811,7 @@ class App {
|
|||
}
|
||||
|
||||
await this.externalHooks.run('workflow.afterCreate', [savedWorkflow]);
|
||||
void InternalHooksManager.getInstance().onWorkflowCreated(req.user.id, newWorkflow);
|
||||
void InternalHooksManager.getInstance().onWorkflowCreated(req.user.id, newWorkflow, false);
|
||||
|
||||
const { id, ...rest } = savedWorkflow;
|
||||
|
||||
|
|
@ -1076,7 +1101,11 @@ class App {
|
|||
}
|
||||
|
||||
await this.externalHooks.run('workflow.afterUpdate', [updatedWorkflow]);
|
||||
void InternalHooksManager.getInstance().onWorkflowSaved(req.user.id, updatedWorkflow);
|
||||
void InternalHooksManager.getInstance().onWorkflowSaved(
|
||||
req.user.id,
|
||||
updatedWorkflow,
|
||||
false,
|
||||
);
|
||||
|
||||
if (updatedWorkflow.active) {
|
||||
// When the workflow is supposed to be active add it again
|
||||
|
|
@ -1144,7 +1173,7 @@ class App {
|
|||
|
||||
await Db.collections.Workflow.delete(workflowId);
|
||||
|
||||
void InternalHooksManager.getInstance().onWorkflowDeleted(req.user.id, workflowId);
|
||||
void InternalHooksManager.getInstance().onWorkflowDeleted(req.user.id, workflowId, false);
|
||||
await this.externalHooks.run('workflow.afterDelete', [workflowId]);
|
||||
|
||||
return true;
|
||||
|
|
@ -1241,7 +1270,7 @@ class App {
|
|||
return TagHelpers.getTagsWithCountDb(tablePrefix);
|
||||
}
|
||||
|
||||
return Db.collections.Tag.find({ select: ['id', 'name'] });
|
||||
return Db.collections.Tag.find({ select: ['id', 'name', 'createdAt', 'updatedAt'] });
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
@ -1925,302 +1954,10 @@ class App {
|
|||
);
|
||||
|
||||
// ----------------------------------------
|
||||
// OAuth2-Credential/Auth
|
||||
// OAuth2-Credential
|
||||
// ----------------------------------------
|
||||
|
||||
// Authorize OAuth Data
|
||||
this.app.get(
|
||||
`/${this.restEndpoint}/oauth2-credential/auth`,
|
||||
ResponseHelper.send(async (req: OAuthRequest.OAuth2Credential.Auth): Promise<string> => {
|
||||
const { id: credentialId } = req.query;
|
||||
|
||||
if (!credentialId) {
|
||||
throw new ResponseHelper.ResponseError(
|
||||
'Required credential ID is missing',
|
||||
undefined,
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
const credential = await getCredentialForUser(credentialId, req.user);
|
||||
|
||||
if (!credential) {
|
||||
LoggerProxy.error('Failed to authorize OAuth2 due to lack of permissions', {
|
||||
userId: req.user.id,
|
||||
credentialId,
|
||||
});
|
||||
throw new ResponseHelper.ResponseError(
|
||||
RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL,
|
||||
undefined,
|
||||
404,
|
||||
);
|
||||
}
|
||||
|
||||
let encryptionKey: string;
|
||||
try {
|
||||
encryptionKey = await UserSettings.getEncryptionKey();
|
||||
} catch (error) {
|
||||
throw new ResponseHelper.ResponseError(error.message, undefined, 500);
|
||||
}
|
||||
|
||||
const mode: WorkflowExecuteMode = 'internal';
|
||||
const timezone = config.getEnv('generic.timezone');
|
||||
const credentialsHelper = new CredentialsHelper(encryptionKey);
|
||||
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
||||
credential as INodeCredentialsDetails,
|
||||
credential.type,
|
||||
mode,
|
||||
timezone,
|
||||
true,
|
||||
);
|
||||
|
||||
const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(
|
||||
decryptedDataOriginal,
|
||||
credential.type,
|
||||
mode,
|
||||
timezone,
|
||||
);
|
||||
|
||||
const token = new csrf();
|
||||
// Generate a CSRF prevention token and send it as a OAuth2 state stringma/ERR
|
||||
const csrfSecret = token.secretSync();
|
||||
const state = {
|
||||
token: token.create(csrfSecret),
|
||||
cid: req.query.id,
|
||||
};
|
||||
const stateEncodedStr = Buffer.from(JSON.stringify(state)).toString('base64');
|
||||
|
||||
const oAuthOptions: clientOAuth2.Options = {
|
||||
clientId: _.get(oauthCredentials, 'clientId') as string,
|
||||
clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string,
|
||||
accessTokenUri: _.get(oauthCredentials, 'accessTokenUrl', '') as string,
|
||||
authorizationUri: _.get(oauthCredentials, 'authUrl', '') as string,
|
||||
redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}${
|
||||
this.restEndpoint
|
||||
}/oauth2-credential/callback`,
|
||||
scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ','),
|
||||
state: stateEncodedStr,
|
||||
};
|
||||
|
||||
await this.externalHooks.run('oauth2.authenticate', [oAuthOptions]);
|
||||
|
||||
const oAuthObj = new clientOAuth2(oAuthOptions);
|
||||
|
||||
// Encrypt the data
|
||||
const credentials = new Credentials(
|
||||
credential as INodeCredentialsDetails,
|
||||
credential.type,
|
||||
credential.nodesAccess,
|
||||
);
|
||||
decryptedDataOriginal.csrfSecret = csrfSecret;
|
||||
|
||||
credentials.setData(decryptedDataOriginal, encryptionKey);
|
||||
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
||||
|
||||
// Add special database related data
|
||||
newCredentialsData.updatedAt = this.getCurrentDate();
|
||||
|
||||
// Update the credentials in DB
|
||||
await Db.collections.Credentials.update(req.query.id as string, newCredentialsData);
|
||||
|
||||
const authQueryParameters = _.get(oauthCredentials, 'authQueryParameters', '') as string;
|
||||
let returnUri = oAuthObj.code.getUri();
|
||||
|
||||
// if scope uses comma, change it as the library always return then with spaces
|
||||
if ((_.get(oauthCredentials, 'scope') as string).includes(',')) {
|
||||
const data = querystring.parse(returnUri.split('?')[1]);
|
||||
data.scope = _.get(oauthCredentials, 'scope') as string;
|
||||
returnUri = `${_.get(oauthCredentials, 'authUrl', '')}?${querystring.stringify(data)}`;
|
||||
}
|
||||
|
||||
if (authQueryParameters) {
|
||||
returnUri += `&${authQueryParameters}`;
|
||||
}
|
||||
|
||||
LoggerProxy.verbose('OAuth2 authentication successful for new credential', {
|
||||
userId: req.user.id,
|
||||
credentialId,
|
||||
});
|
||||
return returnUri;
|
||||
}),
|
||||
);
|
||||
|
||||
// ----------------------------------------
|
||||
// OAuth2-Credential/Callback
|
||||
// ----------------------------------------
|
||||
|
||||
// Verify and store app code. Generate access tokens and store for respective credential.
|
||||
this.app.get(
|
||||
`/${this.restEndpoint}/oauth2-credential/callback`,
|
||||
async (req: OAuthRequest.OAuth2Credential.Callback, res: express.Response) => {
|
||||
try {
|
||||
// realmId it's currently just use for the quickbook OAuth2 flow
|
||||
const { code, state: stateEncoded } = req.query;
|
||||
|
||||
if (!code || !stateEncoded) {
|
||||
const errorResponse = new ResponseHelper.ResponseError(
|
||||
`Insufficient parameters for OAuth2 callback. Received following query parameters: ${JSON.stringify(
|
||||
req.query,
|
||||
)}`,
|
||||
undefined,
|
||||
503,
|
||||
);
|
||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||
}
|
||||
|
||||
let state;
|
||||
try {
|
||||
state = JSON.parse(Buffer.from(stateEncoded, 'base64').toString());
|
||||
} catch (error) {
|
||||
const errorResponse = new ResponseHelper.ResponseError(
|
||||
'Invalid state format returned',
|
||||
undefined,
|
||||
503,
|
||||
);
|
||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||
}
|
||||
|
||||
const credential = await getCredentialWithoutUser(state.cid);
|
||||
|
||||
if (!credential) {
|
||||
LoggerProxy.error('OAuth2 callback failed because of insufficient permissions', {
|
||||
userId: req.user?.id,
|
||||
credentialId: state.cid,
|
||||
});
|
||||
const errorResponse = new ResponseHelper.ResponseError(
|
||||
RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL,
|
||||
undefined,
|
||||
404,
|
||||
);
|
||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||
}
|
||||
|
||||
let encryptionKey: string;
|
||||
try {
|
||||
encryptionKey = await UserSettings.getEncryptionKey();
|
||||
} catch (error) {
|
||||
throw new ResponseHelper.ResponseError(error.message, undefined, 500);
|
||||
}
|
||||
|
||||
const mode: WorkflowExecuteMode = 'internal';
|
||||
const timezone = config.getEnv('generic.timezone');
|
||||
const credentialsHelper = new CredentialsHelper(encryptionKey);
|
||||
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
||||
credential as INodeCredentialsDetails,
|
||||
credential.type,
|
||||
mode,
|
||||
timezone,
|
||||
true,
|
||||
);
|
||||
const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(
|
||||
decryptedDataOriginal,
|
||||
credential.type,
|
||||
mode,
|
||||
timezone,
|
||||
);
|
||||
|
||||
const token = new csrf();
|
||||
if (
|
||||
decryptedDataOriginal.csrfSecret === undefined ||
|
||||
!token.verify(decryptedDataOriginal.csrfSecret as string, state.token)
|
||||
) {
|
||||
LoggerProxy.debug('OAuth2 callback state is invalid', {
|
||||
userId: req.user?.id,
|
||||
credentialId: state.cid,
|
||||
});
|
||||
const errorResponse = new ResponseHelper.ResponseError(
|
||||
'The OAuth2 callback state is invalid!',
|
||||
undefined,
|
||||
404,
|
||||
);
|
||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||
}
|
||||
|
||||
let options = {};
|
||||
|
||||
const oAuth2Parameters = {
|
||||
clientId: _.get(oauthCredentials, 'clientId') as string,
|
||||
clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string | undefined,
|
||||
accessTokenUri: _.get(oauthCredentials, 'accessTokenUrl', '') as string,
|
||||
authorizationUri: _.get(oauthCredentials, 'authUrl', '') as string,
|
||||
redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}${
|
||||
this.restEndpoint
|
||||
}/oauth2-credential/callback`,
|
||||
scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ','),
|
||||
};
|
||||
|
||||
if ((_.get(oauthCredentials, 'authentication', 'header') as string) === 'body') {
|
||||
options = {
|
||||
body: {
|
||||
client_id: _.get(oauthCredentials, 'clientId') as string,
|
||||
client_secret: _.get(oauthCredentials, 'clientSecret', '') as string,
|
||||
},
|
||||
};
|
||||
delete oAuth2Parameters.clientSecret;
|
||||
}
|
||||
|
||||
await this.externalHooks.run('oauth2.callback', [oAuth2Parameters]);
|
||||
|
||||
const oAuthObj = new clientOAuth2(oAuth2Parameters);
|
||||
|
||||
const queryParameters = req.originalUrl.split('?').splice(1, 1).join('');
|
||||
|
||||
const oauthToken = await oAuthObj.code.getToken(
|
||||
`${oAuth2Parameters.redirectUri}?${queryParameters}`,
|
||||
options,
|
||||
);
|
||||
|
||||
if (Object.keys(req.query).length > 2) {
|
||||
_.set(oauthToken.data, 'callbackQueryString', _.omit(req.query, 'state', 'code'));
|
||||
}
|
||||
|
||||
if (oauthToken === undefined) {
|
||||
LoggerProxy.error('OAuth2 callback failed: unable to get access tokens', {
|
||||
userId: req.user?.id,
|
||||
credentialId: state.cid,
|
||||
});
|
||||
const errorResponse = new ResponseHelper.ResponseError(
|
||||
'Unable to get access tokens!',
|
||||
undefined,
|
||||
404,
|
||||
);
|
||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||
}
|
||||
|
||||
if (decryptedDataOriginal.oauthTokenData) {
|
||||
// Only overwrite supplied data as some providers do for example just return the
|
||||
// refresh_token on the very first request and not on subsequent ones.
|
||||
Object.assign(decryptedDataOriginal.oauthTokenData, oauthToken.data);
|
||||
} else {
|
||||
// No data exists so simply set
|
||||
decryptedDataOriginal.oauthTokenData = oauthToken.data;
|
||||
}
|
||||
|
||||
_.unset(decryptedDataOriginal, 'csrfSecret');
|
||||
|
||||
const credentials = new Credentials(
|
||||
credential as INodeCredentialsDetails,
|
||||
credential.type,
|
||||
credential.nodesAccess,
|
||||
);
|
||||
credentials.setData(decryptedDataOriginal, encryptionKey);
|
||||
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
||||
// Add special database related data
|
||||
newCredentialsData.updatedAt = this.getCurrentDate();
|
||||
// Save the credentials in DB
|
||||
await Db.collections.Credentials.update(state.cid, newCredentialsData);
|
||||
LoggerProxy.verbose('OAuth2 callback successful for new credential', {
|
||||
userId: req.user?.id,
|
||||
credentialId: state.cid,
|
||||
});
|
||||
|
||||
res.sendFile(pathResolve(__dirname, '../../templates/oauth-callback.html'));
|
||||
} catch (error) {
|
||||
// Error response
|
||||
return ResponseHelper.sendErrorResponse(res, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
this.app.use(`/${this.restEndpoint}/oauth2-credential`, oauth2CredentialController);
|
||||
|
||||
// ----------------------------------------
|
||||
// Executions
|
||||
|
|
@ -2828,6 +2565,10 @@ class App {
|
|||
`/${this.restEndpoint}/settings`,
|
||||
ResponseHelper.send(
|
||||
async (req: express.Request, res: express.Response): Promise<IN8nUISettings> => {
|
||||
void InternalHooksManager.getInstance().onFrontendSettingsAPI(
|
||||
req.headers.sessionid as string,
|
||||
);
|
||||
|
||||
return this.getSettingsForFrontend();
|
||||
},
|
||||
),
|
||||
|
|
@ -2865,6 +2606,8 @@ class App {
|
|||
return;
|
||||
}
|
||||
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, {}, true, 204);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { getConnection } from 'typeorm';
|
||||
import { EntityManager, getConnection } from 'typeorm';
|
||||
|
||||
import { TagEntity } from './databases/entities/TagEntity';
|
||||
|
||||
import { ITagWithCountDb } from './Interfaces';
|
||||
import { ITagToImport, ITagWithCountDb, IWorkflowToImport } from './Interfaces';
|
||||
|
||||
// ----------------------------------
|
||||
// utils
|
||||
|
|
@ -38,6 +38,8 @@ export async function getTagsWithCountDb(tablePrefix: string): Promise<ITagWithC
|
|||
.createQueryBuilder()
|
||||
.select(`${tablePrefix}tag_entity.id`, 'id')
|
||||
.addSelect(`${tablePrefix}tag_entity.name`, 'name')
|
||||
.addSelect(`${tablePrefix}tag_entity.createdAt`, 'createdAt')
|
||||
.addSelect(`${tablePrefix}tag_entity.updatedAt`, 'updatedAt')
|
||||
.addSelect(`COUNT(${tablePrefix}workflows_tags.workflowId)`, 'usageCount')
|
||||
.from(`${tablePrefix}tag_entity`, 'tag_entity')
|
||||
.leftJoin(
|
||||
|
|
@ -86,3 +88,70 @@ export async function removeRelations(workflowId: string, tablePrefix: string) {
|
|||
.where('workflowId = :id', { id: workflowId })
|
||||
.execute();
|
||||
}
|
||||
|
||||
const createTag = async (transactionManager: EntityManager, name: string): Promise<TagEntity> => {
|
||||
const tag = new TagEntity();
|
||||
tag.name = name;
|
||||
return transactionManager.save<TagEntity>(tag);
|
||||
};
|
||||
|
||||
const findOrCreateTag = async (
|
||||
transactionManager: EntityManager,
|
||||
importTag: ITagToImport,
|
||||
tagsEntities: TagEntity[],
|
||||
): Promise<TagEntity> => {
|
||||
// Assume tag is identical if createdAt date is the same to preserve a changed tag name
|
||||
const identicalMatch = tagsEntities.find(
|
||||
(existingTag) =>
|
||||
existingTag.id.toString() === importTag.id.toString() &&
|
||||
existingTag.createdAt &&
|
||||
importTag.createdAt &&
|
||||
existingTag.createdAt.getTime() === new Date(importTag.createdAt).getTime(),
|
||||
);
|
||||
if (identicalMatch) {
|
||||
return identicalMatch;
|
||||
}
|
||||
|
||||
const nameMatch = tagsEntities.find((existingTag) => existingTag.name === importTag.name);
|
||||
if (nameMatch) {
|
||||
return nameMatch;
|
||||
}
|
||||
|
||||
const created = await createTag(transactionManager, importTag.name);
|
||||
tagsEntities.push(created);
|
||||
return created;
|
||||
};
|
||||
|
||||
const hasTags = (workflow: IWorkflowToImport) =>
|
||||
'tags' in workflow && Array.isArray(workflow.tags) && workflow.tags.length > 0;
|
||||
|
||||
/**
|
||||
* Set tag IDs to use existing tags, creates a new tag if no matching tag could be found
|
||||
*/
|
||||
export async function setTagsForImport(
|
||||
transactionManager: EntityManager,
|
||||
workflow: IWorkflowToImport,
|
||||
tags: TagEntity[],
|
||||
): Promise<void> {
|
||||
if (!hasTags(workflow)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const workflowTags = workflow.tags;
|
||||
const tagLookupPromises = [];
|
||||
for (let i = 0; i < workflowTags.length; i++) {
|
||||
if (workflowTags[i]?.name) {
|
||||
const lookupPromise = findOrCreateTag(transactionManager, workflowTags[i], tags).then(
|
||||
(tag) => {
|
||||
workflowTags[i] = {
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
};
|
||||
},
|
||||
);
|
||||
tagLookupPromises.push(lookupPromise);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(tagLookupPromises);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ export function sanitizeUser(user: User, withoutKeys?: string[]): PublicUser {
|
|||
resetPasswordTokenExpiration,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
apiKey,
|
||||
...sanitizedUser
|
||||
} = user;
|
||||
if (withoutKeys) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Response } from 'express';
|
|||
import { createHash } from 'crypto';
|
||||
import { Db } from '../..';
|
||||
import { AUTH_COOKIE_NAME } from '../../constants';
|
||||
import { JwtToken, JwtPayload } from '../Interfaces';
|
||||
import { JwtPayload, JwtToken } from '../Interfaces';
|
||||
import { User } from '../../databases/entities/User';
|
||||
import * as config from '../../../config';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable import/no-cycle */
|
||||
|
||||
import express from 'express';
|
||||
import validator from 'validator';
|
||||
import { LoggerProxy as Logger } from 'n8n-workflow';
|
||||
|
||||
import { randomBytes } from 'crypto';
|
||||
import { Db, InternalHooksManager, ResponseHelper } from '../..';
|
||||
import { issueCookie } from '../auth/jwt';
|
||||
import { N8nApp, PublicUser } from '../Interfaces';
|
||||
|
|
@ -149,4 +151,58 @@ export function meNamespace(this: N8nApp): void {
|
|||
return { success: true };
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Creates an API Key
|
||||
*/
|
||||
this.app.post(
|
||||
`/${this.restEndpoint}/me/api-key`,
|
||||
ResponseHelper.send(async (req: AuthenticatedRequest) => {
|
||||
const apiKey = `n8n_api_${randomBytes(40).toString('hex')}`;
|
||||
|
||||
await Db.collections.User.update(req.user.id, {
|
||||
apiKey,
|
||||
});
|
||||
|
||||
const telemetryData = {
|
||||
user_id: req.user.id,
|
||||
public_api: false,
|
||||
};
|
||||
|
||||
void InternalHooksManager.getInstance().onApiKeyCreated(telemetryData);
|
||||
|
||||
return { apiKey };
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Deletes an API Key
|
||||
*/
|
||||
this.app.delete(
|
||||
`/${this.restEndpoint}/me/api-key`,
|
||||
ResponseHelper.send(async (req: AuthenticatedRequest) => {
|
||||
await Db.collections.User.update(req.user.id, {
|
||||
apiKey: null,
|
||||
});
|
||||
|
||||
const telemetryData = {
|
||||
user_id: req.user.id,
|
||||
public_api: false,
|
||||
};
|
||||
|
||||
void InternalHooksManager.getInstance().onApiKeyDeleted(telemetryData);
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Get an API Key
|
||||
*/
|
||||
this.app.get(
|
||||
`/${this.restEndpoint}/me/api-key`,
|
||||
ResponseHelper.send(async (req: AuthenticatedRequest) => {
|
||||
return { apiKey: req.user.apiKey };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ export function passwordResetNamespace(this: N8nApp): void {
|
|||
void InternalHooksManager.getInstance().onEmailFailed({
|
||||
user_id: user.id,
|
||||
message_type: 'Reset password',
|
||||
public_api: false,
|
||||
});
|
||||
if (error instanceof Error) {
|
||||
throw new ResponseHelper.ResponseError(
|
||||
|
|
@ -103,6 +104,7 @@ export function passwordResetNamespace(this: N8nApp): void {
|
|||
void InternalHooksManager.getInstance().onUserTransactionalEmail({
|
||||
user_id: id,
|
||||
message_type: 'Reset password',
|
||||
public_api: false,
|
||||
});
|
||||
|
||||
void InternalHooksManager.getInstance().onUserPasswordResetRequestClick({
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ export function usersNamespace(this: N8nApp): void {
|
|||
void InternalHooksManager.getInstance().onUserInvite({
|
||||
user_id: req.user.id,
|
||||
target_user_id: Object.values(createUsers) as string[],
|
||||
public_api: false,
|
||||
});
|
||||
} catch (error) {
|
||||
Logger.error('Failed to create user shells', { userShells: createUsers });
|
||||
|
|
@ -193,11 +194,13 @@ export function usersNamespace(this: N8nApp): void {
|
|||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
user_id: id!,
|
||||
message_type: 'New user invite',
|
||||
public_api: false,
|
||||
});
|
||||
} else {
|
||||
void InternalHooksManager.getInstance().onEmailFailed({
|
||||
user_id: req.user.id,
|
||||
message_type: 'New user invite',
|
||||
public_api: false,
|
||||
});
|
||||
Logger.error('Failed to send email', {
|
||||
userId: req.user.id,
|
||||
|
|
@ -378,6 +381,7 @@ export function usersNamespace(this: N8nApp): void {
|
|||
*/
|
||||
this.app.delete(
|
||||
`/${this.restEndpoint}/users/:id`,
|
||||
// @ts-ignore
|
||||
ResponseHelper.send(async (req: UserRequest.Delete) => {
|
||||
const { id: idToDelete } = req.params;
|
||||
|
||||
|
|
@ -472,7 +476,7 @@ export function usersNamespace(this: N8nApp): void {
|
|||
telemetryData.migration_user_id = transferId;
|
||||
}
|
||||
|
||||
void InternalHooksManager.getInstance().onUserDeletion(req.user.id, telemetryData);
|
||||
void InternalHooksManager.getInstance().onUserDeletion(req.user.id, telemetryData, false);
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
|
|
@ -538,6 +542,7 @@ export function usersNamespace(this: N8nApp): void {
|
|||
void InternalHooksManager.getInstance().onEmailFailed({
|
||||
user_id: req.user.id,
|
||||
message_type: 'Resend invite',
|
||||
public_api: false,
|
||||
});
|
||||
Logger.error('Failed to send email', {
|
||||
email: reinvitee.email,
|
||||
|
|
@ -554,11 +559,13 @@ export function usersNamespace(this: N8nApp): void {
|
|||
void InternalHooksManager.getInstance().onUserReinvite({
|
||||
user_id: req.user.id,
|
||||
target_user_id: reinvitee.id,
|
||||
public_api: false,
|
||||
});
|
||||
|
||||
void InternalHooksManager.getInstance().onUserTransactionalEmail({
|
||||
user_id: reinvitee.id,
|
||||
message_type: 'Resend invite',
|
||||
public_api: false,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
|
|
|
|||
|
|
@ -198,6 +198,7 @@ export async function executeWebhook(
|
|||
executionMode,
|
||||
additionalData.timezone,
|
||||
additionalKeys,
|
||||
undefined,
|
||||
'onReceived',
|
||||
);
|
||||
const responseCode = workflow.expression.getSimpleParameterValue(
|
||||
|
|
@ -206,6 +207,7 @@ export async function executeWebhook(
|
|||
executionMode,
|
||||
additionalData.timezone,
|
||||
additionalKeys,
|
||||
undefined,
|
||||
200,
|
||||
) as number;
|
||||
|
||||
|
|
@ -215,6 +217,7 @@ export async function executeWebhook(
|
|||
executionMode,
|
||||
additionalData.timezone,
|
||||
additionalKeys,
|
||||
undefined,
|
||||
'firstEntryJson',
|
||||
);
|
||||
|
||||
|
|
@ -288,6 +291,7 @@ export async function executeWebhook(
|
|||
additionalData.timezone,
|
||||
additionalKeys,
|
||||
undefined,
|
||||
undefined,
|
||||
) as {
|
||||
entries?:
|
||||
| Array<{
|
||||
|
|
@ -373,6 +377,7 @@ export async function executeWebhook(
|
|||
data: {
|
||||
main: webhookResultData.workflowData,
|
||||
},
|
||||
source: null,
|
||||
});
|
||||
|
||||
runExecutionData =
|
||||
|
|
@ -546,6 +551,7 @@ export async function executeWebhook(
|
|||
additionalData.timezone,
|
||||
additionalKeys,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
if (responsePropertyName !== undefined) {
|
||||
|
|
@ -559,6 +565,7 @@ export async function executeWebhook(
|
|||
additionalData.timezone,
|
||||
additionalKeys,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
if (responseContentType !== undefined) {
|
||||
|
|
@ -603,6 +610,7 @@ export async function executeWebhook(
|
|||
executionMode,
|
||||
additionalData.timezone,
|
||||
additionalKeys,
|
||||
undefined,
|
||||
'data',
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ import express from 'express';
|
|||
import { readFileSync } from 'fs';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import bodyParser from 'body-parser';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies, @typescript-eslint/no-unused-vars
|
||||
import _ from 'lodash';
|
||||
|
||||
import compression from 'compression';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
|
|
@ -68,6 +66,8 @@ export function registerProductionWebhooks() {
|
|||
return;
|
||||
}
|
||||
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, {}, true, 204);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import {
|
|||
IRun,
|
||||
IRunExecutionData,
|
||||
ITaskData,
|
||||
IWorkflowCredentials,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
IWorkflowExecuteHooks,
|
||||
IWorkflowHooksOptionalParameters,
|
||||
|
|
@ -57,7 +56,6 @@ import {
|
|||
Push,
|
||||
ResponseHelper,
|
||||
WebhookHelpers,
|
||||
WorkflowCredentials,
|
||||
WorkflowHelpers,
|
||||
} from '.';
|
||||
import {
|
||||
|
|
@ -66,7 +64,7 @@ import {
|
|||
getWorkflowOwner,
|
||||
} from './UserManagement/UserManagementHelper';
|
||||
import { whereClause } from './WorkflowHelpers';
|
||||
import { RESPONSE_ERROR_MESSAGES } from './constants';
|
||||
import { IWorkflowErrorData } from './Interfaces';
|
||||
|
||||
const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');
|
||||
|
||||
|
|
@ -79,7 +77,7 @@ const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');
|
|||
* @param {WorkflowExecuteMode} mode The mode in which the workflow got started in
|
||||
* @param {string} [executionId] The id the execution got saved as
|
||||
*/
|
||||
function executeErrorWorkflow(
|
||||
export function executeErrorWorkflow(
|
||||
workflowData: IWorkflowBase,
|
||||
fullRunData: IRun,
|
||||
mode: WorkflowExecuteMode,
|
||||
|
|
@ -94,20 +92,37 @@ function executeErrorWorkflow(
|
|||
}
|
||||
|
||||
if (fullRunData.data.resultData.error !== undefined) {
|
||||
const workflowErrorData = {
|
||||
execution: {
|
||||
id: executionId,
|
||||
url: pastExecutionUrl,
|
||||
error: fullRunData.data.resultData.error,
|
||||
lastNodeExecuted: fullRunData.data.resultData.lastNodeExecuted!,
|
||||
mode,
|
||||
retryOf,
|
||||
},
|
||||
workflow: {
|
||||
id: workflowData.id !== undefined ? workflowData.id.toString() : undefined,
|
||||
name: workflowData.name,
|
||||
},
|
||||
};
|
||||
let workflowErrorData: IWorkflowErrorData;
|
||||
|
||||
if (executionId) {
|
||||
// The error did happen in an execution
|
||||
workflowErrorData = {
|
||||
execution: {
|
||||
id: executionId,
|
||||
url: pastExecutionUrl,
|
||||
error: fullRunData.data.resultData.error,
|
||||
lastNodeExecuted: fullRunData.data.resultData.lastNodeExecuted!,
|
||||
mode,
|
||||
retryOf,
|
||||
},
|
||||
workflow: {
|
||||
id: workflowData.id !== undefined ? workflowData.id.toString() : undefined,
|
||||
name: workflowData.name,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// The error did happen in a trigger
|
||||
workflowErrorData = {
|
||||
trigger: {
|
||||
error: fullRunData.data.resultData.error,
|
||||
mode,
|
||||
},
|
||||
workflow: {
|
||||
id: workflowData.id !== undefined ? workflowData.id.toString() : undefined,
|
||||
name: workflowData.name,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Run the error workflow
|
||||
// To avoid an infinite loop do not run the error workflow again if the error-workflow itself failed and it is its own error-workflow.
|
||||
|
|
@ -135,15 +150,26 @@ function executeErrorWorkflow(
|
|||
// make sure there are no possible security gaps
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
getWorkflowOwner(workflowData.id).then((user) => {
|
||||
void WorkflowHelpers.executeErrorWorkflow(
|
||||
workflowData.settings!.errorWorkflow as string,
|
||||
workflowErrorData,
|
||||
user,
|
||||
);
|
||||
});
|
||||
getWorkflowOwner(workflowData.id)
|
||||
.then((user) => {
|
||||
void WorkflowHelpers.executeErrorWorkflow(
|
||||
workflowData.settings!.errorWorkflow as string,
|
||||
workflowErrorData,
|
||||
user,
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
Logger.error(
|
||||
`Could not execute ErrorWorkflow for execution ID ${this.executionId} because of error querying the workflow owner`,
|
||||
{
|
||||
executionId,
|
||||
errorWorkflowId: workflowData.settings!.errorWorkflow!.toString(),
|
||||
workflowId: workflowData.id,
|
||||
error,
|
||||
workflowErrorData,
|
||||
},
|
||||
);
|
||||
});
|
||||
} else if (
|
||||
mode !== 'error' &&
|
||||
workflowData.id !== undefined &&
|
||||
|
|
@ -397,6 +423,7 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx
|
|||
contextData: {},
|
||||
nodeExecutionStack: [],
|
||||
waitingExecution: {},
|
||||
waitingExecutionSource: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -752,6 +779,7 @@ export async function getRunData(
|
|||
data: {
|
||||
main: [inputData],
|
||||
},
|
||||
source: null,
|
||||
});
|
||||
|
||||
const runExecutionData: IRunExecutionData = {
|
||||
|
|
@ -763,6 +791,7 @@ export async function getRunData(
|
|||
contextData: {},
|
||||
nodeExecutionStack,
|
||||
waitingExecution: {},
|
||||
waitingExecutionSource: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -1014,7 +1043,7 @@ export function sendMessageToUI(source: string, messages: any[]) {
|
|||
* Returns the base additional data without webhooks
|
||||
*
|
||||
* @export
|
||||
* @param {IWorkflowCredentials} credentials
|
||||
* @param {userId} string
|
||||
* @param {INodeParameters} currentNodeParameters
|
||||
* @returns {Promise<IWorkflowExecuteAdditionalData>}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -189,6 +189,7 @@ export async function executeErrorWorkflow(
|
|||
],
|
||||
],
|
||||
},
|
||||
source: null,
|
||||
});
|
||||
|
||||
const runExecutionData: IRunExecutionData = {
|
||||
|
|
@ -200,6 +201,7 @@ export async function executeErrorWorkflow(
|
|||
contextData: {},
|
||||
nodeExecutionStack,
|
||||
waitingExecution: {},
|
||||
waitingExecutionSource: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -561,6 +561,12 @@ export class WorkflowRunner {
|
|||
},
|
||||
);
|
||||
|
||||
workflowExecution.catch(() => {
|
||||
// We `reject` this promise if the execution fails
|
||||
// but the error is handled already by processError
|
||||
// So we're just preventing crashes here.
|
||||
});
|
||||
|
||||
this.activeExecutions.attachWorkflowExecution(executionId, workflowExecution);
|
||||
return executionId;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,12 @@
|
|||
import express from 'express';
|
||||
import { In } from 'typeorm';
|
||||
import { UserSettings, Credentials } from 'n8n-core';
|
||||
import { INodeCredentialTestResult, LoggerProxy } from 'n8n-workflow';
|
||||
import {
|
||||
INodeCredentialsDetails,
|
||||
INodeCredentialTestResult,
|
||||
LoggerProxy,
|
||||
WorkflowExecuteMode,
|
||||
} from 'n8n-workflow';
|
||||
import { getLogger } from '../Logger';
|
||||
|
||||
import {
|
||||
|
|
@ -17,12 +22,14 @@ import {
|
|||
ICredentialsResponse,
|
||||
whereClause,
|
||||
ResponseHelper,
|
||||
CredentialTypes,
|
||||
} from '..';
|
||||
|
||||
import { RESPONSE_ERROR_MESSAGES } from '../constants';
|
||||
import { CredentialsEntity } from '../databases/entities/CredentialsEntity';
|
||||
import { SharedCredentials } from '../databases/entities/SharedCredentials';
|
||||
import { validateEntity } from '../GenericHelpers';
|
||||
import { createCredentiasFromCredentialsEntity } from '../CredentialsHelper';
|
||||
import type { CredentialRequest } from '../requests';
|
||||
import * as config from '../../config';
|
||||
import { externalHooks } from '../Server';
|
||||
|
|
@ -129,7 +136,6 @@ credentialsController.post(
|
|||
}
|
||||
|
||||
const helper = new CredentialsHelper(encryptionKey);
|
||||
|
||||
return helper.testCredentials(req.user, credentials.type, credentials, nodeToTestWith);
|
||||
}),
|
||||
);
|
||||
|
|
@ -165,11 +171,7 @@ credentialsController.post(
|
|||
}
|
||||
|
||||
// Encrypt the data
|
||||
const coreCredential = new Credentials(
|
||||
{ id: null, name: newCredential.name },
|
||||
newCredential.type,
|
||||
newCredential.nodesAccess,
|
||||
);
|
||||
const coreCredential = createCredentiasFromCredentialsEntity(newCredential, true);
|
||||
|
||||
// @ts-ignore
|
||||
coreCredential.setData(newCredential.data, encryptionKey);
|
||||
|
|
@ -301,12 +303,7 @@ credentialsController.patch(
|
|||
);
|
||||
}
|
||||
|
||||
const coreCredential = new Credentials(
|
||||
{ id: credential.id.toString(), name: credential.name },
|
||||
credential.type,
|
||||
credential.nodesAccess,
|
||||
credential.data,
|
||||
);
|
||||
const coreCredential = createCredentiasFromCredentialsEntity(credential);
|
||||
|
||||
const decryptedData = coreCredential.getData(encryptionKey);
|
||||
|
||||
|
|
@ -410,12 +407,7 @@ credentialsController.get(
|
|||
);
|
||||
}
|
||||
|
||||
const coreCredential = new Credentials(
|
||||
{ id: credential.id.toString(), name: credential.name },
|
||||
credential.type,
|
||||
credential.nodesAccess,
|
||||
credential.data,
|
||||
);
|
||||
const coreCredential = createCredentiasFromCredentialsEntity(credential);
|
||||
|
||||
return {
|
||||
id: id.toString(),
|
||||
|
|
|
|||
344
packages/cli/src/api/oauth2Credential.api.ts
Normal file
344
packages/cli/src/api/oauth2Credential.api.ts
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
/* eslint-disable import/no-cycle */
|
||||
import ClientOAuth2 from 'client-oauth2';
|
||||
import Csrf from 'csrf';
|
||||
import express from 'express';
|
||||
import get from 'lodash.get';
|
||||
import omit from 'lodash.omit';
|
||||
import set from 'lodash.set';
|
||||
import split from 'lodash.split';
|
||||
import unset from 'lodash.unset';
|
||||
import { Credentials, UserSettings } from 'n8n-core';
|
||||
import {
|
||||
LoggerProxy,
|
||||
WorkflowExecuteMode,
|
||||
INodeCredentialsDetails,
|
||||
ICredentialsEncrypted,
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
import { resolve as pathResolve } from 'path';
|
||||
import querystring from 'querystring';
|
||||
|
||||
import { Db, ICredentialsDb, ResponseHelper, WebhookHelpers } from '..';
|
||||
import { RESPONSE_ERROR_MESSAGES } from '../constants';
|
||||
import {
|
||||
CredentialsHelper,
|
||||
getCredentialForUser,
|
||||
getCredentialWithoutUser,
|
||||
} from '../CredentialsHelper';
|
||||
import { getLogger } from '../Logger';
|
||||
import { OAuthRequest } from '../requests';
|
||||
import { externalHooks } from '../Server';
|
||||
import config from '../../config';
|
||||
|
||||
export const oauth2CredentialController = express.Router();
|
||||
|
||||
/**
|
||||
* Initialize Logger if needed
|
||||
*/
|
||||
oauth2CredentialController.use((req, res, next) => {
|
||||
try {
|
||||
LoggerProxy.getInstance();
|
||||
} catch (error) {
|
||||
LoggerProxy.init(getLogger());
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
const restEndpoint = config.getEnv('endpoints.rest');
|
||||
|
||||
/**
|
||||
* GET /oauth2-credential/auth
|
||||
*
|
||||
* Authorize OAuth Data
|
||||
*/
|
||||
oauth2CredentialController.get(
|
||||
'/auth',
|
||||
ResponseHelper.send(async (req: OAuthRequest.OAuth1Credential.Auth): Promise<string> => {
|
||||
const { id: credentialId } = req.query;
|
||||
|
||||
if (!credentialId) {
|
||||
throw new ResponseHelper.ResponseError('Required credential ID is missing', undefined, 400);
|
||||
}
|
||||
|
||||
const credential = await getCredentialForUser(credentialId, req.user);
|
||||
|
||||
if (!credential) {
|
||||
LoggerProxy.error('Failed to authorize OAuth2 due to lack of permissions', {
|
||||
userId: req.user.id,
|
||||
credentialId,
|
||||
});
|
||||
throw new ResponseHelper.ResponseError(RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL, undefined, 404);
|
||||
}
|
||||
|
||||
let encryptionKey: string;
|
||||
try {
|
||||
encryptionKey = await UserSettings.getEncryptionKey();
|
||||
} catch (error) {
|
||||
throw new ResponseHelper.ResponseError((error as Error).message, undefined, 500);
|
||||
}
|
||||
|
||||
const mode: WorkflowExecuteMode = 'internal';
|
||||
const timezone = config.getEnv('generic.timezone');
|
||||
const credentialsHelper = new CredentialsHelper(encryptionKey);
|
||||
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
||||
credential as INodeCredentialsDetails,
|
||||
(credential as unknown as ICredentialsEncrypted).type,
|
||||
mode,
|
||||
timezone,
|
||||
true,
|
||||
);
|
||||
|
||||
const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(
|
||||
decryptedDataOriginal,
|
||||
(credential as unknown as ICredentialsEncrypted).type,
|
||||
mode,
|
||||
timezone,
|
||||
);
|
||||
|
||||
const token = new Csrf();
|
||||
// Generate a CSRF prevention token and send it as a OAuth2 state stringma/ERR
|
||||
const csrfSecret = token.secretSync();
|
||||
const state = {
|
||||
token: token.create(csrfSecret),
|
||||
cid: req.query.id,
|
||||
};
|
||||
const stateEncodedStr = Buffer.from(JSON.stringify(state)).toString('base64');
|
||||
|
||||
const oAuthOptions: ClientOAuth2.Options = {
|
||||
clientId: get(oauthCredentials, 'clientId') as string,
|
||||
clientSecret: get(oauthCredentials, 'clientSecret', '') as string,
|
||||
accessTokenUri: get(oauthCredentials, 'accessTokenUrl', '') as string,
|
||||
authorizationUri: get(oauthCredentials, 'authUrl', '') as string,
|
||||
redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}${restEndpoint}/oauth2-credential/callback`,
|
||||
scopes: split(get(oauthCredentials, 'scope', 'openid,') as string, ','),
|
||||
state: stateEncodedStr,
|
||||
};
|
||||
|
||||
await externalHooks.run('oauth2.authenticate', [oAuthOptions]);
|
||||
|
||||
const oAuthObj = new ClientOAuth2(oAuthOptions);
|
||||
|
||||
// Encrypt the data
|
||||
const credentials = new Credentials(
|
||||
credential as INodeCredentialsDetails,
|
||||
(credential as unknown as ICredentialsEncrypted).type,
|
||||
(credential as unknown as ICredentialsEncrypted).nodesAccess,
|
||||
);
|
||||
decryptedDataOriginal.csrfSecret = csrfSecret;
|
||||
|
||||
credentials.setData(decryptedDataOriginal, encryptionKey);
|
||||
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
||||
|
||||
// Add special database related data
|
||||
newCredentialsData.updatedAt = new Date();
|
||||
|
||||
// Update the credentials in DB
|
||||
await Db.collections.Credentials.update(req.query.id, newCredentialsData);
|
||||
|
||||
const authQueryParameters = get(oauthCredentials, 'authQueryParameters', '') as string;
|
||||
let returnUri = oAuthObj.code.getUri();
|
||||
|
||||
// if scope uses comma, change it as the library always return then with spaces
|
||||
if ((get(oauthCredentials, 'scope') as string).includes(',')) {
|
||||
const data = querystring.parse(returnUri.split('?')[1]);
|
||||
data.scope = get(oauthCredentials, 'scope') as string;
|
||||
returnUri = `${get(oauthCredentials, 'authUrl', '') as string}?${querystring.stringify(
|
||||
data,
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (authQueryParameters) {
|
||||
returnUri += `&${authQueryParameters}`;
|
||||
}
|
||||
|
||||
LoggerProxy.verbose('OAuth2 authentication successful for new credential', {
|
||||
userId: req.user.id,
|
||||
credentialId,
|
||||
});
|
||||
return returnUri;
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /oauth2-credential/callback
|
||||
*
|
||||
* Verify and store app code. Generate access tokens and store for respective credential.
|
||||
*/
|
||||
|
||||
oauth2CredentialController.get(
|
||||
'/callback',
|
||||
async (req: OAuthRequest.OAuth2Credential.Callback, res: express.Response) => {
|
||||
try {
|
||||
// realmId it's currently just use for the quickbook OAuth2 flow
|
||||
const { code, state: stateEncoded } = req.query;
|
||||
|
||||
if (!code || !stateEncoded) {
|
||||
const errorResponse = new ResponseHelper.ResponseError(
|
||||
`Insufficient parameters for OAuth2 callback. Received following query parameters: ${JSON.stringify(
|
||||
req.query,
|
||||
)}`,
|
||||
undefined,
|
||||
503,
|
||||
);
|
||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||
}
|
||||
|
||||
let state;
|
||||
try {
|
||||
state = JSON.parse(Buffer.from(stateEncoded, 'base64').toString()) as {
|
||||
cid: string;
|
||||
token: string;
|
||||
};
|
||||
} catch (error) {
|
||||
const errorResponse = new ResponseHelper.ResponseError(
|
||||
'Invalid state format returned',
|
||||
undefined,
|
||||
503,
|
||||
);
|
||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||
}
|
||||
|
||||
const credential = await getCredentialWithoutUser(state.cid);
|
||||
|
||||
if (!credential) {
|
||||
LoggerProxy.error('OAuth2 callback failed because of insufficient permissions', {
|
||||
userId: req.user?.id,
|
||||
credentialId: state.cid,
|
||||
});
|
||||
const errorResponse = new ResponseHelper.ResponseError(
|
||||
RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL,
|
||||
undefined,
|
||||
404,
|
||||
);
|
||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||
}
|
||||
|
||||
let encryptionKey: string;
|
||||
try {
|
||||
encryptionKey = await UserSettings.getEncryptionKey();
|
||||
} catch (error) {
|
||||
throw new ResponseHelper.ResponseError(
|
||||
(error as IDataObject).message as string,
|
||||
undefined,
|
||||
500,
|
||||
);
|
||||
}
|
||||
|
||||
const mode: WorkflowExecuteMode = 'internal';
|
||||
const timezone = config.getEnv('generic.timezone');
|
||||
const credentialsHelper = new CredentialsHelper(encryptionKey);
|
||||
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
||||
credential as INodeCredentialsDetails,
|
||||
(credential as unknown as ICredentialsEncrypted).type,
|
||||
mode,
|
||||
timezone,
|
||||
true,
|
||||
);
|
||||
const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(
|
||||
decryptedDataOriginal,
|
||||
(credential as unknown as ICredentialsEncrypted).type,
|
||||
mode,
|
||||
timezone,
|
||||
);
|
||||
|
||||
const token = new Csrf();
|
||||
if (
|
||||
decryptedDataOriginal.csrfSecret === undefined ||
|
||||
!token.verify(decryptedDataOriginal.csrfSecret as string, state.token)
|
||||
) {
|
||||
LoggerProxy.debug('OAuth2 callback state is invalid', {
|
||||
userId: req.user?.id,
|
||||
credentialId: state.cid,
|
||||
});
|
||||
const errorResponse = new ResponseHelper.ResponseError(
|
||||
'The OAuth2 callback state is invalid!',
|
||||
undefined,
|
||||
404,
|
||||
);
|
||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||
}
|
||||
|
||||
let options = {};
|
||||
|
||||
const oAuth2Parameters = {
|
||||
clientId: get(oauthCredentials, 'clientId') as string,
|
||||
clientSecret: get(oauthCredentials, 'clientSecret', '') as string | undefined,
|
||||
accessTokenUri: get(oauthCredentials, 'accessTokenUrl', '') as string,
|
||||
authorizationUri: get(oauthCredentials, 'authUrl', '') as string,
|
||||
redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}${restEndpoint}/oauth2-credential/callback`,
|
||||
scopes: split(get(oauthCredentials, 'scope', 'openid,') as string, ','),
|
||||
};
|
||||
|
||||
if ((get(oauthCredentials, 'authentication', 'header') as string) === 'body') {
|
||||
options = {
|
||||
body: {
|
||||
client_id: get(oauthCredentials, 'clientId') as string,
|
||||
client_secret: get(oauthCredentials, 'clientSecret', '') as string,
|
||||
},
|
||||
};
|
||||
delete oAuth2Parameters.clientSecret;
|
||||
}
|
||||
|
||||
await externalHooks.run('oauth2.callback', [oAuth2Parameters]);
|
||||
|
||||
const oAuthObj = new ClientOAuth2(oAuth2Parameters);
|
||||
|
||||
const queryParameters = req.originalUrl.split('?').splice(1, 1).join('');
|
||||
|
||||
const oauthToken = await oAuthObj.code.getToken(
|
||||
`${oAuth2Parameters.redirectUri}?${queryParameters}`,
|
||||
options,
|
||||
);
|
||||
|
||||
if (Object.keys(req.query).length > 2) {
|
||||
set(oauthToken.data, 'callbackQueryString', omit(req.query, 'state', 'code'));
|
||||
}
|
||||
|
||||
if (oauthToken === undefined) {
|
||||
LoggerProxy.error('OAuth2 callback failed: unable to get access tokens', {
|
||||
userId: req.user?.id,
|
||||
credentialId: state.cid,
|
||||
});
|
||||
const errorResponse = new ResponseHelper.ResponseError(
|
||||
'Unable to get access tokens!',
|
||||
undefined,
|
||||
404,
|
||||
);
|
||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||
}
|
||||
|
||||
if (decryptedDataOriginal.oauthTokenData) {
|
||||
// Only overwrite supplied data as some providers do for example just return the
|
||||
// refresh_token on the very first request and not on subsequent ones.
|
||||
Object.assign(decryptedDataOriginal.oauthTokenData, oauthToken.data);
|
||||
} else {
|
||||
// No data exists so simply set
|
||||
decryptedDataOriginal.oauthTokenData = oauthToken.data;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
unset(decryptedDataOriginal, 'csrfSecret');
|
||||
|
||||
const credentials = new Credentials(
|
||||
credential as INodeCredentialsDetails,
|
||||
(credential as unknown as ICredentialsEncrypted).type,
|
||||
(credential as unknown as ICredentialsEncrypted).nodesAccess,
|
||||
);
|
||||
credentials.setData(decryptedDataOriginal, encryptionKey);
|
||||
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
||||
// Add special database related data
|
||||
newCredentialsData.updatedAt = new Date();
|
||||
// Save the credentials in DB
|
||||
await Db.collections.Credentials.update(state.cid, newCredentialsData);
|
||||
LoggerProxy.verbose('OAuth2 callback successful for new credential', {
|
||||
userId: req.user?.id,
|
||||
credentialId: state.cid,
|
||||
});
|
||||
|
||||
return res.sendFile(pathResolve(__dirname, '../../../templates/oauth-callback.html'));
|
||||
} catch (error) {
|
||||
// Error response
|
||||
return ResponseHelper.sendErrorResponse(res, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import { QueryRunner } from 'typeorm';
|
||||
|
||||
export class MigrationHelpers {
|
||||
queryRunner: QueryRunner;
|
||||
|
||||
constructor(queryRunner: QueryRunner) {
|
||||
this.queryRunner = queryRunner;
|
||||
}
|
||||
|
||||
// runs an operation sequential on chunks of a query that returns a potentially large Array.
|
||||
/* eslint-disable no-await-in-loop */
|
||||
async runChunked(
|
||||
query: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
operation: (results: any[]) => Promise<void>,
|
||||
limit = 100,
|
||||
): Promise<void> {
|
||||
let offset = 0;
|
||||
let chunkedQuery: string;
|
||||
let chunkedQueryResults: unknown[];
|
||||
|
||||
do {
|
||||
chunkedQuery = this.chunkQuery(query, limit, offset);
|
||||
chunkedQueryResults = (await this.queryRunner.query(chunkedQuery)) as unknown[];
|
||||
// pass a copy to prevent errors from mutation
|
||||
await operation([...chunkedQueryResults]);
|
||||
offset += limit;
|
||||
} while (chunkedQueryResults.length === limit);
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
|
||||
private chunkQuery(query: string, limit: number, offset = 0): string {
|
||||
return `
|
||||
${query}
|
||||
LIMIT ${limit}
|
||||
OFFSET ${offset}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ import { Role } from './Role';
|
|||
import { SharedWorkflow } from './SharedWorkflow';
|
||||
import { SharedCredentials } from './SharedCredentials';
|
||||
import { NoXss } from '../utils/customValidators';
|
||||
import { answersFormatter, lowerCaser } from '../utils/transformers';
|
||||
import { objectRetriever, lowerCaser } from '../utils/transformers';
|
||||
|
||||
export const MIN_PASSWORD_LENGTH = 8;
|
||||
|
||||
|
|
@ -98,7 +98,7 @@ export class User {
|
|||
@Column({
|
||||
type: resolveDataType('json') as ColumnOptions['type'],
|
||||
nullable: true,
|
||||
transformer: answersFormatter,
|
||||
transformer: objectRetriever,
|
||||
})
|
||||
personalizationAnswers: IPersonalizationSurveyAnswers | null;
|
||||
|
||||
|
|
@ -133,10 +133,14 @@ export class User {
|
|||
@BeforeInsert()
|
||||
@BeforeUpdate()
|
||||
preUpsertHook(): void {
|
||||
this.email = this.email?.toLowerCase();
|
||||
this.email = this.email?.toLowerCase() ?? null;
|
||||
this.updatedAt = new Date();
|
||||
}
|
||||
|
||||
@Column({ type: String, nullable: true })
|
||||
@Index({ unique: true })
|
||||
apiKey?: string | null;
|
||||
|
||||
/**
|
||||
* Whether the user is pending setup completion.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import * as config from '../../../config';
|
|||
import { DatabaseType, IWorkflowDb } from '../..';
|
||||
import { TagEntity } from './TagEntity';
|
||||
import { SharedWorkflow } from './SharedWorkflow';
|
||||
import { objectRetriever } from '../utils/transformers';
|
||||
|
||||
function resolveDataType(dataType: string) {
|
||||
const dbType = config.getEnv('database.type');
|
||||
|
|
@ -95,6 +96,7 @@ export class WorkflowEntity implements IWorkflowDb {
|
|||
@Column({
|
||||
type: resolveDataType('json') as ColumnOptions['type'],
|
||||
nullable: true,
|
||||
transformer: objectRetriever,
|
||||
})
|
||||
staticData?: IDataObject;
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user