2019-06-23 03:35:23 -07:00
import * as express from 'express' ;
2020-03-29 03:46:55 -07:00
import {
readFileSync ,
} from 'fs' ;
2019-08-19 11:41:10 -07:00
import {
dirname as pathDirname ,
join as pathJoin ,
2020-01-07 16:29:11 -08:00
resolve as pathResolve ,
2019-08-19 11:41:10 -07:00
} from 'path' ;
2019-09-19 05:00:14 -07:00
import {
getConnectionManager ,
2019-09-19 05:14:37 -07:00
} from 'typeorm' ;
2019-06-23 03:35:23 -07:00
import * as bodyParser from 'body-parser' ;
2019-12-08 16:07:57 -08:00
require ( 'body-parser-xml' ) ( bodyParser ) ;
2019-06-23 03:35:23 -07:00
import * as history from 'connect-history-api-fallback' ;
2020-01-01 22:47:11 -08:00
import * as _ from 'lodash' ;
import * as clientOAuth2 from 'client-oauth2' ;
2020-06-01 17:42:38 -07:00
import * as clientOAuth1 from 'oauth-1.0a' ;
import { RequestOptions } from 'oauth-1.0a' ;
2020-01-01 22:47:11 -08:00
import * as csrf from 'csrf' ;
2020-07-14 04:59:37 -07:00
import * as requestPromise from 'request-promise-native' ;
2020-06-01 17:42:38 -07:00
import { createHmac } from 'crypto' ;
2020-09-10 08:49:44 -07:00
import { compareSync } from 'bcrypt' ;
2019-06-23 03:35:23 -07:00
import {
2019-08-08 11:38:25 -07:00
ActiveExecutions ,
2019-06-23 03:35:23 -07:00
ActiveWorkflowRunner ,
2020-05-14 05:27:19 -07:00
CredentialsHelper ,
2019-08-08 11:38:25 -07:00
CredentialTypes ,
Db ,
2020-05-03 23:56:01 -07:00
ExternalHooks ,
2019-08-08 11:38:25 -07:00
IActivationError ,
2019-06-23 03:35:23 -07:00
ICustomRequest ,
ICredentialsDb ,
ICredentialsDecryptedDb ,
ICredentialsDecryptedResponse ,
ICredentialsResponse ,
IExecutionDeleteFilter ,
IExecutionFlatted ,
IExecutionFlattedDb ,
IExecutionFlattedResponse ,
IExecutionPushResponse ,
IExecutionsListResponse ,
IExecutionsStopData ,
IExecutionsSummary ,
2020-06-10 04:00:28 -07:00
IExternalHooksClass ,
2019-06-23 03:35:23 -07:00
IN8nUISettings ,
2019-09-19 04:21:10 -07:00
IPackageVersions ,
2019-06-23 03:35:23 -07:00
IWorkflowBase ,
IWorkflowShortResponse ,
IWorkflowResponse ,
2019-08-08 11:38:25 -07:00
IWorkflowExecutionDataProcess ,
2019-06-23 03:35:23 -07:00
NodeTypes ,
Push ,
ResponseHelper ,
TestWebhooks ,
2019-08-08 11:38:25 -07:00
WorkflowCredentials ,
2019-06-23 03:35:23 -07:00
WebhookHelpers ,
WorkflowExecuteAdditionalData ,
2019-08-08 11:38:25 -07:00
WorkflowRunner ,
2019-06-23 03:35:23 -07:00
GenericHelpers ,
2020-07-07 10:03:53 -07:00
CredentialsOverwrites ,
ICredentialsOverwrite ,
LoadNodesAndCredentials ,
2019-06-23 03:35:23 -07:00
} from './' ;
import {
Credentials ,
LoadNodeParameterOptions ,
UserSettings ,
} from 'n8n-core' ;
import {
2020-05-14 05:27:19 -07:00
ICredentialsEncrypted ,
2019-06-23 03:35:23 -07:00
ICredentialType ,
IDataObject ,
INodeCredentials ,
INodeTypeDescription ,
2020-04-09 11:23:33 -07:00
INodeParameters ,
2019-06-23 03:35:23 -07:00
INodePropertyOptions ,
IRunData ,
2020-05-14 05:27:19 -07:00
IWorkflowCredentials ,
2019-06-23 03:35:23 -07:00
Workflow ,
} from 'n8n-workflow' ;
import {
FindManyOptions ,
2019-06-30 10:09:08 -07:00
FindOneOptions ,
2019-06-23 03:35:23 -07:00
LessThan ,
LessThanOrEqual ,
2019-06-30 10:09:08 -07:00
Not ,
2019-06-23 03:35:23 -07:00
} from 'typeorm' ;
2019-08-04 05:24:48 -07:00
import * as basicAuth from 'basic-auth' ;
import * as compression from 'compression' ;
2019-07-21 10:47:41 -07:00
import * as config from '../config' ;
2019-10-14 13:37:51 -07:00
import * as jwt from 'jsonwebtoken' ;
import * as jwks from 'jwks-rsa' ;
2019-06-23 03:35:23 -07:00
// @ts-ignore
import * as timezones from 'google-timezones-json' ;
2019-08-04 05:24:48 -07:00
import * as parseUrl from 'parseurl' ;
2020-06-01 17:42:38 -07:00
import * as querystring from 'querystring' ;
import { OptionsWithUrl } from 'request-promise-native' ;
2019-06-23 03:35:23 -07:00
class App {
app : express.Application ;
activeWorkflowRunner : ActiveWorkflowRunner.ActiveWorkflowRunner ;
testWebhooks : TestWebhooks.TestWebhooks ;
endpointWebhook : string ;
endpointWebhookTest : string ;
2020-07-07 10:03:53 -07:00
endpointPresetCredentials : string ;
2020-06-10 04:00:28 -07:00
externalHooks : IExternalHooksClass ;
2019-07-10 11:53:13 -07:00
saveDataErrorExecution : string ;
saveDataSuccessExecution : string ;
2019-07-10 09:06:26 -07:00
saveManualExecutions : boolean ;
2020-07-29 05:12:54 -07:00
executionTimeout : number ;
maxExecutionTimeout : number ;
2019-06-23 03:35:23 -07:00
timezone : string ;
activeExecutionsInstance : ActiveExecutions.ActiveExecutions ;
push : Push.Push ;
2019-09-19 04:21:10 -07:00
versions : IPackageVersions | undefined ;
2020-06-23 07:12:53 -07:00
restEndpoint : string ;
2019-06-23 03:35:23 -07:00
2020-03-27 10:19:30 -07:00
protocol : string ;
2020-07-14 04:59:37 -07:00
sslKey : string ;
2020-03-29 03:46:55 -07:00
sslCert : string ;
2020-03-27 10:19:30 -07:00
2020-07-07 10:03:53 -07:00
presetCredentialsLoaded : boolean ;
2019-06-23 03:35:23 -07:00
constructor ( ) {
this . app = express ( ) ;
2019-07-21 10:47:41 -07:00
this . endpointWebhook = config . get ( 'endpoints.webhook' ) as string ;
this . endpointWebhookTest = config . get ( 'endpoints.webhookTest' ) 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 ;
2020-07-29 05:12:54 -07:00
this . executionTimeout = config . get ( 'executions.timeout' ) as number ;
this . maxExecutionTimeout = config . get ( 'executions.maxTimeout' ) as number ;
2019-07-21 10:47:41 -07:00
this . timezone = config . get ( 'generic.timezone' ) as string ;
2020-06-23 07:12:53 -07:00
this . restEndpoint = config . get ( 'endpoints.rest' ) as string ;
2019-06-23 03:35:23 -07:00
this . activeWorkflowRunner = ActiveWorkflowRunner . getInstance ( ) ;
this . testWebhooks = TestWebhooks . getInstance ( ) ;
this . push = Push . getInstance ( ) ;
this . activeExecutionsInstance = ActiveExecutions . getInstance ( ) ;
2020-03-27 10:19:30 -07:00
this . protocol = config . get ( 'protocol' ) ;
2020-07-14 04:59:37 -07:00
this . sslKey = config . get ( 'ssl_key' ) ;
2020-03-29 03:46:55 -07:00
this . sslCert = config . get ( 'ssl_cert' ) ;
2020-05-03 23:56:01 -07:00
this . externalHooks = ExternalHooks ( ) ;
2020-07-07 10:03:53 -07:00
this . presetCredentialsLoaded = false ;
this . endpointPresetCredentials = config . get ( 'credentials.overwrite.endpoint' ) as string ;
2019-06-23 03:35:23 -07:00
}
/ * *
* Returns the current epoch time
*
* @returns { number }
* @memberof App
* /
2019-07-22 11:29:06 -07:00
getCurrentDate ( ) : Date {
return new Date ( ) ;
2019-06-23 03:35:23 -07:00
}
2019-08-04 05:24:48 -07:00
async config ( ) : Promise < void > {
2019-06-23 03:35:23 -07:00
2019-09-19 04:21:10 -07:00
this . versions = await GenericHelpers . getVersions ( ) ;
2020-03-01 00:45:29 -08:00
const authIgnoreRegex = new RegExp ( ` ^ \ /(healthz| ${ this . endpointWebhook } | ${ this . endpointWebhookTest } ) \ /?.* $ ` ) ;
2019-09-19 04:21:10 -07:00
2019-08-04 05:24:48 -07:00
// Check for basic auth credentials if activated
2020-01-22 15:06:43 -08:00
const basicAuthActive = config . get ( 'security.basicAuth.active' ) as boolean ;
2019-08-04 05:24:48 -07:00
if ( basicAuthActive === true ) {
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!' ) ;
}
2020-09-10 08:49:44 -07:00
const basicAuthHashEnabled = await GenericHelpers . getConfigValue ( 'security.basicAuth.hash' ) as boolean ;
2019-08-04 05:24:48 -07:00
this . app . use ( ( req : express.Request , res : express.Response , next : express.NextFunction ) = > {
if ( req . url . match ( authIgnoreRegex ) ) {
return next ( ) ;
}
const realm = 'n8n - Editor UI' ;
const basicAuthData = basicAuth ( req ) ;
if ( basicAuthData === undefined ) {
// Authorization data is missing
return ResponseHelper . basicAuthAuthorizationError ( res , realm , 'Authorization is required!' ) ;
}
2020-09-10 08:49:44 -07:00
if ( basicAuthData . name !== basicAuthUser || ( ! basicAuthHashEnabled && basicAuthData . pass !== basicAuthPassword ) || ( basicAuthHashEnabled && compareSync ( basicAuthData . pass , basicAuthPassword ) === false ) ) {
2019-08-04 05:24:48 -07:00
// Provided authentication data is wrong
return ResponseHelper . basicAuthAuthorizationError ( res , realm , 'Authorization data is wrong!' ) ;
}
next ( ) ;
} ) ;
}
2019-10-14 13:37:51 -07:00
// Check for and validate JWT if configured
2020-07-14 04:59:37 -07:00
const jwtAuthActive = config . get ( 'security.jwtAuth.active' ) as boolean ;
2019-10-14 13:37:51 -07:00
if ( jwtAuthActive === true ) {
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!' ) ;
2020-08-27 06:09:31 -07:00
}
2019-10-14 13:37:51 -07:00
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!' ) ;
2020-08-27 06:09:31 -07:00
}
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 : object ) : boolean {
if ( jwtNamespace === '' || jwtAllowedTenantKey === '' || jwtAllowedTenant === '' ) return true ;
else {
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 : express.Request , res : express.Response , next : express.NextFunction ) = > {
if ( req . url . match ( authIgnoreRegex ) ) {
return next ( ) ;
}
let token = req . header ( jwtAuthHeader ) as string ;
if ( token === undefined || token === '' ) {
return ResponseHelper . jwtAuthAuthorizationError ( res , "Missing token" ) ;
}
2020-09-01 00:22:41 -07:00
if ( jwtHeaderValuePrefix !== '' && token . startsWith ( jwtHeaderValuePrefix ) ) {
2020-08-27 06:09:31 -07:00
token = token . replace ( jwtHeaderValuePrefix + ' ' , '' ) . trimLeft ( ) ;
}
const jwkClient = jwks ( { cache : true , jwksUri } ) ;
function getKey ( header : any , callback : Function ) { // tslint:disable-line:no-any
jwkClient . getSigningKey ( header . kid , ( err : Error , key : any ) = > { // tslint:disable-line:no-any
if ( err ) throw ResponseHelper . jwtAuthAuthorizationError ( res , err . message ) ;
const signingKey = key . publicKey || key . rsaPublicKey ;
callback ( null , signingKey ) ;
} ) ;
}
const jwtVerifyOptions : jwt.VerifyOptions = {
issuer : jwtIssuer !== '' ? jwtIssuer : undefined ,
ignoreExpiration : false
} ;
jwt . verify ( token , getKey , jwtVerifyOptions , ( err : jwt.VerifyErrors , decoded : object ) = > {
if ( err ) ResponseHelper . jwtAuthAuthorizationError ( res , 'Invalid token' ) ;
else if ( ! isTenantAllowed ( decoded ) ) ResponseHelper . jwtAuthAuthorizationError ( res , 'Tenant not allowed' ) ;
else next ( ) ;
2019-10-14 13:37:51 -07:00
} ) ;
} ) ;
}
2019-06-23 03:35:23 -07:00
// Get push connections
this . app . use ( ( req : express.Request , res : express.Response , next : express.NextFunction ) = > {
2020-06-23 07:12:53 -07:00
if ( req . url . indexOf ( ` / ${ this . restEndpoint } /push ` ) === 0 ) {
2019-06-23 03:35:23 -07:00
// TODO: Later also has to add some kind of authentication token
if ( req . query . sessionId === undefined ) {
next ( new Error ( 'The query parameter "sessionId" is missing!' ) ) ;
return ;
}
2020-04-09 11:23:33 -07:00
this . push . add ( req . query . sessionId as string , req , res ) ;
2019-06-23 03:35:23 -07:00
return ;
}
next ( ) ;
} ) ;
2019-08-06 11:44:36 -07:00
// Compress the response data
this . app . use ( compression ( ) ) ;
2019-06-23 03:35:23 -07:00
// Make sure that each request has the "parsedUrl" parameter
this . app . use ( ( req : express.Request , res : express.Response , next : express.NextFunction ) = > {
( req as ICustomRequest ) . parsedUrl = parseUrl ( req ) ;
2020-08-27 06:19:10 -07:00
// @ts-ignore
2020-09-01 08:50:51 -07:00
req . rawBody = Buffer . from ( '' , 'base64' ) ;
2019-06-23 03:35:23 -07:00
next ( ) ;
} ) ;
// Support application/json type post data
2020-02-09 18:27:06 -08:00
this . app . use ( bodyParser . json ( {
limit : '16mb' , verify : ( req , res , buf ) = > {
// @ts-ignore
req . rawBody = buf ;
}
} ) ) ;
2019-06-23 03:35:23 -07:00
2019-12-08 16:07:57 -08:00
// Support application/xml type post data
// @ts-ignore
2020-08-27 06:09:31 -07:00
this . app . use ( bodyParser . xml ( {
limit : '16mb' , xmlParseOptions : {
normalize : true , // Trim whitespace inside text nodes
normalizeTags : true , // Transform tags to lowercase
explicitArray : false , // Only put properties in array if length > 1
}
} ) ) ;
2019-12-08 16:07:57 -08:00
2020-02-09 18:27:06 -08:00
this . app . use ( bodyParser . text ( {
limit : '16mb' , verify : ( req , res , buf ) = > {
// @ts-ignore
req . rawBody = buf ;
}
} ) ) ;
2019-06-23 03:35:23 -07:00
// Make sure that Vue history mode works properly
this . app . use ( history ( {
rewrites : [
{
2020-06-23 07:12:53 -07:00
from : new RegExp ( ` ^ \ /( ${ this . restEndpoint } |healthz|css|js| ${ this . endpointWebhook } | ${ this . endpointWebhookTest } ) \ /?.* $ ` ) ,
2019-06-23 03:35:23 -07:00
to : ( context ) = > {
return context . parsedUrl ! . pathname ! . toString ( ) ;
}
}
]
} ) ) ;
//support application/x-www-form-urlencoded post data
2020-08-28 09:15:34 -07:00
this . app . use ( bodyParser . urlencoded ( { extended : false ,
verify : ( req , res , buf ) = > {
// @ts-ignore
req . rawBody = buf ;
}
} ) ) ;
2019-06-23 03:35:23 -07:00
2019-09-20 04:06:06 -07:00
if ( process . env [ 'NODE_ENV' ] !== 'production' ) {
this . app . use ( ( req : express.Request , res : express.Response , next : express.NextFunction ) = > {
// Allow access also from frontend when developing
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 ( ) ;
} ) ;
}
2019-06-23 03:35:23 -07:00
this . app . use ( ( req : express.Request , res : express.Response , next : express.NextFunction ) = > {
if ( Db . collections . Workflow === null ) {
2019-08-28 08:16:09 -07:00
const error = new ResponseHelper . ResponseError ( 'Database is not ready!' , undefined , 503 ) ;
2019-06-23 03:35:23 -07:00
return ResponseHelper . sendErrorResponse ( res , error ) ;
}
next ( ) ;
} ) ;
2019-09-17 06:30:49 -07:00
// ----------------------------------------
// Healthcheck
// ----------------------------------------
2019-09-19 05:00:14 -07:00
// Does very basic health check
this . app . get ( '/healthz' , async ( req : express.Request , res : express.Response ) = > {
const connectionManager = getConnectionManager ( ) ;
if ( connectionManager . connections . length === 0 ) {
const error = new ResponseHelper . ResponseError ( 'No Database connection found!' , undefined , 503 ) ;
return ResponseHelper . sendErrorResponse ( res , error ) ;
}
if ( connectionManager . connections [ 0 ] . isConnected === false ) {
// Connection is not active
const error = new ResponseHelper . ResponseError ( 'Database connection not active!' , undefined , 503 ) ;
return ResponseHelper . sendErrorResponse ( res , error ) ;
}
// Everything fine
const responseData = {
2019-09-17 06:30:49 -07:00
status : 'ok' ,
} ;
2019-09-19 05:00:14 -07:00
ResponseHelper . sendSuccessResponse ( res , responseData , true , 200 ) ;
} ) ;
2019-09-17 06:30:49 -07:00
2019-06-23 03:35:23 -07:00
// ----------------------------------------
// Workflow
// ----------------------------------------
// Creates a new workflow
2020-06-23 07:12:53 -07:00
this . app . post ( ` / ${ this . restEndpoint } /workflows ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < IWorkflowResponse > = > {
2019-06-23 03:35:23 -07:00
2020-05-04 16:23:54 -07:00
const newWorkflowData = req . body as IWorkflowBase ;
2019-06-23 03:35:23 -07:00
2019-07-25 06:21:57 -07:00
newWorkflowData . name = newWorkflowData . name . trim ( ) ;
2019-06-23 03:35:23 -07:00
newWorkflowData . createdAt = this . getCurrentDate ( ) ;
newWorkflowData . updatedAt = this . getCurrentDate ( ) ;
newWorkflowData . id = undefined ;
2020-05-04 16:23:54 -07:00
await this . externalHooks . run ( 'workflow.create' , [ newWorkflowData ] ) ;
2019-06-23 03:35:23 -07:00
// Save the workflow in DB
const result = await Db . collections . Workflow ! . save ( newWorkflowData ) ;
// Convert to response format in which the id is a string
( result as IWorkflowBase as IWorkflowResponse ) . id = result . id . toString ( ) ;
return result as IWorkflowBase as IWorkflowResponse ;
} ) ) ;
// Reads and returns workflow data from an URL
2020-06-23 07:12:53 -07:00
this . app . get ( ` / ${ this . restEndpoint } /workflows/from-url ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < IWorkflowResponse > = > {
2019-06-23 03:35:23 -07:00
if ( req . query . url === undefined ) {
2019-08-28 08:16:09 -07:00
throw new ResponseHelper . ResponseError ( ` The parameter "url" is missing! ` , undefined , 400 ) ;
2019-06-23 03:35:23 -07:00
}
2020-04-09 11:23:33 -07:00
if ( ! ( req . query . url as string ) . match ( /^http[s]?:\/\/.*\.json$/i ) ) {
2019-08-28 08:16:09 -07:00
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 ) ;
2019-06-23 03:35:23 -07:00
}
2020-04-09 11:23:33 -07:00
const data = await requestPromise . get ( req . query . url as string ) ;
2019-06-23 03:35:23 -07:00
let workflowData : IWorkflowResponse | undefined ;
try {
workflowData = JSON . parse ( data ) ;
} catch ( error ) {
2019-08-28 08:16:09 -07:00
throw new ResponseHelper . ResponseError ( ` The URL does not point to valid JSON file! ` , undefined , 400 ) ;
2019-06-23 03:35:23 -07:00
}
// Do a very basic check if it is really a n8n-workflow-json
if ( workflowData === undefined || workflowData . nodes === undefined || ! Array . isArray ( workflowData . nodes ) ||
workflowData . connections === undefined || typeof workflowData . connections !== 'object' ||
Array . isArray ( workflowData . connections ) ) {
2019-08-28 08:16:09 -07:00
throw new ResponseHelper . ResponseError ( ` The data in the file does not seem to be a n8n workflow JSON file! ` , undefined , 400 ) ;
2019-06-23 03:35:23 -07:00
}
return workflowData ;
} ) ) ;
// Returns workflows
2020-06-23 07:12:53 -07:00
this . app . get ( ` / ${ this . restEndpoint } /workflows ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < IWorkflowShortResponse [ ] > = > {
2019-06-23 03:35:23 -07:00
const findQuery = { } as FindManyOptions ;
if ( req . query . filter ) {
2020-04-09 11:23:33 -07:00
findQuery . where = JSON . parse ( req . query . filter as string ) ;
2019-06-23 03:35:23 -07:00
}
// Return only the fields we need
findQuery . select = [ 'id' , 'name' , 'active' , 'createdAt' , 'updatedAt' ] ;
const results = await Db . collections . Workflow ! . find ( findQuery ) ;
for ( const entry of results ) {
( entry as unknown as IWorkflowShortResponse ) . id = entry . id . toString ( ) ;
}
return results as unknown as IWorkflowShortResponse [ ] ;
} ) ) ;
// Returns a specific workflow
2020-06-23 07:12:53 -07:00
this . app . get ( ` / ${ this . restEndpoint } /workflows/:id ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < IWorkflowResponse | undefined > = > {
2019-06-23 03:35:23 -07:00
const result = await Db . collections . Workflow ! . findOne ( req . params . id ) ;
if ( result === undefined ) {
return undefined ;
}
// Convert to response format in which the id is a string
( result as IWorkflowBase as IWorkflowResponse ) . id = result . id . toString ( ) ;
return result as IWorkflowBase as IWorkflowResponse ;
} ) ) ;
// Updates an existing workflow
2020-06-23 07:12:53 -07:00
this . app . patch ( ` / ${ this . restEndpoint } /workflows/:id ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < IWorkflowResponse > = > {
2019-06-23 03:35:23 -07:00
2020-05-04 16:23:54 -07:00
const newWorkflowData = req . body as IWorkflowBase ;
2019-06-23 03:35:23 -07:00
const id = req . params . id ;
2020-05-04 16:23:54 -07:00
await this . externalHooks . run ( 'workflow.update' , [ newWorkflowData ] ) ;
2020-05-27 16:32:49 -07:00
const isActive = await this . activeWorkflowRunner . isActive ( id ) ;
if ( isActive ) {
2019-06-23 03:35:23 -07:00
// When workflow gets saved always remove it as the triggers could have been
// changed and so the changes would not take effect
await this . activeWorkflowRunner . remove ( id ) ;
}
if ( newWorkflowData . settings ) {
if ( newWorkflowData . settings . timezone === 'DEFAULT' ) {
// Do not save the default timezone
delete newWorkflowData . settings . timezone ;
}
2019-07-10 11:53:13 -07:00
if ( newWorkflowData . settings . saveDataErrorExecution === 'DEFAULT' ) {
// Do not save when default got set
delete newWorkflowData . settings . saveDataErrorExecution ;
}
if ( newWorkflowData . settings . saveDataSuccessExecution === 'DEFAULT' ) {
// Do not save when default got set
delete newWorkflowData . settings . saveDataSuccessExecution ;
}
2019-07-10 09:06:26 -07:00
if ( newWorkflowData . settings . saveManualExecutions === 'DEFAULT' ) {
2019-06-23 03:35:23 -07:00
// Do not save when default got set
2019-07-10 09:06:26 -07:00
delete newWorkflowData . settings . saveManualExecutions ;
2019-06-23 03:35:23 -07:00
}
2020-07-29 05:19:35 -07:00
if ( parseInt ( newWorkflowData . settings . executionTimeout as string , 10 ) === this . executionTimeout ) {
2020-07-29 05:12:54 -07:00
// Do not save when default got set
2020-07-29 05:19:35 -07:00
delete newWorkflowData . settings . executionTimeout ;
2020-07-29 05:12:54 -07:00
}
2019-06-23 03:35:23 -07:00
}
newWorkflowData . updatedAt = this . getCurrentDate ( ) ;
await Db . collections . Workflow ! . update ( id , newWorkflowData ) ;
// We sadly get nothing back from "update". Neither if it updated a record
// nor the new value. So query now the hopefully updated entry.
2019-08-28 08:16:09 -07:00
const responseData = await Db . collections . Workflow ! . findOne ( id ) ;
2019-06-23 03:35:23 -07:00
2019-08-28 08:16:09 -07:00
if ( responseData === undefined ) {
throw new ResponseHelper . ResponseError ( ` Workflow with id " ${ id } " could not be found to be updated. ` , undefined , 400 ) ;
2019-06-23 03:35:23 -07:00
}
2019-08-28 08:16:09 -07:00
if ( responseData . active === true ) {
2019-06-23 03:35:23 -07:00
// When the workflow is supposed to be active add it again
try {
2020-05-05 15:59:58 -07:00
await this . externalHooks . run ( 'workflow.activate' , [ responseData ] ) ;
2019-06-23 03:35:23 -07:00
await this . activeWorkflowRunner . add ( id ) ;
} catch ( error ) {
// If workflow could not be activated set it again to inactive
newWorkflowData . active = false ;
await Db . collections . Workflow ! . update ( id , newWorkflowData ) ;
// Also set it in the returned data
2019-08-28 08:16:09 -07:00
responseData . active = false ;
2019-06-23 03:35:23 -07:00
// Now return the original error for UI to display
throw error ;
}
}
// Convert to response format in which the id is a string
2019-08-28 08:16:09 -07:00
( responseData as IWorkflowBase as IWorkflowResponse ) . id = responseData . id . toString ( ) ;
return responseData as IWorkflowBase as IWorkflowResponse ;
2019-06-23 03:35:23 -07:00
} ) ) ;
// Deletes a specific workflow
2020-06-23 07:12:53 -07:00
this . app . delete ( ` / ${ this . restEndpoint } /workflows/:id ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < boolean > = > {
2019-06-23 03:35:23 -07:00
const id = req . params . id ;
2020-05-04 16:23:54 -07:00
await this . externalHooks . run ( 'workflow.delete' , [ id ] ) ;
2020-05-27 16:32:49 -07:00
const isActive = await this . activeWorkflowRunner . isActive ( id ) ;
if ( isActive ) {
2019-06-23 03:35:23 -07:00
// Before deleting a workflow deactivate it
await this . activeWorkflowRunner . remove ( id ) ;
}
await Db . collections . Workflow ! . delete ( id ) ;
return true ;
} ) ) ;
2020-06-23 07:12:53 -07:00
this . app . post ( ` / ${ this . restEndpoint } /workflows/run ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < IExecutionPushResponse > = > {
2019-06-23 03:35:23 -07:00
const workflowData = req . body . workflowData ;
const runData : IRunData | undefined = req . body . runData ;
const startNodes : string [ ] | undefined = req . body . startNodes ;
const destinationNode : string | undefined = req . body . destinationNode ;
const executionMode = 'manual' ;
const sessionId = GenericHelpers . getSessionId ( req ) ;
2020-05-03 08:55:14 -07:00
// If webhooks nodes exist and are active we have to wait for till we receive a call
if ( runData === undefined || startNodes === undefined || startNodes . length === 0 || destinationNode === undefined ) {
2019-08-08 11:38:25 -07:00
const credentials = await WorkflowCredentials ( workflowData . nodes ) ;
2019-12-19 14:07:55 -08:00
const additionalData = await WorkflowExecuteAdditionalData . getBase ( credentials ) ;
2019-08-08 11:38:25 -07:00
const nodeTypes = NodeTypes ( ) ;
2020-02-15 17:07:01 -08:00
const workflowInstance = new Workflow ( { id : workflowData.id , name : workflowData.name , nodes : workflowData.nodes , connections : workflowData.connections , active : false , nodeTypes , staticData : undefined , settings : workflowData.settings } ) ;
2019-08-08 11:38:25 -07:00
const needsWebhook = await this . testWebhooks . needsWebhookData ( workflowData , workflowInstance , additionalData , executionMode , sessionId , destinationNode ) ;
if ( needsWebhook === true ) {
return {
waitingForWebhook : true ,
} ;
}
}
2019-06-23 03:35:23 -07:00
2019-08-08 11:38:25 -07:00
// For manual testing always set to not active
workflowData . active = false ;
2019-06-23 03:35:23 -07:00
2019-08-08 11:38:25 -07:00
const credentials = await WorkflowCredentials ( workflowData . nodes ) ;
2019-06-23 03:35:23 -07:00
2019-08-08 11:38:25 -07:00
// Start the workflow
const data : IWorkflowExecutionDataProcess = {
credentials ,
destinationNode ,
executionMode ,
runData ,
sessionId ,
startNodes ,
workflowData ,
} ;
const workflowRunner = new WorkflowRunner ( ) ;
const executionId = await workflowRunner . run ( data ) ;
2019-06-23 03:35:23 -07:00
return {
executionId ,
} ;
} ) ) ;
// Returns parameter values which normally get loaded from an external API or
// get generated dynamically
2020-06-23 07:12:53 -07:00
this . app . get ( ` / ${ this . restEndpoint } /node-parameter-options ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < INodePropertyOptions [ ] > = > {
2020-04-09 11:23:33 -07:00
const nodeType = req . query . nodeType as string ;
2019-06-23 03:35:23 -07:00
let credentials : INodeCredentials | undefined = undefined ;
2020-05-04 21:07:19 -07:00
const currentNodeParameters = JSON . parse ( '' + req . query . currentNodeParameters ) as INodeParameters ;
2019-06-23 03:35:23 -07:00
if ( req . query . credentials !== undefined ) {
2020-04-09 11:23:33 -07:00
credentials = JSON . parse ( req . query . credentials as string ) ;
2019-06-23 03:35:23 -07:00
}
2020-04-09 11:23:33 -07:00
const methodName = req . query . methodName as string ;
2019-06-23 03:35:23 -07:00
const nodeTypes = NodeTypes ( ) ;
2020-06-04 00:44:21 -07:00
const loadDataInstance = new LoadNodeParameterOptions ( nodeType , nodeTypes , JSON . parse ( '' + req . query . currentNodeParameters ) , credentials ! ) ;
2019-06-23 03:35:23 -07:00
const workflowData = loadDataInstance . getWorkflowData ( ) as IWorkflowBase ;
2019-08-08 11:38:25 -07:00
const workflowCredentials = await WorkflowCredentials ( workflowData . nodes ) ;
2019-12-19 14:07:55 -08:00
const additionalData = await WorkflowExecuteAdditionalData . getBase ( workflowCredentials , currentNodeParameters ) ;
2019-06-23 03:35:23 -07:00
return loadDataInstance . getOptions ( methodName , additionalData ) ;
} ) ) ;
// Returns all the node-types
2020-06-23 07:12:53 -07:00
this . app . get ( ` / ${ this . restEndpoint } /node-types ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < INodeTypeDescription [ ] > = > {
2019-06-23 03:35:23 -07:00
const returnData : INodeTypeDescription [ ] = [ ] ;
const nodeTypes = NodeTypes ( ) ;
const allNodes = nodeTypes . getAll ( ) ;
allNodes . forEach ( ( nodeData ) = > {
returnData . push ( nodeData . description ) ;
} ) ;
return returnData ;
} ) ) ;
// ----------------------------------------
// Node-Types
// ----------------------------------------
// Returns the node icon
2020-06-23 07:12:53 -07:00
this . app . get ( [ ` / ${ this . restEndpoint } /node-icon/:nodeType ` , ` / ${ this . restEndpoint } /node-icon/:scope/:nodeType ` ] , async ( req : express.Request , res : express.Response ) : Promise < void > = > {
2020-06-03 10:40:39 -07:00
const nodeTypeName = ` ${ req . params . scope ? ` ${ req . params . scope } / ` : '' } ${ req . params . nodeType } ` ;
2019-06-23 03:35:23 -07:00
const nodeTypes = NodeTypes ( ) ;
const nodeType = nodeTypes . getByName ( 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 ) ;
res . sendFile ( filepath ) ;
} ) ;
// ----------------------------------------
// Active Workflows
// ----------------------------------------
// Returns the active workflow ids
2020-06-23 07:12:53 -07:00
this . app . get ( ` / ${ this . restEndpoint } /active ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < string [ ] > = > {
2020-05-27 16:32:49 -07:00
const activeWorkflows = await this . activeWorkflowRunner . getActiveWorkflows ( ) ;
return activeWorkflows . map ( workflow = > workflow . id . toString ( ) ) as string [ ] ;
2019-06-23 03:35:23 -07:00
} ) ) ;
// Returns if the workflow with the given id had any activation errors
2020-06-23 07:12:53 -07:00
this . app . get ( ` / ${ this . restEndpoint } /active/error/:id ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < IActivationError | undefined > = > {
2019-06-23 03:35:23 -07:00
const id = req . params . id ;
return this . activeWorkflowRunner . getActivationError ( id ) ;
} ) ) ;
// ----------------------------------------
// Credentials
// ----------------------------------------
// Deletes a specific credential
2020-06-23 07:12:53 -07:00
this . app . delete ( ` / ${ this . restEndpoint } /credentials/:id ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < boolean > = > {
2019-06-23 03:35:23 -07:00
const id = req . params . id ;
2020-05-04 16:23:54 -07:00
await this . externalHooks . run ( 'credentials.delete' , [ id ] ) ;
2019-06-23 03:35:23 -07:00
await Db . collections . Credentials ! . delete ( { id } ) ;
return true ;
} ) ) ;
// Creates new credentials
2020-06-23 07:12:53 -07:00
this . app . post ( ` / ${ this . restEndpoint } /credentials ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < ICredentialsResponse > = > {
2019-06-23 03:35:23 -07:00
const incomingData = req . body ;
2020-01-25 23:48:38 -08:00
if ( ! incomingData . name || incomingData . name . length < 3 ) {
throw new ResponseHelper . ResponseError ( ` Credentials name must be at least 3 characters long. ` , undefined , 400 ) ;
}
2019-06-23 03:35:23 -07:00
// Add the added date for node access permissions
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!' ) ;
}
2020-01-09 19:23:47 -08:00
if ( incomingData . name === '' ) {
throw new Error ( 'Credentials have to have a name set!' ) ;
}
2019-06-30 10:09:08 -07:00
// Check if credentials with the same name and type exist already
const findQuery = {
where : {
name : incomingData.name ,
type : incomingData . type ,
} ,
} as FindOneOptions ;
const checkResult = await Db . collections . Credentials ! . findOne ( findQuery ) ;
if ( checkResult !== undefined ) {
2019-08-28 08:16:09 -07:00
throw new ResponseHelper . ResponseError ( ` Credentials with the same type and name exist already. ` , undefined , 400 ) ;
2019-06-30 10:09:08 -07:00
}
2019-06-23 03:35:23 -07:00
// Encrypt the data
const credentials = new Credentials ( incomingData . name , incomingData . type , incomingData . nodesAccess ) ;
credentials . setData ( incomingData . data , encryptionKey ) ;
const newCredentialsData = credentials . getDataToSave ( ) as ICredentialsDb ;
2020-05-05 15:59:58 -07:00
await this . externalHooks . run ( 'credentials.create' , [ newCredentialsData ] ) ;
2019-06-23 03:35:23 -07:00
// Add special database related data
newCredentialsData . createdAt = this . getCurrentDate ( ) ;
newCredentialsData . updatedAt = this . getCurrentDate ( ) ;
// TODO: also add user automatically depending on who is logged in, if anybody is logged in
// Save the credentials in DB
const result = await Db . collections . Credentials ! . save ( newCredentialsData ) ;
2020-01-25 23:48:38 -08:00
result . data = incomingData . data ;
2019-06-23 03:35:23 -07:00
// Convert to response format in which the id is a string
( result as unknown as ICredentialsResponse ) . id = result . id . toString ( ) ;
return result as unknown as ICredentialsResponse ;
} ) ) ;
// Updates existing credentials
2020-06-23 07:12:53 -07:00
this . app . patch ( ` / ${ this . restEndpoint } /credentials/:id ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < ICredentialsResponse > = > {
2019-06-23 03:35:23 -07:00
const incomingData = req . body ;
const id = req . params . id ;
2020-01-09 19:23:47 -08:00
if ( incomingData . name === '' ) {
throw new Error ( 'Credentials have to have a name set!' ) ;
}
2019-06-23 03:35:23 -07:00
// Add the date for newly added node access permissions
for ( const nodeAccess of incomingData . nodesAccess ) {
if ( ! nodeAccess . date ) {
nodeAccess . date = this . getCurrentDate ( ) ;
}
}
2019-06-30 10:09:08 -07:00
// Check if credentials with the same name and type exist already
const findQuery = {
where : {
id : Not ( id ) ,
name : incomingData.name ,
type : incomingData . type ,
} ,
} as FindOneOptions ;
const checkResult = await Db . collections . Credentials ! . findOne ( findQuery ) ;
if ( checkResult !== undefined ) {
2019-08-28 08:16:09 -07:00
throw new ResponseHelper . ResponseError ( ` Credentials with the same type and name exist already. ` , undefined , 400 ) ;
2019-06-30 10:09:08 -07:00
}
2019-06-23 03:35:23 -07:00
const encryptionKey = await UserSettings . getEncryptionKey ( ) ;
if ( encryptionKey === undefined ) {
throw new Error ( 'No encryption key got found to encrypt the credentials!' ) ;
}
2020-05-12 15:40:05 -07:00
// Load the currently saved credentials to be able to persist some of the data if
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 . name , result . type , result . nodesAccess , result . data ) ;
const decryptedData = currentlySavedCredentials . getData ( encryptionKey ! ) ;
// Do not overwrite the oauth data else data like the access or refresh token would get lost
// everytime anybody changes anything on the credentials even if it is just the name.
if ( decryptedData . oauthTokenData ) {
incomingData . data . oauthTokenData = decryptedData . oauthTokenData ;
}
2019-06-23 03:35:23 -07:00
// Encrypt the data
const credentials = new Credentials ( incomingData . name , incomingData . type , incomingData . nodesAccess ) ;
credentials . setData ( incomingData . data , encryptionKey ) ;
const newCredentialsData = credentials . getDataToSave ( ) as unknown as ICredentialsDb ;
// Add special database related data
newCredentialsData . updatedAt = this . getCurrentDate ( ) ;
2020-05-05 15:59:58 -07:00
await this . externalHooks . run ( 'credentials.update' , [ newCredentialsData ] ) ;
2019-06-23 03:35:23 -07:00
// Update the credentials in DB
await Db . collections . Credentials ! . update ( id , newCredentialsData ) ;
// We sadly get nothing back from "update". Neither if it updated a record
// nor the new value. So query now the hopefully updated entry.
2019-08-28 08:16:09 -07:00
const responseData = await Db . collections . Credentials ! . findOne ( id ) ;
2019-06-23 03:35:23 -07:00
2019-08-28 08:16:09 -07:00
if ( responseData === undefined ) {
throw new ResponseHelper . ResponseError ( ` Credentials with id " ${ id } " could not be found to be updated. ` , undefined , 400 ) ;
2019-06-23 03:35:23 -07:00
}
// Remove the encrypted data as it is not needed in the frontend
2019-08-28 08:16:09 -07:00
responseData . data = '' ;
2019-06-23 03:35:23 -07:00
// Convert to response format in which the id is a string
2019-08-28 08:16:09 -07:00
( responseData as unknown as ICredentialsResponse ) . id = responseData . id . toString ( ) ;
return responseData as unknown as ICredentialsResponse ;
2019-06-23 03:35:23 -07:00
} ) ) ;
// Returns specific credentials
2020-06-23 07:12:53 -07:00
this . app . get ( ` / ${ this . restEndpoint } /credentials/:id ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < ICredentialsDecryptedResponse | ICredentialsResponse | undefined > = > {
2019-06-23 03:35:23 -07:00
const findQuery = { } as FindManyOptions ;
// Make sure the variable has an expected value
2020-04-09 11:23:33 -07:00
const includeData = [ 'true' , true ] . includes ( req . query . includeData as string ) ;
2019-06-23 03:35:23 -07:00
2020-04-09 11:23:33 -07:00
if ( includeData !== true ) {
2019-06-23 03:35:23 -07:00
// Return only the fields we need
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 = undefined ;
2020-04-09 11:23:33 -07:00
if ( includeData === true ) {
2019-06-23 03:35:23 -07:00
encryptionKey = await UserSettings . getEncryptionKey ( ) ;
if ( encryptionKey === undefined ) {
throw new Error ( 'No encryption key got found to decrypt the credentials!' ) ;
}
const credentials = new Credentials ( result . name , result . type , result . nodesAccess , result . data ) ;
( result as ICredentialsDecryptedDb ) . data = credentials . getData ( encryptionKey ! ) ;
}
( result as ICredentialsDecryptedResponse ) . id = result . id . toString ( ) ;
return result as ICredentialsDecryptedResponse ;
} ) ) ;
// Returns all the saved credentials
2020-06-23 07:12:53 -07:00
this . app . get ( ` / ${ this . restEndpoint } /credentials ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < ICredentialsResponse [ ] > = > {
2019-06-23 03:35:23 -07:00
const findQuery = { } as FindManyOptions ;
if ( req . query . filter ) {
2020-04-09 11:23:33 -07:00
findQuery . where = JSON . parse ( req . query . filter as string ) ;
2019-06-23 03:35:23 -07:00
if ( ( findQuery . where ! as IDataObject ) . id !== undefined ) {
// No idea if multiple where parameters make db search
// slower but to be sure that that is not the case we
// remove all unnecessary fields in case the id is defined.
findQuery . where = { id : ( findQuery . where ! as IDataObject ) . id } ;
}
}
findQuery . select = [ 'id' , 'name' , 'type' , 'nodesAccess' , 'createdAt' , 'updatedAt' ] ;
const results = await Db . collections . Credentials ! . find ( findQuery ) as unknown as ICredentialsResponse [ ] ;
let encryptionKey = undefined ;
2020-04-09 11:23:33 -07:00
const includeData = [ 'true' , true ] . includes ( req . query . includeData as string ) ;
if ( includeData === true ) {
2019-06-23 03:35:23 -07:00
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 ;
} ) ) ;
// ----------------------------------------
// Credential-Types
// ----------------------------------------
// Returns all the credential types which are defined in the loaded n8n-modules
2020-06-23 07:12:53 -07:00
this . app . get ( ` / ${ this . restEndpoint } /credential-types ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < ICredentialType [ ] > = > {
2019-06-23 03:35:23 -07:00
const returnData : ICredentialType [ ] = [ ] ;
const credentialTypes = CredentialTypes ( ) ;
credentialTypes . getAll ( ) . forEach ( ( credentialData ) = > {
returnData . push ( credentialData ) ;
} ) ;
return returnData ;
} ) ) ;
2020-06-01 17:42:38 -07:00
// ----------------------------------------
// OAuth1-Credential/Auth
// ----------------------------------------
// Authorize OAuth Data
2020-06-23 07:12:53 -07:00
this . app . get ( ` / ${ this . restEndpoint } /oauth1-credential/auth ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < string > = > {
2020-06-01 17:42:38 -07:00
if ( req . query . id === undefined ) {
2020-07-12 00:28:08 -07:00
res . status ( 500 ) . send ( 'Required credential id is missing!' ) ;
return '' ;
2020-06-01 17:42:38 -07:00
}
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 = undefined ;
encryptionKey = await UserSettings . getEncryptionKey ( ) ;
if ( encryptionKey === undefined ) {
2020-07-12 00:28:08 -07:00
res . status ( 500 ) . send ( 'No encryption key got found to decrypt the credentials!' ) ;
return '' ;
2020-06-01 17:42:38 -07:00
}
// Decrypt the currently saved credentials
const workflowCredentials : IWorkflowCredentials = {
[ result . type as string ] : {
[ result . name as string ] : result as ICredentialsEncrypted ,
} ,
} ;
const credentialsHelper = new CredentialsHelper ( workflowCredentials , encryptionKey ) ;
const decryptedDataOriginal = credentialsHelper . getDecrypted ( result . name , result . type , true ) ;
const oauthCredentials = credentialsHelper . applyDefaultsAndOverwrites ( decryptedDataOriginal , result . type ) ;
const signatureMethod = _ . get ( oauthCredentials , 'signatureMethod' ) as string ;
2020-09-10 01:16:24 -07:00
const oAuthOptions : clientOAuth1.Options = {
2020-06-01 17:42:38 -07:00
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 )
2020-08-27 06:09:31 -07:00
. update ( base )
. digest ( 'base64' ) ;
2020-06-01 17:42:38 -07:00
} ,
2020-09-10 01:16:24 -07:00
} ;
const oauthRequestData = {
oauth_callback : ` ${ WebhookHelpers . getWebhookBaseUrl ( ) } ${ this . restEndpoint } /oauth1-credential/callback?cid= ${ req . query . id } `
} ;
await this . externalHooks . run ( 'oauth1.authenticate' , [ oAuthOptions , oauthRequestData ] ) ;
2020-06-01 17:42:38 -07:00
2020-09-10 01:16:24 -07:00
const oauth = new clientOAuth1 ( oAuthOptions ) ;
2020-06-01 17:42:38 -07:00
2020-07-14 04:59:37 -07:00
const options : RequestOptions = {
2020-06-01 17:42:38 -07:00
method : 'POST' ,
url : ( _ . get ( oauthCredentials , 'requestTokenUrl' ) as string ) ,
2020-09-10 01:16:24 -07:00
data : oauthRequestData ,
2020-06-01 17:42:38 -07:00
} ;
const data = oauth . toHeader ( oauth . authorize ( options as RequestOptions ) ) ;
//@ts-ignore
options . headers = data ;
const response = await requestPromise ( options ) ;
// Response comes as x-www-form-urlencoded string so convert it to JSON
const responseJson = querystring . parse ( response ) ;
const returnUri = ` ${ _ . get ( oauthCredentials , 'authUrl' ) } ?oauth_token= ${ responseJson . oauth_token } ` ;
// Encrypt the data
const credentials = new Credentials ( result . name , result . type , result . nodesAccess ) ;
credentials . setData ( decryptedDataOriginal , encryptionKey ) ;
const newCredentialsData = credentials . getDataToSave ( ) as unknown as ICredentialsDb ;
// Add special database related data
newCredentialsData . updatedAt = this . getCurrentDate ( ) ;
// Update the credentials in DB
await Db . collections . Credentials ! . update ( req . query . id as string , newCredentialsData ) ;
return returnUri ;
} ) ) ;
// Verify and store app code. Generate access tokens and store for respective credential.
2020-06-23 07:12:53 -07:00
this . app . get ( ` / ${ this . restEndpoint } /oauth1-credential/callback ` , async ( req : express.Request , res : express.Response ) = > {
2020-06-01 17:42:38 -07:00
const { oauth_verifier , oauth_token , cid } = req . query ;
if ( oauth_verifier === undefined || oauth_token === undefined ) {
2020-07-12 00:28:08 -07:00
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 ) ;
2020-06-01 17:42:38 -07:00
}
2020-06-03 06:06:13 -07:00
const result = await Db . collections . Credentials ! . findOne ( cid as any ) ; // tslint:disable-line:no-any
2020-06-01 17:42:38 -07:00
if ( result === undefined ) {
const errorResponse = new ResponseHelper . ResponseError ( 'The credential is not known.' , undefined , 404 ) ;
return ResponseHelper . sendErrorResponse ( res , errorResponse ) ;
}
let encryptionKey = undefined ;
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 ) ;
}
// Decrypt the currently saved credentials
const workflowCredentials : IWorkflowCredentials = {
[ result . type as string ] : {
[ result . name as string ] : result as ICredentialsEncrypted ,
} ,
} ;
const credentialsHelper = new CredentialsHelper ( workflowCredentials , encryptionKey ) ;
const decryptedDataOriginal = credentialsHelper . getDecrypted ( result . name , result . type , true ) ;
const oauthCredentials = credentialsHelper . applyDefaultsAndOverwrites ( decryptedDataOriginal , result . type ) ;
2020-07-14 04:59:37 -07:00
const options : OptionsWithUrl = {
2020-06-01 17:42:38 -07:00
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 ) ;
}
// Response comes as x-www-form-urlencoded string so convert it to JSON
const oauthTokenJson = querystring . parse ( oauthToken ) ;
decryptedDataOriginal . oauthTokenData = oauthTokenJson ;
const credentials = new Credentials ( result . name , result . type , result . nodesAccess ) ;
credentials . setData ( decryptedDataOriginal , encryptionKey ) ;
const newCredentialsData = credentials . getDataToSave ( ) as unknown as ICredentialsDb ;
// Add special database related data
newCredentialsData . updatedAt = this . getCurrentDate ( ) ;
// Save the credentials in DB
2020-06-03 06:06:13 -07:00
await Db . collections . Credentials ! . update ( cid as any , newCredentialsData ) ; // tslint:disable-line:no-any
2020-06-01 17:42:38 -07:00
res . sendFile ( pathResolve ( __dirname , '../../templates/oauth-callback.html' ) ) ;
} ) ;
2019-06-23 03:35:23 -07:00
2020-01-01 22:47:11 -08:00
// ----------------------------------------
// OAuth2-Credential/Auth
// ----------------------------------------
2020-01-07 16:29:11 -08:00
// Authorize OAuth Data
2020-06-23 07:12:53 -07:00
this . app . get ( ` / ${ this . restEndpoint } /oauth2-credential/auth ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < string > = > {
2020-01-01 22:47:11 -08:00
if ( req . query . id === undefined ) {
2020-07-12 00:28:08 -07:00
res . status ( 500 ) . send ( 'Required credential id is missing.' ) ;
return '' ;
2020-01-01 22:47:11 -08:00
}
2020-04-10 00:47:12 -07:00
const result = await Db . collections . Credentials ! . findOne ( req . query . id as string ) ;
2020-01-01 22:47:11 -08:00
if ( result === undefined ) {
res . status ( 404 ) . send ( 'The credential is not known.' ) ;
return '' ;
}
let encryptionKey = undefined ;
encryptionKey = await UserSettings . getEncryptionKey ( ) ;
if ( encryptionKey === undefined ) {
2020-07-12 00:28:08 -07:00
res . status ( 500 ) . send ( 'No encryption key got found to decrypt the credentials!' ) ;
return '' ;
2020-01-01 22:47:11 -08:00
}
2020-05-14 05:27:19 -07:00
// Decrypt the currently saved credentials
const workflowCredentials : IWorkflowCredentials = {
[ result . type as string ] : {
[ result . name as string ] : result as ICredentialsEncrypted ,
} ,
} ;
const credentialsHelper = new CredentialsHelper ( workflowCredentials , encryptionKey ) ;
const decryptedDataOriginal = credentialsHelper . getDecrypted ( result . name , result . type , true ) ;
const oauthCredentials = credentialsHelper . applyDefaultsAndOverwrites ( decryptedDataOriginal , result . type ) ;
2020-01-01 22:47:11 -08:00
2020-01-07 16:29:11 -08:00
const token = new csrf ( ) ;
2020-01-01 22:47:11 -08:00
// Generate a CSRF prevention token and send it as a OAuth2 state stringma/ERR
2020-01-25 23:48:38 -08:00
const csrfSecret = token . secretSync ( ) ;
2020-01-01 22:47:11 -08:00
const state = {
2020-01-25 23:48:38 -08:00
token : token.create ( csrfSecret ) ,
2020-01-07 16:29:11 -08:00
cid : req.query.id
} ;
2020-01-01 22:47:11 -08:00
const stateEncodedStr = Buffer . from ( JSON . stringify ( state ) ) . toString ( 'base64' ) as string ;
2020-09-10 01:16:24 -07:00
const oAuthOptions : clientOAuth2.Options = {
2020-01-01 22:47:11 -08:00
clientId : _.get ( oauthCredentials , 'clientId' ) as string ,
clientSecret : _.get ( oauthCredentials , 'clientSecret' , '' ) as string ,
accessTokenUri : _.get ( oauthCredentials , 'accessTokenUrl' , '' ) as string ,
authorizationUri : _.get ( oauthCredentials , 'authUrl' , '' ) as string ,
2020-06-23 07:12:53 -07:00
redirectUri : ` ${ WebhookHelpers . getWebhookBaseUrl ( ) } ${ this . restEndpoint } /oauth2-credential/callback ` ,
2020-01-01 22:47:11 -08:00
scopes : _.split ( _ . get ( oauthCredentials , 'scope' , 'openid,' ) as string , ',' ) ,
2020-01-07 16:29:11 -08:00
state : stateEncodedStr ,
2020-09-10 01:16:24 -07:00
} ;
await this . externalHooks . run ( 'oauth2.authenticate' , [ oAuthOptions ] ) ;
const oAuthObj = new clientOAuth2 ( oAuthOptions ) ;
2020-01-01 22:47:11 -08:00
2020-05-14 05:27:19 -07:00
// Encrypt the data
const credentials = new Credentials ( result . name , result . type , result . nodesAccess ) ;
decryptedDataOriginal . csrfSecret = csrfSecret ;
credentials . setData ( decryptedDataOriginal , encryptionKey ) ;
2020-01-01 22:47:11 -08:00
const newCredentialsData = credentials . getDataToSave ( ) as unknown as ICredentialsDb ;
// Add special database related data
newCredentialsData . updatedAt = this . getCurrentDate ( ) ;
// Update the credentials in DB
2020-04-10 00:47:12 -07:00
await Db . collections . Credentials ! . update ( req . query . id as string , newCredentialsData ) ;
2020-01-01 22:47:11 -08:00
2020-02-08 16:14:28 -08:00
const authQueryParameters = _ . get ( oauthCredentials , 'authQueryParameters' , '' ) as string ;
let returnUri = oAuthObj . code . getUri ( ) ;
2020-08-05 00:29:29 -07:00
// if scope uses comma, change it as the library always return then with spaces
if ( ( _ . get ( oauthCredentials , 'scope' ) as string ) . includes ( ',' ) ) {
const data = querystring . parse ( returnUri . split ( '?' ) [ 1 ] as string ) ;
data . scope = _ . get ( oauthCredentials , 'scope' ) as string ;
returnUri = ` ${ _ . get ( oauthCredentials , 'authUrl' , '' ) } ? ${ querystring . stringify ( data ) } ` ;
}
2020-02-08 16:14:28 -08:00
if ( authQueryParameters ) {
returnUri += '&' + authQueryParameters ;
}
return returnUri ;
2020-01-01 22:47:11 -08:00
} ) ) ;
2019-06-23 03:35:23 -07:00
2020-01-01 22:47:11 -08:00
// ----------------------------------------
// OAuth2-Credential/Callback
// ----------------------------------------
// Verify and store app code. Generate access tokens and store for respective credential.
2020-06-23 07:12:53 -07:00
this . app . get ( ` / ${ this . restEndpoint } /oauth2-credential/callback ` , async ( req : express.Request , res : express.Response ) = > {
2020-08-27 06:09:31 -07:00
const { code , state : stateEncoded } = req . query ;
2020-01-07 16:29:11 -08:00
2020-01-01 22:47:11 -08:00
if ( code === undefined || stateEncoded === undefined ) {
2020-07-12 00:28:08 -07:00
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 ) ;
2020-01-01 22:47:11 -08:00
}
let state ;
try {
2020-04-10 00:47:12 -07:00
state = JSON . parse ( Buffer . from ( stateEncoded as string , 'base64' ) . toString ( ) ) ;
2020-01-01 22:47:11 -08:00
} catch ( error ) {
2020-01-07 16:29:11 -08:00
const errorResponse = new ResponseHelper . ResponseError ( 'Invalid state format returned' , undefined , 503 ) ;
return ResponseHelper . sendErrorResponse ( res , errorResponse ) ;
2020-01-01 22:47:11 -08:00
}
const result = await Db . collections . Credentials ! . findOne ( state . cid ) ;
if ( result === undefined ) {
2020-01-07 16:29:11 -08:00
const errorResponse = new ResponseHelper . ResponseError ( 'The credential is not known.' , undefined , 404 ) ;
return ResponseHelper . sendErrorResponse ( res , errorResponse ) ;
2020-01-01 22:47:11 -08:00
}
let encryptionKey = undefined ;
encryptionKey = await UserSettings . getEncryptionKey ( ) ;
if ( encryptionKey === undefined ) {
2020-01-07 16:29:11 -08:00
const errorResponse = new ResponseHelper . ResponseError ( 'No encryption key got found to decrypt the credentials!' , undefined , 503 ) ;
return ResponseHelper . sendErrorResponse ( res , errorResponse ) ;
2020-01-01 22:47:11 -08:00
}
2020-05-14 05:27:19 -07:00
// Decrypt the currently saved credentials
const workflowCredentials : IWorkflowCredentials = {
[ result . type as string ] : {
[ result . name as string ] : result as ICredentialsEncrypted ,
} ,
} ;
const credentialsHelper = new CredentialsHelper ( workflowCredentials , encryptionKey ) ;
const decryptedDataOriginal = credentialsHelper . getDecrypted ( result . name , result . type , true ) ;
const oauthCredentials = credentialsHelper . applyDefaultsAndOverwrites ( decryptedDataOriginal , result . type ) ;
2020-01-01 22:47:11 -08:00
2020-01-07 16:29:11 -08:00
const token = new csrf ( ) ;
2020-05-14 05:27:19 -07:00
if ( decryptedDataOriginal . csrfSecret === undefined || ! token . verify ( decryptedDataOriginal . csrfSecret as string , state . token ) ) {
2020-01-07 16:29:11 -08:00
const errorResponse = new ResponseHelper . ResponseError ( 'The OAuth2 callback state is invalid!' , undefined , 404 ) ;
return ResponseHelper . sendErrorResponse ( res , errorResponse ) ;
2020-01-01 22:47:11 -08:00
}
2020-02-12 07:04:43 -08:00
let options = { } ;
2020-07-08 12:15:25 -07:00
const oAuth2Parameters = {
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 , ',' )
} ;
2020-02-14 18:48:58 -08:00
if ( _ . get ( oauthCredentials , 'authentication' , 'header' ) as string === 'body' ) {
options = {
body : {
client_id : _.get ( oauthCredentials , 'clientId' ) as string ,
client_secret : _.get ( oauthCredentials , 'clientSecret' , '' ) as string ,
} ,
} ;
2020-07-08 12:15:25 -07:00
delete oAuth2Parameters . clientSecret ;
2020-02-14 18:48:58 -08:00
}
2020-07-12 00:28:08 -07:00
const redirectUri = ` ${ WebhookHelpers . getWebhookBaseUrl ( ) } ${ this . restEndpoint } /oauth2-credential/callback ` ;
2020-02-12 07:04:43 -08:00
2020-07-08 12:15:25 -07:00
const oAuthObj = new clientOAuth2 ( oAuth2Parameters ) ;
2020-01-01 22:47:11 -08:00
2020-07-12 00:28:08 -07:00
const queryParameters = req . originalUrl . split ( '?' ) . splice ( 1 , 1 ) . join ( '' ) ;
const oauthToken = await oAuthObj . code . getToken ( ` ${ redirectUri } ? ${ queryParameters } ` , options ) ;
2020-01-07 16:29:11 -08:00
2020-01-01 22:47:11 -08:00
if ( oauthToken === undefined ) {
2020-01-07 16:29:11 -08:00
const errorResponse = new ResponseHelper . ResponseError ( 'Unable to get access tokens!' , undefined , 404 ) ;
return ResponseHelper . sendErrorResponse ( res , errorResponse ) ;
2020-01-01 22:47:11 -08:00
}
2020-05-14 05:27:19 -07:00
if ( decryptedDataOriginal . oauthTokenData ) {
2020-05-12 16:07:34 -07:00
// Only overwrite supplied data as some providers do for example just return the
// refresh_token on the very first request and not on subsequent ones.
2020-05-14 05:27:19 -07:00
Object . assign ( decryptedDataOriginal . oauthTokenData , oauthToken . data ) ;
2020-05-12 16:07:34 -07:00
} else {
// No data exists so simply set
2020-05-14 05:27:19 -07:00
decryptedDataOriginal . oauthTokenData = oauthToken . data ;
2020-05-12 16:07:34 -07:00
}
2020-05-14 05:27:19 -07:00
_ . unset ( decryptedDataOriginal , 'csrfSecret' ) ;
2020-01-25 23:48:38 -08:00
2020-05-14 05:27:19 -07:00
const credentials = new Credentials ( result . name , result . type , result . nodesAccess ) ;
credentials . setData ( decryptedDataOriginal , encryptionKey ) ;
2020-01-01 22:47:11 -08:00
const newCredentialsData = credentials . getDataToSave ( ) as unknown as ICredentialsDb ;
// Add special database related data
newCredentialsData . updatedAt = this . getCurrentDate ( ) ;
// Save the credentials in DB
await Db . collections . Credentials ! . update ( state . cid , newCredentialsData ) ;
2020-02-09 15:39:14 -08:00
res . sendFile ( pathResolve ( __dirname , '../../templates/oauth-callback.html' ) ) ;
2020-01-07 16:29:11 -08:00
} ) ;
2019-06-23 03:35:23 -07:00
// ----------------------------------------
// Executions
// ----------------------------------------
// Returns all finished executions
2020-06-23 07:12:53 -07:00
this . app . get ( ` / ${ this . restEndpoint } /executions ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < IExecutionsListResponse > = > {
2019-06-23 03:35:23 -07:00
let filter : any = { } ; // tslint:disable-line:no-any
if ( req . query . filter ) {
2020-04-09 11:23:33 -07:00
filter = JSON . parse ( req . query . filter as string ) ;
2019-06-23 03:35:23 -07:00
}
let limit = 20 ;
if ( req . query . limit ) {
2020-04-09 11:23:33 -07:00
limit = parseInt ( req . query . limit as string , 10 ) ;
2019-06-23 03:35:23 -07:00
}
const countFilter = JSON . parse ( JSON . stringify ( filter ) ) ;
2019-12-12 11:57:11 -08:00
if ( req . query . lastId ) {
filter . id = LessThan ( req . query . lastId ) ;
2019-06-23 03:35:23 -07:00
}
2019-07-31 05:16:55 -07:00
countFilter . select = [ 'id' ] ;
2019-06-23 03:35:23 -07:00
const resultsPromise = Db . collections . Execution ! . find ( {
2019-08-05 10:13:18 -07:00
select : [
'id' ,
'finished' ,
'mode' ,
'retryOf' ,
'retrySuccessId' ,
'startedAt' ,
'stoppedAt' ,
'workflowData' ,
] ,
2019-06-23 03:35:23 -07:00
where : filter ,
order : {
2019-12-12 11:57:11 -08:00
id : 'DESC' ,
2019-06-23 03:35:23 -07:00
} ,
take : limit ,
} ) ;
const countPromise = Db . collections . Execution ! . count ( countFilter ) ;
const results : IExecutionFlattedDb [ ] = await resultsPromise ;
const count = await countPromise ;
const returnResults : IExecutionsSummary [ ] = [ ] ;
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 ,
startedAt : result.startedAt ,
stoppedAt : result.stoppedAt ,
2020-07-24 06:16:55 -07:00
workflowId : result.workflowData ! . id ? result . workflowData ! . id ! . toString ( ) : '' ,
2019-06-23 03:35:23 -07:00
workflowName : result.workflowData ! . name ,
} ) ;
}
return {
count ,
results : returnResults ,
} ;
} ) ) ;
// Returns a specific execution
2020-06-23 07:12:53 -07:00
this . app . get ( ` / ${ this . restEndpoint } /executions/:id ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < IExecutionFlattedResponse | undefined > = > {
2019-06-23 03:35:23 -07:00
const result = await Db . collections . Execution ! . findOne ( req . params . id ) ;
if ( result === undefined ) {
return undefined ;
}
// Convert to response format in which the id is a string
( result as IExecutionFlatted as IExecutionFlattedResponse ) . id = result . id . toString ( ) ;
return result as IExecutionFlatted as IExecutionFlattedResponse ;
} ) ) ;
// Retries a failed execution
2020-06-23 07:12:53 -07:00
this . app . post ( ` / ${ this . restEndpoint } /executions/:id/retry ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < boolean > = > {
2019-06-23 03:35:23 -07:00
// Get the data to execute
const fullExecutionDataFlatted = await Db . collections . Execution ! . findOne ( req . params . id ) ;
if ( fullExecutionDataFlatted === undefined ) {
2019-08-28 08:16:09 -07:00
throw new ResponseHelper . ResponseError ( ` The execution with the id " ${ req . params . id } " does not exist. ` , 404 , 404 ) ;
2019-06-23 03:35:23 -07:00
}
const fullExecutionData = ResponseHelper . unflattenExecutionData ( fullExecutionDataFlatted ) ;
if ( fullExecutionData . finished === true ) {
throw new Error ( 'The execution did succeed and can so not be retried.' ) ;
}
const executionMode = 'retry' ;
2019-08-08 11:38:25 -07:00
const credentials = await WorkflowCredentials ( fullExecutionData . workflowData . nodes ) ;
fullExecutionData . workflowData . active = false ;
2019-06-23 03:35:23 -07:00
2019-08-08 11:38:25 -07:00
// Start the workflow
const data : IWorkflowExecutionDataProcess = {
credentials ,
executionMode ,
executionData : fullExecutionData.data ,
retryOf : req.params.id ,
workflowData : fullExecutionData.workflowData ,
} ;
2019-12-12 16:12:38 -08:00
2020-03-17 01:07:12 -07:00
const lastNodeExecuted = data ! . executionData ! . resultData . lastNodeExecuted as string ;
// Remove the old error and the data of the last run of the node that it can be replaced
delete data ! . executionData ! . resultData . error ;
data ! . executionData ! . resultData . runData [ lastNodeExecuted ] . pop ( ) ;
2019-12-12 16:12:38 -08:00
if ( req . body . loadWorkflow === true ) {
// Loads the currently saved workflow to execute instead of the
// one saved at the time of the execution.
const workflowId = fullExecutionData . workflowData . id ;
data . workflowData = await Db . collections . Workflow ! . findOne ( workflowId ) as IWorkflowBase ;
if ( data . 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. ` ) ;
}
2020-03-17 01:07:12 -07:00
// Replace all of the nodes in the execution stack with the ones of the new workflow
for ( const stack of data ! . executionData ! . executionData ! . nodeExecutionStack ) {
// Find the data of the last executed node in the new workflow
const node = data . workflowData . nodes . find ( node = > node . name === stack . node . name ) ;
if ( node === undefined ) {
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. ` ) ;
}
// Replace the node data in the stack that it really uses the current data
stack . node = node ;
}
2019-12-12 16:12:38 -08:00
}
2019-08-08 11:38:25 -07:00
const workflowRunner = new WorkflowRunner ( ) ;
const executionId = await workflowRunner . run ( data ) ;
2019-06-23 03:35:23 -07:00
2019-08-08 22:37:10 -07:00
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 ;
2019-06-23 03:35:23 -07:00
} ) ) ;
// Delete Executions
// INFORMATION: We use POST instead of DELETE to not run into any issues
// with the query data getting to long
2020-06-23 07:12:53 -07:00
this . app . post ( ` / ${ this . restEndpoint } /executions/delete ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < void > = > {
2019-06-23 03:35:23 -07:00
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 ) ;
}
await Db . collections . Execution ! . delete ( filters ) ;
} else if ( deleteData . ids !== undefined ) {
// Deletes all executions with the given ids
await Db . collections . Execution ! . delete ( deleteData . ids ) ;
} else {
throw new Error ( 'Required body-data "ids" or "deleteBefore" is missing!' ) ;
}
} ) ) ;
// ----------------------------------------
// Executing Workflows
// ----------------------------------------
// Returns all the currently working executions
2020-06-23 07:12:53 -07:00
this . app . get ( ` / ${ this . restEndpoint } /executions-current ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < IExecutionsSummary [ ] > = > {
2019-06-23 03:35:23 -07:00
const executingWorkflows = this . activeExecutionsInstance . getActiveExecutions ( ) ;
const returnData : IExecutionsSummary [ ] = [ ] ;
let filter : any = { } ; // tslint:disable-line:no-any
if ( req . query . filter ) {
2020-04-09 11:23:33 -07:00
filter = JSON . parse ( req . query . filter as string ) ;
2019-06-23 03:35:23 -07:00
}
for ( const data of executingWorkflows ) {
if ( filter . workflowId !== undefined && filter . workflowId !== data . workflowId ) {
continue ;
}
returnData . push (
{
2019-07-24 05:25:30 -07:00
idActive : data.id.toString ( ) ,
2019-08-14 05:30:20 -07:00
workflowId : data.workflowId.toString ( ) ,
2019-08-08 22:37:10 -07:00
mode : data.mode ,
retryOf : data.retryOf ,
2019-07-22 11:29:06 -07:00
startedAt : new Date ( data . startedAt ) ,
2019-06-23 03:35:23 -07:00
}
) ;
}
return returnData ;
} ) ) ;
// Forces the execution to stop
2020-06-23 07:12:53 -07:00
this . app . post ( ` / ${ this . restEndpoint } /executions-current/:id/stop ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < IExecutionsStopData > = > {
2019-06-23 03:35:23 -07:00
const executionId = req . params . id ;
// Stopt he execution and wait till it is done and we got the data
const result = await this . activeExecutionsInstance . stopExecution ( executionId ) ;
if ( result === undefined ) {
throw new Error ( ` The execution id " ${ executionId } " could not be found. ` ) ;
}
const returnData : IExecutionsStopData = {
mode : result.mode ,
2019-07-22 11:29:06 -07:00
startedAt : new Date ( result . startedAt ) ,
stoppedAt : new Date ( result . stoppedAt ) ,
2019-06-23 03:35:23 -07:00
finished : result.finished ,
} ;
return returnData ;
} ) ) ;
// Removes a test webhook
2020-06-23 07:12:53 -07:00
this . app . delete ( ` / ${ this . restEndpoint } /test-webhook/:id ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < boolean > = > {
2019-06-23 03:35:23 -07:00
const workflowId = req . params . id ;
return this . testWebhooks . cancelTestWebhook ( workflowId ) ;
} ) ) ;
// ----------------------------------------
// Options
// ----------------------------------------
// Returns all the available timezones
2020-06-23 07:12:53 -07:00
this . app . get ( ` / ${ this . restEndpoint } /options/timezones ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < object > = > {
2019-06-23 03:35:23 -07:00
return timezones ;
} ) ) ;
// ----------------------------------------
// Settings
// ----------------------------------------
// Returns the settings which are needed in the UI
2020-06-23 07:12:53 -07:00
this . app . get ( ` / ${ this . restEndpoint } /settings ` , ResponseHelper . send ( async ( req : express.Request , res : express.Response ) : Promise < IN8nUISettings > = > {
2019-06-23 03:35:23 -07:00
return {
endpointWebhook : this.endpointWebhook ,
endpointWebhookTest : this.endpointWebhookTest ,
2019-07-10 11:53:13 -07:00
saveDataErrorExecution : this.saveDataErrorExecution ,
saveDataSuccessExecution : this.saveDataSuccessExecution ,
2019-07-10 09:06:26 -07:00
saveManualExecutions : this.saveManualExecutions ,
2020-07-29 05:12:54 -07:00
executionTimeout : this.executionTimeout ,
maxExecutionTimeout : this.maxExecutionTimeout ,
2019-06-23 03:35:23 -07:00
timezone : this.timezone ,
urlBaseWebhook : WebhookHelpers.getWebhookBaseUrl ( ) ,
2019-09-19 04:21:10 -07:00
versionCli : this.versions ! . cli ,
2019-06-23 03:35:23 -07:00
} ;
} ) ) ;
// ----------------------------------------
// Webhooks
// ----------------------------------------
2020-04-26 10:51:20 -07:00
// HEAD webhook requests
this . app . head ( ` / ${ this . endpointWebhook } /* ` , async ( req : express.Request , res : express.Response ) = > {
// Cut away the "/webhook/" to get the registred part of the url
const requestUrl = ( req as ICustomRequest ) . parsedUrl ! . pathname ! . slice ( this . endpointWebhook . length + 2 ) ;
let response ;
try {
response = await this . activeWorkflowRunner . executeWebhook ( 'HEAD' , requestUrl , req , res ) ;
} catch ( error ) {
ResponseHelper . sendErrorResponse ( res , error ) ;
return ;
}
if ( response . noWebhookResponse === true ) {
// Nothing else to do as the response got already sent
return ;
}
ResponseHelper . sendSuccessResponse ( res , response . data , true , response . responseCode ) ;
} ) ;
2019-06-23 03:35:23 -07:00
2020-07-24 07:24:18 -07:00
// OPTIONS webhook requests
this . app . options ( ` / ${ this . endpointWebhook } /* ` , async ( req : express.Request , res : express.Response ) = > {
// Cut away the "/webhook/" to get the registred part of the url
const requestUrl = ( req as ICustomRequest ) . parsedUrl ! . pathname ! . slice ( this . endpointWebhook . length + 2 ) ;
2020-07-24 07:43:23 -07:00
let allowedMethods : string [ ] ;
2020-07-24 07:24:18 -07:00
try {
allowedMethods = await this . activeWorkflowRunner . getWebhookMethods ( requestUrl ) ;
2020-07-24 07:43:23 -07:00
allowedMethods . push ( 'OPTIONS' ) ;
2020-07-24 07:24:18 -07:00
// Add custom "Allow" header to satisfy OPTIONS response.
res . append ( 'Allow' , allowedMethods ) ;
} catch ( error ) {
ResponseHelper . sendErrorResponse ( res , error ) ;
return ;
}
ResponseHelper . sendSuccessResponse ( res , { } , true , 204 ) ;
} ) ;
2019-06-23 03:35:23 -07:00
// GET webhook requests
this . app . get ( ` / ${ this . endpointWebhook } /* ` , async ( req : express.Request , res : express.Response ) = > {
// Cut away the "/webhook/" to get the registred part of the url
const requestUrl = ( req as ICustomRequest ) . parsedUrl ! . pathname ! . slice ( this . endpointWebhook . length + 2 ) ;
let response ;
try {
response = await this . activeWorkflowRunner . executeWebhook ( 'GET' , requestUrl , req , res ) ;
} catch ( error ) {
ResponseHelper . sendErrorResponse ( res , error ) ;
2020-08-27 06:09:31 -07:00
return ;
2019-06-23 03:35:23 -07:00
}
if ( response . noWebhookResponse === true ) {
// Nothing else to do as the response got already sent
return ;
}
2019-08-28 08:03:35 -07:00
ResponseHelper . sendSuccessResponse ( res , response . data , true , response . responseCode ) ;
2019-06-23 03:35:23 -07:00
} ) ;
// POST webhook requests
this . app . post ( ` / ${ this . endpointWebhook } /* ` , async ( req : express.Request , res : express.Response ) = > {
// Cut away the "/webhook/" to get the registred part of the url
const requestUrl = ( req as ICustomRequest ) . parsedUrl ! . pathname ! . slice ( this . endpointWebhook . length + 2 ) ;
let response ;
try {
response = await this . activeWorkflowRunner . executeWebhook ( 'POST' , requestUrl , req , res ) ;
} catch ( error ) {
ResponseHelper . sendErrorResponse ( res , error ) ;
return ;
}
if ( response . noWebhookResponse === true ) {
// Nothing else to do as the response got already sent
return ;
}
2019-08-28 08:03:35 -07:00
ResponseHelper . sendSuccessResponse ( res , response . data , true , response . responseCode ) ;
2019-06-23 03:35:23 -07:00
} ) ;
2020-04-26 10:51:20 -07:00
// HEAD webhook requests (test for UI)
this . app . head ( ` / ${ this . endpointWebhookTest } /* ` , async ( req : express.Request , res : express.Response ) = > {
// Cut away the "/webhook-test/" to get the registred part of the url
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 ) {
// Nothing else to do as the response got already sent
return ;
}
ResponseHelper . sendSuccessResponse ( res , response . data , true , response . responseCode ) ;
} ) ;
2019-06-23 03:35:23 -07:00
2020-07-24 07:24:18 -07:00
// HEAD webhook requests (test for UI)
this . app . options ( ` / ${ this . endpointWebhookTest } /* ` , async ( req : express.Request , res : express.Response ) = > {
// Cut away the "/webhook-test/" to get the registred part of the url
const requestUrl = ( req as ICustomRequest ) . parsedUrl ! . pathname ! . slice ( this . endpointWebhookTest . length + 2 ) ;
2020-07-24 07:43:23 -07:00
let allowedMethods : string [ ] ;
2020-07-24 07:24:18 -07:00
try {
allowedMethods = await this . testWebhooks . getWebhookMethods ( requestUrl ) ;
2020-07-24 07:43:23 -07:00
allowedMethods . push ( 'OPTIONS' ) ;
2020-07-24 07:24:18 -07:00
// Add custom "Allow" header to satisfy OPTIONS response.
res . append ( 'Allow' , allowedMethods ) ;
} catch ( error ) {
ResponseHelper . sendErrorResponse ( res , error ) ;
return ;
}
ResponseHelper . sendSuccessResponse ( res , { } , true , 204 ) ;
} ) ;
2019-06-23 03:35:23 -07:00
// GET webhook requests (test for UI)
this . app . get ( ` / ${ this . endpointWebhookTest } /* ` , async ( req : express.Request , res : express.Response ) = > {
// Cut away the "/webhook-test/" to get the registred part of the url
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 ) {
// Nothing else to do as the response got already sent
return ;
}
2019-08-28 08:03:35 -07:00
ResponseHelper . sendSuccessResponse ( res , response . data , true , response . responseCode ) ;
2019-06-23 03:35:23 -07:00
} ) ;
// POST webhook requests (test for UI)
this . app . post ( ` / ${ this . endpointWebhookTest } /* ` , async ( req : express.Request , res : express.Response ) = > {
// Cut away the "/webhook-test/" to get the registred part of the url
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 ) {
// Nothing else to do as the response got already sent
return ;
}
2019-08-28 08:03:35 -07:00
ResponseHelper . sendSuccessResponse ( res , response . data , true , response . responseCode ) ;
2019-06-23 03:35:23 -07:00
} ) ;
2020-07-07 10:03:53 -07:00
if ( this . endpointPresetCredentials !== '' ) {
// POST endpoint to set preset credentials
this . app . post ( ` / ${ this . endpointPresetCredentials } ` , async ( req : express.Request , res : express.Response ) = > {
if ( this . presetCredentialsLoaded === false ) {
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 loadNodesAndCredentials = LoadNodesAndCredentials ( ) ;
const credentialsOverwrites = CredentialsOverwrites ( ) ;
await credentialsOverwrites . init ( body ) ;
const credentialTypes = CredentialTypes ( ) ;
await credentialTypes . init ( loadNodesAndCredentials . credentialTypes ) ;
this . presetCredentialsLoaded = true ;
ResponseHelper . sendSuccessResponse ( res , { success : true } , true , 200 ) ;
} else {
ResponseHelper . sendErrorResponse ( res , new Error ( 'Preset credentials can be set once' ) ) ;
}
} ) ;
}
2020-07-12 00:28:08 -07:00
// Read the index file and replace the path placeholder
const editorUiPath = require . resolve ( 'n8n-editor-ui' ) ;
const filePath = pathJoin ( pathDirname ( editorUiPath ) , 'dist' , 'index.html' ) ;
const n8nPath = config . get ( 'path' ) ;
2020-07-14 14:36:05 -07:00
let readIndexFile = readFileSync ( filePath , 'utf8' ) ;
2020-07-12 00:28:08 -07:00
readIndexFile = readIndexFile . replace ( /\/%BASE_PATH%\//g , n8nPath ) ;
2020-08-14 14:20:07 -07:00
readIndexFile = readIndexFile . replace ( /\/favicon.ico/g , ` ${ n8nPath } favicon.ico ` ) ;
2020-07-12 00:28:08 -07:00
// Serve the altered index.html file separately
this . app . get ( ` /index.html ` , async ( req : express.Request , res : express.Response ) = > {
res . send ( readIndexFile ) ;
} ) ;
2019-06-23 03:35:23 -07:00
// Serve the website
2019-09-20 04:54:06 -07:00
const startTime = ( new Date ( ) ) . toUTCString ( ) ;
this . app . use ( '/' , express . static ( pathJoin ( pathDirname ( editorUiPath ) , 'dist' ) , {
index : 'index.html' ,
setHeaders : ( res , path ) = > {
if ( res . req && res . req . url === '/index.html' ) {
// Set last modified date manually to n8n start time so
// that it hopefully refreshes the page when a new version
// got used
res . setHeader ( 'Last-Modified' , startTime ) ;
}
}
} ) ) ;
2019-06-23 03:35:23 -07:00
}
}
2019-08-04 05:24:48 -07:00
export async function start ( ) : Promise < void > {
2019-07-21 10:47:41 -07:00
const PORT = config . get ( 'port' ) ;
2020-06-04 00:36:10 -07:00
const ADDRESS = config . get ( 'listen_address' ) ;
2019-06-23 03:35:23 -07:00
2019-08-04 05:24:48 -07:00
const app = new App ( ) ;
await app . config ( ) ;
2019-06-23 03:35:23 -07:00
2020-03-29 03:46:55 -07:00
let server ;
2020-03-27 10:19:30 -07:00
2020-08-27 06:09:31 -07:00
if ( app . protocol === 'https' && app . sslKey && app . sslCert ) {
2020-03-29 03:46:55 -07:00
const https = require ( 'https' ) ;
2020-03-30 04:29:42 -07:00
const privateKey = readFileSync ( app . sslKey , 'utf8' ) ;
const cert = readFileSync ( app . sslCert , 'utf8' ) ;
2020-08-27 06:09:31 -07:00
const credentials = { key : privateKey , cert } ;
server = https . createServer ( credentials , app . app ) ;
} else {
2020-03-29 03:46:55 -07:00
const http = require ( 'http' ) ;
2020-03-27 10:19:30 -07:00
server = http . createServer ( app . app ) ;
}
2019-06-23 03:35:23 -07:00
2020-06-04 00:36:10 -07:00
server . listen ( PORT , ADDRESS , async ( ) = > {
2019-09-19 04:21:10 -07:00
const versions = await GenericHelpers . getVersions ( ) ;
2020-06-04 00:36:10 -07:00
console . log ( ` n8n ready on ${ ADDRESS } , port ${ PORT } ` ) ;
2019-09-19 04:21:10 -07:00
console . log ( ` Version: ${ versions . cli } ` ) ;
2019-06-23 03:35:23 -07:00
} ) ;
}