import * as express from 'express' ; import { readFileSync , existsSync } from 'fs' ; import { readFile } from 'fs/promises' ; import { dirname as pathDirname , join as pathJoin , resolve as pathResolve } from 'path' ; import { FindManyOptions , getConnectionManager , In , IsNull , LessThanOrEqual , Not } from 'typeorm' ; import * as bodyParser from 'body-parser' ; import * as history from 'connect-history-api-fallback' ; import * as os from 'os' ; import * as _ from 'lodash' ; import * as clientOAuth2 from 'client-oauth2' ; import * as clientOAuth1 from 'oauth-1.0a' ; import { RequestOptions } from 'oauth-1.0a' ; import * as csrf from 'csrf' ; import * as requestPromise from 'request-promise-native' ; import { createHmac } from 'crypto' ; import { compare } from 'bcryptjs' ; import * as promClient from 'prom-client' ; import { BinaryDataManager , Credentials , IBinaryDataConfig , ICredentialTestFunctions , LoadNodeParameterOptions , NodeExecuteFunctions , UserSettings , } from 'n8n-core' ; import { ICredentialsDecrypted , ICredentialType , IDataObject , INodeCredentials , INodeCredentialsDetails , INodeParameters , INodePropertyOptions , INodeType , INodeTypeDescription , INodeTypeNameVersion , INodeVersionedType , ITelemetrySettings , IWorkflowBase , LoggerProxy , NodeCredentialTestRequest , NodeCredentialTestResult , NodeHelpers , Workflow , ICredentialsEncrypted , WorkflowExecuteMode , } from 'n8n-workflow' ; import { NodeVersionedType } from 'n8n-nodes-base' ; import * as basicAuth from 'basic-auth' ; import * as compression from 'compression' ; import * as jwt from 'jsonwebtoken' ; import * as jwks from 'jwks-rsa' ; import * as timezones from 'google-timezones-json' ; import * as parseUrl from 'parseurl' ; import * as querystring from 'querystring' ; import { OptionsWithUrl } from 'request-promise-native' ; import { Registry } from 'prom-client' ; import * as Queue from './Queue' ; import { ActiveExecutions , ActiveWorkflowRunner , CredentialsHelper , CredentialsOverwrites , CredentialTypes , DatabaseType , Db , ExternalHooks , GenericHelpers , IActivationError , ICredentialsDb , ICredentialsDecryptedDb , ICredentialsDecryptedResponse , ICredentialsOverwrite , ICredentialsResponse , ICustomRequest , IExecutionDeleteFilter , IExecutionFlatted , IExecutionFlattedDb , IExecutionFlattedResponse , IExecutionPushResponse , IExecutionResponse , IExecutionsListResponse , IExecutionsStopData , IExecutionsSummary , IExternalHooksClass , IDiagnosticInfo , IN8nUISettings , IPackageVersions , ITagWithCountDb , IWorkflowExecutionDataProcess , IWorkflowResponse , IPersonalizationSurveyAnswers , NodeTypes , Push , ResponseHelper , TestWebhooks , WaitTracker , WaitTrackerClass , WebhookHelpers , WebhookServer , WorkflowExecuteAdditionalData , WorkflowHelpers , WorkflowRunner , } from '.' ; import * as config from '../config' ; import * as TagHelpers from './TagHelpers' ; import * as PersonalizationSurvey from './PersonalizationSurvey' ; import { InternalHooksManager } from './InternalHooksManager' ; import { TagEntity } from './databases/entities/TagEntity' ; import { WorkflowEntity } from './databases/entities/WorkflowEntity' ; import { NameRequest } from './WorkflowHelpers' ; import { getCredentialTranslationPath , getNodeTranslationPath } from './TranslationHelpers' ; require ( 'body-parser-xml' ) ( bodyParser ) ; class App { app : express . Application ; activeWorkflowRunner : ActiveWorkflowRunner . ActiveWorkflowRunner ; testWebhooks : TestWebhooks . TestWebhooks ; endpointWebhook : string ; endpointWebhookWaiting : string ; endpointWebhookTest : string ; endpointPresetCredentials : string ; externalHooks : IExternalHooksClass ; waitTracker : WaitTrackerClass ; defaultWorkflowName : string ; defaultCredentialsName : string ; saveDataErrorExecution : string ; saveDataSuccessExecution : string ; saveManualExecutions : boolean ; executionTimeout : number ; maxExecutionTimeout : number ; timezone : string ; activeExecutionsInstance : ActiveExecutions . ActiveExecutions ; push : Push . Push ; versions : IPackageVersions | undefined ; restEndpoint : string ; frontendSettings : IN8nUISettings ; protocol : string ; sslKey : string ; sslCert : string ; payloadSizeMax : number ; presetCredentialsLoaded : boolean ; constructor ( ) { this . app = express ( ) ; this . endpointWebhook = config . get ( 'endpoints.webhook' ) as string ; this . endpointWebhookWaiting = config . get ( 'endpoints.webhookWaiting' ) as string ; this . endpointWebhookTest = config . get ( 'endpoints.webhookTest' ) as string ; this . defaultWorkflowName = config . get ( 'workflows.defaultName' ) as string ; this . defaultCredentialsName = config . get ( 'credentials.defaultName' ) as string ; this . saveDataErrorExecution = config . get ( 'executions.saveDataOnError' ) as string ; this . saveDataSuccessExecution = config . get ( 'executions.saveDataOnSuccess' ) as string ; this . saveManualExecutions = config . get ( 'executions.saveDataManualExecutions' ) as boolean ; this . executionTimeout = config . get ( 'executions.timeout' ) as number ; this . maxExecutionTimeout = config . get ( 'executions.maxTimeout' ) as number ; this . payloadSizeMax = config . get ( 'endpoints.payloadSizeMax' ) as number ; this . timezone = config . get ( 'generic.timezone' ) as string ; this . restEndpoint = config . get ( 'endpoints.rest' ) as string ; this . activeWorkflowRunner = ActiveWorkflowRunner . getInstance ( ) ; this . testWebhooks = TestWebhooks . getInstance ( ) ; this . push = Push . getInstance ( ) ; this . activeExecutionsInstance = ActiveExecutions . getInstance ( ) ; this . waitTracker = WaitTracker ( ) ; this . protocol = config . get ( 'protocol' ) ; this . sslKey = config . get ( 'ssl_key' ) ; this . sslCert = config . get ( 'ssl_cert' ) ; this . externalHooks = ExternalHooks ( ) ; this . presetCredentialsLoaded = false ; this . endpointPresetCredentials = config . get ( 'credentials.overwrite.endpoint' ) as string ; const urlBaseWebhook = WebhookHelpers . getWebhookBaseUrl ( ) ; const telemetrySettings = { enabled : config . get ( 'diagnostics.enabled' ) as boolean , } ; if ( telemetrySettings . enabled ) { const conf = config . get ( 'diagnostics.config.frontend' ) as string ; const [ key , url ] = conf . split ( ';' ) ; if ( ! key || ! url ) { 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 , } , defaultLocale : config . get ( 'defaultLocale' ) , } ; } returns { number } memberof getCurrentDate ( ) { return new Date ( ) ; } async config ( ) { const enableMetrics = config . get ( 'endpoints.metrics.enable' ) as boolean ; let register ; if ( enableMetrics ) { const prefix = config . get ( 'endpoints.metrics.prefix' ) as string ; register = new promClient . Registry ( ) ; register . setDefaultLabels ( { prefix } ) ; promClient . collectDefaultMetrics ( { register } ) ; } this . versions = await GenericHelpers . getVersions ( ) ; this . frontendSettings . versionCli = this . versions . cli ; this . frontendSettings . instanceId = await UserSettings . getInstanceId ( ) ; this . frontendSettings . personalizationSurvey = await PersonalizationSurvey . preparePersonalizationSurvey ( ) ; await this . externalHooks . run ( 'frontend.settings' , [ this . frontendSettings ] ) ; const excludeEndpoints = config . get ( 'security.excludeEndpoints' ) as string ; 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' ) as boolean ; if ( basicAuthActive ) { const basicAuthUser = ( await GenericHelpers . getConfigValue ( 'security.basicAuth.user' ) ) as string ; if ( basicAuthUser === '' ) { throw new Error ( 'Basic auth is activated but no user got defined. Please set one!' ) ; } const basicAuthPassword = ( await GenericHelpers . getConfigValue ( 'security.basicAuth.password' ) ) as string ; if ( basicAuthPassword === '' ) { throw new Error ( 'Basic auth is activated but no password got defined. Please set one!' ) ; } const basicAuthHashEnabled = ( await GenericHelpers . getConfigValue ( 'security.basicAuth.hash' ) ) as boolean ; 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 ResponseHelper . basicAuthAuthorizationError ( res , realm , 'Authorization is required!' ) ; } if ( basicAuthData . name === basicAuthUser ) { if ( basicAuthHashEnabled ) { if ( validPassword === null && ( await compare ( basicAuthData . pass , basicAuthPassword ) ) ) { validPassword = basicAuthData . pass ; } if ( validPassword === basicAuthData . pass && validPassword !== null ) { return next ( ) ; } } else if ( basicAuthData . pass === basicAuthPassword ) { return next ( ) ; } } return ResponseHelper . basicAuthAuthorizationError ( res , realm , 'Authorization data is wrong!' ) ; } ) ; } const jwtAuthActive = config . get ( 'security.jwtAuth.active' ) as boolean ; if ( jwtAuthActive ) { const jwtAuthHeader = ( await GenericHelpers . getConfigValue ( 'security.jwtAuth.jwtHeader' ) ) as string ; if ( jwtAuthHeader === '' ) { throw new Error ( 'JWT auth is activated but no request header was defined. Please set one!' ) ; } const jwksUri = ( await GenericHelpers . getConfigValue ( 'security.jwtAuth.jwksUri' ) ) as string ; if ( jwksUri === '' ) { throw new Error ( 'JWT auth is activated but no JWK Set URI was defined. Please set one!' ) ; } const jwtHeaderValuePrefix = ( await GenericHelpers . getConfigValue ( 'security.jwtAuth.jwtHeaderValuePrefix' ) ) as string ; const jwtIssuer = ( await GenericHelpers . getConfigValue ( 'security.jwtAuth.jwtIssuer' ) ) as string ; const jwtNamespace = ( await GenericHelpers . getConfigValue ( 'security.jwtAuth.jwtNamespace' ) ) as string ; const jwtAllowedTenantKey = ( await GenericHelpers . getConfigValue ( 'security.jwtAuth.jwtAllowedTenantKey' ) ) as string ; const jwtAllowedTenant = ( await GenericHelpers . getConfigValue ( 'security.jwtAuth.jwtAllowedTenant' ) ) as string ; 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 ) as string ; if ( token === undefined || token === '' ) { return 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 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 ) { ResponseHelper . jwtAuthAuthorizationError ( res , 'Invalid token' ) ; } else if ( ! isTenantAllowed ( decoded ) ) { 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 as string , req , res ) ; return ; } next ( ) ; } ) ; this . app . use ( compression ( ) ) ; this . app . use ( ( req , res , next ) => { ( req as ICustomRequest ) . 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 ( Db . collections . Workflow === null ) { const error = new ResponseHelper . ResponseError ( 'Database is not ready!' , undefined , 503 ) ; return ResponseHelper . sendErrorResponse ( res , error ) ; } next ( ) ; } ) ; this . app . get ( '/healthz' , async ( req , res ) => { const connection = getConnectionManager ( ) . get ( ) ; try { if ( ! connection . isConnected ) { throw new Error ( 'No active database connection!' ) ; } await connection . query ( 'SELECT 1' ) ; } catch ( err ) { LoggerProxy . error ( 'No Database connection!' , err ) ; const error = new ResponseHelper . ResponseError ( 'No Database connection!' , undefined , 503 ) ; return ResponseHelper . sendErrorResponse ( res , error ) ; } const responseData = { status : 'ok' , } ; 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 ) ; ResponseHelper . sendSuccessResponse ( res , response , true , 200 ) ; } ) ; } this . app . post ( `/${ this . restEndpoint }/workflows` , ResponseHelper . send ( async ( req , res ) : Promise < WorkflowEntity > => { delete req . body . id ; const incomingData = req . body ; const newWorkflow = new WorkflowEntity ( ) ; Object . assign ( newWorkflow , incomingData ) ; newWorkflow . name = incomingData . name . trim ( ) ; const incomingTagOrder = incomingData . tags . slice ( ) ; if ( incomingData . tags . length ) { newWorkflow . tags = await Db . collections . Tag ! . findByIds ( incomingData . tags , { select : [ 'id' , 'name' ] , } ) ; } await WorkflowHelpers . replaceInvalidCredentials ( newWorkflow ) ; await this . externalHooks . run ( 'workflow.create' , [ newWorkflow ] ) ; await WorkflowHelpers . validateWorkflow ( newWorkflow ) ; const savedWorkflow = ( await Db . collections . Workflow ! . save ( newWorkflow ) . catch ( WorkflowHelpers . throwDuplicateEntryError ) ) as WorkflowEntity ; savedWorkflow . tags = TagHelpers . sortByRequestOrder ( savedWorkflow . tags , incomingTagOrder ) ; savedWorkflow . id = savedWorkflow . id . toString ( ) ; await this . externalHooks . run ( 'workflow.afterCreate' , [ savedWorkflow ] ) ; void InternalHooksManager . getInstance ( ) . onWorkflowCreated ( newWorkflow as IWorkflowBase ) ; return savedWorkflow ; } ) ) ; this . app . get ( `/${ this . restEndpoint }/workflows/from-url` , ResponseHelper . send ( async ( req , res ) : Promise < IWorkflowResponse > => { if ( req . query . url === undefined ) { throw new ResponseHelper . ResponseError ( `The parameter "url" is missing!` , undefined , 400 ) ; } if ( ! /^http[s]?:\/\/.*\.json$/i . exec ( req . query . url as string ) ) { throw new 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 as string ) ; let workflowData ; try { workflowData = JSON . parse ( data ) ; } catch ( error ) { throw new 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 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` , 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 as string ) ; } const workflows = await 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` , ResponseHelper . send ( async ( req , res ) : Promise < { name : string ; } > => { const requestedName = req . query . name && req . query . name !== '' ? req . query . name : this . defaultWorkflowName ; return await GenericHelpers . generateUniqueName ( requestedName , 'workflow' ) ; } ) ) ; this . app . get ( `/${ this . restEndpoint }/workflows/:id` , ResponseHelper . send ( async ( req , res ) : Promise < WorkflowEntity | undefined > => { const workflow = await 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` , ResponseHelper . send ( async ( req , res ) : Promise < WorkflowEntity > => { const { tags , ... updateData } = req . body ; const { id } = req . params ; updateData . id = id ; await WorkflowHelpers . replaceInvalidCredentials ( updateData as WorkflowEntity ) ; 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 as string , 10 ) === this . executionTimeout ) { delete updateData . settings . executionTimeout ; } } updateData . updatedAt = this . getCurrentDate ( ) ; await WorkflowHelpers . validateWorkflow ( updateData ) ; await Db . collections . Workflow ! . update ( id , updateData ) . catch ( 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 Db . collections . Workflow ! . findOne ( id , { relations : [ 'tags' ] } ) ; if ( workflow === undefined ) { throw new ResponseHelper . ResponseError ( `Workflow with id "${ id }" could not be found to be updated.` , undefined , 400 ) ; } if ( tags ?. length ) { workflow . tags = TagHelpers . sortByRequestOrder ( workflow . tags , tags ) ; } await this . externalHooks . run ( 'workflow.afterUpdate' , [ workflow ] ) ; void 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 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` , ResponseHelper . send ( async ( req , res ) : Promise < boolean > => { 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 Db . collections . Workflow ! . delete ( id ) ; void InternalHooksManager . getInstance ( ) . onWorkflowDeleted ( id ) ; await this . externalHooks . run ( 'workflow.afterDelete' , [ id ] ) ; return true ; } ) ) ; this . app . post ( `/${ this . restEndpoint }/workflows/run` , ResponseHelper . send ( async ( req , res ) : Promise < IExecutionPushResponse > => { const { workflowData } = req . body ; const { runData } = req . body ; const { startNodes } = req . body ; const { destinationNode } = req . body ; const executionMode = 'manual' ; const activationMode = 'manual' ; const sessionId = GenericHelpers . getSessionId ( req ) ; if ( runData === undefined || startNodes === undefined || startNodes . length === 0 || destinationNode === undefined ) { const additionalData = await WorkflowExecuteAdditionalData . getBase ( ) ; const nodeTypes = NodeTypes ( ) ; const workflowInstance = new 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 WorkflowRunner ( ) ; const executionId = await workflowRunner . run ( data ) ; return { executionId , } ; } ) ) ; this . app . get ( `/${ this . restEndpoint }/tags` , ResponseHelper . send ( async ( req , res ) : Promise < TagEntity [ ] | ITagWithCountDb [ ] > => { if ( req . query . withUsageCount === 'true' ) { const tablePrefix = config . get ( 'database.tablePrefix' ) ; return TagHelpers . getTagsWithCountDb ( tablePrefix ) ; } const tags = await Db . collections . Tag ! . find ( { select : [ 'id' , 'name' ] } ) ; tags . forEach ( ( tag ) => ( tag . id = tag . id . toString ( ) ) ) ; return tags ; } ) ) ; this . app . post ( `/${ this . restEndpoint }/tags` , ResponseHelper . send ( async ( req , res ) : Promise < TagEntity | void > => { const newTag = new TagEntity ( ) ; newTag . name = req . body . name . trim ( ) ; await this . externalHooks . run ( 'tag.beforeCreate' , [ newTag ] ) ; await TagHelpers . validateTag ( newTag ) ; const tag = await 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` , ResponseHelper . send ( async ( req , res ) : Promise < TagEntity | void > => { const { name } = req . body ; const { id } = req . params ; const newTag = new TagEntity ( ) ; newTag . id = Number ( id ) ; newTag . name = name . trim ( ) ; await this . externalHooks . run ( 'tag.beforeUpdate' , [ newTag ] ) ; await TagHelpers . validateTag ( newTag ) ; const tag = await 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` , ResponseHelper . send ( async ( req , res ) : Promise < boolean > => { const id = Number ( req . params . id ) ; await this . externalHooks . run ( 'tag.beforeDelete' , [ id ] ) ; await Db . collections . Tag ! . delete ( { id } ) ; await this . externalHooks . run ( 'tag.afterDelete' , [ id ] ) ; return true ; } ) ) ; this . app . get ( `/${ this . restEndpoint }/node-parameter-options` , ResponseHelper . send ( async ( req , res ) : Promise < INodePropertyOptions [ ] > => { const nodeTypeAndVersion = JSON . parse ( `${ req . query . nodeTypeAndVersion }` ) as INodeTypeNameVersion ; const path = req . query . path as string ; let credentials ; const currentNodeParameters = JSON . parse ( `${ req . query . currentNodeParameters }` ) as INodeParameters ; if ( req . query . credentials !== undefined ) { credentials = JSON . parse ( req . query . credentials as string ) ; } const methodName = req . query . methodName as string ; const nodeTypes = NodeTypes ( ) ; const loadDataInstance = new LoadNodeParameterOptions ( nodeTypeAndVersion , nodeTypes , path , currentNodeParameters , credentials ) ; const additionalData = await WorkflowExecuteAdditionalData . getBase ( currentNodeParameters ) ; return loadDataInstance . getOptions ( methodName , additionalData ) ; } ) ) ; this . app . get ( `/${ this . restEndpoint }/node-types` , ResponseHelper . send ( async ( req , res ) : Promise < INodeTypeDescription [ ] > => { const returnData = [ ] ; const onlyLatest = req . query . onlyLatest === 'true' ; const nodeTypes = NodeTypes ( ) ; const allNodes = nodeTypes . getAll ( ) ; const getNodeDescription = ( nodeType ) : INodeTypeDescription => { const nodeInfo = { ... nodeType . description } ; if ( req . query . includeProperties !== 'true' ) { delete nodeInfo . properties ; } return nodeInfo ; } ; if ( onlyLatest ) { allNodes . forEach ( ( nodeData ) => { const nodeType = NodeHelpers . getVersionedNodeType ( nodeData ) ; const nodeInfo = getNodeDescription ( nodeType ) ; returnData . push ( nodeInfo ) ; } ) ; } else { allNodes . forEach ( ( nodeData ) => { const allNodeTypes = NodeHelpers . getVersionedNodeTypeAll ( nodeData ) ; allNodeTypes . forEach ( ( element ) => { const nodeInfo = getNodeDescription ( element ) ; returnData . push ( nodeInfo ) ; } ) ; } ) ; } return returnData ; } ) ) ; this . app . get ( `/${ this . restEndpoint }/credential-translation` , ResponseHelper . send ( async ( req , res ) : Promise < object | null > => { const translationPath = getCredentialTranslationPath ( { locale : this . frontendSettings . defaultLocale , credentialType : req . query . credentialType , } ) ; try { return require ( translationPath ) ; } catch ( error ) { return null ; } } ) ) ; this . app . post ( `/${ this . restEndpoint }/node-types` , ResponseHelper . send ( async ( req , res ) : Promise < INodeTypeDescription [ ] > => { const nodeInfos = _ . get ( req , 'body.nodeInfos' , [ ] ) as INodeTypeNameVersion [ ] ; const { defaultLocale } = this . frontendSettings ; if ( defaultLocale === 'en' ) { return nodeInfos . reduce < INodeTypeDescription [ ] > ( ( acc , { name , version } ) => { const { description } = NodeTypes ( ) . getByNameAndVersion ( name , version ) ; acc . push ( description ) ; return acc ; } , [ ] ) ; } async function populateTranslation ( name , version , nodeTypes ) { const { description , sourcePath } = NodeTypes ( ) . getWithSourcePath ( name , version ) ; const translationPath = await getNodeTranslationPath ( { nodeSourcePath : sourcePath , longNodeType : description . name , locale : defaultLocale , } ) ; try { const translation = await readFile ( translationPath , 'utf8' ) ; description . translation = JSON . parse ( translation ) ; } catch ( error ) { } nodeTypes . push ( description ) ; } const nodeTypes = [ ] ; const promises = nodeInfos . map ( async ( { name , version } ) => populateTranslation ( name , version , nodeTypes ) ) ; await Promise . all ( promises ) ; return nodeTypes ; } ) ) ; this . app . get ( `/${ this . restEndpoint }/node-translation-headers` , ResponseHelper . send ( async ( req , res ) : Promise < object | void > => { const packagesPath = pathJoin ( __dirname , '..' , '..' , '..' ) ; const headersPath = pathJoin ( packagesPath , 'nodes-base' , 'dist' , 'nodes' , 'headers' ) ; try { return require ( headersPath ) ; } catch ( error ) { res . status ( 500 ) . send ( 'Failed to find headers file' ) ; } } ) ) ; this . app . get ( [ `/${ this . restEndpoint }/node-icon/:nodeType` , `/${ this . restEndpoint }/node-icon/:scope/:nodeType` , ] , async ( req , res ) : Promise < void > => { try { const nodeTypeName = `${ req . params . scope ? `${ req . params . scope }/` : '' }${ req . params . nodeType }` ; const nodeTypes = 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 ResponseHelper . sendErrorResponse ( res , error ) ; } } ) ; this . app . get ( `/${ this . restEndpoint }/active` , ResponseHelper . send ( async ( req , res ) : Promise < string [ ] > => { const activeWorkflows = await this . activeWorkflowRunner . getActiveWorkflows ( ) ; return activeWorkflows . map ( ( workflow ) => workflow . id . toString ( ) ) ; } ) ) ; this . app . get ( `/${ this . restEndpoint }/active/error/:id` , ResponseHelper . send ( async ( req , res ) : Promise < IActivationError | undefined > => { const { id } = req . params ; return this . activeWorkflowRunner . getActivationError ( id ) ; } ) ) ; this . app . get ( `/${ this . restEndpoint }/credentials/new` , ResponseHelper . send ( async ( req , res ) : Promise < { name : string ; } > => { const requestedName = req . query . name && req . query . name !== '' ? req . query . name : this . defaultCredentialsName ; return await GenericHelpers . generateUniqueName ( requestedName , 'credentials' ) ; } ) ) ; this . app . delete ( `/${ this . restEndpoint }/credentials/:id` , ResponseHelper . send ( async ( req , res ) : Promise < boolean > => { const { id } = req . params ; await this . externalHooks . run ( 'credentials.delete' , [ id ] ) ; await Db . collections . Credentials ! . delete ( { id } ) ; return true ; } ) ) ; this . app . post ( `/${ this . restEndpoint }/credentials` , ResponseHelper . send ( async ( req , res ) : Promise < ICredentialsResponse > => { const incomingData = req . body ; if ( ! incomingData . name || incomingData . name . length < 3 ) { throw new 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 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 Credentials ( { id : null , name : incomingData . name } , incomingData . type , incomingData . nodesAccess ) ; credentials . setData ( incomingData . data , encryptionKey ) ; const newCredentialsData = credentials . getDataToSave ( ) as ICredentialsDb ; await this . externalHooks . run ( 'credentials.create' , [ newCredentialsData ] ) ; const result = await Db . collections . Credentials ! . save ( newCredentialsData ) ; result . data = incomingData . data ; ( result as unknown as ICredentialsResponse ) . id = result . id . toString ( ) ; return result as unknown as ICredentialsResponse ; } ) ) ; this . app . post ( `/${ this . restEndpoint }/credentials-test` , ResponseHelper . send ( async ( req , res ) : Promise < NodeCredentialTestResult > => { const incomingData = req . body as NodeCredentialTestRequest ; const credentialType = incomingData . credentials . type ; const nodeTypes = NodeTypes ( ) ; const allNodes = nodeTypes . getAll ( ) ; let foundTestFunction ; const nodeThatCanTestThisCredential = allNodes . find ( ( node ) => { if ( incomingData . nodeToTestWith && node . description . name !== incomingData . nodeToTestWith ) { return false ; } if ( node instanceof NodeVersionedType ) { const versionNames = Object . keys ( ( node as INodeVersionedType ) . nodeVersions ) ; for ( const versionName of versionNames ) { const nodeType = ( node as INodeVersionedType ) . nodeVersions [ versionName as unknown as number ] ; const credentialTestable = nodeType . description . credentials ?. find ( ( credential ) => { const testFunctionSearch = credential . name === credentialType && ! ! credential . testedBy ; if ( testFunctionSearch ) { foundTestFunction = ( nodeType as unknown as INodeType ) . methods ! . credentialTest ! [ credential . testedBy ! ] ; } return testFunctionSearch ; } ) ; if ( credentialTestable ) { return true ; } } return false ; } const credentialTestable = ( node as INodeType ) . description . credentials ?. find ( ( credential ) => { const testFunctionSearch = credential . name === credentialType && ! ! credential . testedBy ; if ( testFunctionSearch ) { foundTestFunction = ( node as INodeType ) . 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 = NodeExecuteFunctions . getCredentialTestFunctions ( ) ; const output = await foundTestFunction . call ( credentialTestFunctions , incomingData . credentials ) ; return Promise . resolve ( output ) ; } ) ) ; this . app . patch ( `/${ this . restEndpoint }/credentials/:id` , ResponseHelper . send ( async ( req , res ) : Promise < ICredentialsResponse > => { 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 UserSettings . getEncryptionKey ( ) ; if ( encryptionKey === undefined ) { throw new Error ( 'No encryption key got found to encrypt the credentials!' ) ; } const result = await Db . collections . Credentials ! . findOne ( id ) ; if ( result === undefined ) { throw new ResponseHelper . ResponseError ( `Credentials with the id "${ id }" do not exist.` , undefined , 400 ) ; } const currentlySavedCredentials = new Credentials ( result as INodeCredentialsDetails , result . type , result . nodesAccess , result . data ) ; const decryptedData = currentlySavedCredentials . getData ( encryptionKey ) ; if ( decryptedData . oauthTokenData ) { incomingData . data . oauthTokenData = decryptedData . oauthTokenData ; } const credentials = new Credentials ( { id , name : incomingData . name } , incomingData . type , incomingData . nodesAccess ) ; credentials . setData ( incomingData . data , encryptionKey ) ; const newCredentialsData = credentials . getDataToSave ( ) as unknown as ICredentialsDb ; newCredentialsData . updatedAt = this . getCurrentDate ( ) ; await this . externalHooks . run ( 'credentials.update' , [ newCredentialsData ] ) ; await Db . collections . Credentials ! . update ( id , newCredentialsData ) ; const responseData = await Db . collections . Credentials ! . findOne ( id ) ; if ( responseData === undefined ) { throw new ResponseHelper . ResponseError ( `Credentials with id "${ id }" could not be found to be updated.` , undefined , 400 ) ; } responseData . data = '' ; ( responseData as unknown as ICredentialsResponse ) . id = responseData . id . toString ( ) ; return responseData as unknown as ICredentialsResponse ; } ) ) ; this . app . get ( `/${ this . restEndpoint }/credentials/:id` , ResponseHelper . send ( async ( req , res ) : Promise < ICredentialsDecryptedResponse | ICredentialsResponse | undefined > => { const findQuery = { } as FindManyOptions ; const includeData = [ 'true' , true ] . includes ( req . query . includeData as string ) ; if ( ! includeData ) { findQuery . select = [ 'id' , 'name' , 'type' , 'nodesAccess' , 'createdAt' , 'updatedAt' ] ; } const result = await Db . collections . Credentials ! . findOne ( req . params . id ) ; if ( result === undefined ) { return result ; } let encryptionKey ; if ( includeData ) { encryptionKey = await UserSettings . getEncryptionKey ( ) ; if ( encryptionKey === undefined ) { throw new Error ( 'No encryption key got found to decrypt the credentials!' ) ; } const credentials = new Credentials ( result as INodeCredentialsDetails , result . type , result . nodesAccess , result . data ) ; ( result as ICredentialsDecryptedDb ) . data = credentials . getData ( encryptionKey ) ; } ( result as ICredentialsDecryptedResponse ) . id = result . id . toString ( ) ; return result as ICredentialsDecryptedResponse ; } ) ) ; this . app . get ( `/${ this . restEndpoint }/credentials` , ResponseHelper . send ( async ( req , res ) : Promise < ICredentialsResponse [ ] > => { const findQuery = { } as FindManyOptions ; if ( req . query . filter ) { findQuery . where = JSON . parse ( req . query . filter as string ) as IDataObject ; if ( findQuery . where . id !== undefined ) { findQuery . where = { id : findQuery . where . id } ; } } findQuery . select = [ 'id' , 'name' , 'type' , 'nodesAccess' , 'createdAt' , 'updatedAt' ] ; const results = ( await Db . collections . Credentials ! . find ( findQuery ) ) as unknown as ICredentialsResponse [ ] ; let encryptionKey ; const includeData = [ 'true' , true ] . includes ( req . query . includeData as string ) ; if ( includeData ) { encryptionKey = await UserSettings . getEncryptionKey ( ) ; if ( encryptionKey === undefined ) { throw new Error ( 'No encryption key got found to decrypt the credentials!' ) ; } } let result ; for ( result of results ) { ( result as ICredentialsDecryptedResponse ) . id = result . id . toString ( ) ; } return results ; } ) ) ; this . app . get ( `/${ this . restEndpoint }/credential-types` , ResponseHelper . send ( async ( req , res ) : Promise < ICredentialType [ ] > => { const returnData = [ ] ; const credentialTypes = CredentialTypes ( ) ; credentialTypes . getAll ( ) . forEach ( ( credentialData ) => { returnData . push ( credentialData ) ; } ) ; return returnData ; } ) ) ; this . app . get ( `/${ this . restEndpoint }/credential-icon/:credentialType` , async ( req , res ) : Promise < void > => { try { const credentialName = req . params . credentialType ; const credentialType = 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 ResponseHelper . sendErrorResponse ( res , error ) ; } } ) ; this . app . get ( `/${ this . restEndpoint }/oauth1-credential/auth` , ResponseHelper . send ( async ( req , res ) : Promise < string > => { if ( req . query . id === undefined ) { res . status ( 500 ) . send ( 'Required credential id is missing!' ) ; return '' ; } const result = await Db . collections . Credentials ! . findOne ( req . query . id as string ) ; if ( result === undefined ) { res . status ( 404 ) . send ( 'The credential is not known.' ) ; return '' ; } let encryptionKey ; encryptionKey = await 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 CredentialsHelper ( encryptionKey ) ; const decryptedDataOriginal = await credentialsHelper . getDecrypted ( result as INodeCredentialsDetails , result . type , mode , true ) ; const oauthCredentials = credentialsHelper . applyDefaultsAndOverwrites ( decryptedDataOriginal , result . type , mode ) ; const signatureMethod = _ . get ( oauthCredentials , 'signatureMethod' ) as string ; const oAuthOptions = { consumer : { key : _ . get ( oauthCredentials , 'consumerKey' ) as string , secret : _ . get ( oauthCredentials , 'consumerSecret' ) as string , } , signature_method : signatureMethod , hash_function ( base , key ) { const algorithm = signatureMethod === 'HMAC-SHA1' ? 'sha1' : 'sha256' ; return createHmac ( algorithm , key ) . update ( base ) . digest ( 'base64' ) ; } , } ; const oauthRequestData = { oauth_callback : `${ 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' ) as string , 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 Credentials ( result as INodeCredentialsDetails , result . type , result . nodesAccess ) ; credentials . setData ( decryptedDataOriginal , encryptionKey ) ; const newCredentialsData = credentials . getDataToSave ( ) as unknown as ICredentialsDb ; newCredentialsData . updatedAt = this . getCurrentDate ( ) ; await Db . collections . Credentials ! . update ( req . query . id as string , 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 ResponseHelper . ResponseError ( `Insufficient parameters for OAuth1 callback. Received following query parameters: ${ JSON . stringify ( req . query ) }` , undefined , 503 ) ; return ResponseHelper . sendErrorResponse ( res , errorResponse ) ; } const result = await Db . collections . Credentials ! . findOne ( cid as any ) ; if ( result === undefined ) { const errorResponse = new ResponseHelper . ResponseError ( 'The credential is not known.' , undefined , 404 ) ; return ResponseHelper . sendErrorResponse ( res , errorResponse ) ; } let encryptionKey ; encryptionKey = await UserSettings . getEncryptionKey ( ) ; if ( encryptionKey === undefined ) { const errorResponse = new ResponseHelper . ResponseError ( 'No encryption key got found to decrypt the credentials!' , undefined , 503 ) ; return ResponseHelper . sendErrorResponse ( res , errorResponse ) ; } const mode = 'internal' ; const credentialsHelper = new CredentialsHelper ( encryptionKey ) ; const decryptedDataOriginal = await credentialsHelper . getDecrypted ( result as INodeCredentialsDetails , result . type , mode , true ) ; const oauthCredentials = credentialsHelper . applyDefaultsAndOverwrites ( decryptedDataOriginal , result . type , mode ) ; const options = { method : 'POST' , url : _ . get ( oauthCredentials , 'accessTokenUrl' ) as string , qs : { oauth_token , oauth_verifier , } , } ; let oauthToken ; try { oauthToken = await requestPromise ( options ) ; } catch ( error ) { const errorResponse = new ResponseHelper . ResponseError ( 'Unable to get access tokens!' , undefined , 404 ) ; return ResponseHelper . sendErrorResponse ( res , errorResponse ) ; } const oauthTokenJson = querystring . parse ( oauthToken ) ; decryptedDataOriginal . oauthTokenData = oauthTokenJson ; const credentials = new Credentials ( result as INodeCredentialsDetails , result . type , result . nodesAccess ) ; credentials . setData ( decryptedDataOriginal , encryptionKey ) ; const newCredentialsData = credentials . getDataToSave ( ) as unknown as ICredentialsDb ; newCredentialsData . updatedAt = this . getCurrentDate ( ) ; await Db . collections . Credentials ! . update ( cid as any , newCredentialsData ) ; res . sendFile ( pathResolve ( __dirname , '../../templates/oauth-callback.html' ) ) ; } catch ( error ) { return ResponseHelper . sendErrorResponse ( res , error ) ; } } ) ; this . app . get ( `/${ this . restEndpoint }/oauth2-credential/auth` , ResponseHelper . send ( async ( req , res ) : Promise < string > => { if ( req . query . id === undefined ) { res . status ( 500 ) . send ( 'Required credential id is missing.' ) ; return '' ; } const result = await Db . collections . Credentials ! . findOne ( req . query . id as string ) ; if ( result === undefined ) { res . status ( 404 ) . send ( 'The credential is not known.' ) ; return '' ; } let encryptionKey ; encryptionKey = await 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 CredentialsHelper ( encryptionKey ) ; const decryptedDataOriginal = await credentialsHelper . getDecrypted ( result as INodeCredentialsDetails , result . type , mode , true ) ; const oauthCredentials = credentialsHelper . applyDefaultsAndOverwrites ( decryptedDataOriginal , result . type , mode ) ; const token = new csrf ( ) ; 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 = { 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 ) ; const credentials = new Credentials ( result as INodeCredentialsDetails , result . type , result . nodesAccess ) ; decryptedDataOriginal . csrfSecret = csrfSecret ; credentials . setData ( decryptedDataOriginal , encryptionKey ) ; const newCredentialsData = credentials . getDataToSave ( ) as unknown as ICredentialsDb ; newCredentialsData . updatedAt = this . getCurrentDate ( ) ; await Db . collections . Credentials ! . update ( req . query . id as string , newCredentialsData ) ; const authQueryParameters = _ . get ( oauthCredentials , 'authQueryParameters' , '' ) as string ; let returnUri = oAuthObj . code . getUri ( ) ; 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 }` ; } return returnUri ; } ) ) ; this . app . get ( `/${ this . restEndpoint }/oauth2-credential/callback` , async ( req , res ) => { try { const { code , state : stateEncoded } = req . query ; if ( code === undefined || stateEncoded === undefined ) { 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 as string , 'base64' ) . toString ( ) ) ; } catch ( error ) { const errorResponse = new ResponseHelper . ResponseError ( 'Invalid state format returned' , undefined , 503 ) ; return ResponseHelper . sendErrorResponse ( res , errorResponse ) ; } const result = await Db . collections . Credentials ! . findOne ( state . cid ) ; if ( result === undefined ) { const errorResponse = new ResponseHelper . ResponseError ( 'The credential is not known.' , undefined , 404 ) ; return ResponseHelper . sendErrorResponse ( res , errorResponse ) ; } let encryptionKey ; encryptionKey = await UserSettings . getEncryptionKey ( ) ; if ( encryptionKey === undefined ) { const errorResponse = new ResponseHelper . ResponseError ( 'No encryption key got found to decrypt the credentials!' , undefined , 503 ) ; return ResponseHelper . sendErrorResponse ( res , errorResponse ) ; } const mode = 'internal' ; const credentialsHelper = new CredentialsHelper ( encryptionKey ) ; const decryptedDataOriginal = await credentialsHelper . getDecrypted ( result as INodeCredentialsDetails , result . type , mode , true ) ; const oauthCredentials = credentialsHelper . applyDefaultsAndOverwrites ( decryptedDataOriginal , result . type , mode ) ; const token = new csrf ( ) ; if ( decryptedDataOriginal . csrfSecret === undefined || ! token . verify ( decryptedDataOriginal . csrfSecret as string , state . token ) ) { 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 ) { const errorResponse = new ResponseHelper . ResponseError ( 'Unable to get access tokens!' , undefined , 404 ) ; return ResponseHelper . sendErrorResponse ( res , errorResponse ) ; } if ( decryptedDataOriginal . oauthTokenData ) { Object . assign ( decryptedDataOriginal . oauthTokenData , oauthToken . data ) ; } else { decryptedDataOriginal . oauthTokenData = oauthToken . data ; } _ . unset ( decryptedDataOriginal , 'csrfSecret' ) ; const credentials = new Credentials ( result as INodeCredentialsDetails , result . type , result . nodesAccess ) ; credentials . setData ( decryptedDataOriginal , encryptionKey ) ; const newCredentialsData = credentials . getDataToSave ( ) as unknown as ICredentialsDb ; newCredentialsData . updatedAt = this . getCurrentDate ( ) ; await Db . collections . Credentials ! . update ( state . cid , newCredentialsData ) ; res . sendFile ( pathResolve ( __dirname , '../../templates/oauth-callback.html' ) ) ; } catch ( error ) { return ResponseHelper . sendErrorResponse ( res , error ) ; } } ) ; this . app . get ( `/${ this . restEndpoint }/executions` , ResponseHelper . send ( async ( req , res ) : Promise < IExecutionsListResponse > => { let filter = { } ; if ( req . query . filter ) { filter = JSON . parse ( req . query . filter as string ) ; } let limit = 20 ; if ( req . query . limit ) { limit = parseInt ( req . query . limit as string , 10 ) ; } const executingWorkflowIds = [ ] ; if ( config . get ( 'executions.mode' ) === 'queue' ) { const currentJobs = await Queue . getInstance ( ) . getJobs ( [ 'active' , 'waiting' ] ) ; executingWorkflowIds . push ( ... ( currentJobs . map ( ( job ) => job . data . executionId ) as string [ ] ) ) ; } executingWorkflowIds . push ( ... this . activeExecutionsInstance . getActiveExecutions ( ) . map ( ( execution ) => execution . id . toString ( ) ) ) ; const countFilter = JSON . parse ( JSON . stringify ( filter ) ) ; if ( countFilter . waitTill !== undefined ) { countFilter . waitTill = Not ( IsNull ( ) ) ; } countFilter . id = Not ( In ( executingWorkflowIds ) ) ; const resultsQuery = await Db . collections . Execution ! . createQueryBuilder ( 'execution' ) . select ( [ 'execution.id' , 'execution.finished' , 'execution.mode' , 'execution.retryOf' , 'execution.retrySuccessId' , 'execution.waitTill' , 'execution.startedAt' , 'execution.stoppedAt' , 'execution.workflowData' , ] ) . orderBy ( 'execution.id' , 'DESC' ) . take ( limit ) ; Object . keys ( filter ) . forEach ( ( filterField ) => { if ( filterField === 'waitTill' ) { resultsQuery . andWhere ( `execution.${ filterField } is not null` ) ; } else if ( filterField === 'finished' && filter [ filterField ] === false ) { resultsQuery . andWhere ( `execution.${ filterField } = :${ filterField }` , { [ filterField ] : filter [ filterField ] , } ) ; resultsQuery . andWhere ( `execution.waitTill is null` ) ; } else { resultsQuery . andWhere ( `execution.${ filterField } = :${ filterField }` , { [ filterField ] : filter [ filterField ] , } ) ; } } ) ; if ( req . query . lastId ) { resultsQuery . andWhere ( `execution.id < :lastId` , { lastId : req . query . lastId } ) ; } if ( req . query . firstId ) { resultsQuery . andWhere ( `execution.id > :firstId` , { firstId : req . query . firstId } ) ; } if ( executingWorkflowIds . length > 0 ) { resultsQuery . andWhere ( `execution.id NOT IN (:...ids)` , { ids : executingWorkflowIds } ) ; } const resultsPromise = resultsQuery . getMany ( ) ; const countPromise = getExecutionsCount ( countFilter ) ; const results = await resultsPromise ; const countedObjects = await countPromise ; const returnResults = [ ] ; for ( const result of results ) { returnResults . push ( { id : result . id . toString ( ) , finished : result . finished , mode : result . mode , retryOf : result . retryOf ? result . retryOf . toString ( ) : undefined , retrySuccessId : result . retrySuccessId ? result . retrySuccessId . toString ( ) : undefined , waitTill : result . waitTill as Date | undefined , startedAt : result . startedAt , stoppedAt : result . stoppedAt , workflowId : result . workflowData . id ? result . workflowData . id . toString ( ) : '' , workflowName : result . workflowData . name , } ) ; } return { count : countedObjects . count , results : returnResults , estimated : countedObjects . estimate , } ; } ) ) ; this . app . get ( `/${ this . restEndpoint }/executions/:id` , ResponseHelper . send ( async ( req , res ) : Promise < IExecutionResponse | IExecutionFlattedResponse | undefined > => { const result = await Db . collections . Execution ! . findOne ( req . params . id ) ; if ( result === undefined ) { return undefined ; } if ( req . query . unflattedResponse === 'true' ) { const fullExecutionData = ResponseHelper . unflattenExecutionData ( result ) ; return fullExecutionData ; } ( result as IExecutionFlatted as IExecutionFlattedResponse ) . id = result . id . toString ( ) ; return result as IExecutionFlatted as IExecutionFlattedResponse ; } ) ) ; this . app . post ( `/${ this . restEndpoint }/executions/:id/retry` , ResponseHelper . send ( async ( req , res ) : Promise < boolean > => { const fullExecutionDataFlatted = await Db . collections . Execution ! . findOne ( req . params . id ) ; if ( fullExecutionDataFlatted === undefined ) { throw new ResponseHelper . ResponseError ( `The execution with the id "${ req . params . id }" does not exist.` , 404 , 404 ) ; } const fullExecutionData = ResponseHelper . unflattenExecutionData ( fullExecutionDataFlatted ) ; if ( fullExecutionData . finished ) { throw new Error ( 'The execution did succeed and can so not be retried.' ) ; } const executionMode = 'retry' ; fullExecutionData . workflowData . active = false ; const data = { executionMode , executionData : fullExecutionData . data , retryOf : req . params . id , workflowData : fullExecutionData . workflowData , } ; const { lastNodeExecuted } = data . executionData ! . resultData ; if ( lastNodeExecuted ) { delete data . executionData ! . resultData . error ; const { length } = data . executionData ! . resultData . runData [ lastNodeExecuted ] ; if ( length > 0 && data . executionData ! . resultData . runData [ lastNodeExecuted ] [ length - 1 ] . error !== undefined ) { data . executionData ! . resultData . runData [ lastNodeExecuted ] . pop ( ) ; } } if ( req . body . loadWorkflow === true ) { const workflowId = fullExecutionData . workflowData . id ; const workflowData = ( await Db . collections . Workflow ! . findOne ( workflowId ) ) as IWorkflowBase ; if ( workflowData === undefined ) { throw new Error ( `The workflow with the ID "${ workflowId }" could not be found and so the data not be loaded for the retry.` ) ; } data . workflowData = workflowData ; const nodeTypes = NodeTypes ( ) ; const workflowInstance = new Workflow ( { id : workflowData . id as string , name : workflowData . name , nodes : workflowData . nodes , connections : workflowData . connections , active : false , nodeTypes , staticData : undefined , settings : workflowData . settings , } ) ; for ( const stack of data . executionData ! . executionData ! . nodeExecutionStack ) { const node = workflowInstance . getNode ( stack . node . name ) ; if ( node === null ) { throw new Error ( `Could not find the node "${ stack . node . name }" in workflow. It probably got deleted or renamed. Without it the workflow can sadly not be retried.` ) ; } stack . node = node ; } } const workflowRunner = new WorkflowRunner ( ) ; const executionId = await workflowRunner . run ( data ) ; const executionData = await this . activeExecutionsInstance . getPostExecutePromise ( executionId ) ; if ( executionData === undefined ) { throw new Error ( 'The retry did not start for an unknown reason.' ) ; } return ! ! executionData . finished ; } ) ) ; this . app . post ( `/${ this . restEndpoint }/executions/delete` , ResponseHelper . send ( async ( req , res ) : Promise < void > => { const deleteData = req . body as IExecutionDeleteFilter ; if ( deleteData . deleteBefore !== undefined ) { const filters = { startedAt : LessThanOrEqual ( deleteData . deleteBefore ) , } ; if ( deleteData . filters !== undefined ) { Object . assign ( filters , deleteData . filters ) ; } const execs = await Db . collections . Execution ! . find ( { ... filters , select : [ 'id' ] } ) ; await Promise . all ( execs . map ( async ( item ) => BinaryDataManager . getInstance ( ) . deleteBinaryDataByExecutionId ( item . id . toString ( ) ) ) ) ; await Db . collections . Execution ! . delete ( filters ) ; } else if ( deleteData . ids !== undefined ) { await Promise . all ( deleteData . ids . map ( async ( id ) => BinaryDataManager . getInstance ( ) . deleteBinaryDataByExecutionId ( id ) ) ) ; await Db . collections . Execution ! . delete ( deleteData . ids ) ; } else { throw new Error ( 'Required body-data "ids" or "deleteBefore" is missing!' ) ; } } ) ) ; this . app . get ( `/${ this . restEndpoint }/executions-current` , ResponseHelper . send ( async ( req , res ) : Promise < IExecutionsSummary [ ] > => { if ( config . get ( 'executions.mode' ) === 'queue' ) { const currentJobs = await Queue . getInstance ( ) . getJobs ( [ 'active' , 'waiting' ] ) ; const currentlyRunningQueueIds = currentJobs . map ( ( job ) => job . data . executionId ) ; const currentlyRunningManualExecutions = this . activeExecutionsInstance . getActiveExecutions ( ) ; const manualExecutionIds = currentlyRunningManualExecutions . map ( ( execution ) => execution . id ) ; const currentlyRunningExecutionIds = currentlyRunningQueueIds . concat ( manualExecutionIds ) ; if ( currentlyRunningExecutionIds . length === 0 ) { return [ ] ; } const resultsQuery = await Db . collections . Execution ! . createQueryBuilder ( 'execution' ) . select ( [ 'execution.id' , 'execution.workflowId' , 'execution.mode' , 'execution.retryOf' , 'execution.startedAt' , ] ) . orderBy ( 'execution.id' , 'DESC' ) . andWhere ( `execution.id IN (:...ids)` , { ids : currentlyRunningExecutionIds } ) ; if ( req . query . filter ) { const filter = JSON . parse ( req . query . filter as string ) ; if ( filter . workflowId !== undefined ) { resultsQuery . andWhere ( 'execution.workflowId = :workflowId' , { workflowId : filter . workflowId , } ) ; } } const results = await resultsQuery . getMany ( ) ; return results . map ( ( result ) => { return { id : result . id , workflowId : result . workflowId , mode : result . mode , retryOf : result . retryOf !== null ? result . retryOf : undefined , startedAt : new Date ( result . startedAt ) , } as IExecutionsSummary ; } ) ; } const executingWorkflows = this . activeExecutionsInstance . getActiveExecutions ( ) ; const returnData = [ ] ; let filter = { } ; if ( req . query . filter ) { filter = JSON . parse ( req . query . filter as string ) ; } for ( const data of executingWorkflows ) { if ( filter . workflowId !== undefined && filter . workflowId !== data . workflowId ) { continue ; } returnData . push ( { id : data . id . toString ( ) , workflowId : data . workflowId === undefined ? '' : data . workflowId . toString ( ) , mode : data . mode , retryOf : data . retryOf , startedAt : new Date ( data . startedAt ) , } ) ; } returnData . sort ( ( a , b ) => parseInt ( b . id , 10 ) - parseInt ( a . id , 10 ) ) ; return returnData ; } ) ) ; this . app . post ( `/${ this . restEndpoint }/executions-current/:id/stop` , ResponseHelper . send ( async ( req , res ) : Promise < IExecutionsStopData > => { if ( config . get ( 'executions.mode' ) === 'queue' ) { const result = await this . activeExecutionsInstance . stopExecution ( req . params . id ) ; if ( result === undefined ) { try { return await this . waitTracker . stopExecution ( req . params . id ) ; } catch ( error ) { } } else { return { mode : result . mode , startedAt : new Date ( result . startedAt ) , stoppedAt : result . stoppedAt ? new Date ( result . stoppedAt ) : undefined , finished : result . finished , } as IExecutionsStopData ; } const currentJobs = await Queue . getInstance ( ) . getJobs ( [ 'active' , 'waiting' ] ) ; const job = currentJobs . find ( ( job ) => job . data . executionId . toString ( ) === req . params . id ) ; if ( ! job ) { throw new Error ( `Could not stop "${ req . params . id }" as it is no longer in queue.` ) ; } else { await Queue . getInstance ( ) . stopJob ( job ) ; } const executionDb = ( await Db . collections . Execution ?. findOne ( req . params . id ) ) as IExecutionFlattedDb ; const fullExecutionData = ResponseHelper . unflattenExecutionData ( executionDb ) ; const returnData = { mode : fullExecutionData . mode , startedAt : new Date ( fullExecutionData . startedAt ) , stoppedAt : fullExecutionData . stoppedAt ? new Date ( fullExecutionData . stoppedAt ) : undefined , finished : fullExecutionData . finished , } ; return returnData ; } const executionId = req . params . id ; const result = await this . activeExecutionsInstance . stopExecution ( executionId ) ; let returnData ; if ( result === undefined ) { returnData = await this . waitTracker . stopExecution ( executionId ) ; } else { returnData = { mode : result . mode , startedAt : new Date ( result . startedAt ) , stoppedAt : result . stoppedAt ? new Date ( result . stoppedAt ) : undefined , finished : result . finished , } ; } return returnData ; } ) ) ; this . app . delete ( `/${ this . restEndpoint }/test-webhook/:id` , ResponseHelper . send ( async ( req , res ) : Promise < boolean > => { const workflowId = req . params . id ; return this . testWebhooks . cancelTestWebhook ( workflowId ) ; } ) ) ; this . app . get ( `/${ this . restEndpoint }/options/timezones` , ResponseHelper . send ( async ( req , res ) : Promise < object > => { return timezones ; } ) ) ; this . app . get ( `/${ this . restEndpoint }/data/:path` , ResponseHelper . send ( async ( req , res ) : Promise < string > => { const dataPath = req . params . path ; return BinaryDataManager . getInstance ( ) . retrieveBinaryDataByIdentifier ( dataPath ) . then ( ( buffer ) => { return buffer . toString ( 'base64' ) ; } ) ; } ) ) ; this . app . get ( `/${ this . restEndpoint }/settings` , ResponseHelper . send ( async ( req , res ) : Promise < IN8nUISettings > => { return this . frontendSettings ; } ) ) ; this . app . post ( `/${ this . restEndpoint }/user-survey` , async ( req , res ) => { if ( ! this . frontendSettings . personalizationSurvey . shouldShow ) { ResponseHelper . sendErrorResponse ( res , new ResponseHelper . ResponseError ( 'User survey already submitted' , undefined , 400 ) , false ) ; } const answers = req . body as IPersonalizationSurveyAnswers ; await PersonalizationSurvey . writeSurveyToDisk ( answers ) ; this . frontendSettings . personalizationSurvey . shouldShow = false ; this . frontendSettings . personalizationSurvey . answers = answers ; ResponseHelper . sendSuccessResponse ( res , undefined , true , 200 ) ; void InternalHooksManager . getInstance ( ) . onPersonalizationSurveySubmitted ( answers ) ; } ) ; if ( config . get ( 'endpoints.disableProductionWebhooksOnMainProcess' ) !== true ) { WebhookServer . registerProductionWebhooks . apply ( this ) ; } this . app . head ( `/${ this . endpointWebhookTest }/*` , async ( req , res ) => { const requestUrl = ( req as ICustomRequest ) . parsedUrl ! . pathname ! . slice ( this . endpointWebhookTest . length + 2 ) ; let response ; try { response = await this . testWebhooks . callTestWebhook ( 'HEAD' , requestUrl , req , res ) ; } catch ( error ) { ResponseHelper . sendErrorResponse ( res , error ) ; return ; } if ( response . noWebhookResponse === true ) { return ; } ResponseHelper . sendSuccessResponse ( res , response . data , true , response . responseCode , response . headers ) ; } ) ; this . app . options ( `/${ this . endpointWebhookTest }/*` , async ( req , res ) => { const requestUrl = ( req as ICustomRequest ) . parsedUrl ! . pathname ! . slice ( this . endpointWebhookTest . length + 2 ) ; let allowedMethods ; try { allowedMethods = await this . testWebhooks . getWebhookMethods ( requestUrl ) ; allowedMethods . push ( 'OPTIONS' ) ; res . append ( 'Allow' , allowedMethods ) ; } catch ( error ) { ResponseHelper . sendErrorResponse ( res , error ) ; return ; } ResponseHelper . sendSuccessResponse ( res , { } , true , 204 ) ; } ) ; this . app . get ( `/${ this . endpointWebhookTest }/*` , async ( req , res ) => { const requestUrl = ( req as ICustomRequest ) . parsedUrl ! . pathname ! . slice ( this . endpointWebhookTest . length + 2 ) ; let response ; try { response = await this . testWebhooks . callTestWebhook ( 'GET' , requestUrl , req , res ) ; } catch ( error ) { ResponseHelper . sendErrorResponse ( res , error ) ; return ; } if ( response . noWebhookResponse === true ) { return ; } ResponseHelper . sendSuccessResponse ( res , response . data , true , response . responseCode , response . headers ) ; } ) ; this . app . post ( `/${ this . endpointWebhookTest }/*` , async ( req , res ) => { const requestUrl = ( req as ICustomRequest ) . parsedUrl ! . pathname ! . slice ( this . endpointWebhookTest . length + 2 ) ; let response ; try { response = await this . testWebhooks . callTestWebhook ( 'POST' , requestUrl , req , res ) ; } catch ( error ) { ResponseHelper . sendErrorResponse ( res , error ) ; return ; } if ( response . noWebhookResponse === true ) { return ; } ResponseHelper . sendSuccessResponse ( res , response . data , true , response . responseCode , response . headers ) ; } ) ; if ( this . endpointPresetCredentials !== '' ) { this . app . post ( `/${ this . endpointPresetCredentials }` , async ( req , res ) => { if ( ! this . presetCredentialsLoaded ) { const body = req . body as ICredentialsOverwrite ; if ( req . headers [ 'content-type' ] !== 'application/json' ) { ResponseHelper . sendErrorResponse ( res , new Error ( 'Body must be a valid JSON, make sure the content-type is application/json' ) ) ; return ; } const credentialsOverwrites = CredentialsOverwrites ( ) ; await credentialsOverwrites . init ( body ) ; this . presetCredentialsLoaded = true ; ResponseHelper . sendSuccessResponse ( res , { success : true } , true , 200 ) ; } else { ResponseHelper . sendErrorResponse ( res , new Error ( 'Preset credentials can be set once' ) ) ; } } ) ; } if ( config . get ( 'endpoints.disableUi' ) !== true ) { const editorUiPath = require . resolve ( 'n8n-editor-ui' ) ; const filePath = pathJoin ( pathDirname ( editorUiPath ) , 'dist' , 'index.html' ) ; const n8nPath = config . get ( 'path' ) ; let readIndexFile = readFileSync ( filePath , 'utf8' ) ; readIndexFile = readIndexFile . replace ( /\/%BASE_PATH%\//g , n8nPath ) ; readIndexFile = readIndexFile . replace ( /\/favicon.ico/g , `${ n8nPath }favicon.ico` ) ; this . app . get ( `/index.html` , async ( req , res ) => { res . send ( readIndexFile ) ; } ) ; this . app . use ( '/' , express . static ( pathJoin ( pathDirname ( editorUiPath ) , 'dist' ) , { index : 'index.html' , setHeaders : ( res , path ) => { if ( res . req && res . req . url === '/index.html' ) { res . setHeader ( 'Last-Modified' , startTime ) ; } } , } ) ) ; } const startTime = new Date ( ) . toUTCString ( ) ; } } export async function start ( ) { const PORT = config . get ( 'port' ) ; const ADDRESS = config . get ( 'listen_address' ) ; const app = new App ( ) ; await app . config ( ) ; let server ; if ( app . protocol === 'https' && app . sslKey && app . sslCert ) { const https = require ( 'https' ) ; const privateKey = readFileSync ( app . sslKey , 'utf8' ) ; const cert = readFileSync ( app . sslCert , 'utf8' ) ; const credentials = { key : privateKey , cert } ; server = https . createServer ( credentials , app . app ) ; } else { const http = require ( 'http' ) ; server = http . createServer ( app . app ) ; } server . listen ( PORT , ADDRESS , async ( ) => { const versions = await GenericHelpers . getVersions ( ) ; console . log ( `n8n ready on ${ ADDRESS }, port ${ PORT }` ) ; console . log ( `Version: ${ versions . cli }` ) ; const defaultLocale = config . get ( 'defaultLocale' ) ; if ( defaultLocale !== 'en' ) { console . log ( `Locale: ${ defaultLocale }` ) ; } await app . externalHooks . run ( 'n8n.ready' , [ app ] ) ; const cpus = os . cpus ( ) ; const binarDataConfig = config . get ( 'binaryDataManager' ) as IBinaryDataConfig ; const diagnosticInfo = { basicAuthActive : config . get ( 'security.basicAuth.active' ) as boolean , databaseType : ( await GenericHelpers . getConfigValue ( 'database.type' ) ) as DatabaseType , disableProductionWebhooksOnMainProcess : config . get ( 'endpoints.disableProductionWebhooksOnMainProcess' ) === true , notificationsEnabled : config . get ( 'versionNotifications.enabled' ) === true , versionCli : versions . cli , systemInfo : { os : { type : os . type ( ) , version : os . version ( ) , } , memory : os . totalmem ( ) / 1024 , cpus : { count : cpus . length , model : cpus [ 0 ] . model , speed : cpus [ 0 ] . speed , } , } , executionVariables : { executions_process : config . get ( 'executions.process' ) , executions_mode : config . get ( 'executions.mode' ) , executions_timeout : config . get ( 'executions.timeout' ) , executions_timeout_max : config . get ( 'executions.maxTimeout' ) , executions_data_save_on_error : config . get ( 'executions.saveDataOnError' ) , executions_data_save_on_success : config . get ( 'executions.saveDataOnSuccess' ) , executions_data_save_on_progress : config . get ( 'executions.saveExecutionProgress' ) , executions_data_save_manual_executions : config . get ( 'executions.saveDataManualExecutions' ) , executions_data_prune : config . get ( 'executions.pruneData' ) , executions_data_max_age : config . get ( 'executions.pruneDataMaxAge' ) , executions_data_prune_timeout : config . get ( 'executions.pruneDataTimeout' ) , } , deploymentType : config . get ( 'deployment.type' ) , binaryDataMode : binarDataConfig . mode , } ; void Db . collections . Workflow ! . findOne ( { select : [ 'createdAt' ] , order : { createdAt : 'ASC' } , } ) . then ( async ( workflow ) => InternalHooksManager . getInstance ( ) . onServerStarted ( diagnosticInfo , workflow ?. createdAt ) ) ; } ) ; server . on ( 'error' , ( error ) => { if ( error . code === 'EADDRINUSE' ) { console . log ( `n8n's port ${ PORT } is already in use. Do you have another instance of n8n running already?` ) ; process . exit ( 1 ) ; } } ) ; } async function getExecutionsCount ( countFilter ) { const dbType = ( await GenericHelpers . getConfigValue ( 'database.type' ) ) as DatabaseType ; const filteredFields = Object . keys ( countFilter ) . filter ( ( field ) => field !== 'id' ) ; if ( dbType !== 'postgresdb' || filteredFields . length > 0 ) { const count = await Db . collections . Execution ! . count ( countFilter ) ; return { count , estimate : false } ; } try { const estimateRowsNumberSql = "SELECT n_live_tup FROM pg_stat_all_tables WHERE relname = 'execution_entity';" ; const rows = await Db . collections . Execution ! . query ( estimateRowsNumberSql ) ; const estimate = parseInt ( rows [ 0 ] . n_live_tup , 10 ) ; if ( estimate > 100000 ) { return { count : estimate , estimate : true } ; } } catch ( err ) { LoggerProxy . warn ( `Unable to get executions count from postgres: ${ err }` ) ; } const count = await Db . collections . Execution ! . count ( countFilter ) ; return { count , estimate : false } ; }