/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
/* eslint-disable @typescript-eslint/no-use-before-define */
/* eslint-disable @typescript-eslint/await-thenable */
/* eslint-disable new-cap */
/* eslint-disable prefer-const */
/* eslint-disable @typescript-eslint/no-invalid-void-type */
/* eslint-disable no-return-assign */
/* eslint-disable no-param-reassign */
/* eslint-disable consistent-return */
/* eslint-disable import/no-cycle */
/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable id-denylist */
/* eslint-disable no-console */
/* eslint-disable global-require */
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/return-await */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable no-continue */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable import/no-dynamic-require */
/* eslint-disable no-await-in-loop */

import * as express from 'express';
import { readFileSync } from 'fs';
import { readFile } from 'fs/promises';
import { cloneDeep } from 'lodash';
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve } from 'path';
import {
	FindConditions,
	FindManyOptions,
	getConnection,
	getConnectionManager,
	In,
	IsNull,
	LessThan,
	LessThanOrEqual,
	MoreThan,
	Not,
	Raw,
} from 'typeorm';
import * as bodyParser from 'body-parser';
import * as cookieParser from 'cookie-parser';
import * as history from 'connect-history-api-fallback';
import * as os from 'os';
// eslint-disable-next-line import/no-extraneous-dependencies
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';
// IMPORTANT! Do not switch to anther bcrypt library unless really necessary and
// tested with all possible systems like Windows, Alpine on ARM, FreeBSD, ...
import { compare } from 'bcryptjs';
import * as promClient from 'prom-client';

import {
	BinaryDataManager,
	Credentials,
	IBinaryDataConfig,
	LoadNodeParameterOptions,
	UserSettings,
} from 'n8n-core';

import {
	ICredentialType,
	IDataObject,
	INodeCredentials,
	INodeCredentialsDetails,
	INodeParameters,
	INodePropertyOptions,
	INodeType,
	INodeTypeDescription,
	INodeTypeNameVersion,
	ITelemetrySettings,
	IWorkflowBase,
	LoggerProxy,
	NodeHelpers,
	WebhookHttpMethod,
	Workflow,
	WorkflowExecuteMode,
} from 'n8n-workflow';

import * as basicAuth from 'basic-auth';
import * as compression from 'compression';
import * as jwt from 'jsonwebtoken';
import * as jwks from 'jwks-rsa';
// @ts-ignore
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,
	ICredentialsDb,
	ICredentialsOverwrite,
	ICustomRequest,
	IExecutionFlattedDb,
	IExecutionFlattedResponse,
	IExecutionPushResponse,
	IExecutionResponse,
	IExecutionsListResponse,
	IExecutionsStopData,
	IExecutionsSummary,
	IExternalHooksClass,
	IDiagnosticInfo,
	IN8nUISettings,
	IPackageVersions,
	ITagWithCountDb,
	IWorkflowExecutionDataProcess,
	IWorkflowResponse,
	NodeTypes,
	Push,
	ResponseHelper,
	TestWebhooks,
	WaitTracker,
	WaitTrackerClass,
	WebhookHelpers,
	WebhookServer,
	WorkflowExecuteAdditionalData,
	WorkflowHelpers,
	WorkflowRunner,
	getCredentialForUser,
} from '.';

import * as config from '../config';

import * as TagHelpers from './TagHelpers';

import { InternalHooksManager } from './InternalHooksManager';
import { TagEntity } from './databases/entities/TagEntity';
import { WorkflowEntity } from './databases/entities/WorkflowEntity';
import { getSharedWorkflowIds, whereClause } from './WorkflowHelpers';
import { getCredentialTranslationPath, getNodeTranslationPath } from './TranslationHelpers';
import { WEBHOOK_METHODS } from './WebhookHelpers';

import { userManagementRouter } from './UserManagement';
import { resolveJwt } from './UserManagement/auth/jwt';
import { User } from './databases/entities/User';
import { CredentialsEntity } from './databases/entities/CredentialsEntity';
import type {
	CredentialRequest,
	ExecutionRequest,
	WorkflowRequest,
	NodeParameterOptionsRequest,
	OAuthRequest,
	AuthenticatedRequest,
	TagsRequest,
} from './requests';
import { DEFAULT_EXECUTIONS_GET_ALL_LIMIT, validateEntity } from './GenericHelpers';
import { ExecutionEntity } from './databases/entities/ExecutionEntity';
import { SharedWorkflow } from './databases/entities/SharedWorkflow';
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from './constants';
import { credentialsController } from './api/credentials.api';
import { getInstanceBaseUrl, isEmailSetUp } from './UserManagement/UserManagementHelper';

require('body-parser-xml')(bodyParser);

