UNPKG

n8n

Version:

n8n Workflow Automation Tool

967 lines 84.8 kB
"use strict"; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.start = void 0; const express = require("express"); const fs_1 = require("fs"); const path_1 = require("path"); const typeorm_1 = require("typeorm"); const bodyParser = require("body-parser"); const history = require("connect-history-api-fallback"); const os = require("os"); const _ = require("lodash"); const clientOAuth2 = require("client-oauth2"); const clientOAuth1 = require("oauth-1.0a"); const csrf = require("csrf"); const requestPromise = require("request-promise-native"); const crypto_1 = require("crypto"); const bcryptjs_1 = require("bcryptjs"); const promClient = require("prom-client"); const n8n_core_1 = require("n8n-core"); const n8n_workflow_1 = require("n8n-workflow"); const n8n_nodes_base_1 = require("n8n-nodes-base"); const basicAuth = require("basic-auth"); const compression = require("compression"); const jwt = require("jsonwebtoken"); const jwks = require("jwks-rsa"); const timezones = require("google-timezones-json"); const parseUrl = require("parseurl"); const querystring = require("querystring"); const Queue = require("./Queue"); const _1 = require("."); const config = require("../config"); const TagHelpers = require("./TagHelpers"); const PersonalizationSurvey = require("./PersonalizationSurvey"); const InternalHooksManager_1 = require("./InternalHooksManager"); const TagEntity_1 = require("./databases/entities/TagEntity"); const WorkflowEntity_1 = require("./databases/entities/WorkflowEntity"); require('body-parser-xml')(bodyParser); class App { constructor() { this.app = express(); this.endpointWebhook = config.get('endpoints.webhook'); this.endpointWebhookWaiting = config.get('endpoints.webhookWaiting'); this.endpointWebhookTest = config.get('endpoints.webhookTest'); this.defaultWorkflowName = config.get('workflows.defaultName'); this.defaultCredentialsName = config.get('credentials.defaultName'); 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.activeWorkflowRunner = _1.ActiveWorkflowRunner.getInstance(); this.testWebhooks = _1.TestWebhooks.getInstance(); this.push = _1.Push.getInstance(); this.activeExecutionsInstance = _1.ActiveExecutions.getInstance(); this.waitTracker = _1.WaitTracker(); this.protocol = config.get('protocol'); this.sslKey = config.get('ssl_key'); this.sslCert = config.get('ssl_cert'); this.externalHooks = _1.ExternalHooks(); this.presetCredentialsLoaded = false; this.endpointPresetCredentials = config.get('credentials.overwrite.endpoint'); const urlBaseWebhook = _1.WebhookHelpers.getWebhookBaseUrl(); const telemetrySettings = { enabled: config.get('diagnostics.enabled'), }; if (telemetrySettings.enabled) { const conf = config.get('diagnostics.config.frontend'); const [key, url] = conf.split(';'); if (!key || !url) { n8n_workflow_1.LoggerProxy.warn('Diagnostics frontend config is invalid'); telemetrySettings.enabled = false; } telemetrySettings.config = { key, url }; } this.frontendSettings = { endpointWebhook: this.endpointWebhook, endpointWebhookTest: this.endpointWebhookTest, saveDataErrorExecution: this.saveDataErrorExecution, saveDataSuccessExecution: this.saveDataSuccessExecution, saveManualExecutions: this.saveManualExecutions, executionTimeout: this.executionTimeout, maxExecutionTimeout: this.maxExecutionTimeout, timezone: this.timezone, urlBaseWebhook, versionCli: '', oauthCallbackUrls: { oauth1: `${urlBaseWebhook}${this.restEndpoint}/oauth1-credential/callback`, oauth2: `${urlBaseWebhook}${this.restEndpoint}/oauth2-credential/callback`, }, versionNotifications: { enabled: config.get('versionNotifications.enabled'), endpoint: config.get('versionNotifications.endpoint'), infoUrl: config.get('versionNotifications.infoUrl'), }, instanceId: '', telemetry: telemetrySettings, personalizationSurvey: { shouldShow: false, }, }; } getCurrentDate() { return new Date(); } async config() { const enableMetrics = config.get('endpoints.metrics.enable'); let register; if (enableMetrics) { const prefix = config.get('endpoints.metrics.prefix'); register = new promClient.Registry(); register.setDefaultLabels({ prefix }); promClient.collectDefaultMetrics({ register }); } this.versions = await _1.GenericHelpers.getVersions(); this.frontendSettings.versionCli = this.versions.cli; this.frontendSettings.instanceId = await n8n_core_1.UserSettings.getInstanceId(); this.frontendSettings.personalizationSurvey = await PersonalizationSurvey.preparePersonalizationSurvey(); await this.externalHooks.run('frontend.settings', [this.frontendSettings]); const excludeEndpoints = config.get('security.excludeEndpoints'); const ignoredEndpoints = [ 'healthz', 'metrics', this.endpointWebhook, this.endpointWebhookTest, this.endpointPresetCredentials, ]; ignoredEndpoints.push.apply(ignoredEndpoints, excludeEndpoints.split(':')); const authIgnoreRegex = new RegExp(`^\/(${_(ignoredEndpoints).compact().join('|')})\/?.*$`); const basicAuthActive = config.get('security.basicAuth.active'); if (basicAuthActive) { const basicAuthUser = (await _1.GenericHelpers.getConfigValue('security.basicAuth.user')); if (basicAuthUser === '') { throw new Error('Basic auth is activated but no user got defined. Please set one!'); } const basicAuthPassword = (await _1.GenericHelpers.getConfigValue('security.basicAuth.password')); if (basicAuthPassword === '') { throw new Error('Basic auth is activated but no password got defined. Please set one!'); } const basicAuthHashEnabled = (await _1.GenericHelpers.getConfigValue('security.basicAuth.hash')); let validPassword = null; this.app.use(async (req, res, next) => { if (authIgnoreRegex.exec(req.url)) { return next(); } const realm = 'n8n - Editor UI'; const basicAuthData = basicAuth(req); if (basicAuthData === undefined) { return _1.ResponseHelper.basicAuthAuthorizationError(res, realm, 'Authorization is required!'); } if (basicAuthData.name === basicAuthUser) { if (basicAuthHashEnabled) { if (validPassword === null && (await bcryptjs_1.compare(basicAuthData.pass, basicAuthPassword))) { validPassword = basicAuthData.pass; } if (validPassword === basicAuthData.pass && validPassword !== null) { return next(); } } else if (basicAuthData.pass === basicAuthPassword) { return next(); } } return _1.ResponseHelper.basicAuthAuthorizationError(res, realm, 'Authorization data is wrong!'); }); } const jwtAuthActive = config.get('security.jwtAuth.active'); if (jwtAuthActive) { const jwtAuthHeader = (await _1.GenericHelpers.getConfigValue('security.jwtAuth.jwtHeader')); if (jwtAuthHeader === '') { throw new Error('JWT auth is activated but no request header was defined. Please set one!'); } const jwksUri = (await _1.GenericHelpers.getConfigValue('security.jwtAuth.jwksUri')); if (jwksUri === '') { throw new Error('JWT auth is activated but no JWK Set URI was defined. Please set one!'); } const jwtHeaderValuePrefix = (await _1.GenericHelpers.getConfigValue('security.jwtAuth.jwtHeaderValuePrefix')); const jwtIssuer = (await _1.GenericHelpers.getConfigValue('security.jwtAuth.jwtIssuer')); const jwtNamespace = (await _1.GenericHelpers.getConfigValue('security.jwtAuth.jwtNamespace')); const jwtAllowedTenantKey = (await _1.GenericHelpers.getConfigValue('security.jwtAuth.jwtAllowedTenantKey')); const jwtAllowedTenant = (await _1.GenericHelpers.getConfigValue('security.jwtAuth.jwtAllowedTenant')); function isTenantAllowed(decodedToken) { if (jwtNamespace === '' || jwtAllowedTenantKey === '' || jwtAllowedTenant === '') return true; for (const [k, v] of Object.entries(decodedToken)) { if (k === jwtNamespace) { for (const [kn, kv] of Object.entries(v)) { if (kn === jwtAllowedTenantKey && kv === jwtAllowedTenant) { return true; } } } } return false; } this.app.use((req, res, next) => { if (authIgnoreRegex.exec(req.url)) { return next(); } let token = req.header(jwtAuthHeader); if (token === undefined || token === '') { return _1.ResponseHelper.jwtAuthAuthorizationError(res, 'Missing token'); } if (jwtHeaderValuePrefix !== '' && token.startsWith(jwtHeaderValuePrefix)) { token = token.replace(`${jwtHeaderValuePrefix} `, '').trimLeft(); } const jwkClient = jwks({ cache: true, jwksUri }); function getKey(header, callback) { jwkClient.getSigningKey(header.kid, (err, key) => { if (err) throw _1.ResponseHelper.jwtAuthAuthorizationError(res, err.message); const signingKey = key.publicKey || key.rsaPublicKey; callback(null, signingKey); }); } const jwtVerifyOptions = { issuer: jwtIssuer !== '' ? jwtIssuer : undefined, ignoreExpiration: false, }; jwt.verify(token, getKey, jwtVerifyOptions, (err, decoded) => { if (err) { _1.ResponseHelper.jwtAuthAuthorizationError(res, 'Invalid token'); } else if (!isTenantAllowed(decoded)) { _1.ResponseHelper.jwtAuthAuthorizationError(res, 'Tenant not allowed'); } else { next(); } }); }); } this.app.use((req, res, next) => { if (req.url.indexOf(`/${this.restEndpoint}/push`) === 0) { if (req.query.sessionId === undefined) { next(new Error('The query parameter "sessionId" is missing!')); return; } this.push.add(req.query.sessionId, req, res); return; } next(); }); this.app.use(compression()); this.app.use((req, res, next) => { req.parsedUrl = parseUrl(req); req.rawBody = Buffer.from('', 'base64'); next(); }); this.app.use(bodyParser.json({ limit: `${this.payloadSizeMax}mb`, verify: (req, res, buf) => { req.rawBody = buf; }, })); this.app.use(bodyParser.xml({ limit: `${this.payloadSizeMax}mb`, xmlParseOptions: { normalize: true, normalizeTags: true, explicitArray: false, }, })); this.app.use(bodyParser.text({ limit: `${this.payloadSizeMax}mb`, verify: (req, res, buf) => { req.rawBody = buf; }, })); this.app.use(history({ rewrites: [ { from: new RegExp(`^\/(${this.restEndpoint}|healthz|metrics|css|js|${this.endpointWebhook}|${this.endpointWebhookTest})\/?.*$`), to: (context) => { return context.parsedUrl.pathname.toString(); }, }, ], })); this.app.use(bodyParser.urlencoded({ limit: `${this.payloadSizeMax}mb`, extended: false, verify: (req, res, buf) => { req.rawBody = buf; }, })); if (process.env.NODE_ENV !== 'production') { this.app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', 'http://localhost:8080'); res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, sessionid'); next(); }); } this.app.use((req, res, next) => { if (_1.Db.collections.Workflow === null) { const error = new _1.ResponseHelper.ResponseError('Database is not ready!', undefined, 503); return _1.ResponseHelper.sendErrorResponse(res, error); } next(); }); this.app.get('/healthz', async (req, res) => { const connection = typeorm_1.getConnectionManager().get(); try { if (!connection.isConnected) { throw new Error('No active database connection!'); } await connection.query('SELECT 1'); } catch (err) { n8n_workflow_1.LoggerProxy.error('No Database connection!', err); const error = new _1.ResponseHelper.ResponseError('No Database connection!', undefined, 503); return _1.ResponseHelper.sendErrorResponse(res, error); } const responseData = { status: 'ok', }; _1.ResponseHelper.sendSuccessResponse(res, responseData, true, 200); }); if (enableMetrics) { this.app.get('/metrics', async (req, res) => { const response = await register.metrics(); res.setHeader('Content-Type', register.contentType); _1.ResponseHelper.sendSuccessResponse(res, response, true, 200); }); } this.app.post(`/${this.restEndpoint}/workflows`, _1.ResponseHelper.send(async (req, res) => { delete req.body.id; const incomingData = req.body; const newWorkflow = new WorkflowEntity_1.WorkflowEntity(); Object.assign(newWorkflow, incomingData); newWorkflow.name = incomingData.name.trim(); const incomingTagOrder = incomingData.tags.slice(); if (incomingData.tags.length) { newWorkflow.tags = await _1.Db.collections.Tag.findByIds(incomingData.tags, { select: ['id', 'name'], }); } await _1.WorkflowHelpers.replaceInvalidCredentials(newWorkflow); await this.externalHooks.run('workflow.create', [newWorkflow]); await _1.WorkflowHelpers.validateWorkflow(newWorkflow); const savedWorkflow = (await _1.Db.collections .Workflow.save(newWorkflow) .catch(_1.WorkflowHelpers.throwDuplicateEntryError)); savedWorkflow.tags = TagHelpers.sortByRequestOrder(savedWorkflow.tags, incomingTagOrder); savedWorkflow.id = savedWorkflow.id.toString(); await this.externalHooks.run('workflow.afterCreate', [savedWorkflow]); void InternalHooksManager_1.InternalHooksManager.getInstance().onWorkflowCreated(newWorkflow); return savedWorkflow; })); this.app.get(`/${this.restEndpoint}/workflows/from-url`, _1.ResponseHelper.send(async (req, res) => { if (req.query.url === undefined) { throw new _1.ResponseHelper.ResponseError(`The parameter "url" is missing!`, undefined, 400); } if (!/^http[s]?:\/\/.*\.json$/i.exec(req.query.url)) { throw new _1.ResponseHelper.ResponseError(`The parameter "url" is not valid! It does not seem to be a URL pointing to a n8n workflow JSON file.`, undefined, 400); } const data = await requestPromise.get(req.query.url); let workflowData; try { workflowData = JSON.parse(data); } catch (error) { throw new _1.ResponseHelper.ResponseError(`The URL does not point to valid JSON file!`, undefined, 400); } if (workflowData === undefined || workflowData.nodes === undefined || !Array.isArray(workflowData.nodes) || workflowData.connections === undefined || typeof workflowData.connections !== 'object' || Array.isArray(workflowData.connections)) { throw new _1.ResponseHelper.ResponseError(`The data in the file does not seem to be a n8n workflow JSON file!`, undefined, 400); } return workflowData; })); this.app.get(`/${this.restEndpoint}/workflows`, _1.ResponseHelper.send(async (req, res) => { const findQuery = { select: ['id', 'name', 'active', 'createdAt', 'updatedAt'], relations: ['tags'], }; if (req.query.filter) { findQuery.where = JSON.parse(req.query.filter); } const workflows = await _1.Db.collections.Workflow.find(findQuery); workflows.forEach((workflow) => { workflow.id = workflow.id.toString(); workflow.tags = workflow.tags.map(({ id, name }) => ({ id: id.toString(), name })); }); return workflows; })); this.app.get(`/${this.restEndpoint}/workflows/new`, _1.ResponseHelper.send(async (req, res) => { const requestedName = req.query.name && req.query.name !== '' ? req.query.name : this.defaultWorkflowName; return await _1.GenericHelpers.generateUniqueName(requestedName, 'workflow'); })); this.app.get(`/${this.restEndpoint}/workflows/:id`, _1.ResponseHelper.send(async (req, res) => { const workflow = await _1.Db.collections.Workflow.findOne(req.params.id, { relations: ['tags'], }); if (workflow === undefined) { return undefined; } workflow.id = workflow.id.toString(); workflow.tags.forEach((tag) => (tag.id = tag.id.toString())); return workflow; })); this.app.patch(`/${this.restEndpoint}/workflows/:id`, _1.ResponseHelper.send(async (req, res) => { const _a = req.body, { tags } = _a, updateData = __rest(_a, ["tags"]); const { id } = req.params; updateData.id = id; await _1.WorkflowHelpers.replaceInvalidCredentials(updateData); await this.externalHooks.run('workflow.update', [updateData]); const isActive = await this.activeWorkflowRunner.isActive(id); if (isActive) { await this.activeWorkflowRunner.remove(id); } if (updateData.settings) { if (updateData.settings.timezone === 'DEFAULT') { delete updateData.settings.timezone; } if (updateData.settings.saveDataErrorExecution === 'DEFAULT') { delete updateData.settings.saveDataErrorExecution; } if (updateData.settings.saveDataSuccessExecution === 'DEFAULT') { delete updateData.settings.saveDataSuccessExecution; } if (updateData.settings.saveManualExecutions === 'DEFAULT') { delete updateData.settings.saveManualExecutions; } if (parseInt(updateData.settings.executionTimeout, 10) === this.executionTimeout) { delete updateData.settings.executionTimeout; } } updateData.updatedAt = this.getCurrentDate(); await _1.WorkflowHelpers.validateWorkflow(updateData); await _1.Db.collections .Workflow.update(id, updateData) .catch(_1.WorkflowHelpers.throwDuplicateEntryError); if (tags) { const tablePrefix = config.get('database.tablePrefix'); await TagHelpers.removeRelations(req.params.id, tablePrefix); if (tags.length) { await TagHelpers.createRelations(req.params.id, tags, tablePrefix); } } const workflow = await _1.Db.collections.Workflow.findOne(id, { relations: ['tags'] }); if (workflow === undefined) { throw new _1.ResponseHelper.ResponseError(`Workflow with id "${id}" could not be found to be updated.`, undefined, 400); } if (tags === null || tags === void 0 ? void 0 : tags.length) { workflow.tags = TagHelpers.sortByRequestOrder(workflow.tags, tags); } await this.externalHooks.run('workflow.afterUpdate', [workflow]); void InternalHooksManager_1.InternalHooksManager.getInstance().onWorkflowSaved(workflow); if (workflow.active) { try { await this.externalHooks.run('workflow.activate', [workflow]); await this.activeWorkflowRunner.add(id, isActive ? 'update' : 'activate'); } catch (error) { updateData.active = false; await _1.Db.collections.Workflow.update(id, updateData); workflow.active = false; throw error; } } workflow.id = workflow.id.toString(); return workflow; })); this.app.delete(`/${this.restEndpoint}/workflows/:id`, _1.ResponseHelper.send(async (req, res) => { const { id } = req.params; await this.externalHooks.run('workflow.delete', [id]); const isActive = await this.activeWorkflowRunner.isActive(id); if (isActive) { await this.activeWorkflowRunner.remove(id); } await _1.Db.collections.Workflow.delete(id); void InternalHooksManager_1.InternalHooksManager.getInstance().onWorkflowDeleted(id); await this.externalHooks.run('workflow.afterDelete', [id]); return true; })); this.app.post(`/${this.restEndpoint}/workflows/run`, _1.ResponseHelper.send(async (req, res) => { const { workflowData } = req.body; const { runData } = req.body; const { startNodes } = req.body; const { destinationNode } = req.body; const executionMode = 'manual'; const activationMode = 'manual'; const sessionId = _1.GenericHelpers.getSessionId(req); if (runData === undefined || startNodes === undefined || startNodes.length === 0 || destinationNode === undefined) { const additionalData = await _1.WorkflowExecuteAdditionalData.getBase(); const nodeTypes = _1.NodeTypes(); const workflowInstance = new n8n_workflow_1.Workflow({ id: workflowData.id, name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: false, nodeTypes, staticData: undefined, settings: workflowData.settings, }); const needsWebhook = await this.testWebhooks.needsWebhookData(workflowData, workflowInstance, additionalData, executionMode, activationMode, sessionId, destinationNode); if (needsWebhook) { return { waitingForWebhook: true, }; } } workflowData.active = false; const data = { destinationNode, executionMode, runData, sessionId, startNodes, workflowData, }; const workflowRunner = new _1.WorkflowRunner(); const executionId = await workflowRunner.run(data); return { executionId, }; })); this.app.get(`/${this.restEndpoint}/tags`, _1.ResponseHelper.send(async (req, res) => { if (req.query.withUsageCount === 'true') { const tablePrefix = config.get('database.tablePrefix'); return TagHelpers.getTagsWithCountDb(tablePrefix); } const tags = await _1.Db.collections.Tag.find({ select: ['id', 'name'] }); tags.forEach((tag) => (tag.id = tag.id.toString())); return tags; })); this.app.post(`/${this.restEndpoint}/tags`, _1.ResponseHelper.send(async (req, res) => { const newTag = new TagEntity_1.TagEntity(); newTag.name = req.body.name.trim(); await this.externalHooks.run('tag.beforeCreate', [newTag]); await TagHelpers.validateTag(newTag); const tag = await _1.Db.collections .Tag.save(newTag) .catch(TagHelpers.throwDuplicateEntryError); await this.externalHooks.run('tag.afterCreate', [tag]); tag.id = tag.id.toString(); return tag; })); this.app.patch(`/${this.restEndpoint}/tags/:id`, _1.ResponseHelper.send(async (req, res) => { const { name } = req.body; const { id } = req.params; const newTag = new TagEntity_1.TagEntity(); newTag.id = Number(id); newTag.name = name.trim(); await this.externalHooks.run('tag.beforeUpdate', [newTag]); await TagHelpers.validateTag(newTag); const tag = await _1.Db.collections .Tag.save(newTag) .catch(TagHelpers.throwDuplicateEntryError); await this.externalHooks.run('tag.afterUpdate', [tag]); tag.id = tag.id.toString(); return tag; })); this.app.delete(`/${this.restEndpoint}/tags/:id`, _1.ResponseHelper.send(async (req, res) => { const id = Number(req.params.id); await this.externalHooks.run('tag.beforeDelete', [id]); await _1.Db.collections.Tag.delete({ id }); await this.externalHooks.run('tag.afterDelete', [id]); return true; })); this.app.get(`/${this.restEndpoint}/node-parameter-options`, _1.ResponseHelper.send(async (req, res) => { const nodeTypeAndVersion = JSON.parse(`${req.query.nodeTypeAndVersion}`); const path = req.query.path; let credentials; const currentNodeParameters = JSON.parse(`${req.query.currentNodeParameters}`); if (req.query.credentials !== undefined) { credentials = JSON.parse(req.query.credentials); } const methodName = req.query.methodName; const nodeTypes = _1.NodeTypes(); const loadDataInstance = new n8n_core_1.LoadNodeParameterOptions(nodeTypeAndVersion, nodeTypes, path, currentNodeParameters, credentials); const additionalData = await _1.WorkflowExecuteAdditionalData.getBase(currentNodeParameters); return loadDataInstance.getOptions(methodName, additionalData); })); this.app.get(`/${this.restEndpoint}/node-types`, _1.ResponseHelper.send(async (req, res) => { const returnData = []; const onlyLatest = req.query.onlyLatest === 'true'; const nodeTypes = _1.NodeTypes(); const allNodes = nodeTypes.getAll(); const getNodeDescription = (nodeType) => { const nodeInfo = Object.assign({}, nodeType.description); if (req.query.includeProperties !== 'true') { delete nodeInfo.properties; } return nodeInfo; }; if (onlyLatest) { allNodes.forEach((nodeData) => { const nodeType = n8n_workflow_1.NodeHelpers.getVersionedTypeNode(nodeData); const nodeInfo = getNodeDescription(nodeType); returnData.push(nodeInfo); }); } else { allNodes.forEach((nodeData) => { const allNodeTypes = n8n_workflow_1.NodeHelpers.getVersionedTypeNodeAll(nodeData); allNodeTypes.forEach((element) => { const nodeInfo = getNodeDescription(element); returnData.push(nodeInfo); }); }); } return returnData; })); this.app.post(`/${this.restEndpoint}/node-types`, _1.ResponseHelper.send(async (req, res) => { const nodeInfos = _.get(req, 'body.nodeInfos', []); const nodeTypes = _1.NodeTypes(); const returnData = []; nodeInfos.forEach((nodeInfo) => { const nodeType = nodeTypes.getByNameAndVersion(nodeInfo.name, nodeInfo.version); if (nodeType === null || nodeType === void 0 ? void 0 : nodeType.description) { returnData.push(nodeType.description); } }); return returnData; })); this.app.get([ `/${this.restEndpoint}/node-icon/:nodeType`, `/${this.restEndpoint}/node-icon/:scope/:nodeType`, ], async (req, res) => { try { const nodeTypeName = `${req.params.scope ? `${req.params.scope}/` : ''}${req.params.nodeType}`; const nodeTypes = _1.NodeTypes(); const nodeType = nodeTypes.getByNameAndVersion(nodeTypeName); if (nodeType === undefined) { res.status(404).send('The nodeType is not known.'); return; } if (nodeType.description.icon === undefined) { res.status(404).send('No icon found for node.'); return; } if (!nodeType.description.icon.startsWith('file:')) { res.status(404).send('Node does not have a file icon.'); return; } const filepath = nodeType.description.icon.substr(5); const maxAge = 7 * 24 * 60 * 60 * 1000; res.setHeader('Cache-control', `private max-age=${maxAge}`); res.sendFile(filepath); } catch (error) { return _1.ResponseHelper.sendErrorResponse(res, error); } }); this.app.get(`/${this.restEndpoint}/active`, _1.ResponseHelper.send(async (req, res) => { const activeWorkflows = await this.activeWorkflowRunner.getActiveWorkflows(); return activeWorkflows.map((workflow) => workflow.id.toString()); })); this.app.get(`/${this.restEndpoint}/active/error/:id`, _1.ResponseHelper.send(async (req, res) => { const { id } = req.params; return this.activeWorkflowRunner.getActivationError(id); })); this.app.get(`/${this.restEndpoint}/credentials/new`, _1.ResponseHelper.send(async (req, res) => { const requestedName = req.query.name && req.query.name !== '' ? req.query.name : this.defaultCredentialsName; return await _1.GenericHelpers.generateUniqueName(requestedName, 'credentials'); })); this.app.delete(`/${this.restEndpoint}/credentials/:id`, _1.ResponseHelper.send(async (req, res) => { const { id } = req.params; await this.externalHooks.run('credentials.delete', [id]); await _1.Db.collections.Credentials.delete({ id }); return true; })); this.app.post(`/${this.restEndpoint}/credentials`, _1.ResponseHelper.send(async (req, res) => { const incomingData = req.body; if (!incomingData.name || incomingData.name.length < 3) { throw new _1.ResponseHelper.ResponseError(`Credentials name must be at least 3 characters long.`, undefined, 400); } for (const nodeAccess of incomingData.nodesAccess) { nodeAccess.date = this.getCurrentDate(); } const encryptionKey = await n8n_core_1.UserSettings.getEncryptionKey(); if (encryptionKey === undefined) { throw new Error('No encryption key got found to encrypt the credentials!'); } if (incomingData.name === '') { throw new Error('Credentials have to have a name set!'); } const credentials = new n8n_core_1.Credentials({ id: null, name: incomingData.name }, incomingData.type, incomingData.nodesAccess); credentials.setData(incomingData.data, encryptionKey); const newCredentialsData = credentials.getDataToSave(); await this.externalHooks.run('credentials.create', [newCredentialsData]); const result = await _1.Db.collections.Credentials.save(newCredentialsData); result.data = incomingData.data; result.id = result.id.toString(); return result; })); this.app.post(`/${this.restEndpoint}/credentials-test`, _1.ResponseHelper.send(async (req, res) => { const incomingData = req.body; const credentialType = incomingData.credentials.type; const nodeTypes = _1.NodeTypes(); const allNodes = nodeTypes.getAll(); let foundTestFunction; const nodeThatCanTestThisCredential = allNodes.find((node) => { var _a, _b; if (incomingData.nodeToTestWith && node.description.name !== incomingData.nodeToTestWith) { return false; } if (node instanceof n8n_nodes_base_1.NodeVersionedType) { const versionNames = Object.keys(node.nodeVersions); for (const versionName of versionNames) { const nodeType = node.nodeVersions[versionName]; const credentialTestable = (_a = nodeType.description.credentials) === null || _a === void 0 ? void 0 : _a.find((credential) => { const testFunctionSearch = credential.name === credentialType && !!credential.testedBy; if (testFunctionSearch) { foundTestFunction = node.methods.credentialTest[credential.testedBy]; } return testFunctionSearch; }); if (credentialTestable) { return true; } } return false; } const credentialTestable = (_b = node.description.credentials) === null || _b === void 0 ? void 0 : _b.find((credential) => { const testFunctionSearch = credential.name === credentialType && !!credential.testedBy; if (testFunctionSearch) { foundTestFunction = node.methods.credentialTest[credential.testedBy]; } return testFunctionSearch; }); return !!credentialTestable; }); if (!nodeThatCanTestThisCredential) { return Promise.resolve({ status: 'Error', message: 'There are no nodes that can test this credential.', }); } if (foundTestFunction === undefined) { return Promise.resolve({ status: 'Error', message: 'No testing function found for this credential.', }); } const credentialTestFunctions = n8n_core_1.NodeExecuteFunctions.getCredentialTestFunctions(); const output = await foundTestFunction.call(credentialTestFunctions, incomingData.credentials); return Promise.resolve(output); })); this.app.patch(`/${this.restEndpoint}/credentials/:id`, _1.ResponseHelper.send(async (req, res) => { const incomingData = req.body; const { id } = req.params; if (incomingData.name === '') { throw new Error('Credentials have to have a name set!'); } for (const nodeAccess of incomingData.nodesAccess) { if (!nodeAccess.date) { nodeAccess.date = this.getCurrentDate(); } } const encryptionKey = await n8n_core_1.UserSettings.getEncryptionKey(); if (encryptionKey === undefined) { throw new Error('No encryption key got found to encrypt the credentials!'); } const result = await _1.Db.collections.Credentials.findOne(id); if (result === undefined) { throw new _1.ResponseHelper.ResponseError(`Credentials with the id "${id}" do not exist.`, undefined, 400); } const currentlySavedCredentials = new n8n_core_1.Credentials(result, result.type, result.nodesAccess, result.data); const decryptedData = currentlySavedCredentials.getData(encryptionKey); if (decryptedData.oauthTokenData) { incomingData.data.oauthTokenData = decryptedData.oauthTokenData; } const credentials = new n8n_core_1.Credentials({ id, name: incomingData.name }, incomingData.type, incomingData.nodesAccess); credentials.setData(incomingData.data, encryptionKey); const newCredentialsData = credentials.getDataToSave(); newCredentialsData.updatedAt = this.getCurrentDate(); await this.externalHooks.run('credentials.update', [newCredentialsData]); await _1.Db.collections.Credentials.update(id, newCredentialsData); const responseData = await _1.Db.collections.Credentials.findOne(id); if (responseData === undefined) { throw new _1.ResponseHelper.ResponseError(`Credentials with id "${id}" could not be found to be updated.`, undefined, 400); } responseData.data = ''; responseData.id = responseData.id.toString(); return responseData; })); this.app.get(`/${this.restEndpoint}/credentials/:id`, _1.ResponseHelper.send(async (req, res) => { const findQuery = {}; const includeData = ['true', true].includes(req.query.includeData); if (!includeData) { findQuery.select = ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt']; } const result = await _1.Db.collections.Credentials.findOne(req.params.id); if (result === undefined) { return result; } let encryptionKey; if (includeData) { encryptionKey = await n8n_core_1.UserSettings.getEncryptionKey(); if (encryptionKey === undefined) { throw new Error('No encryption key got found to decrypt the credentials!'); } const credentials = new n8n_core_1.Credentials(result, result.type, result.nodesAccess, result.data); result.data = credentials.getData(encryptionKey); } result.id = result.id.toString(); return result; })); this.app.get(`/${this.restEndpoint}/credentials`, _1.ResponseHelper.send(async (req, res) => { const findQuery = {}; if (req.query.filter) { findQuery.where = JSON.parse(req.query.filter); if (findQuery.where.id !== undefined) { findQuery.where = { id: findQuery.where.id }; } } findQuery.select = ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt']; const results = (await _1.Db.collections.Credentials.find(findQuery)); let encryptionKey; const includeData = ['true', true].includes(req.query.includeData); if (includeData) { encryptionKey = await n8n_core_1.UserSettings.getEncryptionKey(); if (encryptionKey === undefined) { throw new Error('No encryption key got found to decrypt the credentials!'); } } let result; for (result of results) { result.id = result.id.toString(); } return results; })); this.app.get(`/${this.restEndpoint}/credential-types`, _1.ResponseHelper.send(async (req, res) => { const returnData = []; const credentialTypes = _1.CredentialTypes(); credentialTypes.getAll().forEach((credentialData) => { returnData.push(credentialData); }); return returnData; })); this.app.get(`/${this.restEndpoint}/credential-icon/:credentialType`, async (req, res) => { try { const credentialName = req.params.credentialType; const credentialType = _1.CredentialTypes().getByName(credentialName); if (credentialType === undefined) { res.status(404).send('The credentialType is not known.'); return; } if (credentialType.icon === undefined) { res.status(404).send('No icon found for credential.'); return; } if (!credentialType.icon.startsWith('file:')) { res.status(404).send('Credential does not have a file icon.'); return; } const filepath = credentialType.icon.substr(5); const maxAge = 7 * 24 * 60 * 60 * 1000; res.setHeader('Cache-control', `private max-age=${maxAge}`); res.sendFile(filepath); } catch (error) { return _1.ResponseHelper.sendErrorResponse(res, error); } }); this.app.get(`/${this.restEndpoint}/oauth1-credential/auth`, _1.ResponseHelper.send(async (req, res) => { if (req.query.id === undefined) { res.status(500).send('Required credential id is missing!'); return ''; } const result = await _1.Db.collections.Credentials.findOne(req.query.id); if (result === undefined) { res.status(404).send('The credential is not known.'); return ''; } let encryptionKey; encryptionKey = await n8n_core_1.UserSettings.getEncryptionKey(); if (encryptionKey === undefined) { res.status(500).send('No encryption key got found to decrypt the credentials!'); return ''; } const mode = 'internal'; const credentialsHelper = new _1.CredentialsHelper(encryptionKey); const decryptedDataOriginal = await credentialsHelper.getDecrypted(result, result.type, mode, true); const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type, mode); const signatureMethod = _.get(oauthCredentials, 'signatureMethod'); const oAuthOptions = { consumer: { key: _.get(oauthCredentials, 'consumerKey'), secret: _.get(oauthCredentials, 'consumerSecret'), }, signature_method: signatureMethod, hash_function(base, key) { const algorithm = signatureMethod === 'HMAC-SHA1' ? 'sha1' : 'sha256'; return crypto_1.createHmac(algorithm, key).update(base).digest('base64'); }, }; const oauthRequestData = { oauth_callback: `${_1.WebhookHelpers.getWebhookBaseUrl()}${this.restEndpoint}/oauth1-credential/callback?cid=${req.query.id}`, }; await this.externalHooks.run('oauth1.authenticate', [oAuthOptions, oauthRequestData]); const oauth = new clientOAuth1(oAuthOptions); const options = { method: 'POST', url: _.get(oauthCredentials, 'requestTokenUrl'), data: oauthRequestData, }; const data = oauth.toHeader(oauth.authorize(options)); options.headers = data; const response = await requestPromise(options); const responseJson = querystring.parse(response); const returnUri = `${_.get(oauthCredentials, 'authUrl')}?oauth_token=${responseJson.oauth_token}`; const credentials = new n8n_core_1.Credentials(result, result.type, result.nodesAccess); credentials.setData(decryptedDataOriginal, encryptionKey); const newCredentialsData = credentials.getDataToSave(); newCredentialsData.updatedAt = this.getCurrentDate(); await _1.Db.collections.Credentials.update(req.query.id, newCredentialsData); return returnUri; })); this.app.get(`/${this.restEndpoint}/oauth1-credential/callback`, async (req, res) => { try { const { oauth_verifier, oauth_token, cid } = req.query; if (oauth_verifier === undefined || oauth_token === undefined) { const errorResponse = new _1.ResponseHelper.ResponseError(`Insufficient parameters for OAuth1 callback. Received following query parameters: ${JSON.stringify(req.query)}`, undefined, 503); return _1.ResponseHelper.sendErrorResponse(res, errorResponse); } const result = await _1.Db.collections.Credentials.findOne(cid); if (result === undefined) { const errorResponse = new _1.ResponseHelper.ResponseError('The credential is not known.', undefined, 404); return _1.ResponseHelper.sendErrorResponse(res, errorResponse); } let encryptionKey; encryptionKey = await n8n_core_1.UserSettings.getEncryptionKey(); if (encryptionKey === undefined) { const errorResponse = new _1.ResponseHelper.ResponseError('No encryption key got found to decrypt the credentials!', undefined, 503); return _1.ResponseHelper.sendErrorResponse(res, errorResponse); } const mode = 'internal'; const credentialsHelper = new _1.CredentialsHelper(encryptionKey); const decryptedDataOriginal = await credentialsHelper.getDecrypted(result, result.type, mode, true); const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type, mode); const options = { method: 'POST', url: _.get(oauthCredentials, 'accessTokenUrl'), qs: { oauth_token, oauth_verifier, }, }; let oauthToken;