Merge branch 'master' of github.com:n8n-io/n8n into enhancement/automatic-credential-selection

This commit is contained in:
Mutasem 2022-07-20 10:17:54 +02:00
commit f6920c2e99
1408 changed files with 43333 additions and 22316 deletions

View File

@ -1,2 +1,3 @@
packages/editor-ui
packages/design-system
packages/cli/scripts/build.mjs

View File

@ -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',

View File

@ -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 ""

View File

@ -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
View 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
View 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.
*/
}

View File

@ -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)

View File

@ -1,6 +1,6 @@
# n8n - Workflow Automation Tool
![n8n.io - Workflow Automation](https://user-images.githubusercontent.com/65276001/173571060-9f2f6d7b-bac0-43b6-bdb2-001da9694058.png)
![n8n.io - Workflow Automation](https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-logo.png)
# 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.

View File

@ -1,8 +1,8 @@
# n8n - Workflow Automation
![n8n.io - Workflow Automation](https://user-images.githubusercontent.com/65276001/173571060-9f2f6d7b-bac0-43b6-bdb2-001da9694058.png)
![n8n.io - Workflow Automation](https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-logo.png)
# 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

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "n8n",
"version": "0.178.0",
"version": "0.186.1",
"private": true,
"homepage": "https://n8n.io",
"scripts": {

View 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 });
}
}

View File

@ -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.');

View File

@ -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);
}
});

View File

@ -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);
}
}

View File

@ -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;
}
});

View File

@ -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);
}

View File

@ -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',
},
},

View File

@ -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',
},
},
];

View File

@ -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"
}
}

View 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 });
});
}

View File

@ -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
*

View File

@ -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);
}

View File

@ -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();

View File

@ -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;
}

View File

@ -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',

View File

@ -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:')) {

View 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);
}
}

View 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,
};
};

File diff suppressed because one or more lines are too long

165
packages/cli/src/PublicApi/types.d.ts vendored Normal file
View 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[];
}

View File

@ -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));
},
],
};

View File

@ -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();
};

View File

@ -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;
}

View File

@ -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'

View File

@ -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'

View File

@ -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.

View File

@ -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'

View File

@ -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

View File

@ -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,
}),
});
},
],
};

View File

@ -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);
}

View File

@ -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'

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,6 @@
name: id
in: path
description: The ID of the execution.
required: true
schema:
type: number

View File

@ -0,0 +1,6 @@
name: includeData
in: query
description: Whether or not to include the execution's detailed data.
required: false
schema:
type: boolean

View File

@ -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',
});
}

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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

View File

@ -0,0 +1,6 @@
name: id
in: path
description: The ID of the workflow.
required: true
schema:
type: number

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);
},
],
};

View File

@ -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());
}

View 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: []

View File

@ -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();
};

View File

@ -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);
};

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,6 @@
NotFound:
$ref: './notFound.yml'
Unauthorized:
$ref: './unauthorized.yml'
BadRequest:
$ref: './badRequest.yml'

View File

@ -0,0 +1 @@
description: The request is invalid or provides malformed data.

View File

@ -0,0 +1 @@
description: The specified resource was not found.

View File

@ -0,0 +1 @@
description: Unauthorized

View 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'

View File

@ -0,0 +1,10 @@
required:
- message
type: object
properties:
code:
type: string
message:
type: string
description:
type: string

View File

@ -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';

View File

@ -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;
}

View File

@ -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);
}

View File

@ -123,6 +123,7 @@ export function sanitizeUser(user: User, withoutKeys?: string[]): PublicUser {
resetPasswordTokenExpiration,
createdAt,
updatedAt,
apiKey,
...sanitizedUser
} = user;
if (withoutKeys) {

View File

@ -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';

View File

@ -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 };
}),
);
}

View File

@ -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({

View File

@ -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 };

View File

@ -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',
);

View File

@ -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;
}

View File

@ -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>}
*/

View File

@ -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: {},
},
};

View File

@ -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;
}

View File

@ -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(),

View 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);
}
},
);

View File

@ -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}
`;
}
}

View File

@ -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.
*/

View File

@ -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