export const externalHooks: IExternalHooksClass = ExternalHooks();

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;

	webhookMethods: WebhookHttpMethod[];

	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: ITelemetrySettings = {
			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,
			urlBaseEditor: getInstanceBaseUrl(),
			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,
			personalizationSurveyEnabled:
				config.get('personalization.enabled') && config.get('diagnostics.enabled'),
			defaultLocale: config.get('defaultLocale'),
			userManagement: {
				enabled:
					config.get('userManagement.disabled') === false ||
					config.get('userManagement.isInstanceOwnerSetUp') === true,
				showSetupOnFirstLoad:
					config.get('userManagement.disabled') === false &&
					config.get('userManagement.isInstanceOwnerSetUp') === false &&
					config.get('userManagement.skipInstanceOwnerSetup') === false,
				smtpSetup: isEmailSetUp(),
			},
			workflowTagsDisabled: config.get('workflowTagsDisabled'),
			logLevel: config.get('logs.level'),
			hiringBannerEnabled: config.get('hiringBanner.enabled'),
			templates: {
				enabled: config.get('templates.enabled'),
				host: config.get('templates.host'),
			},
		};
	}

	/**
	 * Returns the current epoch time
	 *
	 * @returns {number}
	 * @memberof App
	 */
	getCurrentDate(): Date {
		return new Date();
	}

	/**
	 * Returns the current settings for the frontend
	 */
	getSettingsForFrontend(): IN8nUISettings {
		// refresh user management status
		Object.assign(this.frontendSettings.userManagement, {
			enabled:
				config.get('userManagement.disabled') === false ||
				config.get('userManagement.isInstanceOwnerSetUp') === true,
			showSetupOnFirstLoad:
				config.get('userManagement.disabled') === false &&
				config.get('userManagement.isInstanceOwnerSetUp') === false &&
				config.get('userManagement.skipInstanceOwnerSetup') === false,
		});

		return this.frontendSettings;
	}

	async config(): Promise<void> {
		const enableMetrics = config.get('endpoints.metrics.enable') as boolean;
		let register: Registry;

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

		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,
		];
		// eslint-disable-next-line prefer-spread
		ignoredEndpoints.push.apply(ignoredEndpoints, excludeEndpoints.split(':'));

		// eslint-disable-next-line no-useless-escape
		const authIgnoreRegex = new RegExp(`^\/(${_(ignoredEndpoints).compact().join('|')})\/?.*$`);

		// Check for basic auth credentials if activated
		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 | string = null;

			this.app.use(
				async (req: express.Request, res: express.Response, next: express.NextFunction) => {
					// Skip basic auth for a few listed endpoints or when instance owner has been setup
					if (authIgnoreRegex.exec(req.url) || config.get('userManagement.isInstanceOwnerSetUp')) {
						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!',
						);
					}

					if (basicAuthData.name === basicAuthUser) {
						if (basicAuthHashEnabled) {
							if (
								validPassword === null &&
								(await compare(basicAuthData.pass, basicAuthPassword))
							) {
								// Password is valid so save for future requests
								validPassword = basicAuthData.pass;
							}

							if (validPassword === basicAuthData.pass && validPassword !== null) {
								// Provided hash is correct
								return next();
							}
						} else if (basicAuthData.pass === basicAuthPassword) {
							// Provided password is correct
							return next();
						}
					}

					// Provided authentication data is wrong
					return ResponseHelper.basicAuthAuthorizationError(
						res,
						realm,
						'Authorization data is wrong!',
					);
				},
			);
		}

		// Check for and validate JWT if configured
		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;

			// eslint-disable-next-line no-inner-declarations
			function isTenantAllowed(decodedToken: object): boolean {
				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;
			}

			// eslint-disable-next-line consistent-return
			this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
				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 });
				// eslint-disable-next-line @typescript-eslint/ban-types
				function getKey(header: any, callback: Function) {
					jwkClient.getSigningKey(header.kid, (err: Error, key: any) => {
						// eslint-disable-next-line @typescript-eslint/no-throw-literal
						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();
					}
				});
			});
		}

		// Parse cookies for easier access
		this.app.use(cookieParser());

		// Get push connections
		this.app.use(
			async (req: express.Request, res: express.Response, next: express.NextFunction) => {
				if (req.url.indexOf(`/${this.restEndpoint}/push`) === 0) {
					if (req.query.sessionId === undefined) {
						next(new Error('The query parameter "sessionId" is missing!'));
						return;
					}

					try {
						const authCookie = req.cookies?.[AUTH_COOKIE_NAME] ?? '';
						await resolveJwt(authCookie);
					} catch (error) {
						res.status(401).send('Unauthorized');
						return;
					}

					this.push.add(req.query.sessionId as string, req, res);
					return;
				}
				next();
			},
		);

		// Compress the response data
		this.app.use(compression());

		// 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);
			// @ts-ignore
			req.rawBody = Buffer.from('', 'base64');
			next();
		});

		// Support application/json type post data
		this.app.use(
			bodyParser.json({
				limit: `${this.payloadSizeMax}mb`,
				verify: (req, res, buf) => {
					// @ts-ignore
					req.rawBody = buf;
				},
			}),
		);

		// Support application/xml type post data
		this.app.use(
			// @ts-ignore
			bodyParser.xml({
				limit: `${this.payloadSizeMax}mb`,
				xmlParseOptions: {
					normalize: true, // Trim whitespace inside text nodes
					normalizeTags: true, // Transform tags to lowercase
					explicitArray: false, // Only put properties in array if length > 1
				},
			}),
		);

		this.app.use(
			bodyParser.text({
				limit: `${this.payloadSizeMax}mb`,
				verify: (req, res, buf) => {
					// @ts-ignore
					req.rawBody = buf;
				},
			}),
		);

		// Make sure that Vue history mode works properly
		this.app.use(
			history({
				rewrites: [
					{
						from: new RegExp(
							// eslint-disable-next-line no-useless-escape
							`^\/(${this.restEndpoint}|healthz|metrics|css|js|${this.endpointWebhook}|${this.endpointWebhookTest})\/?.*$`,
						),
						to: (context) => {
							return context.parsedUrl.pathname!.toString();
						},
					},
				],
			}),
		);

		// support application/x-www-form-urlencoded post data
		this.app.use(
			bodyParser.urlencoded({
				limit: `${this.payloadSizeMax}mb`,
				extended: false,
				verify: (req, res, buf) => {
					// @ts-ignore
					req.rawBody = buf;
				},
			}),
		);

		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-Credentials', 'true');
				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();
			});
		}

		// eslint-disable-next-line consistent-return
		this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
			if (Db.collections.Workflow === null) {
				const error = new ResponseHelper.ResponseError('Database is not ready!', undefined, 503);
				return ResponseHelper.sendErrorResponse(res, error);
			}

			next();
		});

		// ----------------------------------------
		// User Management
		// ----------------------------------------
		await userManagementRouter.addRoutes.apply(this, [ignoredEndpoints, this.restEndpoint]);

		this.app.use(`/${this.restEndpoint}/credentials`, credentialsController);

		// ----------------------------------------
		// Healthcheck
		// ----------------------------------------

		// Does very basic health check
		this.app.get('/healthz', async (req: express.Request, res: express.Response) => {
			LoggerProxy.debug('Health check started!');

			const connection = getConnectionManager().get();

			try {
				if (!connection.isConnected) {
					// Connection is not active
					throw new Error('No active database connection!');
				}
				// DB ping
				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);
			}

			// Everything fine
			const responseData = {
				status: 'ok',
			};

			LoggerProxy.debug('Health check completed successfully!');

			ResponseHelper.sendSuccessResponse(res, responseData, true, 200);
		});

		// ----------------------------------------
		// Metrics
		// ----------------------------------------
		if (enableMetrics) {
			this.app.get('/metrics', async (req: express.Request, res: express.Response) => {
				const response = await register.metrics();
				res.setHeader('Content-Type', register.contentType);
				ResponseHelper.sendSuccessResponse(res, response, true, 200);
			});
		}

		// ----------------------------------------
		// Workflow
		// ----------------------------------------

		// Creates a new workflow
		this.app.post(
			`/${this.restEndpoint}/workflows`,
			ResponseHelper.send(async (req: WorkflowRequest.Create) => {
				delete req.body.id; // delete if sent

				const newWorkflow = new WorkflowEntity();

				Object.assign(newWorkflow, req.body);

				await validateEntity(newWorkflow);

				await this.externalHooks.run('workflow.create', [newWorkflow]);

				const { tags: tagIds } = req.body;

				if (tagIds?.length && !config.get('workflowTagsDisabled')) {
					newWorkflow.tags = await Db.collections.Tag!.findByIds(tagIds, {
						select: ['id', 'name'],
					});
				}

				await WorkflowHelpers.replaceInvalidCredentials(newWorkflow);

				let savedWorkflow: undefined | WorkflowEntity;

				await getConnection().transaction(async (transactionManager) => {
					savedWorkflow = await transactionManager.save<WorkflowEntity>(newWorkflow);

					const role = await Db.collections.Role!.findOneOrFail({
						name: 'owner',
						scope: 'workflow',
					});

					const newSharedWorkflow = new SharedWorkflow();

					Object.assign(newSharedWorkflow, {
						role,
						user: req.user,
						workflow: savedWorkflow,
					});

					await transactionManager.save<SharedWorkflow>(newSharedWorkflow);
				});

				if (!savedWorkflow) {
					LoggerProxy.error('Failed to create workflow', { userId: req.user.id });
					throw new ResponseHelper.ResponseError('Failed to save workflow');
				}

				if (tagIds && !config.get('workflowTagsDisabled')) {
					savedWorkflow.tags = TagHelpers.sortByRequestOrder(savedWorkflow.tags, {
						requestOrder: tagIds,
					});
				}

				await this.externalHooks.run('workflow.afterCreate', [savedWorkflow]);
				void InternalHooksManager.getInstance().onWorkflowCreated(req.user.id, newWorkflow);

				const { id, ...rest } = savedWorkflow;

				return {
					id: id.toString(),
					...rest,
				};
			}),
		);

		// Reads and returns workflow data from an URL
		this.app.get(
			`/${this.restEndpoint}/workflows/from-url`,
			ResponseHelper.send(
				async (req: express.Request, res: express.Response): 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: IWorkflowResponse | undefined;
					try {
						workflowData = JSON.parse(data);
					} catch (error) {
						throw new ResponseHelper.ResponseError(
							`The URL does not point to valid JSON file!`,
							undefined,
							400,
						);
					}

					// 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)
					) {
						throw new ResponseHelper.ResponseError(
							`The data in the file does not seem to be a n8n workflow JSON file!`,
							undefined,
							400,
						);
					}

					return workflowData;
				},
			),
		);

		// Returns workflows
		this.app.get(
			`/${this.restEndpoint}/workflows`,
			ResponseHelper.send(async (req: WorkflowRequest.GetAll) => {
				let workflows: WorkflowEntity[] = [];

				const filter: Record<string, string> = req.query.filter ? JSON.parse(req.query.filter) : {};

				const query: FindManyOptions<WorkflowEntity> = {
					select: ['id', 'name', 'active', 'createdAt', 'updatedAt'],
					relations: ['tags'],
				};

				if (config.get('workflowTagsDisabled')) {
					delete query.relations;
				}

				if (req.user.globalRole.name === 'owner') {
					workflows = await Db.collections.Workflow!.find(
						Object.assign(query, {
							where: filter,
						}),
					);
				} else {
					const shared = await Db.collections.SharedWorkflow!.find({
						relations: ['workflow'],
						where: whereClause({
							user: req.user,
							entityType: 'workflow',
						}),
					});

					if (!shared.length) return [];

					workflows = await Db.collections.Workflow!.find(
						Object.assign(query, {
							where: {
								id: In(shared.map(({ workflow }) => workflow.id)),
								...filter,
							},
						}),
					);
				}

				return workflows.map((workflow) => {
					const { id, ...rest } = workflow;

					return {
						id: id.toString(),
						...rest,
					};
				});
			}),
		);

		this.app.get(
			`/${this.restEndpoint}/workflows/new`,
			ResponseHelper.send(async (req: WorkflowRequest.NewName) => {
				const requestedName =
					req.query.name && req.query.name !== '' ? req.query.name : this.defaultWorkflowName;

				return await GenericHelpers.generateUniqueName(requestedName, 'workflow');
			}),
		);

		// Returns a specific workflow
		this.app.get(
			`/${this.restEndpoint}/workflows/:id`,
			ResponseHelper.send(async (req: WorkflowRequest.Get) => {
				const { id: workflowId } = req.params;

				let relations = ['workflow', 'workflow.tags'];

				if (config.get('workflowTagsDisabled')) {
					relations = relations.filter((relation) => relation !== 'workflow.tags');
				}

				const shared = await Db.collections.SharedWorkflow!.findOne({
					relations,
					where: whereClause({
						user: req.user,
						entityType: 'workflow',
						entityId: workflowId,
					}),
				});

				if (!shared) {
					LoggerProxy.info('User attempted to access a workflow without permissions', {
						workflowId,
						userId: req.user.id,
					});
					throw new ResponseHelper.ResponseError(
						`Workflow with ID "${workflowId}" could not be found.`,
						undefined,
						404,
					);
				}

				const {
					workflow: { id, ...rest },
				} = shared;

				return {
					id: id.toString(),
					...rest,
				};
			}),
		);

		// Updates an existing workflow
		this.app.patch(
			`/${this.restEndpoint}/workflows/:id`,
			ResponseHelper.send(async (req: WorkflowRequest.Update) => {
				const { id: workflowId } = req.params;

				const updateData = new WorkflowEntity();
				const { tags, ...rest } = req.body;
				Object.assign(updateData, rest);

				const shared = await Db.collections.SharedWorkflow!.findOne({
					relations: ['workflow'],
					where: whereClause({
						user: req.user,
						entityType: 'workflow',
						entityId: workflowId,
					}),
				});

				if (!shared) {
					LoggerProxy.info('User attempted to update a workflow without permissions', {
						workflowId,
						userId: req.user.id,
					});
					throw new ResponseHelper.ResponseError(
						`Workflow with ID "${workflowId}" could not be found to be updated.`,
						undefined,
						404,
					);
				}

				// check credentials for old format
				await WorkflowHelpers.replaceInvalidCredentials(updateData);

				await this.externalHooks.run('workflow.update', [updateData]);

				if (shared.workflow.active) {
					// When workflow gets saved always remove it as the triggers could have been
					// changed and so the changes would not take effect
					await this.activeWorkflowRunner.remove(workflowId);
				}

				if (updateData.settings) {
					if (updateData.settings.timezone === 'DEFAULT') {
						// Do not save the default timezone
						delete updateData.settings.timezone;
					}
					if (updateData.settings.saveDataErrorExecution === 'DEFAULT') {
						// Do not save when default got set
						delete updateData.settings.saveDataErrorExecution;
					}
					if (updateData.settings.saveDataSuccessExecution === 'DEFAULT') {
						// Do not save when default got set
						delete updateData.settings.saveDataSuccessExecution;
					}
					if (updateData.settings.saveManualExecutions === 'DEFAULT') {
						// Do not save when default got set
						delete updateData.settings.saveManualExecutions;
					}
					if (
						parseInt(updateData.settings.executionTimeout as string, 10) === this.executionTimeout
					) {
						// Do not save when default got set
						delete updateData.settings.executionTimeout;
					}
				}

				if (updateData.name) {
					updateData.updatedAt = this.getCurrentDate(); // required due to atomic update
					await validateEntity(updateData);
				}

				await Db.collections.Workflow!.update(workflowId, updateData);

				if (tags && !config.get('workflowTagsDisabled')) {
					const tablePrefix = config.get('database.tablePrefix');
					await TagHelpers.removeRelations(workflowId, tablePrefix);

					if (tags.length) {
						await TagHelpers.createRelations(workflowId, tags, tablePrefix);
					}
				}

				const options: FindManyOptions<WorkflowEntity> = {
					relations: ['tags'],
				};

				if (config.get('workflowTagsDisabled')) {
					delete options.relations;
				}

				// We sadly get nothing back from "update". Neither if it updated a record
				// nor the new value. So query now the hopefully updated entry.
				const updatedWorkflow = await Db.collections.Workflow!.findOne(workflowId, options);

				if (updatedWorkflow === undefined) {
					throw new ResponseHelper.ResponseError(
						`Workflow with ID "${workflowId}" could not be found to be updated.`,
						undefined,
						400,
					);
				}

				if (updatedWorkflow.tags.length && tags?.length) {
					updatedWorkflow.tags = TagHelpers.sortByRequestOrder(updatedWorkflow.tags, {
						requestOrder: tags,
					});
				}

				await this.externalHooks.run('workflow.afterUpdate', [updatedWorkflow]);
				// @ts-ignore
				void InternalHooksManager.getInstance().onWorkflowSaved(req.user.id, updatedWorkflow);

				if (updatedWorkflow.active) {
					// When the workflow is supposed to be active add it again
					try {
						await this.externalHooks.run('workflow.activate', [updatedWorkflow]);
						await this.activeWorkflowRunner.add(
							workflowId,
							shared.workflow.active ? 'update' : 'activate',
						);
					} catch (error) {
						// If workflow could not be activated set it again to inactive
						updateData.active = false;
						// @ts-ignore
						await Db.collections.Workflow!.update(workflowId, updateData);

						// Also set it in the returned data
						updatedWorkflow.active = false;

						// Now return the original error for UI to display
						throw error;
					}
				}

				const { id, ...remainder } = updatedWorkflow;

				return {
					id: id.toString(),
					...remainder,
				};
			}),
		);

		// Deletes a specific workflow
		this.app.delete(
			`/${this.restEndpoint}/workflows/:id`,
			ResponseHelper.send(async (req: WorkflowRequest.Delete) => {
				const { id: workflowId } = req.params;

				await this.externalHooks.run('workflow.delete', [workflowId]);

				const shared = await Db.collections.SharedWorkflow!.findOne({
					relations: ['workflow'],
					where: whereClause({
						user: req.user,
						entityType: 'workflow',
						entityId: workflowId,
					}),
				});

				if (!shared) {
					LoggerProxy.info('User attempted to delete a workflow without permissions', {
						workflowId,
						userId: req.user.id,
					});
					throw new ResponseHelper.ResponseError(
						`Workflow with ID "${workflowId}" could not be found to be deleted.`,
						undefined,
						400,
					);
				}

				if (shared.workflow.active) {
					// deactivate before deleting
					await this.activeWorkflowRunner.remove(workflowId);
				}

				await Db.collections.Workflow!.delete(workflowId);

				void InternalHooksManager.getInstance().onWorkflowDeleted(req.user.id, workflowId);
				await this.externalHooks.run('workflow.afterDelete', [workflowId]);

				return true;
			}),
		);

		this.app.post(
			`/${this.restEndpoint}/workflows/run`,
			ResponseHelper.send(
				async (
					req: WorkflowRequest.ManualRun,
					res: express.Response,
				): 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 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
					) {
						const additionalData = await WorkflowExecuteAdditionalData.getBase(req.user.id);
						const nodeTypes = NodeTypes();
						const workflowInstance = new Workflow({
							id: workflowData.id?.toString(),
							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,
							};
						}
					}

					// For manual testing always set to not active
					workflowData.active = false;

					// Start the workflow
					const data: IWorkflowExecutionDataProcess = {
						destinationNode,
						executionMode,
						runData,
						sessionId,
						startNodes,
						workflowData,
						userId: req.user.id,
					};
					const workflowRunner = new WorkflowRunner();
					const executionId = await workflowRunner.run(data);

					return {
						executionId,
					};
				},
			),
		);

		// Retrieves all tags, with or without usage count
		this.app.get(
			`/${this.restEndpoint}/tags`,
			ResponseHelper.send(
				async (
					req: express.Request,
					res: express.Response,
				): Promise<TagEntity[] | ITagWithCountDb[]> => {
					if (config.get('workflowTagsDisabled')) {
						throw new ResponseHelper.ResponseError('Workflow tags are disabled');
					}
					if (req.query.withUsageCount === 'true') {
						const tablePrefix = config.get('database.tablePrefix');
						return TagHelpers.getTagsWithCountDb(tablePrefix);
					}

					return Db.collections.Tag!.find({ select: ['id', 'name'] });
				},
			),
		);

		// Creates a tag
		this.app.post(
			`/${this.restEndpoint}/tags`,
			ResponseHelper.send(
				async (req: express.Request, res: express.Response): Promise<TagEntity | void> => {
					if (config.get('workflowTagsDisabled')) {
						throw new ResponseHelper.ResponseError('Workflow tags are disabled');
					}
					const newTag = new TagEntity();
					newTag.name = req.body.name.trim();

					await this.externalHooks.run('tag.beforeCreate', [newTag]);

					await validateEntity(newTag);
					const tag = await Db.collections.Tag!.save(newTag);

					await this.externalHooks.run('tag.afterCreate', [tag]);

					return tag;
				},
			),
		);

		// Updates a tag
		this.app.patch(
			`/${this.restEndpoint}/tags/:id`,
			ResponseHelper.send(
				async (req: express.Request, res: express.Response): Promise<TagEntity | void> => {
					if (config.get('workflowTagsDisabled')) {
						throw new ResponseHelper.ResponseError('Workflow tags are disabled');
					}

					const { name } = req.body;
					const { id } = req.params;

					const newTag = new TagEntity();
					// @ts-ignore
					newTag.id = id;
					newTag.name = name.trim();

					await this.externalHooks.run('tag.beforeUpdate', [newTag]);

					await validateEntity(newTag);
					const tag = await Db.collections.Tag!.save(newTag);

					await this.externalHooks.run('tag.afterUpdate', [tag]);

					return tag;
				},
			),
		);

		// Deletes a tag
		this.app.delete(
			`/${this.restEndpoint}/tags/:id`,
			ResponseHelper.send(
				async (req: TagsRequest.Delete, res: express.Response): Promise<boolean> => {
					if (config.get('workflowTagsDisabled')) {
						throw new ResponseHelper.ResponseError('Workflow tags are disabled');
					}
					if (
						config.get('userManagement.isInstanceOwnerSetUp') === true &&
						req.user.globalRole.name !== 'owner'
					) {
						throw new ResponseHelper.ResponseError(
							'You are not allowed to perform this action',
							undefined,
							403,
							'Only owners can remove tags',
						);
					}
					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;
				},
			),
		);

		// Returns parameter values which normally get loaded from an external API or
		// get generated dynamically
		this.app.get(
			`/${this.restEndpoint}/node-parameter-options`,
			ResponseHelper.send(
				async (req: NodeParameterOptionsRequest): Promise<INodePropertyOptions[]> => {
					const nodeTypeAndVersion = JSON.parse(
						req.query.nodeTypeAndVersion,
					) as INodeTypeNameVersion;

					const { path, methodName } = req.query;

					const currentNodeParameters = JSON.parse(
						req.query.currentNodeParameters,
					) as INodeParameters;

					let credentials: INodeCredentials | undefined;

					if (req.query.credentials) {
						credentials = JSON.parse(req.query.credentials);
					}

					const loadDataInstance = new LoadNodeParameterOptions(
						nodeTypeAndVersion,
						NodeTypes(),
						path,
						currentNodeParameters,
						credentials,
					);

					const additionalData = await WorkflowExecuteAdditionalData.getBase(
						req.user.id,
						currentNodeParameters,
					);

					if (methodName) {
						return loadDataInstance.getOptionsViaMethodName(methodName, additionalData);
					}
					// @ts-ignore
					if (req.query.loadOptions) {
						return loadDataInstance.getOptionsViaRequestProperty(
							// @ts-ignore
							JSON.parse(req.query.loadOptions as string),
							additionalData,
						);
					}

					return [];
				},
			),
		);

		// Returns all the node-types
		this.app.get(
			`/${this.restEndpoint}/node-types`,
			ResponseHelper.send(
				async (req: express.Request, res: express.Response): Promise<INodeTypeDescription[]> => {
					const returnData: INodeTypeDescription[] = [];
					const onlyLatest = req.query.onlyLatest === 'true';

					const nodeTypes = NodeTypes();
					const allNodes = nodeTypes.getAll();

					const getNodeDescription = (nodeType: INodeType): INodeTypeDescription => {
						const nodeInfo: INodeTypeDescription = { ...nodeType.description };
						if (req.query.includeProperties !== 'true') {
							// @ts-ignore
							delete nodeInfo.properties;
						}
						return nodeInfo;
					};

					if (onlyLatest) {
						allNodes.forEach((nodeData) => {
							const nodeType = NodeHelpers.getVersionedNodeType(nodeData);
							const nodeInfo: INodeTypeDescription = getNodeDescription(nodeType);
							returnData.push(nodeInfo);
						});
					} else {
						allNodes.forEach((nodeData) => {
							const allNodeTypes = NodeHelpers.getVersionedNodeTypeAll(nodeData);
							allNodeTypes.forEach((element) => {
								const nodeInfo: INodeTypeDescription = getNodeDescription(element);
								returnData.push(nodeInfo);
							});
						});
					}

					return returnData;
				},
			),
		);

		this.app.get(
			`/${this.restEndpoint}/credential-translation`,
			ResponseHelper.send(
				async (
					req: express.Request & { query: { credentialType: string } },
					res: express.Response,
				): Promise<object | null> => {
					const translationPath = getCredentialTranslationPath({
						locale: this.frontendSettings.defaultLocale,
						credentialType: req.query.credentialType,
					});

					try {
						return require(translationPath);
					} catch (error) {
						return null;
					}
				},
			),
		);

		// Returns node information based on node names and versions
		this.app.post(
			`/${this.restEndpoint}/node-types`,
			ResponseHelper.send(
				async (req: express.Request, res: express.Response): 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: string,
						version: number,
						nodeTypes: INodeTypeDescription[],
					) {
						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) {
							// ignore - no translation exists at path
						}

						nodeTypes.push(description);
					}

					const nodeTypes: INodeTypeDescription[] = [];

					const promises = nodeInfos.map(async ({ name, version }) =>
						populateTranslation(name, version, nodeTypes),
					);

					await Promise.all(promises);

					return nodeTypes;
				},
			),
		);

		// Returns node information based on node names and versions
		this.app.get(
			`/${this.restEndpoint}/node-translation-headers`,
			ResponseHelper.send(
				async (req: express.Request, res: express.Response): 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');
					}
				},
			),
		);

		// ----------------------------------------
		// Node-Types
		// ----------------------------------------

		// Returns the node icon
		this.app.get(
			[
				`/${this.restEndpoint}/node-icon/:nodeType`,
				`/${this.restEndpoint}/node-icon/:scope/:nodeType`,
			],
			async (req: express.Request, res: express.Response): 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; // 7 days
					res.setHeader('Cache-control', `private max-age=${maxAge}`);

					res.sendFile(filepath);
				} catch (error) {
					// Error response
					return ResponseHelper.sendErrorResponse(res, error);
				}
			},
		);

		// ----------------------------------------
		// Active Workflows
		// ----------------------------------------

		// Returns the active workflow ids
		this.app.get(
			`/${this.restEndpoint}/active`,
			ResponseHelper.send(async (req: WorkflowRequest.GetAllActive) => {
				const activeWorkflows = await this.activeWorkflowRunner.getActiveWorkflows(req.user);

				return activeWorkflows.map(({ id }) => id.toString());
			}),
		);

		// Returns if the workflow with the given id had any activation errors
		this.app.get(
			`/${this.restEndpoint}/active/error/:id`,
			ResponseHelper.send(async (req: WorkflowRequest.GetAllActivationErrors) => {
				const { id: workflowId } = req.params;

				const shared = await Db.collections.SharedWorkflow!.findOne({
					relations: ['workflow'],
					where: whereClause({
						user: req.user,
						entityType: 'workflow',
						entityId: workflowId,
					}),
				});

				if (!shared) {
					LoggerProxy.info('User attempted to access workflow errors without permissions', {
						workflowId,
						userId: req.user.id,
					});

					throw new ResponseHelper.ResponseError(
						`Workflow with ID "${workflowId}" could not be found.`,
						undefined,
						400,
					);
				}

				return this.activeWorkflowRunner.getActivationError(workflowId);
			}),
		);

		// ----------------------------------------
		// Credential-Types
		// ----------------------------------------

		// Returns all the credential types which are defined in the loaded n8n-modules
		this.app.get(
			`/${this.restEndpoint}/credential-types`,
			ResponseHelper.send(
				async (req: express.Request, res: express.Response): Promise<ICredentialType[]> => {
					const returnData: ICredentialType[] = [];

					const credentialTypes = CredentialTypes();

					credentialTypes.getAll().forEach((credentialData) => {
						returnData.push(credentialData);
					});

					return returnData;
				},
			),
		);

		this.app.get(
			`/${this.restEndpoint}/credential-icon/:credentialType`,
			async (req: express.Request, res: express.Response): 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; // 7 days
					res.setHeader('Cache-control', `private max-age=${maxAge}`);

					res.sendFile(filepath);
				} catch (error) {
					// Error response
					return ResponseHelper.sendErrorResponse(res, error);
				}
			},
		);

		// ----------------------------------------
		// OAuth1-Credential/Auth
		// ----------------------------------------

		// Authorize OAuth Data
		this.app.get(
			`/${this.restEndpoint}/oauth1-credential/auth`,
			ResponseHelper.send(async (req: OAuthRequest.OAuth1Credential.Auth): Promise<string> => {
				const { id: credentialId } = req.query;

				if (!credentialId) {
					LoggerProxy.error('OAuth1 credential authorization failed due to missing credential ID');
					throw new ResponseHelper.ResponseError(
						'Required credential ID is missing',
						undefined,
						400,
					);
				}

				const credential = await getCredentialForUser(credentialId, req.user);

				if (!credential) {
					LoggerProxy.error(
						'OAuth1 credential authorization failed because the current user does not have the correct permissions',
						{ userId: req.user.id },
					);
					throw new ResponseHelper.ResponseError(
						RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL,
						undefined,
						404,
					);
				}

				const encryptionKey = await UserSettings.getEncryptionKey();
				if (!encryptionKey) {
					throw new ResponseHelper.ResponseError(
						RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY,
						undefined,
						500,
					);
				}

				const mode: WorkflowExecuteMode = 'internal';
				const credentialsHelper = new CredentialsHelper(encryptionKey);
				const decryptedDataOriginal = await credentialsHelper.getDecrypted(
					credential as INodeCredentialsDetails,
					credential.type,
					mode,
					true,
				);

				const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(
					decryptedDataOriginal,
					credential.type,
					mode,
				);

				const signatureMethod = _.get(oauthCredentials, 'signatureMethod') as string;

				const oAuthOptions: clientOAuth1.Options = {
					consumer: {
						key: _.get(oauthCredentials, 'consumerKey') as string,
						secret: _.get(oauthCredentials, 'consumerSecret') as string,
					},
					signature_method: signatureMethod,
					// eslint-disable-next-line @typescript-eslint/naming-convention
					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=${credentialId}`,
				};

				await this.externalHooks.run('oauth1.authenticate', [oAuthOptions, oauthRequestData]);

				// eslint-disable-next-line new-cap
				const oauth = new clientOAuth1(oAuthOptions);

				const options: RequestOptions = {
					method: 'POST',
					url: _.get(oauthCredentials, 'requestTokenUrl') as string,
					data: oauthRequestData,
				};

				const data = oauth.toHeader(oauth.authorize(options));

				// @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(
					credential as INodeCredentialsDetails,
					credential.type,
					credential.nodesAccess,
				);

				credentials.setData(decryptedDataOriginal, encryptionKey);
				const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;

				// Add special database related data
				newCredentialsData.updatedAt = this.getCurrentDate();

				// Update the credentials in DB
				await Db.collections.Credentials!.update(credentialId, newCredentialsData);

				LoggerProxy.verbose('OAuth1 authorization successful for new credential', {
					userId: req.user.id,
					credentialId,
				});

				return returnUri;
			}),
		);

		// Verify and store app code. Generate access tokens and store for respective credential.
		this.app.get(
			`/${this.restEndpoint}/oauth1-credential/callback`,
			async (req: OAuthRequest.OAuth1Credential.Callback, res: express.Response) => {
				try {
					const { oauth_verifier, oauth_token, cid: credentialId } = req.query;

					if (!oauth_verifier || !oauth_token) {
						const errorResponse = new ResponseHelper.ResponseError(
							`Insufficient parameters for OAuth1 callback. Received following query parameters: ${JSON.stringify(
								req.query,
							)}`,
							undefined,
							503,
						);
						LoggerProxy.error(
							'OAuth1 callback failed because of insufficient parameters received',
							{
								userId: req.user.id,
								credentialId,
							},
						);
						return ResponseHelper.sendErrorResponse(res, errorResponse);
					}

					const credential = await getCredentialForUser(credentialId, req.user);

					if (!credential) {
						LoggerProxy.error('OAuth1 callback failed because of insufficient user permissions', {
							userId: req.user.id,
							credentialId,
						});
						const errorResponse = new ResponseHelper.ResponseError(
							RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL,
							undefined,
							404,
						);
						return ResponseHelper.sendErrorResponse(res, errorResponse);
					}

					const encryptionKey = await UserSettings.getEncryptionKey();

					if (!encryptionKey) {
						const errorResponse = new ResponseHelper.ResponseError(
							RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY,
							undefined,
							503,
						);
						return ResponseHelper.sendErrorResponse(res, errorResponse);
					}

					const mode: WorkflowExecuteMode = 'internal';
					const credentialsHelper = new CredentialsHelper(encryptionKey);
					const decryptedDataOriginal = await credentialsHelper.getDecrypted(
						credential as INodeCredentialsDetails,
						credential.type,
						mode,
						true,
					);
					const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(
						decryptedDataOriginal,
						credential.type,
						mode,
					);

					const options: OptionsWithUrl = {
						method: 'POST',
						url: _.get(oauthCredentials, 'accessTokenUrl') as string,
						qs: {
							oauth_token,
							oauth_verifier,
						},
					};

					let oauthToken;

					try {
						oauthToken = await requestPromise(options);
					} catch (error) {
						LoggerProxy.error('Unable to fetch tokens for OAuth1 callback', {
							userId: req.user.id,
							credentialId,
						});
						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(
						credential as INodeCredentialsDetails,
						credential.type,
						credential.nodesAccess,
					);
					credentials.setData(decryptedDataOriginal, encryptionKey);
					const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
					// Add special database related data
					newCredentialsData.updatedAt = this.getCurrentDate();
					// Save the credentials in DB
					await Db.collections.Credentials!.update(credentialId, newCredentialsData);

					LoggerProxy.verbose('OAuth1 callback successful for new credential', {
						userId: req.user.id,
						credentialId,
					});
					res.sendFile(pathResolve(__dirname, '../../templates/oauth-callback.html'));
				} catch (error) {
					LoggerProxy.error('OAuth1 callback failed because of insufficient user permissions', {
						userId: req.user.id,
						credentialId: req.query.cid,
					});
					// Error response
					return ResponseHelper.sendErrorResponse(res, error);
				}
			},
		);

		// ----------------------------------------
		// OAuth2-Credential/Auth
		// ----------------------------------------

		// Authorize OAuth Data
		this.app.get(
			`/${this.restEndpoint}/oauth2-credential/auth`,
			ResponseHelper.send(async (req: OAuthRequest.OAuth2Credential.Auth): Promise<string> => {
				const { id: credentialId } = req.query;

				if (!credentialId) {
					throw new ResponseHelper.ResponseError(
						'Required credential ID is missing',
						undefined,
						400,
					);
				}

				const credential = await getCredentialForUser(credentialId, req.user);

				if (!credential) {
					LoggerProxy.error('Failed to authorize OAuth2 due to lack of permissions', {
						userId: req.user.id,
						credentialId,
					});
					throw new ResponseHelper.ResponseError(
						RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL,
						undefined,
						404,
					);
				}

				const encryptionKey = await UserSettings.getEncryptionKey();
				if (!encryptionKey) {
					throw new ResponseHelper.ResponseError(
						RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY,
						undefined,
						500,
					);
				}

				const mode: WorkflowExecuteMode = 'internal';
				const credentialsHelper = new CredentialsHelper(encryptionKey);
				const decryptedDataOriginal = await credentialsHelper.getDecrypted(
					credential as INodeCredentialsDetails,
					credential.type,
					mode,
					true,
				);

				const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(
					decryptedDataOriginal,
					credential.type,
					mode,
				);

				const token = new csrf();
				// Generate a CSRF prevention token and send it as a OAuth2 state stringma/ERR
				const csrfSecret = token.secretSync();
				const state = {
					token: token.create(csrfSecret),
					cid: req.query.id,
				};
				const stateEncodedStr = Buffer.from(JSON.stringify(state)).toString('base64');

				const oAuthOptions: clientOAuth2.Options = {
					clientId: _.get(oauthCredentials, 'clientId') as string,
					clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string,
					accessTokenUri: _.get(oauthCredentials, 'accessTokenUrl', '') as string,
					authorizationUri: _.get(oauthCredentials, 'authUrl', '') as string,
					redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}${
						this.restEndpoint
					}/oauth2-credential/callback`,
					scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ','),
					state: stateEncodedStr,
				};

				await this.externalHooks.run('oauth2.authenticate', [oAuthOptions]);

				const oAuthObj = new clientOAuth2(oAuthOptions);

				// Encrypt the data
				const credentials = new Credentials(
					credential as INodeCredentialsDetails,
					credential.type,
					credential.nodesAccess,
				);
				decryptedDataOriginal.csrfSecret = csrfSecret;

				credentials.setData(decryptedDataOriginal, encryptionKey);
				const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;

				// Add special database related data
				newCredentialsData.updatedAt = this.getCurrentDate();

				// Update the credentials in DB
				await Db.collections.Credentials!.update(req.query.id as string, newCredentialsData);

				const authQueryParameters = _.get(oauthCredentials, 'authQueryParameters', '') as string;
				let returnUri = oAuthObj.code.getUri();

				// if scope uses comma, change it as the library always return then with spaces
				if ((_.get(oauthCredentials, 'scope') as string).includes(',')) {
					const data = querystring.parse(returnUri.split('?')[1]);
					data.scope = _.get(oauthCredentials, 'scope') as string;
					returnUri = `${_.get(oauthCredentials, 'authUrl', '')}?${querystring.stringify(data)}`;
				}

				if (authQueryParameters) {
					returnUri += `&${authQueryParameters}`;
				}

				LoggerProxy.verbose('OAuth2 authentication successful for new credential', {
					userId: req.user.id,
					credentialId,
				});
				return returnUri;
			}),
		);

		// ----------------------------------------
		// OAuth2-Credential/Callback
		// ----------------------------------------

		// Verify and store app code. Generate access tokens and store for respective credential.
		this.app.get(
			`/${this.restEndpoint}/oauth2-credential/callback`,
			async (req: OAuthRequest.OAuth2Credential.Callback, res: express.Response) => {
				try {
					// realmId it's currently just use for the quickbook OAuth2 flow
					const { code, state: stateEncoded } = req.query;

					if (!code || !stateEncoded) {
						const errorResponse = new ResponseHelper.ResponseError(
							`Insufficient parameters for OAuth2 callback. Received following query parameters: ${JSON.stringify(
								req.query,
							)}`,
							undefined,
							503,
						);
						return ResponseHelper.sendErrorResponse(res, errorResponse);
					}

					let state;
					try {
						state = JSON.parse(Buffer.from(stateEncoded, 'base64').toString());
					} catch (error) {
						const errorResponse = new ResponseHelper.ResponseError(
							'Invalid state format returned',
							undefined,
							503,
						);
						return ResponseHelper.sendErrorResponse(res, errorResponse);
					}

					const credential = await getCredentialForUser(state.cid, req.user);

					if (!credential) {
						LoggerProxy.error('OAuth2 callback failed because of insufficient permissions', {
							userId: req.user.id,
							credentialId: state.cid,
						});
						const errorResponse = new ResponseHelper.ResponseError(
							RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL,
							undefined,
							404,
						);
						return ResponseHelper.sendErrorResponse(res, errorResponse);
					}

					const encryptionKey = await UserSettings.getEncryptionKey();

					if (!encryptionKey) {
						const errorResponse = new ResponseHelper.ResponseError(
							RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY,
							undefined,
							503,
						);
						return ResponseHelper.sendErrorResponse(res, errorResponse);
					}

					const mode: WorkflowExecuteMode = 'internal';
					const credentialsHelper = new CredentialsHelper(encryptionKey);
					const decryptedDataOriginal = await credentialsHelper.getDecrypted(
						credential as INodeCredentialsDetails,
						credential.type,
						mode,
						true,
					);
					const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(
						decryptedDataOriginal,
						credential.type,
						mode,
					);

					const token = new csrf();
					if (
						decryptedDataOriginal.csrfSecret === undefined ||
						!token.verify(decryptedDataOriginal.csrfSecret as string, state.token)
					) {
						LoggerProxy.debug('OAuth2 callback state is invalid', {
							userId: req.user.id,
							credentialId: state.cid,
						});
						const errorResponse = new ResponseHelper.ResponseError(
							'The OAuth2 callback state is invalid!',
							undefined,
							404,
						);
						return ResponseHelper.sendErrorResponse(res, errorResponse);
					}

					let options = {};

					const oAuth2Parameters = {
						clientId: _.get(oauthCredentials, 'clientId') as string,
						clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string | undefined,
						accessTokenUri: _.get(oauthCredentials, 'accessTokenUrl', '') as string,
						authorizationUri: _.get(oauthCredentials, 'authUrl', '') as string,
						redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}${
							this.restEndpoint
						}/oauth2-credential/callback`,
						scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ','),
					};

					if ((_.get(oauthCredentials, 'authentication', 'header') as string) === 'body') {
						options = {
							body: {
								client_id: _.get(oauthCredentials, 'clientId') as string,
								client_secret: _.get(oauthCredentials, 'clientSecret', '') as string,
							},
						};
						delete oAuth2Parameters.clientSecret;
					}

					await this.externalHooks.run('oauth2.callback', [oAuth2Parameters]);

					const oAuthObj = new clientOAuth2(oAuth2Parameters);

					const queryParameters = req.originalUrl.split('?').splice(1, 1).join('');

					const oauthToken = await oAuthObj.code.getToken(
						`${oAuth2Parameters.redirectUri}?${queryParameters}`,
						options,
					);

					if (Object.keys(req.query).length > 2) {
						_.set(oauthToken.data, 'callbackQueryString', _.omit(req.query, 'state', 'code'));
					}

					if (oauthToken === undefined) {
						LoggerProxy.error('OAuth2 callback failed: unable to get access tokens', {
							userId: req.user.id,
							credentialId: state.cid,
						});
						const errorResponse = new ResponseHelper.ResponseError(
							'Unable to get access tokens!',
							undefined,
							404,
						);
						return ResponseHelper.sendErrorResponse(res, errorResponse);
					}

					if (decryptedDataOriginal.oauthTokenData) {
						// Only overwrite supplied data as some providers do for example just return the
						// refresh_token on the very first request and not on subsequent ones.
						Object.assign(decryptedDataOriginal.oauthTokenData, oauthToken.data);
					} else {
						// No data exists so simply set
						decryptedDataOriginal.oauthTokenData = oauthToken.data;
					}

					_.unset(decryptedDataOriginal, 'csrfSecret');

					const credentials = new Credentials(
						credential as INodeCredentialsDetails,
						credential.type,
						credential.nodesAccess,
					);
					credentials.setData(decryptedDataOriginal, encryptionKey);
					const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
					// Add special database related data
					newCredentialsData.updatedAt = this.getCurrentDate();
					// Save the credentials in DB
					await Db.collections.Credentials!.update(state.cid, newCredentialsData);
					LoggerProxy.verbose('OAuth2 callback successful for new credential', {
						userId: req.user.id,
						credentialId: state.cid,
					});

					res.sendFile(pathResolve(__dirname, '../../templates/oauth-callback.html'));
				} catch (error) {
					// Error response
					return ResponseHelper.sendErrorResponse(res, error);
				}
			},
		);

		// ----------------------------------------
		// Executions
		// ----------------------------------------

		// Returns all finished executions
		this.app.get(
			`/${this.restEndpoint}/executions`,
			ResponseHelper.send(
				async (req: ExecutionRequest.GetAll): Promise<IExecutionsListResponse> => {
					const filter = req.query.filter ? JSON.parse(req.query.filter) : {};

					const limit = req.query.limit
						? parseInt(req.query.limit, 10)
						: DEFAULT_EXECUTIONS_GET_ALL_LIMIT;

					const executingWorkflowIds: string[] = [];

					if (config.get('executions.mode') === 'queue') {
						const currentJobs = await Queue.getInstance().getJobs(['active', 'waiting']);
						executingWorkflowIds.push(...currentJobs.map(({ data }) => data.executionId));
					}

					// We may have manual executions even with queue so we must account for these.
					executingWorkflowIds.push(
						...this.activeExecutionsInstance.getActiveExecutions().map(({ id }) => id),
					);

					const countFilter = cloneDeep(filter);
					countFilter.waitTill &&= Not(IsNull());
					countFilter.id = Not(In(executingWorkflowIds));

					const sharedWorkflowIds = await getSharedWorkflowIds(req.user);

					const findOptions: FindManyOptions<ExecutionEntity> = {
						select: [
							'id',
							'finished',
							'mode',
							'retryOf',
							'retrySuccessId',
							'waitTill',
							'startedAt',
							'stoppedAt',
							'workflowData',
						],
						where: { workflowId: In(sharedWorkflowIds) },
						order: { id: 'DESC' },
						take: limit,
					};

					Object.entries(filter).forEach(([key, value]) => {
						let filterToAdd = {};

						if (key === 'waitTill') {
							filterToAdd = { waitTill: !IsNull() };
						} else if (key === 'finished' && value === false) {
							filterToAdd = { finished: false, waitTill: IsNull() };
						} else {
							filterToAdd = { [key]: value };
						}

						Object.assign(findOptions.where, filterToAdd);
					});

					const rangeQuery: string[] = [];
					const rangeQueryParams: {
						lastId?: string;
						firstId?: string;
						executingWorkflowIds?: string[];
					} = {};

					if (req.query.lastId) {
						rangeQuery.push('id < :lastId');
						rangeQueryParams.lastId = req.query.lastId;
					}

					if (req.query.firstId) {
						rangeQuery.push('id > :firstId');
						rangeQueryParams.firstId = req.query.firstId;
					}

					if (executingWorkflowIds.length > 0) {
						rangeQuery.push(`id NOT IN (:...executingWorkflowIds)`);
						rangeQueryParams.executingWorkflowIds = executingWorkflowIds;
					}

					if (rangeQuery.length) {
						Object.assign(findOptions.where, {
							id: Raw(() => rangeQuery.join(' and '), rangeQueryParams),
						});
					}

					const executions = await Db.collections.Execution!.find(findOptions);

					const { count, estimated } = await getExecutionsCount(countFilter, req.user);

					const formattedExecutions = executions.map((execution) => {
						return {
							id: execution.id.toString(),
							finished: execution.finished,
							mode: execution.mode,
							retryOf: execution.retryOf?.toString(),
							retrySuccessId: execution?.retrySuccessId?.toString(),
							waitTill: execution.waitTill as Date | undefined,
							startedAt: execution.startedAt,
							stoppedAt: execution.stoppedAt,
							workflowId: execution.workflowData?.id?.toString() ?? '',
							workflowName: execution.workflowData.name,
						};
					});

					return {
						count,
						results: formattedExecutions,
						estimated,
					};
				},
			),
		);

		// Returns a specific execution
		this.app.get(
			`/${this.restEndpoint}/executions/:id`,
			ResponseHelper.send(
				async (
					req: ExecutionRequest.Get,
				): Promise<IExecutionResponse | IExecutionFlattedResponse | undefined> => {
					const { id: executionId } = req.params;

					const sharedWorkflowIds = await getSharedWorkflowIds(req.user);

					if (!sharedWorkflowIds.length) return undefined;

					const execution = await Db.collections.Execution!.findOne({
						where: {
							id: executionId,
							workflowId: In(sharedWorkflowIds),
						},
					});

					if (!execution) {
						LoggerProxy.info(
							'Attempt to read execution was blocked due to insufficient permissions',
							{
								userId: req.user.id,
								executionId,
							},
						);
						return undefined;
					}

					if (req.query.unflattedResponse === 'true') {
						return ResponseHelper.unflattenExecutionData(execution);
					}

					const { id, ...rest } = execution;

					// @ts-ignore
					return {
						id: id.toString(),
						...rest,
					};
				},
			),
		);

		// Retries a failed execution
		this.app.post(
			`/${this.restEndpoint}/executions/:id/retry`,
			ResponseHelper.send(async (req: ExecutionRequest.Retry): Promise<boolean> => {
				const { id: executionId } = req.params;

				const sharedWorkflowIds = await getSharedWorkflowIds(req.user);

				if (!sharedWorkflowIds.length) return false;

				const execution = await Db.collections.Execution!.findOne({
					where: {
						id: executionId,
						workflowId: In(sharedWorkflowIds),
					},
				});

				if (!execution) {
					LoggerProxy.info(
						'Attempt to retry an execution was blocked due to insufficient permissions',
						{
							userId: req.user.id,
							executionId,
						},
					);
					throw new ResponseHelper.ResponseError(
						`The execution with the ID "${executionId}" does not exist.`,
						404,
						404,
					);
				}

				const fullExecutionData = ResponseHelper.unflattenExecutionData(execution);

				if (fullExecutionData.finished) {
					throw new Error('The execution succeeded, so it cannot be retried.');
				}

				const executionMode = 'retry';

				fullExecutionData.workflowData.active = false;

				// Start the workflow
				const data: IWorkflowExecutionDataProcess = {
					executionMode,
					executionData: fullExecutionData.data,
					retryOf: req.params.id,
					workflowData: fullExecutionData.workflowData,
					userId: req.user.id,
				};

				const { lastNodeExecuted } = data.executionData!.resultData;

				if (lastNodeExecuted) {
					// Remove the old error and the data of the last run of the node that it can be replaced
					delete data.executionData!.resultData.error;
					const { length } = data.executionData!.resultData.runData[lastNodeExecuted];
					if (
						length > 0 &&
						data.executionData!.resultData.runData[lastNodeExecuted][length - 1].error !== undefined
					) {
						// Remove results only if it is an error.
						// If we are retrying due to a crash, the information is simply success info from last node
						data.executionData!.resultData.runData[lastNodeExecuted].pop();
						// Stack will determine what to run next
					}
				}

				if (req.body.loadWorkflow) {
					// Loads the currently saved workflow to execute instead of the
					// one saved at the time of the execution.
					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,
					});

					// 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 = workflowInstance.getNode(stack.node.name);
						if (node === null) {
							LoggerProxy.error('Failed to retry an execution because a node could not be found', {
								userId: req.user.id,
								executionId,
								nodeName: stack.node.name,
							});
							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;
					}
				}

				const workflowRunner = new WorkflowRunner();
				const retriedExecutionId = await workflowRunner.run(data);

				const executionData = await this.activeExecutionsInstance.getPostExecutePromise(
					retriedExecutionId,
				);

				if (!executionData) {
					throw new Error('The retry did not start for an unknown reason.');
				}

				return !!executionData.finished;
			}),
		);

		// Delete Executions
		// INFORMATION: We use POST instead of DELETE to not run into any issues
		// with the query data getting to long
		this.app.post(
			`/${this.restEndpoint}/executions/delete`,
			ResponseHelper.send(async (req: ExecutionRequest.Delete): Promise<void> => {
				const { deleteBefore, ids, filters: requestFilters } = req.body;

				if (!deleteBefore && !ids) {
					throw new Error('Either "deleteBefore" or "ids" must be present in the request body');
				}

				const sharedWorkflowIds = await getSharedWorkflowIds(req.user);
				const binaryDataManager = BinaryDataManager.getInstance();

				// delete executions by date, if user may access the underyling worfklows

				if (deleteBefore) {
					const filters: IDataObject = {
						startedAt: LessThanOrEqual(deleteBefore),
					};

					if (filters) {
						Object.assign(filters, requestFilters);
					}

					const executions = await Db.collections.Execution!.find({
						where: {
							workflowId: In(sharedWorkflowIds),
							...filters,
						},
					});

					if (!executions.length) return;

					const idsToDelete = executions.map(({ id }) => id.toString());

					await Promise.all(
						idsToDelete.map(async (id) => binaryDataManager.deleteBinaryDataByExecutionId(id)),
					);

					await Db.collections.Execution!.delete({ id: In(idsToDelete) });

					return;
				}

				// delete executions by IDs, if user may access the underyling worfklows

				if (ids) {
					const executions = await Db.collections.Execution!.find({
						where: {
							id: In(ids),
							workflowId: In(sharedWorkflowIds),
						},
					});

					if (!executions.length) {
						LoggerProxy.error('Failed to delete an execution due to insufficient permissions', {
							userId: req.user.id,
							executionIds: ids,
						});
						return;
					}

					const idsToDelete = executions.map(({ id }) => id.toString());

					await Promise.all(
						idsToDelete.map(async (id) => binaryDataManager.deleteBinaryDataByExecutionId(id)),
					);

					await Db.collections.Execution!.delete(idsToDelete);
				}
			}),
		);

		// ----------------------------------------
		// Executing Workflows
		// ----------------------------------------

		// Returns all the currently working executions
		this.app.get(
			`/${this.restEndpoint}/executions-current`,
			ResponseHelper.send(
				async (req: ExecutionRequest.GetAllCurrent): 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) return [];

						const findOptions: FindManyOptions<ExecutionEntity> = {
							select: ['id', 'workflowId', 'mode', 'retryOf', 'startedAt'],
							order: { id: 'DESC' },
							where: {
								id: In(currentlyRunningExecutionIds),
							},
						};

						const sharedWorkflowIds = await getSharedWorkflowIds(req.user);

						if (!sharedWorkflowIds.length) return [];

						if (req.query.filter) {
							const { workflowId } = JSON.parse(req.query.filter);
							if (workflowId && sharedWorkflowIds.includes(workflowId)) {
								Object.assign(findOptions.where, { workflowId });
							}
						} else {
							Object.assign(findOptions.where, { workflowId: In(sharedWorkflowIds) });
						}

						const executions = await Db.collections.Execution!.find(findOptions);

						if (!executions.length) return [];

						return executions.map((execution) => {
							return {
								id: execution.id,
								workflowId: execution.workflowId,
								mode: execution.mode,
								retryOf: execution.retryOf !== null ? execution.retryOf : undefined,
								startedAt: new Date(execution.startedAt),
							} as IExecutionsSummary;
						});
					}

					const executingWorkflows = this.activeExecutionsInstance.getActiveExecutions();

					const returnData: IExecutionsSummary[] = [];

					const filter = req.query.filter ? JSON.parse(req.query.filter) : {};

					const sharedWorkflowIds = await getSharedWorkflowIds(req.user).then((ids) =>
						ids.map((id) => id.toString()),
					);

					for (const data of executingWorkflows) {
						if (
							(filter.workflowId !== undefined && filter.workflowId !== data.workflowId) ||
							!sharedWorkflowIds.includes(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;
				},
			),
		);

		// Forces the execution to stop
		this.app.post(
			`/${this.restEndpoint}/executions-current/:id/stop`,
			ResponseHelper.send(async (req: ExecutionRequest.Stop): Promise<IExecutionsStopData> => {
				const { id: executionId } = req.params;

				const sharedWorkflowIds = await getSharedWorkflowIds(req.user);

				if (!sharedWorkflowIds.length) {
					throw new ResponseHelper.ResponseError('Execution not found', undefined, 404);
				}

				const execution = await Db.collections.Execution!.findOne({
					where: {
						id: executionId,
						workflowId: In(sharedWorkflowIds),
					},
				});

				if (!execution) {
					throw new ResponseHelper.ResponseError('Execution not found', undefined, 404);
				}

				if (config.get('executions.mode') === 'queue') {
					// Manual executions should still be stoppable, so
					// try notifying the `activeExecutions` to stop it.
					const result = await this.activeExecutionsInstance.stopExecution(req.params.id);

					if (result === undefined) {
						// If active execution could not be found check if it is a waiting one
						try {
							return await this.waitTracker.stopExecution(req.params.id);
						} catch (error) {
							// Ignore, if it errors as then it is probably a currently running
							// execution
						}
					} 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: IExecutionsStopData = {
						mode: fullExecutionData.mode,
						startedAt: new Date(fullExecutionData.startedAt),
						stoppedAt: fullExecutionData.stoppedAt
							? new Date(fullExecutionData.stoppedAt)
							: undefined,
						finished: fullExecutionData.finished,
					};

					return returnData;
				}

				// Stop the execution and wait till it is done and we got the data
				const result = await this.activeExecutionsInstance.stopExecution(executionId);

				let returnData: IExecutionsStopData;
				if (result === undefined) {
					// If active execution could not be found check if it is a waiting one
					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;
			}),
		);

		// Removes a test webhook
		this.app.delete(
			`/${this.restEndpoint}/test-webhook/:id`,
			ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<boolean> => {
				// TODO UM: check if this needs validation with user management.
				const workflowId = req.params.id;
				return this.testWebhooks.cancelTestWebhook(workflowId);
			}),
		);

		// ----------------------------------------
		// Options
		// ----------------------------------------

		// Returns all the available timezones
		this.app.get(
			`/${this.restEndpoint}/options/timezones`,
			ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<object> => {
				return timezones;
			}),
		);

		// ----------------------------------------
		// Binary data
		// ----------------------------------------

		// Returns binary buffer
		this.app.get(
			`/${this.restEndpoint}/data/:path`,
			ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<string> => {
				// TODO UM: check if this needs permission check for UM
				const dataPath = req.params.path;
				return BinaryDataManager.getInstance()
					.retrieveBinaryDataByIdentifier(dataPath)
					.then((buffer: Buffer) => {
						return buffer.toString('base64');
					});
			}),
		);

		// ----------------------------------------
		// Settings
		// ----------------------------------------

		// Returns the current settings for the UI
		this.app.get(
			`/${this.restEndpoint}/settings`,
			ResponseHelper.send(
				async (req: express.Request, res: express.Response): Promise<IN8nUISettings> => {
					return this.getSettingsForFrontend();
				},
			),
		);

		// ----------------------------------------
		// Webhooks
		// ----------------------------------------

		if (config.get('endpoints.disableProductionWebhooksOnMainProcess') !== true) {
			WebhookServer.registerProductionWebhooks.apply(this);
		}

		// Register all webhook requests (test for UI)
		this.app.all(
			`/${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,
				);

				const method = req.method.toUpperCase() as WebhookHttpMethod;

				if (method === 'OPTIONS') {
					let allowedMethods: string[];
					try {
						allowedMethods = await this.testWebhooks.getWebhookMethods(requestUrl);
						allowedMethods.push('OPTIONS');

						// Add custom "Allow" header to satisfy OPTIONS response.
						res.append('Allow', allowedMethods);
					} catch (error) {
						ResponseHelper.sendErrorResponse(res, error);
						return;
					}

					ResponseHelper.sendSuccessResponse(res, {}, true, 204);
					return;
				}

				if (!WEBHOOK_METHODS.includes(method)) {
					ResponseHelper.sendErrorResponse(
						res,
						new Error(`The method ${method} is not supported.`),
					);
					return;
				}

				let response;
				try {
					response = await this.testWebhooks.callTestWebhook(method, 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,
					response.headers,
				);
			},
		);

		if (this.endpointPresetCredentials !== '') {
			// POST endpoint to set preset credentials
			this.app.post(
				`/${this.endpointPresetCredentials}`,
				async (req: express.Request, res: express.Response) => {
					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) {
			// 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');

			let readIndexFile = readFileSync(filePath, 'utf8');
			readIndexFile = readIndexFile.replace(/\/%BASE_PATH%\//g, n8nPath);
			readIndexFile = readIndexFile.replace(/\/favicon.ico/g, `${n8nPath}favicon.ico`);

			// Serve the altered index.html file separately
			this.app.get(`/index.html`, async (req: express.Request, res: express.Response) => {
				res.send(readIndexFile);
			});

			// Serve the website
			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);
						}
					},
				}),
			);
		}
		const startTime = new Date().toUTCString();
	}
}

export async function start(): Promise<void> {
	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: IDiagnosticInfo = {
			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,
			n8n_multi_user_allowed:
				config.get('userManagement.disabled') === false ||
				config.get('userManagement.isInstanceOwnerSetUp') === true,
			smtp_set_up: config.get('userManagement.emails.mode') === 'smtp',
		};

		void Db.collections
			.Workflow!.findOne({
				select: ['createdAt'],
				order: { createdAt: 'ASC' },
			})
			.then(async (workflow) =>
				InternalHooksManager.getInstance().onServerStarted(diagnosticInfo, workflow?.createdAt),
			);
	});

	server.on('error', (error: Error & { code: string }) => {
		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: IDataObject,
	user: User,
): Promise<{ count: number; estimated: boolean }> {
	const dbType = (await GenericHelpers.getConfigValue('database.type')) as DatabaseType;
	const filteredFields = Object.keys(countFilter).filter((field) => field !== 'id');

	// For databases other than Postgres, do a regular count
	// when filtering based on `workflowId` or `finished` fields.
	if (dbType !== 'postgresdb' || filteredFields.length > 0 || user.globalRole.name !== 'owner') {
		const sharedWorkflowIds = await getSharedWorkflowIds(user);

		const count = await Db.collections.Execution!.count({
			where: {
				workflowId: In(sharedWorkflowIds),
				...countFilter,
			},
		});

		return { count, estimated: false };
	}

	try {
		// Get an estimate of rows count.
		const estimateRowsNumberSql =
			"SELECT n_live_tup FROM pg_stat_all_tables WHERE relname = 'execution_entity';";
		const rows: Array<{ n_live_tup: string }> = await Db.collections.Execution!.query(
			estimateRowsNumberSql,
		);

		const estimate = parseInt(rows[0].n_live_tup, 10);
		// If over 100k, return just an estimate.
		if (estimate > 100_000) {
			// if less than 100k, we get the real count as even a full
			// table scan should not take so long.
			return { count: estimate, estimated: true };
		}
	} catch (error) {
		LoggerProxy.warn(`Failed to get executions count from Postgres: ${error}`);
	}

	const sharedWorkflowIds = await getSharedWorkflowIds(user);

	const count = await Db.collections.Execution!.count({
		where: {
			workflowId: In(sharedWorkflowIds),
		},
	});

	return { count, estimated: false };
}
