From df691fba0c834aaf5dd9eec589df6118379325a0 Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Mon, 27 Nov 2023 19:50:20 +0200 Subject: [PATCH 01/17] fix(core): Node version in the user added node to workflow canvas event (no-changelog) (#7814) Github issue / Community forum post (link here to close automatically): --- packages/editor-ui/src/views/NodeView.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index bb72c700f89fb..6c4236a170cf7 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -2216,6 +2216,7 @@ export default defineComponent({ useSegment().trackAddedTrigger(nodeTypeName); const trackProperties: ITelemetryTrackProperties = { node_type: nodeTypeName, + node_version: newNodeData.typeVersion, is_auto_add: isAutoAdd, workflow_id: this.workflowsStore.workflowId, drag_and_drop: options.dragAndDrop, From d5488725a83f6705b95c9de9d8736adf1b870134 Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Tue, 28 Nov 2023 11:02:11 +0200 Subject: [PATCH 02/17] fix(Google Sheets Node): Read operation execute for each item (#7800) Github issue / Community forum post (link here to close automatically): --- .../nodes/Google/Sheet/GoogleSheets.node.ts | 3 +- .../Sheet/v2/actions/sheet/read.operation.ts | 121 +++++++++++------- .../Sheet/v2/actions/versionDescription.ts | 2 +- 3 files changed, 75 insertions(+), 51 deletions(-) diff --git a/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts b/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts index 3c5fd6ad49800..47784b22a5fa5 100644 --- a/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts +++ b/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts @@ -11,7 +11,7 @@ export class GoogleSheets extends VersionedNodeType { name: 'googleSheets', icon: 'file:googleSheets.svg', group: ['input', 'output'], - defaultVersion: 4.1, + defaultVersion: 4.2, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', description: 'Read, update and write data to Google Sheets', }; @@ -22,6 +22,7 @@ export class GoogleSheets extends VersionedNodeType { 3: new GoogleSheetsV2(baseDescription), 4: new GoogleSheetsV2(baseDescription), 4.1: new GoogleSheetsV2(baseDescription), + 4.2: new GoogleSheetsV2(baseDescription), }; super(nodeVersions, baseDescription); diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/read.operation.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/read.operation.ts index d9779625b8c42..f9c777e6e3ff9 100644 --- a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/read.operation.ts +++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/read.operation.ts @@ -12,7 +12,7 @@ import type { SheetRangeData, ValueRenderOption, } from '../../helpers/GoogleSheets.types'; -import { generatePairedItemData } from '../../../../../../utils/utilities'; + import { dataLocationOnSheet, outputFormatting } from './commonDescription'; export const description: SheetProperties = [ @@ -111,70 +111,93 @@ export async function execute( sheet: GoogleSheet, sheetName: string, ): Promise { - const options = this.getNodeParameter('options', 0, {}); - const outputFormattingOption = - ((options.outputFormatting as IDataObject)?.values as IDataObject) || {}; - - const dataLocationOnSheetOptions = - ((options.dataLocationOnSheet as IDataObject)?.values as RangeDetectionOptions) || {}; + const items = this.getInputData(); + const nodeVersion = this.getNode().typeVersion; + let length = 1; - if (dataLocationOnSheetOptions.rangeDefinition === undefined) { - dataLocationOnSheetOptions.rangeDefinition = 'detectAutomatically'; + if (nodeVersion > 4.1) { + length = items.length; } - const range = getRangeString(sheetName, dataLocationOnSheetOptions); + const returnData: INodeExecutionData[] = []; - const valueRenderMode = (outputFormattingOption.general || - 'UNFORMATTED_VALUE') as ValueRenderOption; - const dateTimeRenderOption = (outputFormattingOption.date || 'FORMATTED_STRING') as string; + for (let itemIndex = 0; itemIndex < length; itemIndex++) { + const options = this.getNodeParameter('options', itemIndex, {}); + const outputFormattingOption = + ((options.outputFormatting as IDataObject)?.values as IDataObject) || {}; - const sheetData = (await sheet.getData( - range, - valueRenderMode, - dateTimeRenderOption, - )) as SheetRangeData; + const dataLocationOnSheetOptions = + ((options.dataLocationOnSheet as IDataObject)?.values as RangeDetectionOptions) || {}; - if (sheetData === undefined || sheetData.length === 0) { - return []; - } + if (dataLocationOnSheetOptions.rangeDefinition === undefined) { + dataLocationOnSheetOptions.rangeDefinition = 'detectAutomatically'; + } - const { data, headerRow, firstDataRow } = prepareSheetData(sheetData, dataLocationOnSheetOptions); + const range = getRangeString(sheetName, dataLocationOnSheetOptions); - let responseData = []; + const valueRenderMode = (outputFormattingOption.general || + 'UNFORMATTED_VALUE') as ValueRenderOption; + const dateTimeRenderOption = (outputFormattingOption.date || 'FORMATTED_STRING') as string; - const lookupValues = this.getNodeParameter('filtersUI.values', 0, []) as ILookupValues[]; + const sheetData = (await sheet.getData( + range, + valueRenderMode, + dateTimeRenderOption, + )) as SheetRangeData; - if (lookupValues.length) { - const returnAllMatches = options.returnAllMatches === 'returnAllMatches' ? true : false; + if (sheetData === undefined || sheetData.length === 0) { + return []; + } - const items = this.getInputData(); - for (let i = 1; i < items.length; i++) { - const itemLookupValues = this.getNodeParameter('filtersUI.values', i, []) as ILookupValues[]; - if (itemLookupValues.length) { - lookupValues.push(...itemLookupValues); + const { data, headerRow, firstDataRow } = prepareSheetData( + sheetData, + dataLocationOnSheetOptions, + ); + + let responseData = []; + + const lookupValues = this.getNodeParameter( + 'filtersUI.values', + itemIndex, + [], + ) as ILookupValues[]; + + if (lookupValues.length) { + const returnAllMatches = options.returnAllMatches === 'returnAllMatches' ? true : false; + + if (nodeVersion <= 4.1) { + for (let i = 1; i < items.length; i++) { + const itemLookupValues = this.getNodeParameter( + 'filtersUI.values', + i, + [], + ) as ILookupValues[]; + if (itemLookupValues.length) { + lookupValues.push(...itemLookupValues); + } + } } + + responseData = await sheet.lookupValues( + data as string[][], + headerRow, + firstDataRow, + lookupValues, + returnAllMatches, + ); + } else { + responseData = sheet.structureArrayDataByColumn(data as string[][], headerRow, firstDataRow); } - responseData = await sheet.lookupValues( - data as string[][], - headerRow, - firstDataRow, - lookupValues, - returnAllMatches, + returnData.push( + ...responseData.map((item, index) => { + return { + json: item, + pairedItem: { item: itemIndex }, + }; + }), ); - } else { - responseData = sheet.structureArrayDataByColumn(data as string[][], headerRow, firstDataRow); } - const items = this.getInputData(); - const pairedItem = generatePairedItemData(items.length); - - const returnData: INodeExecutionData[] = responseData.map((item, index) => { - return { - json: item, - pairedItem, - }; - }); - return returnData; } diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/versionDescription.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/versionDescription.ts index 170ecb50f53d9..89360623831d8 100644 --- a/packages/nodes-base/nodes/Google/Sheet/v2/actions/versionDescription.ts +++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/versionDescription.ts @@ -9,7 +9,7 @@ export const versionDescription: INodeTypeDescription = { name: 'googleSheets', icon: 'file:googleSheets.svg', group: ['input', 'output'], - version: [3, 4, 4.1], + version: [3, 4, 4.1, 4.2], subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', description: 'Read, update and write data to Google Sheets', defaults: { From 38f24a6184b9bb79f38dfc9186d067a0644dab61 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 28 Nov 2023 09:11:05 +0000 Subject: [PATCH 03/17] fix(Google Calendar Trigger Node): Fix issue preventing birthday and holiday calendars from working (#7832) Github issue / Community forum post (link here to close automatically): https://community.n8n.io/t/issue-with-some-google-calendars/33411 --------- Co-authored-by: Michael Kret --- .../Google/Calendar/GoogleCalendarTrigger.node.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/nodes/Google/Calendar/GoogleCalendarTrigger.node.ts b/packages/nodes-base/nodes/Google/Calendar/GoogleCalendarTrigger.node.ts index 48c091b235d0f..ed3456220e810 100644 --- a/packages/nodes-base/nodes/Google/Calendar/GoogleCalendarTrigger.node.ts +++ b/packages/nodes-base/nodes/Google/Calendar/GoogleCalendarTrigger.node.ts @@ -9,7 +9,12 @@ import { NodeApiError, NodeOperationError } from 'n8n-workflow'; import moment from 'moment'; -import { getCalendars, googleApiRequest, googleApiRequestAllItems } from './GenericFunctions'; +import { + encodeURIComponentOnce, + getCalendars, + googleApiRequest, + googleApiRequestAllItems, +} from './GenericFunctions'; export class GoogleCalendarTrigger implements INodeType { description: INodeTypeDescription = { @@ -132,7 +137,9 @@ export class GoogleCalendarTrigger implements INodeType { async poll(this: IPollFunctions): Promise { const poolTimes = this.getNodeParameter('pollTimes.item', []) as IDataObject[]; const triggerOn = this.getNodeParameter('triggerOn', '') as string; - const calendarId = this.getNodeParameter('calendarId', '', { extractValue: true }) as string; + const calendarId = encodeURIComponentOnce( + this.getNodeParameter('calendarId', '', { extractValue: true }) as string, + ); const webhookData = this.getWorkflowStaticData('node'); const matchTerm = this.getNodeParameter('options.matchTerm', '') as string; From 1c6178759c6bc414741d069ebf0e6b5e527e3584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 28 Nov 2023 10:19:27 +0100 Subject: [PATCH 04/17] refactor(core): Reorganize error hierarchy in `cli` package (no-changelog) (#7839) Ensure all errors in `cli` inherit from `ApplicationError` to continue normalizing all the errors we report to Sentry Follow-up to: https://github.com/n8n-io/n8n/pull/7820 --- packages/cli/src/AbstractServer.ts | 3 +- packages/cli/src/ActiveWorkflowRunner.ts | 10 ++- packages/cli/src/CredentialsHelper.ts | 11 +-- .../ExternalSecrets.controller.ee.ts | 15 ++-- .../ExternalSecrets.service.ee.ts | 17 ++--- packages/cli/src/GenericHelpers.ts | 4 +- packages/cli/src/Ldap/helpers.ts | 3 +- packages/cli/src/License.ts | 8 --- packages/cli/src/NodeTypes.ts | 11 +-- packages/cli/src/ResponseHelper.ts | 71 +------------------ packages/cli/src/Server.ts | 12 ++-- packages/cli/src/TestWebhooks.ts | 13 ++-- .../UserManagement/UserManagementHelper.ts | 6 +- packages/cli/src/WaitingWebhooks.ts | 13 ++-- packages/cli/src/WebhookHelpers.ts | 13 ++-- packages/cli/src/auth/jwt.ts | 7 +- packages/cli/src/auth/methods/email.ts | 4 +- packages/cli/src/commands/start.ts | 3 +- packages/cli/src/commands/worker.ts | 5 +- packages/cli/src/config/utils.ts | 7 +- .../cli/src/controllers/auth.controller.ts | 10 ++- .../communityPackages.controller.ts | 3 +- .../dynamicNodeParameters.controller.ts | 2 +- .../src/controllers/invitation.controller.ts | 3 +- .../cli/src/controllers/ldap.controller.ts | 2 +- packages/cli/src/controllers/me.controller.ts | 2 +- .../cli/src/controllers/mfa.controller.ts | 2 +- .../oauth/abstractOAuth.controller.ts | 3 +- .../oauth/oAuth1Credential.controller.ts | 4 +- .../cli/src/controllers/owner.controller.ts | 2 +- .../controllers/passwordReset.controller.ts | 12 ++-- .../cli/src/controllers/tags.controller.ts | 2 +- .../src/controllers/translation.controller.ts | 3 +- .../cli/src/controllers/users.controller.ts | 4 +- .../workflowStatistics.controller.ts | 2 +- .../credentials/credentials.controller.ee.ts | 13 ++-- .../src/credentials/credentials.controller.ts | 13 ++-- .../sourceControl.controller.ee.ts | 2 +- .../sourceControl/sourceControl.service.ee.ts | 2 +- .../variables/variables.controller.ee.ts | 36 +++++----- .../variables/variables.service.ee.ts | 13 ++-- .../src/errors/credential-not-found.error.ts | 10 +++ ...ternal-secrets-provider-not-found.error.ts | 7 ++ .../src/errors/feature-not-licensed.error.ts | 10 +++ packages/cli/src/errors/invalid-role.error.ts | 3 + .../cli/src/errors/not-string-array.error.ts | 7 ++ .../abstract/response.error.ts | 23 ++++++ .../src/errors/response-errors/auth.error.ts | 7 ++ .../response-errors/bad-request.error.ts | 7 ++ .../errors/response-errors/conflict.error.ts | 7 ++ .../response-errors/internal-server.error.ts | 7 ++ .../errors/response-errors/not-found.error.ts | 7 ++ .../service-unavailable.error.ts | 7 ++ .../response-errors/unauthorized.error.ts | 7 ++ .../response-errors/unprocessable.error.ts | 7 ++ .../errors/shared-workflow-not-found.error.ts | 3 + .../errors/unrecognized-node-type.error.ts | 9 +++ .../variable-count-limit-reached.error.ts | 3 + .../src/errors/variable-validation.error.ts | 3 + ...orkflow-history-version-not-found.error.ts | 3 + .../src/eventbus/eventBus.controller.ee.ts | 2 +- .../cli/src/eventbus/eventBus.controller.ts | 2 +- .../cli/src/executions/executions.service.ts | 15 ++-- .../cli/src/license/license.controller.ts | 10 +-- packages/cli/src/middlewares/bodyParser.ts | 2 +- packages/cli/src/services/role.service.ts | 3 +- packages/cli/src/services/user.service.ts | 2 +- .../src/sso/saml/routes/saml.controller.ee.ts | 3 +- packages/cli/src/sso/saml/saml.service.ee.ts | 3 +- packages/cli/src/sso/saml/samlHelpers.ts | 4 +- .../workflowHistory.controller.ee.ts | 13 ++-- .../workflowHistory.service.ee.ts | 11 ++- .../src/workflows/workflows.controller.ee.ts | 16 +++-- .../cli/src/workflows/workflows.controller.ts | 17 +++-- .../src/workflows/workflows.services.ee.ts | 9 +-- .../cli/src/workflows/workflows.services.ts | 11 +-- .../unit/controllers/me.controller.test.ts | 2 +- .../oAuth1Credential.controller.test.ts | 3 +- .../oAuth2Credential.controller.test.ts | 3 +- .../unit/controllers/owner.controller.test.ts | 2 +- .../translation.controller.test.ts | 2 +- 81 files changed, 346 insertions(+), 297 deletions(-) create mode 100644 packages/cli/src/errors/credential-not-found.error.ts create mode 100644 packages/cli/src/errors/external-secrets-provider-not-found.error.ts create mode 100644 packages/cli/src/errors/feature-not-licensed.error.ts create mode 100644 packages/cli/src/errors/invalid-role.error.ts create mode 100644 packages/cli/src/errors/not-string-array.error.ts create mode 100644 packages/cli/src/errors/response-errors/abstract/response.error.ts create mode 100644 packages/cli/src/errors/response-errors/auth.error.ts create mode 100644 packages/cli/src/errors/response-errors/bad-request.error.ts create mode 100644 packages/cli/src/errors/response-errors/conflict.error.ts create mode 100644 packages/cli/src/errors/response-errors/internal-server.error.ts create mode 100644 packages/cli/src/errors/response-errors/not-found.error.ts create mode 100644 packages/cli/src/errors/response-errors/service-unavailable.error.ts create mode 100644 packages/cli/src/errors/response-errors/unauthorized.error.ts create mode 100644 packages/cli/src/errors/response-errors/unprocessable.error.ts create mode 100644 packages/cli/src/errors/shared-workflow-not-found.error.ts create mode 100644 packages/cli/src/errors/unrecognized-node-type.error.ts create mode 100644 packages/cli/src/errors/variable-count-limit-reached.error.ts create mode 100644 packages/cli/src/errors/variable-validation.error.ts create mode 100644 packages/cli/src/errors/workflow-history-version-not-found.error.ts diff --git a/packages/cli/src/AbstractServer.ts b/packages/cli/src/AbstractServer.ts index 4050b65a80a97..66ce5855f004b 100644 --- a/packages/cli/src/AbstractServer.ts +++ b/packages/cli/src/AbstractServer.ts @@ -11,13 +11,14 @@ import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import * as Db from '@/Db'; import type { N8nInstanceType, IExternalHooksClass } from '@/Interfaces'; import { ExternalHooks } from '@/ExternalHooks'; -import { send, sendErrorResponse, ServiceUnavailableError } from '@/ResponseHelper'; +import { send, sendErrorResponse } from '@/ResponseHelper'; import { rawBodyReader, bodyParser, corsMiddleware } from '@/middlewares'; import { TestWebhooks } from '@/TestWebhooks'; import { WaitingWebhooks } from '@/WaitingWebhooks'; import { webhookRequestHandler } from '@/WebhookHelpers'; import { generateHostInstanceId } from './databases/utils/generators'; import { Logger } from '@/Logger'; +import { ServiceUnavailableError } from './errors/response-errors/service-unavailable.error'; export abstract class AbstractServer { protected logger: Logger; diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index efa7e47521ad1..1c8d1cc7a778b 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -42,7 +42,6 @@ import type { WebhookAccessControlOptions, WebhookRequest, } from '@/Interfaces'; -import * as ResponseHelper from '@/ResponseHelper'; import * as WebhookHelpers from '@/WebhookHelpers'; import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; @@ -68,6 +67,7 @@ import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.reposi import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; import { ActivationErrorsService } from '@/ActivationErrors.service'; +import { NotFoundError } from './errors/response-errors/not-found.error'; const WEBHOOK_PROD_UNREGISTERED_HINT = "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"; @@ -165,9 +165,7 @@ export class ActiveWorkflowRunner implements IWebhookManager { }); if (workflowData === null) { - throw new ResponseHelper.NotFoundError( - `Could not find workflow with id "${webhook.workflowId}"`, - ); + throw new NotFoundError(`Could not find workflow with id "${webhook.workflowId}"`); } const workflow = new Workflow({ @@ -196,7 +194,7 @@ export class ActiveWorkflowRunner implements IWebhookManager { const workflowStartNode = workflow.getNode(webhookData.node); if (workflowStartNode === null) { - throw new ResponseHelper.NotFoundError('Could not find node to process webhook.'); + throw new NotFoundError('Could not find node to process webhook.'); } return new Promise((resolve, reject) => { @@ -252,7 +250,7 @@ export class ActiveWorkflowRunner implements IWebhookManager { const webhook = await this.webhookService.findWebhook(httpMethod, path); if (webhook === null) { - throw new ResponseHelper.NotFoundError( + throw new NotFoundError( webhookNotFoundErrorMessage(path, httpMethod), WEBHOOK_PROD_UNREGISTERED_HINT, ); diff --git a/packages/cli/src/CredentialsHelper.ts b/packages/cli/src/CredentialsHelper.ts index 9144c9fb2de5a..8b6876944c4a1 100644 --- a/packages/cli/src/CredentialsHelper.ts +++ b/packages/cli/src/CredentialsHelper.ts @@ -32,7 +32,6 @@ import type { INodeTypes, IWorkflowExecuteAdditionalData, ICredentialTestFunctions, - Severity, } from 'n8n-workflow'; import { ICredentialsHelper, @@ -55,6 +54,7 @@ import { isObjectLiteral } from './utils'; import { Logger } from '@/Logger'; import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; +import { CredentialNotFoundError } from './errors/credential-not-found.error'; const { OAUTH2_CREDENTIAL_TEST_SUCCEEDED, OAUTH2_CREDENTIAL_TEST_FAILED } = RESPONSE_ERROR_MESSAGES; @@ -87,15 +87,6 @@ const mockNodeTypes: INodeTypes = { }, }; -class CredentialNotFoundError extends Error { - severity: Severity; - - constructor(credentialId: string, credentialType: string) { - super(`Credential with ID "${credentialId}" does not exist for type "${credentialType}".`); - this.severity = 'warning'; - } -} - @Service() export class CredentialsHelper extends ICredentialsHelper { constructor( diff --git a/packages/cli/src/ExternalSecrets/ExternalSecrets.controller.ee.ts b/packages/cli/src/ExternalSecrets/ExternalSecrets.controller.ee.ts index 10eed808a6eb0..d46bcc6113091 100644 --- a/packages/cli/src/ExternalSecrets/ExternalSecrets.controller.ee.ts +++ b/packages/cli/src/ExternalSecrets/ExternalSecrets.controller.ee.ts @@ -1,9 +1,10 @@ import { Authorized, Get, Post, RestController } from '@/decorators'; import { ExternalSecretsRequest } from '@/requests'; -import { NotFoundError } from '@/ResponseHelper'; import { Response } from 'express'; import { Service } from 'typedi'; -import { ProviderNotFoundError, ExternalSecretsService } from './ExternalSecrets.service.ee'; +import { ExternalSecretsService } from './ExternalSecrets.service.ee'; +import { ExternalSecretsProviderNotFoundError } from '@/errors/external-secrets-provider-not-found.error'; +import { NotFoundError } from '@/errors/response-errors/not-found.error'; @Service() @Authorized(['global', 'owner']) @@ -22,7 +23,7 @@ export class ExternalSecretsController { try { return this.secretsService.getProvider(providerName); } catch (e) { - if (e instanceof ProviderNotFoundError) { + if (e instanceof ExternalSecretsProviderNotFoundError) { throw new NotFoundError(`Could not find provider "${e.providerName}"`); } throw e; @@ -41,7 +42,7 @@ export class ExternalSecretsController { } return result; } catch (e) { - if (e instanceof ProviderNotFoundError) { + if (e instanceof ExternalSecretsProviderNotFoundError) { throw new NotFoundError(`Could not find provider "${e.providerName}"`); } throw e; @@ -54,7 +55,7 @@ export class ExternalSecretsController { try { await this.secretsService.saveProviderSettings(providerName, req.body, req.user.id); } catch (e) { - if (e instanceof ProviderNotFoundError) { + if (e instanceof ExternalSecretsProviderNotFoundError) { throw new NotFoundError(`Could not find provider "${e.providerName}"`); } throw e; @@ -68,7 +69,7 @@ export class ExternalSecretsController { try { await this.secretsService.saveProviderConnected(providerName, req.body.connected); } catch (e) { - if (e instanceof ProviderNotFoundError) { + if (e instanceof ExternalSecretsProviderNotFoundError) { throw new NotFoundError(`Could not find provider "${e.providerName}"`); } throw e; @@ -88,7 +89,7 @@ export class ExternalSecretsController { } return { updated: resp }; } catch (e) { - if (e instanceof ProviderNotFoundError) { + if (e instanceof ExternalSecretsProviderNotFoundError) { throw new NotFoundError(`Could not find provider "${e.providerName}"`); } throw e; diff --git a/packages/cli/src/ExternalSecrets/ExternalSecrets.service.ee.ts b/packages/cli/src/ExternalSecrets/ExternalSecrets.service.ee.ts index ab630e27e69d9..1087443ddbc96 100644 --- a/packages/cli/src/ExternalSecrets/ExternalSecrets.service.ee.ts +++ b/packages/cli/src/ExternalSecrets/ExternalSecrets.service.ee.ts @@ -5,12 +5,7 @@ import type { IDataObject } from 'n8n-workflow'; import { deepCopy } from 'n8n-workflow'; import Container, { Service } from 'typedi'; import { ExternalSecretsManager } from './ExternalSecretsManager.ee'; - -export class ProviderNotFoundError extends Error { - constructor(public providerName: string) { - super(undefined); - } -} +import { ExternalSecretsProviderNotFoundError } from '@/errors/external-secrets-provider-not-found.error'; @Service() export class ExternalSecretsService { @@ -18,7 +13,7 @@ export class ExternalSecretsService { const providerAndSettings = Container.get(ExternalSecretsManager).getProviderWithSettings(providerName); if (!providerAndSettings) { - throw new ProviderNotFoundError(providerName); + throw new ExternalSecretsProviderNotFoundError(providerName); } const { provider, settings } = providerAndSettings; return { @@ -110,7 +105,7 @@ export class ExternalSecretsService { const providerAndSettings = Container.get(ExternalSecretsManager).getProviderWithSettings(providerName); if (!providerAndSettings) { - throw new ProviderNotFoundError(providerName); + throw new ExternalSecretsProviderNotFoundError(providerName); } const { settings } = providerAndSettings; const newData = this.unredact(data, settings.settings); @@ -121,7 +116,7 @@ export class ExternalSecretsService { const providerAndSettings = Container.get(ExternalSecretsManager).getProviderWithSettings(providerName); if (!providerAndSettings) { - throw new ProviderNotFoundError(providerName); + throw new ExternalSecretsProviderNotFoundError(providerName); } await Container.get(ExternalSecretsManager).setProviderConnected(providerName, connected); return this.getProvider(providerName); @@ -135,7 +130,7 @@ export class ExternalSecretsService { const providerAndSettings = Container.get(ExternalSecretsManager).getProviderWithSettings(providerName); if (!providerAndSettings) { - throw new ProviderNotFoundError(providerName); + throw new ExternalSecretsProviderNotFoundError(providerName); } const { settings } = providerAndSettings; const newData = this.unredact(data, settings.settings); @@ -146,7 +141,7 @@ export class ExternalSecretsService { const providerAndSettings = Container.get(ExternalSecretsManager).getProviderWithSettings(providerName); if (!providerAndSettings) { - throw new ProviderNotFoundError(providerName); + throw new ExternalSecretsProviderNotFoundError(providerName); } return Container.get(ExternalSecretsManager).updateProvider(providerName); } diff --git a/packages/cli/src/GenericHelpers.ts b/packages/cli/src/GenericHelpers.ts index e27ad52967388..054422bdeb6ac 100644 --- a/packages/cli/src/GenericHelpers.ts +++ b/packages/cli/src/GenericHelpers.ts @@ -11,7 +11,6 @@ import { Container } from 'typedi'; import { Like } from 'typeorm'; import config from '@/config'; import type { ExecutionPayload, ICredentialsDb, IWorkflowDb } from '@/Interfaces'; -import * as ResponseHelper from '@/ResponseHelper'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import type { TagEntity } from '@db/entities/TagEntity'; @@ -20,6 +19,7 @@ import type { UserUpdatePayload } from '@/requests'; import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { ExecutionRepository } from '@db/repositories/execution.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; +import { BadRequestError } from './errors/response-errors/bad-request.error'; /** * Returns the base URL n8n is reachable from @@ -109,7 +109,7 @@ export async function validateEntity( .join(' | '); if (errorMessages) { - throw new ResponseHelper.BadRequestError(errorMessages); + throw new BadRequestError(errorMessages); } } diff --git a/packages/cli/src/Ldap/helpers.ts b/packages/cli/src/Ldap/helpers.ts index b43533c9d414e..adf66e63f0adb 100644 --- a/packages/cli/src/Ldap/helpers.ts +++ b/packages/cli/src/Ldap/helpers.ts @@ -29,13 +29,14 @@ import { isLdapCurrentAuthenticationMethod, setCurrentAuthenticationMethod, } from '@/sso/ssoHelpers'; -import { BadRequestError, InternalServerError } from '../ResponseHelper'; import { RoleService } from '@/services/role.service'; import { Logger } from '@/Logger'; import { UserRepository } from '@db/repositories/user.repository'; import { SettingsRepository } from '@db/repositories/settings.repository'; import { AuthProviderSyncHistoryRepository } from '@db/repositories/authProviderSyncHistory.repository'; import { AuthIdentityRepository } from '@db/repositories/authIdentity.repository'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; +import { InternalServerError } from '@/errors/response-errors/internal-server.error'; /** * Check whether the LDAP feature is disabled in the instance diff --git a/packages/cli/src/License.ts b/packages/cli/src/License.ts index fa9f6ca32de63..4a12ad99f689f 100644 --- a/packages/cli/src/License.ts +++ b/packages/cli/src/License.ts @@ -24,14 +24,6 @@ type FeatureReturnType = Partial< } & { [K in NumericLicenseFeature]: number } & { [K in BooleanLicenseFeature]: boolean } >; -export class FeatureNotLicensedError extends Error { - constructor(feature: (typeof LICENSE_FEATURES)[keyof typeof LICENSE_FEATURES]) { - super( - `Your license does not allow for ${feature}. To enable ${feature}, please upgrade to a license that supports this feature.`, - ); - } -} - @Service() export class License { private manager: LicenseManager | undefined; diff --git a/packages/cli/src/NodeTypes.ts b/packages/cli/src/NodeTypes.ts index 19b052a2087d4..0ce7af0578ef2 100644 --- a/packages/cli/src/NodeTypes.ts +++ b/packages/cli/src/NodeTypes.ts @@ -12,14 +12,7 @@ import { LoadNodesAndCredentials } from './LoadNodesAndCredentials'; import { join, dirname } from 'path'; import { readdir } from 'fs/promises'; import type { Dirent } from 'fs'; - -class UnrecognizedNodeError extends Error { - severity = 'warning'; - - constructor(nodeType: string) { - super(`Unrecognized node type: ${nodeType}".`); - } -} +import { UnrecognizedNodeTypeError } from './errors/unrecognized-node-type.error'; @Service() export class NodeTypes implements INodeTypes { @@ -75,7 +68,7 @@ export class NodeTypes implements INodeTypes { return loadedNodes[type]; } - throw new UnrecognizedNodeError(type); + throw new UnrecognizedNodeTypeError(type); } async getNodeTranslationPath({ diff --git a/packages/cli/src/ResponseHelper.ts b/packages/cli/src/ResponseHelper.ts index 3581d4e51b0e5..4684aac2a1f2a 100644 --- a/packages/cli/src/ResponseHelper.ts +++ b/packages/cli/src/ResponseHelper.ts @@ -12,76 +12,7 @@ import type { IWorkflowDb, } from '@/Interfaces'; import { inDevelopment } from '@/constants'; - -/** - * Special Error which allows to return also an error code and http status code - */ -abstract class ResponseError extends Error { - /** - * Creates an instance of ResponseError. - * Must be used inside a block with `ResponseHelper.send()`. - */ - constructor( - message: string, - // The HTTP status code of response - readonly httpStatusCode: number, - // The error code in the response - readonly errorCode: number = httpStatusCode, - // The error hint the response - readonly hint: string | undefined = undefined, - ) { - super(message); - this.name = 'ResponseError'; - } -} - -export class BadRequestError extends ResponseError { - constructor(message: string, errorCode?: number) { - super(message, 400, errorCode); - } -} - -export class AuthError extends ResponseError { - constructor(message: string, errorCode?: number) { - super(message, 401, errorCode); - } -} - -export class UnauthorizedError extends ResponseError { - constructor(message: string, hint: string | undefined = undefined) { - super(message, 403, 403, hint); - } -} - -export class NotFoundError extends ResponseError { - constructor(message: string, hint: string | undefined = undefined) { - super(message, 404, 404, hint); - } -} - -export class ConflictError extends ResponseError { - constructor(message: string, hint: string | undefined = undefined) { - super(message, 409, 409, hint); - } -} - -export class UnprocessableRequestError extends ResponseError { - constructor(message: string, hint: string | undefined = undefined) { - super(message, 422, 422, hint); - } -} - -export class InternalServerError extends ResponseError { - constructor(message: string, errorCode = 500) { - super(message, 500, errorCode); - } -} - -export class ServiceUnavailableError extends ResponseError { - constructor(message: string, errorCode = 503) { - super(message, 503, errorCode); - } -} +import { ResponseError } from './errors/response-errors/abstract/response.error'; export function sendSuccessResponse( res: Response, diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index f5f3af489b8af..4d6e0c05d69e0 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -117,6 +117,8 @@ import { OrchestrationController } from './controllers/orchestration.controller' import { WorkflowHistoryController } from './workflows/workflowHistory/workflowHistory.controller.ee'; import { InvitationController } from './controllers/invitation.controller'; import { CollaborationService } from './collaboration/collaboration.service'; +import { BadRequestError } from './errors/response-errors/bad-request.error'; +import { NotFoundError } from './errors/response-errors/not-found.error'; const exec = promisify(callbackExec); @@ -466,9 +468,7 @@ export class Server extends AbstractServer { userId: req.user.id, }); - throw new ResponseHelper.BadRequestError( - `Workflow with ID "${workflowId}" could not be found.`, - ); + throw new BadRequestError(`Workflow with ID "${workflowId}" could not be found.`); } return this.activeWorkflowRunner.getActivationError(workflowId); @@ -491,7 +491,7 @@ export class Server extends AbstractServer { const parameters = toHttpNodeParameters(curlCommand); return ResponseHelper.flattenObject(parameters, 'parameters'); } catch (e) { - throw new ResponseHelper.BadRequestError('Invalid cURL command'); + throw new BadRequestError('Invalid cURL command'); } }, ), @@ -624,7 +624,7 @@ export class Server extends AbstractServer { const sharedWorkflowIds = await getSharedWorkflowIds(req.user); if (!sharedWorkflowIds.length) { - throw new ResponseHelper.NotFoundError('Execution not found'); + throw new NotFoundError('Execution not found'); } const fullExecutionData = await Container.get(ExecutionRepository).findSingleExecution( @@ -637,7 +637,7 @@ export class Server extends AbstractServer { ); if (!fullExecutionData) { - throw new ResponseHelper.NotFoundError('Execution not found'); + throw new NotFoundError('Execution not found'); } if (config.getEnv('executions.mode') === 'queue') { diff --git a/packages/cli/src/TestWebhooks.ts b/packages/cli/src/TestWebhooks.ts index a31e37ea6ba92..f8a5eafab2b7a 100644 --- a/packages/cli/src/TestWebhooks.ts +++ b/packages/cli/src/TestWebhooks.ts @@ -20,9 +20,9 @@ import type { } from '@/Interfaces'; import { Push } from '@/push'; import { NodeTypes } from '@/NodeTypes'; -import * as ResponseHelper from '@/ResponseHelper'; import * as WebhookHelpers from '@/WebhookHelpers'; import { webhookNotFoundErrorMessage } from './utils'; +import { NotFoundError } from './errors/response-errors/not-found.error'; const WEBHOOK_TEST_UNREGISTERED_HINT = "Click the 'Execute workflow' button on the canvas, then try again. (In test mode, the webhook only works for one call after you click this button)"; @@ -80,7 +80,7 @@ export class TestWebhooks implements IWebhookManager { if (webhookData === undefined) { // The requested webhook is not registered const methods = await this.getWebhookMethods(path); - throw new ResponseHelper.NotFoundError( + throw new NotFoundError( webhookNotFoundErrorMessage(path, httpMethod, methods), WEBHOOK_TEST_UNREGISTERED_HINT, ); @@ -108,7 +108,7 @@ export class TestWebhooks implements IWebhookManager { if (testWebhookData[webhookKey] === undefined) { // The requested webhook is not registered const methods = await this.getWebhookMethods(path); - throw new ResponseHelper.NotFoundError( + throw new NotFoundError( webhookNotFoundErrorMessage(path, httpMethod, methods), WEBHOOK_TEST_UNREGISTERED_HINT, ); @@ -121,7 +121,7 @@ export class TestWebhooks implements IWebhookManager { // get additional data const workflowStartNode = workflow.getNode(webhookData.node); if (workflowStartNode === null) { - throw new ResponseHelper.NotFoundError('Could not find node to process webhook.'); + throw new NotFoundError('Could not find node to process webhook.'); } return new Promise(async (resolve, reject) => { @@ -168,10 +168,7 @@ export class TestWebhooks implements IWebhookManager { const webhookMethods = this.activeWebhooks.getWebhookMethods(path); if (!webhookMethods.length) { // The requested webhook is not registered - throw new ResponseHelper.NotFoundError( - webhookNotFoundErrorMessage(path), - WEBHOOK_TEST_UNREGISTERED_HINT, - ); + throw new NotFoundError(webhookNotFoundErrorMessage(path), WEBHOOK_TEST_UNREGISTERED_HINT); } return webhookMethods; diff --git a/packages/cli/src/UserManagement/UserManagementHelper.ts b/packages/cli/src/UserManagement/UserManagementHelper.ts index 50de670329cfa..448a469a6cf4a 100644 --- a/packages/cli/src/UserManagement/UserManagementHelper.ts +++ b/packages/cli/src/UserManagement/UserManagementHelper.ts @@ -2,7 +2,6 @@ import { In } from 'typeorm'; import { compare, genSaltSync, hash } from 'bcryptjs'; import { Container } from 'typedi'; -import * as ResponseHelper from '@/ResponseHelper'; import type { WhereClause } from '@/Interfaces'; import type { User } from '@db/entities/User'; import { MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH } from '@db/entities/User'; @@ -11,6 +10,7 @@ import { License } from '@/License'; import { getWebhookBaseUrl } from '@/WebhookHelpers'; import { RoleService } from '@/services/role.service'; import { UserRepository } from '@db/repositories/user.repository'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; export function isSharingEnabled(): boolean { return Container.get(License).isSharingEnabled(); @@ -43,7 +43,7 @@ export function generateUserInviteUrl(inviterId: string, inviteeId: string): str // TODO: Enforce at model level export function validatePassword(password?: string): string { if (!password) { - throw new ResponseHelper.BadRequestError('Password is mandatory'); + throw new BadRequestError('Password is mandatory'); } const hasInvalidLength = @@ -70,7 +70,7 @@ export function validatePassword(password?: string): string { message.push('Password must contain at least 1 uppercase letter.'); } - throw new ResponseHelper.BadRequestError(message.join(' ')); + throw new BadRequestError(message.join(' ')); } return password; diff --git a/packages/cli/src/WaitingWebhooks.ts b/packages/cli/src/WaitingWebhooks.ts index a37f76ad51bb1..7368c29369e34 100644 --- a/packages/cli/src/WaitingWebhooks.ts +++ b/packages/cli/src/WaitingWebhooks.ts @@ -2,7 +2,6 @@ import { NodeHelpers, Workflow } from 'n8n-workflow'; import { Service } from 'typedi'; import type express from 'express'; -import * as ResponseHelper from '@/ResponseHelper'; import * as WebhookHelpers from '@/WebhookHelpers'; import { NodeTypes } from '@/NodeTypes'; import type { @@ -15,6 +14,8 @@ import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData' import { ExecutionRepository } from '@db/repositories/execution.repository'; import { OwnershipService } from './services/ownership.service'; import { Logger } from '@/Logger'; +import { ConflictError } from './errors/response-errors/conflict.error'; +import { NotFoundError } from './errors/response-errors/not-found.error'; @Service() export class WaitingWebhooks implements IWebhookManager { @@ -43,11 +44,11 @@ export class WaitingWebhooks implements IWebhookManager { }); if (!execution) { - throw new ResponseHelper.NotFoundError(`The execution "${executionId} does not exist.`); + throw new NotFoundError(`The execution "${executionId} does not exist.`); } if (execution.finished || execution.data.resultData.error) { - throw new ResponseHelper.ConflictError(`The execution "${executionId} has finished already.`); + throw new ConflictError(`The execution "${executionId} has finished already.`); } const lastNodeExecuted = execution.data.resultData.lastNodeExecuted as string; @@ -79,12 +80,12 @@ export class WaitingWebhooks implements IWebhookManager { try { workflowOwner = await this.ownershipService.getWorkflowOwnerCached(workflowData.id!); } catch (error) { - throw new ResponseHelper.NotFoundError('Could not find workflow'); + throw new NotFoundError('Could not find workflow'); } const workflowStartNode = workflow.getNode(lastNodeExecuted); if (workflowStartNode === null) { - throw new ResponseHelper.NotFoundError('Could not find node to process webhook.'); + throw new NotFoundError('Could not find node to process webhook.'); } const additionalData = await WorkflowExecuteAdditionalData.getBase(workflowOwner.id); @@ -103,7 +104,7 @@ export class WaitingWebhooks implements IWebhookManager { // If no data got found it means that the execution can not be started via a webhook. // Return 404 because we do not want to give any data if the execution exists or not. const errorMessage = `The workflow for execution "${executionId}" does not contain a waiting webhook with a matching path/method.`; - throw new ResponseHelper.NotFoundError(errorMessage); + throw new NotFoundError(errorMessage); } const runExecutionData = execution.data; diff --git a/packages/cli/src/WebhookHelpers.ts b/packages/cli/src/WebhookHelpers.ts index ae87e130e9597..d80ab2c4159ea 100644 --- a/packages/cli/src/WebhookHelpers.ts +++ b/packages/cli/src/WebhookHelpers.ts @@ -63,6 +63,9 @@ import { OwnershipService } from './services/ownership.service'; import { parseBody } from './middlewares'; import { WorkflowsService } from './workflows/workflows.services'; import { Logger } from './Logger'; +import { NotFoundError } from './errors/response-errors/not-found.error'; +import { InternalServerError } from './errors/response-errors/internal-server.error'; +import { UnprocessableRequestError } from './errors/response-errors/unprocessable.error'; const pipeline = promisify(stream.pipeline); @@ -237,7 +240,7 @@ export async function executeWebhook( if (nodeType === undefined) { const errorMessage = `The type of the webhook node "${workflowStartNode.name}" is not known`; responseCallback(new Error(errorMessage), {}); - throw new ResponseHelper.InternalServerError(errorMessage); + throw new InternalServerError(errorMessage); } const additionalKeys: IWorkflowDataProxyAdditionalKeys = { @@ -254,7 +257,7 @@ export async function executeWebhook( try { user = await Container.get(OwnershipService).getWorkflowOwnerCached(workflowData.id); } catch (error) { - throw new ResponseHelper.NotFoundError('Cannot find workflow'); + throw new NotFoundError('Cannot find workflow'); } } @@ -294,7 +297,7 @@ export async function executeWebhook( // that something does not resolve properly. const errorMessage = `The response mode '${responseMode}' is not valid!`; responseCallback(new Error(errorMessage), {}); - throw new ResponseHelper.InternalServerError(errorMessage); + throw new InternalServerError(errorMessage); } // Add the Response and Request so that this data can be accessed in the node @@ -781,13 +784,13 @@ export async function executeWebhook( responseCallback(new Error('There was a problem executing the workflow'), {}); } - throw new ResponseHelper.InternalServerError(e.message); + throw new InternalServerError(e.message); }); } return executionId; } catch (e) { const error = - e instanceof ResponseHelper.UnprocessableRequestError + e instanceof UnprocessableRequestError ? e : new Error('There was a problem executing the workflow', { cause: e }); if (didSendResponse) throw error; diff --git a/packages/cli/src/auth/jwt.ts b/packages/cli/src/auth/jwt.ts index 3b2a810eb058a..52c57533e8dc4 100644 --- a/packages/cli/src/auth/jwt.ts +++ b/packages/cli/src/auth/jwt.ts @@ -4,11 +4,12 @@ import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from '@/constants'; import type { JwtPayload, JwtToken } from '@/Interfaces'; import type { User } from '@db/entities/User'; import config from '@/config'; -import * as ResponseHelper from '@/ResponseHelper'; import { License } from '@/License'; import { Container } from 'typedi'; import { UserRepository } from '@db/repositories/user.repository'; import { JwtService } from '@/services/jwt.service'; +import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error'; +import { AuthError } from '@/errors/response-errors/auth.error'; export function issueJWT(user: User): JwtToken { const { id, email, password } = user; @@ -26,7 +27,7 @@ export function issueJWT(user: User): JwtToken { !user.isOwner && !isWithinUsersLimit ) { - throw new ResponseHelper.UnauthorizedError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED); + throw new UnauthorizedError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED); } if (password) { payload.password = createHash('sha256') @@ -63,7 +64,7 @@ export async function resolveJwtContent(jwtPayload: JwtPayload): Promise { // currently only LDAP users during synchronization // can be set to disabled if (user?.disabled) { - throw new ResponseHelper.AuthError('Unauthorized'); + throw new AuthError('Unauthorized'); } if (!user || jwtPayload.password !== passwordHash || user.email !== jwtPayload.email) { diff --git a/packages/cli/src/auth/methods/email.ts b/packages/cli/src/auth/methods/email.ts index 5f8f79005dc83..7850441697c75 100644 --- a/packages/cli/src/auth/methods/email.ts +++ b/packages/cli/src/auth/methods/email.ts @@ -1,10 +1,10 @@ import type { User } from '@db/entities/User'; import { compareHash } from '@/UserManagement/UserManagementHelper'; -import * as ResponseHelper from '@/ResponseHelper'; import { Container } from 'typedi'; import { InternalHooks } from '@/InternalHooks'; import { isLdapLoginEnabled } from '@/Ldap/helpers'; import { UserRepository } from '@db/repositories/user.repository'; +import { AuthError } from '@/errors/response-errors/auth.error'; export const handleEmailLogin = async ( email: string, @@ -27,7 +27,7 @@ export const handleEmailLogin = async ( user_id: user.id, }); - throw new ResponseHelper.AuthError('Reset your password to gain access to the instance.'); + throw new AuthError('Reset your password to gain access to the instance.'); } return undefined; diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index c9526c924ffbb..42a5b463f50a5 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -23,7 +23,7 @@ import { EDITOR_UI_DIST_DIR, LICENSE_FEATURES } from '@/constants'; import { eventBus } from '@/eventbus'; import { BaseCommand } from './BaseCommand'; import { InternalHooks } from '@/InternalHooks'; -import { License, FeatureNotLicensedError } from '@/License'; +import { License } from '@/License'; import type { IConfig } from '@oclif/config'; import { SingleMainSetup } from '@/services/orchestration/main/SingleMainSetup'; import { OrchestrationHandlerMainService } from '@/services/orchestration/main/orchestration.handler.main.service'; @@ -31,6 +31,7 @@ import { PruningService } from '@/services/pruning.service'; import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; import { SettingsRepository } from '@db/repositories/settings.repository'; import { ExecutionRepository } from '@db/repositories/execution.repository'; +import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error'; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires const open = require('open'); diff --git a/packages/cli/src/commands/worker.ts b/packages/cli/src/commands/worker.ts index d65f39d089930..0f7b0d7f152a8 100644 --- a/packages/cli/src/commands/worker.ts +++ b/packages/cli/src/commands/worker.ts @@ -40,6 +40,7 @@ import type { IConfig } from '@oclif/config'; import { OrchestrationHandlerWorkerService } from '@/services/orchestration/worker/orchestration.handler.worker.service'; import { OrchestrationWorkerService } from '@/services/orchestration/worker/orchestration.worker.service'; import type { WorkerJobStatusSummary } from '../services/orchestration/worker/types'; +import { ServiceUnavailableError } from '@/errors/response-errors/service-unavailable.error'; export class Worker extends BaseCommand { static description = '\nStarts a n8n worker'; @@ -413,7 +414,7 @@ export class Worker extends BaseCommand { await connection.query('SELECT 1'); } catch (e) { this.logger.error('No Database connection!', e as Error); - const error = new ResponseHelper.ServiceUnavailableError('No Database connection!'); + const error = new ServiceUnavailableError('No Database connection!'); return ResponseHelper.sendErrorResponse(res, error); } @@ -424,7 +425,7 @@ export class Worker extends BaseCommand { await Worker.jobQueue.ping(); } catch (e) { this.logger.error('No Redis connection!', e as Error); - const error = new ResponseHelper.ServiceUnavailableError('No Redis connection!'); + const error = new ServiceUnavailableError('No Redis connection!'); return ResponseHelper.sendErrorResponse(res, error); } diff --git a/packages/cli/src/config/utils.ts b/packages/cli/src/config/utils.ts index 4fee1a41108fd..9fe2fcec851d0 100644 --- a/packages/cli/src/config/utils.ts +++ b/packages/cli/src/config/utils.ts @@ -1,11 +1,6 @@ +import { NotStringArrayError } from '@/errors/not-string-array.error'; import type { SchemaObj } from 'convict'; -class NotStringArrayError extends Error { - constructor(env: string) { - super(`${env} is not a string array.`); - } -} - export const ensureStringArray = (values: string[], { env }: SchemaObj) => { if (!env) throw new Error(`Missing env: ${env}`); diff --git a/packages/cli/src/controllers/auth.controller.ts b/packages/cli/src/controllers/auth.controller.ts index d70c68691ccfd..d3b1b4140caa2 100644 --- a/packages/cli/src/controllers/auth.controller.ts +++ b/packages/cli/src/controllers/auth.controller.ts @@ -2,12 +2,6 @@ import validator from 'validator'; import { In } from 'typeorm'; import { Service } from 'typedi'; import { Authorized, Get, Post, RestController } from '@/decorators'; -import { - AuthError, - BadRequestError, - InternalServerError, - UnauthorizedError, -} from '@/ResponseHelper'; import { issueCookie, resolveJwt } from '@/auth/jwt'; import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from '@/constants'; import { Request, Response } from 'express'; @@ -27,6 +21,10 @@ import { License } from '@/License'; import { UserService } from '@/services/user.service'; import { MfaService } from '@/Mfa/mfa.service'; import { Logger } from '@/Logger'; +import { AuthError } from '@/errors/response-errors/auth.error'; +import { InternalServerError } from '@/errors/response-errors/internal-server.error'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; +import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error'; @Service() @RestController() diff --git a/packages/cli/src/controllers/communityPackages.controller.ts b/packages/cli/src/controllers/communityPackages.controller.ts index 7438ae69d8a96..689ae3bbdbe51 100644 --- a/packages/cli/src/controllers/communityPackages.controller.ts +++ b/packages/cli/src/controllers/communityPackages.controller.ts @@ -8,12 +8,13 @@ import { } from '@/constants'; import { Authorized, Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators'; import { NodeRequest } from '@/requests'; -import { BadRequestError, InternalServerError } from '@/ResponseHelper'; import type { InstalledPackages } from '@db/entities/InstalledPackages'; import type { CommunityPackages } from '@/Interfaces'; import { InternalHooks } from '@/InternalHooks'; import { Push } from '@/push'; import { CommunityPackagesService } from '@/services/communityPackages.service'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; +import { InternalServerError } from '@/errors/response-errors/internal-server.error'; const { PACKAGE_NOT_INSTALLED, diff --git a/packages/cli/src/controllers/dynamicNodeParameters.controller.ts b/packages/cli/src/controllers/dynamicNodeParameters.controller.ts index bfb6e7cc37ed3..599b71e65c7c8 100644 --- a/packages/cli/src/controllers/dynamicNodeParameters.controller.ts +++ b/packages/cli/src/controllers/dynamicNodeParameters.controller.ts @@ -12,7 +12,7 @@ import { Authorized, Get, Middleware, RestController } from '@/decorators'; import { getBase } from '@/WorkflowExecuteAdditionalData'; import { DynamicNodeParametersService } from '@/services/dynamicNodeParameters.service'; import { DynamicNodeParametersRequest } from '@/requests'; -import { BadRequestError } from '@/ResponseHelper'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; const assertMethodName: RequestHandler = (req, res, next) => { const { methodName } = req.query as DynamicNodeParametersRequest.BaseRequest['query']; diff --git a/packages/cli/src/controllers/invitation.controller.ts b/packages/cli/src/controllers/invitation.controller.ts index ce94cf4edcd7c..5dc0c23829dfa 100644 --- a/packages/cli/src/controllers/invitation.controller.ts +++ b/packages/cli/src/controllers/invitation.controller.ts @@ -1,7 +1,6 @@ import { In } from 'typeorm'; import Container, { Service } from 'typedi'; import { Authorized, NoAuthRequired, Post, RestController } from '@/decorators'; -import { BadRequestError, UnauthorizedError } from '@/ResponseHelper'; import { issueCookie } from '@/auth/jwt'; import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import { Response } from 'express'; @@ -16,6 +15,8 @@ import { hashPassword, validatePassword } from '@/UserManagement/UserManagementH import { PostHogClient } from '@/posthog'; import type { User } from '@/databases/entities/User'; import validator from 'validator'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; +import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error'; @Service() @RestController('/invitations') diff --git a/packages/cli/src/controllers/ldap.controller.ts b/packages/cli/src/controllers/ldap.controller.ts index 25c19d5d21959..827d84648708c 100644 --- a/packages/cli/src/controllers/ldap.controller.ts +++ b/packages/cli/src/controllers/ldap.controller.ts @@ -4,9 +4,9 @@ import { getLdapConfig, getLdapSynchronizations, updateLdapConfig } from '@/Ldap import { LdapService } from '@/Ldap/LdapService.ee'; import { LdapSync } from '@/Ldap/LdapSync.ee'; import { LdapConfiguration } from '@/Ldap/types'; -import { BadRequestError } from '@/ResponseHelper'; import { NON_SENSIBLE_LDAP_CONFIG_PROPERTIES } from '@/Ldap/constants'; import { InternalHooks } from '@/InternalHooks'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; @Authorized(['global', 'owner']) @RestController('/ldap') diff --git a/packages/cli/src/controllers/me.controller.ts b/packages/cli/src/controllers/me.controller.ts index 3edfd6bbd2672..c7d05cb8cbb2d 100644 --- a/packages/cli/src/controllers/me.controller.ts +++ b/packages/cli/src/controllers/me.controller.ts @@ -5,7 +5,6 @@ import { Service } from 'typedi'; import { randomBytes } from 'crypto'; import { Authorized, Delete, Get, Patch, Post, RestController } from '@/decorators'; import { compareHash, hashPassword, validatePassword } from '@/UserManagement/UserManagementHelper'; -import { BadRequestError } from '@/ResponseHelper'; import { validateEntity } from '@/GenericHelpers'; import { issueCookie } from '@/auth/jwt'; import type { User } from '@db/entities/User'; @@ -21,6 +20,7 @@ import { UserService } from '@/services/user.service'; import { Logger } from '@/Logger'; import { ExternalHooks } from '@/ExternalHooks'; import { InternalHooks } from '@/InternalHooks'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; @Service() @Authorized() diff --git a/packages/cli/src/controllers/mfa.controller.ts b/packages/cli/src/controllers/mfa.controller.ts index 415b4b17166d2..5bdc1407288b7 100644 --- a/packages/cli/src/controllers/mfa.controller.ts +++ b/packages/cli/src/controllers/mfa.controller.ts @@ -1,8 +1,8 @@ import { Service } from 'typedi'; import { Authorized, Delete, Get, Post, RestController } from '@/decorators'; import { AuthenticatedRequest, MFA } from '@/requests'; -import { BadRequestError } from '@/ResponseHelper'; import { MfaService } from '@/Mfa/mfa.service'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; @Service() @Authorized() diff --git a/packages/cli/src/controllers/oauth/abstractOAuth.controller.ts b/packages/cli/src/controllers/oauth/abstractOAuth.controller.ts index babf90f4c6634..9229c3aaa47d0 100644 --- a/packages/cli/src/controllers/oauth/abstractOAuth.controller.ts +++ b/packages/cli/src/controllers/oauth/abstractOAuth.controller.ts @@ -9,12 +9,13 @@ import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials. import type { ICredentialsDb } from '@/Interfaces'; import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper'; import type { OAuthRequest } from '@/requests'; -import { BadRequestError, NotFoundError } from '@/ResponseHelper'; import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import { CredentialsHelper } from '@/CredentialsHelper'; import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; import { Logger } from '@/Logger'; import { ExternalHooks } from '@/ExternalHooks'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; +import { NotFoundError } from '@/errors/response-errors/not-found.error'; @Service() export abstract class AbstractOAuthController { diff --git a/packages/cli/src/controllers/oauth/oAuth1Credential.controller.ts b/packages/cli/src/controllers/oauth/oAuth1Credential.controller.ts index c9d3d31e000e9..e0d72ac05f233 100644 --- a/packages/cli/src/controllers/oauth/oAuth1Credential.controller.ts +++ b/packages/cli/src/controllers/oauth/oAuth1Credential.controller.ts @@ -8,8 +8,10 @@ import { createHmac } from 'crypto'; import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import { Authorized, Get, RestController } from '@/decorators'; import { OAuthRequest } from '@/requests'; -import { NotFoundError, sendErrorResponse, ServiceUnavailableError } from '@/ResponseHelper'; +import { sendErrorResponse } from '@/ResponseHelper'; import { AbstractOAuthController } from './abstractOAuth.controller'; +import { NotFoundError } from '@/errors/response-errors/not-found.error'; +import { ServiceUnavailableError } from '@/errors/response-errors/service-unavailable.error'; interface OAuth1CredentialData { signatureMethod: 'HMAC-SHA256' | 'HMAC-SHA512' | 'HMAC-SHA1'; diff --git a/packages/cli/src/controllers/owner.controller.ts b/packages/cli/src/controllers/owner.controller.ts index f16588d9f6882..a1d4da3bf71ae 100644 --- a/packages/cli/src/controllers/owner.controller.ts +++ b/packages/cli/src/controllers/owner.controller.ts @@ -1,7 +1,6 @@ import validator from 'validator'; import { validateEntity } from '@/GenericHelpers'; import { Authorized, Post, RestController } from '@/decorators'; -import { BadRequestError } from '@/ResponseHelper'; import { hashPassword, validatePassword } from '@/UserManagement/UserManagementHelper'; import { issueCookie } from '@/auth/jwt'; import { Response } from 'express'; @@ -12,6 +11,7 @@ import { SettingsRepository } from '@db/repositories/settings.repository'; import { PostHogClient } from '@/posthog'; import { UserService } from '@/services/user.service'; import { Logger } from '@/Logger'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; @Authorized(['global', 'owner']) @RestController('/owner') diff --git a/packages/cli/src/controllers/passwordReset.controller.ts b/packages/cli/src/controllers/passwordReset.controller.ts index 6396e2d689f7a..902a2071a8ef0 100644 --- a/packages/cli/src/controllers/passwordReset.controller.ts +++ b/packages/cli/src/controllers/passwordReset.controller.ts @@ -5,13 +5,6 @@ import { IsNull, Not } from 'typeorm'; import validator from 'validator'; import { Get, Post, RestController } from '@/decorators'; -import { - BadRequestError, - InternalServerError, - NotFoundError, - UnauthorizedError, - UnprocessableRequestError, -} from '@/ResponseHelper'; import { getInstanceBaseUrl, hashPassword, @@ -29,6 +22,11 @@ import { MfaService } from '@/Mfa/mfa.service'; import { Logger } from '@/Logger'; import { ExternalHooks } from '@/ExternalHooks'; import { InternalHooks } from '@/InternalHooks'; +import { InternalServerError } from '@/errors/response-errors/internal-server.error'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; +import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error'; +import { NotFoundError } from '@/errors/response-errors/not-found.error'; +import { UnprocessableRequestError } from '@/errors/response-errors/unprocessable.error'; const throttle = rateLimit({ windowMs: 5 * 60 * 1000, // 5 minutes diff --git a/packages/cli/src/controllers/tags.controller.ts b/packages/cli/src/controllers/tags.controller.ts index ab81357daf77e..01e40c842ca9a 100644 --- a/packages/cli/src/controllers/tags.controller.ts +++ b/packages/cli/src/controllers/tags.controller.ts @@ -2,9 +2,9 @@ import { Request, Response, NextFunction } from 'express'; import config from '@/config'; import { Authorized, Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators'; import { TagService } from '@/services/tag.service'; -import { BadRequestError } from '@/ResponseHelper'; import { TagsRequest } from '@/requests'; import { Service } from 'typedi'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; @Authorized() @RestController('/tags') diff --git a/packages/cli/src/controllers/translation.controller.ts b/packages/cli/src/controllers/translation.controller.ts index 8238e73ebdaf2..e7ef6bc3cd6b9 100644 --- a/packages/cli/src/controllers/translation.controller.ts +++ b/packages/cli/src/controllers/translation.controller.ts @@ -3,9 +3,10 @@ import { ICredentialTypes } from 'n8n-workflow'; import { join } from 'path'; import { access } from 'fs/promises'; import { Authorized, Get, RestController } from '@/decorators'; -import { BadRequestError, InternalServerError } from '@/ResponseHelper'; import { Config } from '@/config'; import { NODES_BASE_DIR } from '@/constants'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; +import { InternalServerError } from '@/errors/response-errors/internal-server.error'; export const CREDENTIAL_TRANSLATIONS_DIR = 'n8n-nodes-base/dist/credentials/translations'; export const NODE_HEADERS_PATH = join(NODES_BASE_DIR, 'dist/nodes/headers'); diff --git a/packages/cli/src/controllers/users.controller.ts b/packages/cli/src/controllers/users.controller.ts index 3e239c7adc54e..3aab71ce75aa7 100644 --- a/packages/cli/src/controllers/users.controller.ts +++ b/packages/cli/src/controllers/users.controller.ts @@ -4,7 +4,6 @@ import { User } from '@db/entities/User'; import { SharedCredentials } from '@db/entities/SharedCredentials'; import { SharedWorkflow } from '@db/entities/SharedWorkflow'; import { Authorized, Delete, Get, RestController, Patch } from '@/decorators'; -import { BadRequestError, NotFoundError, UnauthorizedError } from '@/ResponseHelper'; import { ListQuery, UserRequest, UserSettingsUpdatePayload } from '@/requests'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import { IExternalHooksClass, IInternalHooksClass } from '@/Interfaces'; @@ -17,6 +16,9 @@ import { RoleService } from '@/services/role.service'; import { UserService } from '@/services/user.service'; import { listQueryMiddleware } from '@/middlewares'; import { Logger } from '@/Logger'; +import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error'; +import { NotFoundError } from '@/errors/response-errors/not-found.error'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; @Authorized() @RestController('/users') diff --git a/packages/cli/src/controllers/workflowStatistics.controller.ts b/packages/cli/src/controllers/workflowStatistics.controller.ts index ef803742969d8..5492f9884cc86 100644 --- a/packages/cli/src/controllers/workflowStatistics.controller.ts +++ b/packages/cli/src/controllers/workflowStatistics.controller.ts @@ -7,9 +7,9 @@ import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.reposi import { WorkflowStatisticsRepository } from '@db/repositories/workflowStatistics.repository'; import { ExecutionRequest } from '@/requests'; import { whereClause } from '@/UserManagement/UserManagementHelper'; -import { NotFoundError } from '@/ResponseHelper'; import type { IWorkflowStatisticsDataLoaded } from '@/Interfaces'; import { Logger } from '@/Logger'; +import { NotFoundError } from '@/errors/response-errors/not-found.error'; interface WorkflowStatisticsData { productionSuccess: T; diff --git a/packages/cli/src/credentials/credentials.controller.ee.ts b/packages/cli/src/credentials/credentials.controller.ee.ts index 8ebfcb657b7c8..0bc21d869ad5a 100644 --- a/packages/cli/src/credentials/credentials.controller.ee.ts +++ b/packages/cli/src/credentials/credentials.controller.ee.ts @@ -11,6 +11,9 @@ import { OwnershipService } from '@/services/ownership.service'; import { Container } from 'typedi'; import { InternalHooks } from '@/InternalHooks'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; +import { NotFoundError } from '@/errors/response-errors/not-found.error'; +import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error'; export const EECredentialsController = express.Router(); @@ -40,7 +43,7 @@ EECredentialsController.get( )) as CredentialsEntity; if (!credential) { - throw new ResponseHelper.NotFoundError( + throw new NotFoundError( 'Could not load the credential. If you think this is an error, ask the owner to share it with you again', ); } @@ -48,7 +51,7 @@ EECredentialsController.get( const userSharing = credential.shared?.find((shared) => shared.user.id === req.user.id); if (!userSharing && req.user.globalRole.name !== 'owner') { - throw new ResponseHelper.UnauthorizedError('Forbidden.'); + throw new UnauthorizedError('Forbidden.'); } credential = Container.get(OwnershipService).addOwnedByAndSharedWith(credential); @@ -82,7 +85,7 @@ EECredentialsController.post( const sharing = await EECredentials.getSharing(req.user, credentialId); if (!ownsCredential) { if (!sharing) { - throw new ResponseHelper.UnauthorizedError('Forbidden'); + throw new UnauthorizedError('Forbidden'); } const decryptedData = EECredentials.decrypt(sharing.credentials); @@ -115,12 +118,12 @@ EECredentialsController.put( !Array.isArray(shareWithIds) || !shareWithIds.every((userId) => typeof userId === 'string') ) { - throw new ResponseHelper.BadRequestError('Bad request'); + throw new BadRequestError('Bad request'); } const { ownsCredential, credential } = await EECredentials.isOwned(req.user, credentialId); if (!ownsCredential || !credential) { - throw new ResponseHelper.UnauthorizedError('Forbidden'); + throw new UnauthorizedError('Forbidden'); } let amountRemoved: number | null = null; diff --git a/packages/cli/src/credentials/credentials.controller.ts b/packages/cli/src/credentials/credentials.controller.ts index 3eddddc3d9b14..11ba2c0f38dd3 100644 --- a/packages/cli/src/credentials/credentials.controller.ts +++ b/packages/cli/src/credentials/credentials.controller.ts @@ -14,6 +14,7 @@ import { Container } from 'typedi'; import { InternalHooks } from '@/InternalHooks'; import { listQueryMiddleware } from '@/middlewares'; import { Logger } from '@/Logger'; +import { NotFoundError } from '@/errors/response-errors/not-found.error'; export const credentialsController = express.Router(); credentialsController.use('/', EECredentialsController); @@ -60,9 +61,7 @@ credentialsController.get( const sharing = await CredentialsService.getSharing(req.user, credentialId, ['credentials']); if (!sharing) { - throw new ResponseHelper.NotFoundError( - `Credential with ID "${credentialId}" could not be found.`, - ); + throw new NotFoundError(`Credential with ID "${credentialId}" could not be found.`); } const { credentials: credential } = sharing; @@ -145,7 +144,7 @@ credentialsController.patch( userId: req.user.id, }, ); - throw new ResponseHelper.NotFoundError( + throw new NotFoundError( 'Credential to be updated not found. You can only update credentials owned by you', ); } @@ -165,9 +164,7 @@ credentialsController.patch( const responseData = await CredentialsService.update(credentialId, newCredentialData); if (responseData === null) { - throw new ResponseHelper.NotFoundError( - `Credential ID "${credentialId}" could not be found to be updated.`, - ); + throw new NotFoundError(`Credential ID "${credentialId}" could not be found to be updated.`); } // Remove the encrypted data as it is not needed in the frontend @@ -197,7 +194,7 @@ credentialsController.delete( userId: req.user.id, }, ); - throw new ResponseHelper.NotFoundError( + throw new NotFoundError( 'Credential to be deleted not found. You can only removed credentials owned by you', ); } diff --git a/packages/cli/src/environments/sourceControl/sourceControl.controller.ee.ts b/packages/cli/src/environments/sourceControl/sourceControl.controller.ee.ts index 63642c70fc0d1..1a808c3b30fdc 100644 --- a/packages/cli/src/environments/sourceControl/sourceControl.controller.ee.ts +++ b/packages/cli/src/environments/sourceControl/sourceControl.controller.ee.ts @@ -12,11 +12,11 @@ import { SourceControlPreferencesService } from './sourceControlPreferences.serv import type { SourceControlPreferences } from './types/sourceControlPreferences'; import type { SourceControlledFile } from './types/sourceControlledFile'; import { SOURCE_CONTROL_API_ROOT, SOURCE_CONTROL_DEFAULT_BRANCH } from './constants'; -import { BadRequestError } from '@/ResponseHelper'; import type { ImportResult } from './types/importResult'; import { InternalHooks } from '../../InternalHooks'; import { getRepoType } from './sourceControlHelper.ee'; import { SourceControlGetStatus } from './types/sourceControlGetStatus'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; @Service() @RestController(`/${SOURCE_CONTROL_API_ROOT}`) diff --git a/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts b/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts index d3c5c481558ca..990e90c0bde5e 100644 --- a/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts +++ b/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts @@ -17,7 +17,6 @@ import { import { SourceControlGitService } from './sourceControlGit.service.ee'; import type { PushResult } from 'simple-git'; import { SourceControlExportService } from './sourceControlExport.service.ee'; -import { BadRequestError } from '@/ResponseHelper'; import type { ImportResult } from './types/importResult'; import type { SourceControlPushWorkFolder } from './types/sourceControlPushWorkFolder'; import type { SourceControllPullOptions } from './types/sourceControlPullWorkFolder'; @@ -35,6 +34,7 @@ import type { ExportableCredential } from './types/exportableCredential'; import { InternalHooks } from '@/InternalHooks'; import { TagRepository } from '@db/repositories/tag.repository'; import { Logger } from '@/Logger'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; @Service() export class SourceControlService { diff --git a/packages/cli/src/environments/variables/variables.controller.ee.ts b/packages/cli/src/environments/variables/variables.controller.ee.ts index 9651b5173ed14..42e1b8153c3f2 100644 --- a/packages/cli/src/environments/variables/variables.controller.ee.ts +++ b/packages/cli/src/environments/variables/variables.controller.ee.ts @@ -1,14 +1,14 @@ import { Container, Service } from 'typedi'; -import * as ResponseHelper from '@/ResponseHelper'; import { VariablesRequest } from '@/requests'; import { Authorized, Delete, Get, Licensed, Patch, Post, RestController } from '@/decorators'; -import { - VariablesService, - VariablesLicenseError, - VariablesValidationError, -} from './variables.service.ee'; +import { VariablesService } from './variables.service.ee'; import { Logger } from '@/Logger'; +import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; +import { NotFoundError } from '@/errors/response-errors/not-found.error'; +import { VariableValidationError } from '@/errors/variable-validation.error'; +import { VariableCountLimitReachedError } from '@/errors/variable-count-limit-reached.error'; @Service() @Authorized() @@ -31,17 +31,17 @@ export class VariablesController { this.logger.info('Attempt to update a variable blocked due to lack of permissions', { userId: req.user.id, }); - throw new ResponseHelper.UnauthorizedError('Unauthorized'); + throw new UnauthorizedError('Unauthorized'); } const variable = req.body; delete variable.id; try { return await Container.get(VariablesService).create(variable); } catch (error) { - if (error instanceof VariablesLicenseError) { - throw new ResponseHelper.BadRequestError(error.message); - } else if (error instanceof VariablesValidationError) { - throw new ResponseHelper.BadRequestError(error.message); + if (error instanceof VariableCountLimitReachedError) { + throw new BadRequestError(error.message); + } else if (error instanceof VariableValidationError) { + throw new BadRequestError(error.message); } throw error; } @@ -52,7 +52,7 @@ export class VariablesController { const id = req.params.id; const variable = await Container.get(VariablesService).getCached(id); if (variable === null) { - throw new ResponseHelper.NotFoundError(`Variable with id ${req.params.id} not found`); + throw new NotFoundError(`Variable with id ${req.params.id} not found`); } return variable; } @@ -66,17 +66,17 @@ export class VariablesController { id, userId: req.user.id, }); - throw new ResponseHelper.UnauthorizedError('Unauthorized'); + throw new UnauthorizedError('Unauthorized'); } const variable = req.body; delete variable.id; try { return await Container.get(VariablesService).update(id, variable); } catch (error) { - if (error instanceof VariablesLicenseError) { - throw new ResponseHelper.BadRequestError(error.message); - } else if (error instanceof VariablesValidationError) { - throw new ResponseHelper.BadRequestError(error.message); + if (error instanceof VariableCountLimitReachedError) { + throw new BadRequestError(error.message); + } else if (error instanceof VariableValidationError) { + throw new BadRequestError(error.message); } throw error; } @@ -90,7 +90,7 @@ export class VariablesController { id, userId: req.user.id, }); - throw new ResponseHelper.UnauthorizedError('Unauthorized'); + throw new UnauthorizedError('Unauthorized'); } await this.variablesService.delete(id); diff --git a/packages/cli/src/environments/variables/variables.service.ee.ts b/packages/cli/src/environments/variables/variables.service.ee.ts index 8253603f99a6e..a962896de4631 100644 --- a/packages/cli/src/environments/variables/variables.service.ee.ts +++ b/packages/cli/src/environments/variables/variables.service.ee.ts @@ -6,9 +6,8 @@ import { canCreateNewVariable } from './enviromentHelpers'; import { CacheService } from '@/services/cache.service'; import { VariablesRepository } from '@db/repositories/variables.repository'; import type { DeepPartial } from 'typeorm'; - -export class VariablesLicenseError extends Error {} -export class VariablesValidationError extends Error {} +import { VariableCountLimitReachedError } from '@/errors/variable-count-limit-reached.error'; +import { VariableValidationError } from '@/errors/variable-validation.error'; @Service() export class VariablesService { @@ -59,19 +58,19 @@ export class VariablesService { validateVariable(variable: Omit): void { if (variable.key.length > 50) { - throw new VariablesValidationError('key cannot be longer than 50 characters'); + throw new VariableValidationError('key cannot be longer than 50 characters'); } if (variable.key.replace(/[A-Za-z0-9_]/g, '').length !== 0) { - throw new VariablesValidationError('key can only contain characters A-Za-z0-9_'); + throw new VariableValidationError('key can only contain characters A-Za-z0-9_'); } if (variable.value?.length > 255) { - throw new VariablesValidationError('value cannot be longer than 255 characters'); + throw new VariableValidationError('value cannot be longer than 255 characters'); } } async create(variable: Omit): Promise { if (!canCreateNewVariable(await this.getCount())) { - throw new VariablesLicenseError('Variables limit reached'); + throw new VariableCountLimitReachedError('Variables limit reached'); } this.validateVariable(variable); diff --git a/packages/cli/src/errors/credential-not-found.error.ts b/packages/cli/src/errors/credential-not-found.error.ts new file mode 100644 index 0000000000000..3826dc70bdcc4 --- /dev/null +++ b/packages/cli/src/errors/credential-not-found.error.ts @@ -0,0 +1,10 @@ +import { ApplicationError, type Severity } from 'n8n-workflow'; + +export class CredentialNotFoundError extends ApplicationError { + severity: Severity; + + constructor(credentialId: string, credentialType: string) { + super(`Credential with ID "${credentialId}" does not exist for type "${credentialType}".`); + this.severity = 'warning'; + } +} diff --git a/packages/cli/src/errors/external-secrets-provider-not-found.error.ts b/packages/cli/src/errors/external-secrets-provider-not-found.error.ts new file mode 100644 index 0000000000000..422236d14859f --- /dev/null +++ b/packages/cli/src/errors/external-secrets-provider-not-found.error.ts @@ -0,0 +1,7 @@ +import { ApplicationError } from 'n8n-workflow'; + +export class ExternalSecretsProviderNotFoundError extends ApplicationError { + constructor(public providerName: string) { + super(`External secrets provider not found: ${providerName}`); + } +} diff --git a/packages/cli/src/errors/feature-not-licensed.error.ts b/packages/cli/src/errors/feature-not-licensed.error.ts new file mode 100644 index 0000000000000..1419501941575 --- /dev/null +++ b/packages/cli/src/errors/feature-not-licensed.error.ts @@ -0,0 +1,10 @@ +import type { LICENSE_FEATURES } from '@/constants'; +import { ApplicationError } from 'n8n-workflow'; + +export class FeatureNotLicensedError extends ApplicationError { + constructor(feature: (typeof LICENSE_FEATURES)[keyof typeof LICENSE_FEATURES]) { + super( + `Your license does not allow for ${feature}. To enable ${feature}, please upgrade to a license that supports this feature.`, + ); + } +} diff --git a/packages/cli/src/errors/invalid-role.error.ts b/packages/cli/src/errors/invalid-role.error.ts new file mode 100644 index 0000000000000..fc5ebb8aa90f1 --- /dev/null +++ b/packages/cli/src/errors/invalid-role.error.ts @@ -0,0 +1,3 @@ +import { ApplicationError } from 'n8n-workflow'; + +export class InvalidRoleError extends ApplicationError {} diff --git a/packages/cli/src/errors/not-string-array.error.ts b/packages/cli/src/errors/not-string-array.error.ts new file mode 100644 index 0000000000000..22e9fae90282b --- /dev/null +++ b/packages/cli/src/errors/not-string-array.error.ts @@ -0,0 +1,7 @@ +import { ApplicationError } from 'n8n-workflow'; + +export class NotStringArrayError extends ApplicationError { + constructor(env: string) { + super(`${env} is not a string array.`); + } +} diff --git a/packages/cli/src/errors/response-errors/abstract/response.error.ts b/packages/cli/src/errors/response-errors/abstract/response.error.ts new file mode 100644 index 0000000000000..f756afce413dc --- /dev/null +++ b/packages/cli/src/errors/response-errors/abstract/response.error.ts @@ -0,0 +1,23 @@ +import { ApplicationError } from 'n8n-workflow'; + +/** + * Special Error which allows to return also an error code and http status code + */ +export abstract class ResponseError extends ApplicationError { + /** + * Creates an instance of ResponseError. + * Must be used inside a block with `ResponseHelper.send()`. + */ + constructor( + message: string, + // The HTTP status code of response + readonly httpStatusCode: number, + // The error code in the response + readonly errorCode: number = httpStatusCode, + // The error hint the response + readonly hint: string | undefined = undefined, + ) { + super(message); + this.name = 'ResponseError'; + } +} diff --git a/packages/cli/src/errors/response-errors/auth.error.ts b/packages/cli/src/errors/response-errors/auth.error.ts new file mode 100644 index 0000000000000..cb3441211ba78 --- /dev/null +++ b/packages/cli/src/errors/response-errors/auth.error.ts @@ -0,0 +1,7 @@ +import { ResponseError } from './abstract/response.error'; + +export class AuthError extends ResponseError { + constructor(message: string, errorCode?: number) { + super(message, 401, errorCode); + } +} diff --git a/packages/cli/src/errors/response-errors/bad-request.error.ts b/packages/cli/src/errors/response-errors/bad-request.error.ts new file mode 100644 index 0000000000000..560688f2cbe7f --- /dev/null +++ b/packages/cli/src/errors/response-errors/bad-request.error.ts @@ -0,0 +1,7 @@ +import { ResponseError } from './abstract/response.error'; + +export class BadRequestError extends ResponseError { + constructor(message: string, errorCode?: number) { + super(message, 400, errorCode); + } +} diff --git a/packages/cli/src/errors/response-errors/conflict.error.ts b/packages/cli/src/errors/response-errors/conflict.error.ts new file mode 100644 index 0000000000000..fdf33903717b0 --- /dev/null +++ b/packages/cli/src/errors/response-errors/conflict.error.ts @@ -0,0 +1,7 @@ +import { ResponseError } from './abstract/response.error'; + +export class ConflictError extends ResponseError { + constructor(message: string, hint: string | undefined = undefined) { + super(message, 409, 409, hint); + } +} diff --git a/packages/cli/src/errors/response-errors/internal-server.error.ts b/packages/cli/src/errors/response-errors/internal-server.error.ts new file mode 100644 index 0000000000000..4c10e93f95343 --- /dev/null +++ b/packages/cli/src/errors/response-errors/internal-server.error.ts @@ -0,0 +1,7 @@ +import { ResponseError } from './abstract/response.error'; + +export class InternalServerError extends ResponseError { + constructor(message: string, errorCode = 500) { + super(message, 500, errorCode); + } +} diff --git a/packages/cli/src/errors/response-errors/not-found.error.ts b/packages/cli/src/errors/response-errors/not-found.error.ts new file mode 100644 index 0000000000000..9d9e0e12d576a --- /dev/null +++ b/packages/cli/src/errors/response-errors/not-found.error.ts @@ -0,0 +1,7 @@ +import { ResponseError } from './abstract/response.error'; + +export class NotFoundError extends ResponseError { + constructor(message: string, hint: string | undefined = undefined) { + super(message, 404, 404, hint); + } +} diff --git a/packages/cli/src/errors/response-errors/service-unavailable.error.ts b/packages/cli/src/errors/response-errors/service-unavailable.error.ts new file mode 100644 index 0000000000000..7736e85f5f233 --- /dev/null +++ b/packages/cli/src/errors/response-errors/service-unavailable.error.ts @@ -0,0 +1,7 @@ +import { ResponseError } from './abstract/response.error'; + +export class ServiceUnavailableError extends ResponseError { + constructor(message: string, errorCode = 503) { + super(message, 503, errorCode); + } +} diff --git a/packages/cli/src/errors/response-errors/unauthorized.error.ts b/packages/cli/src/errors/response-errors/unauthorized.error.ts new file mode 100644 index 0000000000000..bc8993c014874 --- /dev/null +++ b/packages/cli/src/errors/response-errors/unauthorized.error.ts @@ -0,0 +1,7 @@ +import { ResponseError } from './abstract/response.error'; + +export class UnauthorizedError extends ResponseError { + constructor(message: string, hint: string | undefined = undefined) { + super(message, 403, 403, hint); + } +} diff --git a/packages/cli/src/errors/response-errors/unprocessable.error.ts b/packages/cli/src/errors/response-errors/unprocessable.error.ts new file mode 100644 index 0000000000000..723acbfbafa9a --- /dev/null +++ b/packages/cli/src/errors/response-errors/unprocessable.error.ts @@ -0,0 +1,7 @@ +import { ResponseError } from './abstract/response.error'; + +export class UnprocessableRequestError extends ResponseError { + constructor(message: string, hint: string | undefined = undefined) { + super(message, 422, 422, hint); + } +} diff --git a/packages/cli/src/errors/shared-workflow-not-found.error.ts b/packages/cli/src/errors/shared-workflow-not-found.error.ts new file mode 100644 index 0000000000000..ae4c99f3f302b --- /dev/null +++ b/packages/cli/src/errors/shared-workflow-not-found.error.ts @@ -0,0 +1,3 @@ +import { ApplicationError } from 'n8n-workflow'; + +export class SharedWorkflowNotFoundError extends ApplicationError {} diff --git a/packages/cli/src/errors/unrecognized-node-type.error.ts b/packages/cli/src/errors/unrecognized-node-type.error.ts new file mode 100644 index 0000000000000..1ca5281de5913 --- /dev/null +++ b/packages/cli/src/errors/unrecognized-node-type.error.ts @@ -0,0 +1,9 @@ +import { ApplicationError } from 'n8n-workflow'; + +export class UnrecognizedNodeTypeError extends ApplicationError { + severity = 'warning'; + + constructor(nodeType: string) { + super(`Unrecognized node type: ${nodeType}".`); + } +} diff --git a/packages/cli/src/errors/variable-count-limit-reached.error.ts b/packages/cli/src/errors/variable-count-limit-reached.error.ts new file mode 100644 index 0000000000000..67d14efebf605 --- /dev/null +++ b/packages/cli/src/errors/variable-count-limit-reached.error.ts @@ -0,0 +1,3 @@ +import { ApplicationError } from 'n8n-workflow'; + +export class VariableCountLimitReachedError extends ApplicationError {} diff --git a/packages/cli/src/errors/variable-validation.error.ts b/packages/cli/src/errors/variable-validation.error.ts new file mode 100644 index 0000000000000..bd2026e7ef8b7 --- /dev/null +++ b/packages/cli/src/errors/variable-validation.error.ts @@ -0,0 +1,3 @@ +import { ApplicationError } from 'n8n-workflow'; + +export class VariableValidationError extends ApplicationError {} diff --git a/packages/cli/src/errors/workflow-history-version-not-found.error.ts b/packages/cli/src/errors/workflow-history-version-not-found.error.ts new file mode 100644 index 0000000000000..104385672fcc5 --- /dev/null +++ b/packages/cli/src/errors/workflow-history-version-not-found.error.ts @@ -0,0 +1,3 @@ +import { ApplicationError } from 'n8n-workflow'; + +export class WorkflowHistoryVersionNotFoundError extends ApplicationError {} diff --git a/packages/cli/src/eventbus/eventBus.controller.ee.ts b/packages/cli/src/eventbus/eventBus.controller.ee.ts index 4815e8545ddb0..a7d16e1b6519e 100644 --- a/packages/cli/src/eventbus/eventBus.controller.ee.ts +++ b/packages/cli/src/eventbus/eventBus.controller.ee.ts @@ -9,7 +9,6 @@ import { MessageEventBusDestinationSyslog, } from './MessageEventBusDestination/MessageEventBusDestinationSyslog.ee'; import { MessageEventBusDestinationWebhook } from './MessageEventBusDestination/MessageEventBusDestinationWebhook.ee'; -import { BadRequestError } from '@/ResponseHelper'; import type { MessageEventBusDestinationWebhookOptions, MessageEventBusDestinationOptions, @@ -20,6 +19,7 @@ import type { MessageEventBusDestination } from './MessageEventBusDestination/Me import type { DeleteResult } from 'typeorm'; import { AuthenticatedRequest } from '@/requests'; import { logStreamingLicensedMiddleware } from './middleware/logStreamingEnabled.middleware.ee'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; // ---------------------------------------- // TypeGuards diff --git a/packages/cli/src/eventbus/eventBus.controller.ts b/packages/cli/src/eventbus/eventBus.controller.ts index d17fdf884ac11..6fecc8bf69535 100644 --- a/packages/cli/src/eventbus/eventBus.controller.ts +++ b/packages/cli/src/eventbus/eventBus.controller.ts @@ -9,13 +9,13 @@ import type { EventMessageTypes, FailedEventSummary } from './EventMessageClasse import { eventNamesAll } from './EventMessageClasses'; import type { EventMessageAuditOptions } from './EventMessageClasses/EventMessageAudit'; import { EventMessageAudit } from './EventMessageClasses/EventMessageAudit'; -import { BadRequestError } from '@/ResponseHelper'; import type { IRunExecutionData } from 'n8n-workflow'; import { EventMessageTypeNames } from 'n8n-workflow'; import type { EventMessageNodeOptions } from './EventMessageClasses/EventMessageNode'; import { EventMessageNode } from './EventMessageClasses/EventMessageNode'; import { recoverExecutionDataFromEventLogMessages } from './MessageEventBus/recoverEvents'; import { RestController, Get, Post, Authorized } from '@/decorators'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; // ---------------------------------------- // TypeGuards diff --git a/packages/cli/src/executions/executions.service.ts b/packages/cli/src/executions/executions.service.ts index 04479b0539825..006481b46565d 100644 --- a/packages/cli/src/executions/executions.service.ts +++ b/packages/cli/src/executions/executions.service.ts @@ -15,7 +15,6 @@ import type { import { NodeTypes } from '@/NodeTypes'; import { Queue } from '@/Queue'; import type { ExecutionRequest } from '@/requests'; -import * as ResponseHelper from '@/ResponseHelper'; import { getSharedWorkflowIds } from '@/WorkflowHelpers'; import { WorkflowRunner } from '@/WorkflowRunner'; import * as GenericHelpers from '@/GenericHelpers'; @@ -24,6 +23,8 @@ import { getStatusUsingPreviousExecutionStatusMethod } from './executionHelpers' import { ExecutionRepository } from '@db/repositories/execution.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { Logger } from '@/Logger'; +import { InternalServerError } from '@/errors/response-errors/internal-server.error'; +import { NotFoundError } from '@/errors/response-errors/not-found.error'; export interface IGetExecutionsQueryFilter { id?: FindOperator | string; @@ -114,9 +115,7 @@ export class ExecutionsService { userId: req.user.id, filter: req.query.filter, }); - throw new ResponseHelper.InternalServerError( - 'Parameter "filter" contained invalid JSON string.', - ); + throw new InternalServerError('Parameter "filter" contained invalid JSON string.'); } } @@ -231,9 +230,7 @@ export class ExecutionsService { executionId, }, ); - throw new ResponseHelper.NotFoundError( - `The execution with the ID "${executionId}" does not exist.`, - ); + throw new NotFoundError(`The execution with the ID "${executionId}" does not exist.`); } if (execution.finished) { @@ -351,9 +348,7 @@ export class ExecutionsService { requestFilters = requestFiltersRaw as IGetExecutionsQueryFilter; } } catch (error) { - throw new ResponseHelper.InternalServerError( - 'Parameter "filter" contained invalid JSON string.', - ); + throw new InternalServerError('Parameter "filter" contained invalid JSON string.'); } } diff --git a/packages/cli/src/license/license.controller.ts b/packages/cli/src/license/license.controller.ts index 0aa27b74bb558..b907def729d17 100644 --- a/packages/cli/src/license/license.controller.ts +++ b/packages/cli/src/license/license.controller.ts @@ -8,6 +8,8 @@ import { LicenseService } from './License.service'; import { License } from '@/License'; import type { AuthenticatedRequest, LicenseRequest } from '@/requests'; import { InternalHooks } from '@/InternalHooks'; +import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; export const licenseController = express.Router(); @@ -24,9 +26,7 @@ licenseController.use((req: AuthenticatedRequest, res, next) => { }); ResponseHelper.sendErrorResponse( res, - new ResponseHelper.UnauthorizedError( - 'Only an instance owner may activate or renew a license', - ), + new UnauthorizedError('Only an instance owner may activate or renew a license'), ); return; } @@ -85,7 +85,7 @@ licenseController.post( Container.get(Logger).error(message, { stack: error.stack ?? 'n/a' }); } - throw new ResponseHelper.BadRequestError(message); + throw new BadRequestError(message); } // Return the read data, plus the management JWT @@ -113,7 +113,7 @@ licenseController.post( // not awaiting so as not to make the endpoint hang void Container.get(InternalHooks).onLicenseRenewAttempt({ success: false }); if (error instanceof Error) { - throw new ResponseHelper.BadRequestError(error.message); + throw new BadRequestError(error.message); } } diff --git a/packages/cli/src/middlewares/bodyParser.ts b/packages/cli/src/middlewares/bodyParser.ts index 2f2efe709e134..f7ee9615b427b 100644 --- a/packages/cli/src/middlewares/bodyParser.ts +++ b/packages/cli/src/middlewares/bodyParser.ts @@ -7,7 +7,7 @@ import { Parser as XmlParser } from 'xml2js'; import { parseIncomingMessage } from 'n8n-core'; import { jsonParse } from 'n8n-workflow'; import config from '@/config'; -import { UnprocessableRequestError } from '@/ResponseHelper'; +import { UnprocessableRequestError } from '@/errors/response-errors/unprocessable.error'; const xmlParser = new XmlParser({ async: true, diff --git a/packages/cli/src/services/role.service.ts b/packages/cli/src/services/role.service.ts index 8be8350dccdcb..e10523223593e 100644 --- a/packages/cli/src/services/role.service.ts +++ b/packages/cli/src/services/role.service.ts @@ -3,8 +3,7 @@ import { RoleRepository } from '@db/repositories/role.repository'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { CacheService } from './cache.service'; import type { RoleNames, RoleScopes } from '@db/entities/Role'; - -class InvalidRoleError extends Error {} +import { InvalidRoleError } from '@/errors/invalid-role.error'; @Service() export class RoleService { diff --git a/packages/cli/src/services/user.service.ts b/packages/cli/src/services/user.service.ts index 3b3a649731b67..5f6e39625c86f 100644 --- a/packages/cli/src/services/user.service.ts +++ b/packages/cli/src/services/user.service.ts @@ -15,8 +15,8 @@ import { UserManagementMailer } from '@/UserManagement/email'; import { InternalHooks } from '@/InternalHooks'; import { RoleService } from '@/services/role.service'; import { ErrorReporterProxy as ErrorReporter } from 'n8n-workflow'; -import { InternalServerError } from '@/ResponseHelper'; import type { UserRequest } from '@/requests'; +import { InternalServerError } from '@/errors/response-errors/internal-server.error'; @Service() export class UserService { diff --git a/packages/cli/src/sso/saml/routes/saml.controller.ee.ts b/packages/cli/src/sso/saml/routes/saml.controller.ee.ts index 67d441a37c7a1..aa5f98dd67af2 100644 --- a/packages/cli/src/sso/saml/routes/saml.controller.ee.ts +++ b/packages/cli/src/sso/saml/routes/saml.controller.ee.ts @@ -9,7 +9,6 @@ import { } from '../middleware/samlEnabledMiddleware'; import { SamlService } from '../saml.service.ee'; import { SamlConfiguration } from '../types/requests'; -import { AuthError, BadRequestError } from '@/ResponseHelper'; import { getInitSSOFormView } from '../views/initSsoPost'; import { issueCookie } from '@/auth/jwt'; import { validate } from 'class-validator'; @@ -27,6 +26,8 @@ import { getSamlConnectionTestFailedView } from '../views/samlConnectionTestFail import { InternalHooks } from '@/InternalHooks'; import url from 'url'; import querystring from 'querystring'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; +import { AuthError } from '@/errors/response-errors/auth.error'; @Service() @RestController('/sso/saml') diff --git a/packages/cli/src/sso/saml/saml.service.ee.ts b/packages/cli/src/sso/saml/saml.service.ee.ts index 354686cbb3b2c..fc170ff2ca7ba 100644 --- a/packages/cli/src/sso/saml/saml.service.ee.ts +++ b/packages/cli/src/sso/saml/saml.service.ee.ts @@ -2,7 +2,6 @@ import type express from 'express'; import Container, { Service } from 'typedi'; import type { User } from '@db/entities/User'; import { jsonParse } from 'n8n-workflow'; -import { AuthError, BadRequestError } from '@/ResponseHelper'; import { getServiceProviderInstance } from './serviceProvider.ee'; import type { SamlUserAttributes } from './types/samlUserAttributes'; import { isSsoJustInTimeProvisioningEnabled } from '../ssoHelpers'; @@ -29,6 +28,8 @@ import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper'; import { Logger } from '@/Logger'; import { UserRepository } from '@db/repositories/user.repository'; import { SettingsRepository } from '@db/repositories/settings.repository'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; +import { AuthError } from '@/errors/response-errors/auth.error'; @Service() export class SamlService { diff --git a/packages/cli/src/sso/saml/samlHelpers.ts b/packages/cli/src/sso/saml/samlHelpers.ts index d55b751be73bf..5b3f28c25eb69 100644 --- a/packages/cli/src/sso/saml/samlHelpers.ts +++ b/packages/cli/src/sso/saml/samlHelpers.ts @@ -3,7 +3,6 @@ import config from '@/config'; import { AuthIdentity } from '@db/entities/AuthIdentity'; import { User } from '@db/entities/User'; import { License } from '@/License'; -import { AuthError, InternalServerError } from '@/ResponseHelper'; import { hashPassword } from '@/UserManagement/UserManagementHelper'; import type { SamlPreferences } from './types/samlPreferences'; import type { SamlUserAttributes } from './types/samlUserAttributes'; @@ -21,6 +20,9 @@ import type { SamlConfiguration } from './types/requests'; import { RoleService } from '@/services/role.service'; import { UserRepository } from '@db/repositories/user.repository'; import { AuthIdentityRepository } from '@db/repositories/authIdentity.repository'; +import { InternalServerError } from '@/errors/response-errors/internal-server.error'; +import { AuthError } from '@/errors/response-errors/auth.error'; + /** * Check whether the SAML feature is licensed and enabled in the instance */ diff --git a/packages/cli/src/workflows/workflowHistory/workflowHistory.controller.ee.ts b/packages/cli/src/workflows/workflowHistory/workflowHistory.controller.ee.ts index 89e78a4d15d4c..c5f7159a144bb 100644 --- a/packages/cli/src/workflows/workflowHistory/workflowHistory.controller.ee.ts +++ b/packages/cli/src/workflows/workflowHistory/workflowHistory.controller.ee.ts @@ -1,15 +1,14 @@ import { Authorized, RestController, Get, Middleware } from '@/decorators'; import { WorkflowHistoryRequest } from '@/requests'; import { Service } from 'typedi'; -import { - HistoryVersionNotFoundError, - SharedWorkflowNotFoundError, - WorkflowHistoryService, -} from './workflowHistory.service.ee'; +import { WorkflowHistoryService } from './workflowHistory.service.ee'; import { Request, Response, NextFunction } from 'express'; import { isWorkflowHistoryEnabled, isWorkflowHistoryLicensed } from './workflowHistoryHelper.ee'; -import { NotFoundError } from '@/ResponseHelper'; + import { paginationListQueryMiddleware } from '@/middlewares/listQuery/pagination'; +import { NotFoundError } from '@/errors/response-errors/not-found.error'; +import { SharedWorkflowNotFoundError } from '@/errors/shared-workflow-not-found.error'; +import { WorkflowHistoryVersionNotFoundError } from '@/errors/workflow-history-version-not-found.error'; const DEFAULT_TAKE = 20; @@ -67,7 +66,7 @@ export class WorkflowHistoryController { } catch (e) { if (e instanceof SharedWorkflowNotFoundError) { throw new NotFoundError('Could not find workflow'); - } else if (e instanceof HistoryVersionNotFoundError) { + } else if (e instanceof WorkflowHistoryVersionNotFoundError) { throw new NotFoundError('Could not find version'); } throw e; diff --git a/packages/cli/src/workflows/workflowHistory/workflowHistory.service.ee.ts b/packages/cli/src/workflows/workflowHistory/workflowHistory.service.ee.ts index 89b06987484e3..84cb05f9a6d5e 100644 --- a/packages/cli/src/workflows/workflowHistory/workflowHistory.service.ee.ts +++ b/packages/cli/src/workflows/workflowHistory/workflowHistory.service.ee.ts @@ -7,9 +7,8 @@ import { WorkflowHistoryRepository } from '@db/repositories/workflowHistory.repo import { Service } from 'typedi'; import { isWorkflowHistoryEnabled } from './workflowHistoryHelper.ee'; import { Logger } from '@/Logger'; - -export class SharedWorkflowNotFoundError extends Error {} -export class HistoryVersionNotFoundError extends Error {} +import { SharedWorkflowNotFoundError } from '@/errors/shared-workflow-not-found.error'; +import { WorkflowHistoryVersionNotFoundError } from '@/errors/workflow-history-version-not-found.error'; @Service() export class WorkflowHistoryService { @@ -36,7 +35,7 @@ export class WorkflowHistoryService { ): Promise>> { const sharedWorkflow = await this.getSharedWorkflow(user, workflowId); if (!sharedWorkflow) { - throw new SharedWorkflowNotFoundError(); + throw new SharedWorkflowNotFoundError(''); } return this.workflowHistoryRepository.find({ where: { @@ -52,7 +51,7 @@ export class WorkflowHistoryService { async getVersion(user: User, workflowId: string, versionId: string): Promise { const sharedWorkflow = await this.getSharedWorkflow(user, workflowId); if (!sharedWorkflow) { - throw new SharedWorkflowNotFoundError(); + throw new SharedWorkflowNotFoundError(''); } const hist = await this.workflowHistoryRepository.findOne({ where: { @@ -61,7 +60,7 @@ export class WorkflowHistoryService { }, }); if (!hist) { - throw new HistoryVersionNotFoundError(); + throw new WorkflowHistoryVersionNotFoundError(''); } return hist; } diff --git a/packages/cli/src/workflows/workflows.controller.ee.ts b/packages/cli/src/workflows/workflows.controller.ee.ts index a5c60ac043fa2..6fd345060fda6 100644 --- a/packages/cli/src/workflows/workflows.controller.ee.ts +++ b/packages/cli/src/workflows/workflows.controller.ee.ts @@ -23,6 +23,10 @@ import { listQueryMiddleware } from '@/middlewares'; import { TagService } from '@/services/tag.service'; import { Logger } from '@/Logger'; import { WorkflowHistoryService } from './workflowHistory/workflowHistory.service.ee'; +import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; +import { NotFoundError } from '@/errors/response-errors/not-found.error'; +import { InternalServerError } from '@/errors/response-errors/internal-server.error'; export const EEWorkflowController = express.Router(); @@ -52,13 +56,13 @@ EEWorkflowController.put( !Array.isArray(shareWithIds) || !shareWithIds.every((userId) => typeof userId === 'string') ) { - throw new ResponseHelper.BadRequestError('Bad request'); + throw new BadRequestError('Bad request'); } const { ownsWorkflow, workflow } = await EEWorkflows.isOwned(req.user, workflowId); if (!ownsWorkflow || !workflow) { - throw new ResponseHelper.UnauthorizedError('Forbidden'); + throw new UnauthorizedError('Forbidden'); } let newShareeIds: string[] = []; @@ -101,13 +105,13 @@ EEWorkflowController.get( const workflow = await EEWorkflows.get({ id: workflowId }, { relations }); if (!workflow) { - throw new ResponseHelper.NotFoundError(`Workflow with ID "${workflowId}" does not exist`); + throw new NotFoundError(`Workflow with ID "${workflowId}" does not exist`); } const userSharing = workflow.shared?.find((shared) => shared.user.id === req.user.id); if (!userSharing && req.user.globalRole.name !== 'owner') { - throw new ResponseHelper.UnauthorizedError( + throw new UnauthorizedError( 'You do not have permission to access this workflow. Ask the owner to share it with you', ); } @@ -156,7 +160,7 @@ EEWorkflowController.post( try { EEWorkflows.validateCredentialPermissionsToUser(newWorkflow, allCredentials); } catch (error) { - throw new ResponseHelper.BadRequestError( + throw new BadRequestError( 'The workflow you are trying to save contains credentials that are not shared with you', ); } @@ -181,7 +185,7 @@ EEWorkflowController.post( if (!savedWorkflow) { Container.get(Logger).error('Failed to create workflow', { userId: req.user.id }); - throw new ResponseHelper.InternalServerError( + throw new InternalServerError( 'An error occurred while saving your workflow. Please try again.', ); } diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index fc187b9b8497b..588a1040d328b 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -27,6 +27,9 @@ import { TagService } from '@/services/tag.service'; import { WorkflowHistoryService } from './workflowHistory/workflowHistory.service.ee'; import { Logger } from '@/Logger'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; +import { NotFoundError } from '@/errors/response-errors/not-found.error'; +import { InternalServerError } from '@/errors/response-errors/internal-server.error'; export const workflowsController = express.Router(); workflowsController.use('/', EEWorkflowController); @@ -84,7 +87,7 @@ workflowsController.post( if (!savedWorkflow) { Container.get(Logger).error('Failed to create workflow', { userId: req.user.id }); - throw new ResponseHelper.InternalServerError('Failed to save workflow'); + throw new InternalServerError('Failed to save workflow'); } await Container.get(WorkflowHistoryService).saveVersion( @@ -160,10 +163,10 @@ workflowsController.get( '/from-url', ResponseHelper.send(async (req: express.Request): Promise => { if (req.query.url === undefined) { - throw new ResponseHelper.BadRequestError('The parameter "url" is missing!'); + throw new BadRequestError('The parameter "url" is missing!'); } if (!/^http[s]?:\/\/.*\.json$/i.exec(req.query.url as string)) { - throw new ResponseHelper.BadRequestError( + throw new BadRequestError( 'The parameter "url" is not valid! It does not seem to be a URL pointing to a n8n workflow JSON file.', ); } @@ -172,7 +175,7 @@ workflowsController.get( const { data } = await axios.get(req.query.url as string); workflowData = data; } catch (error) { - throw new ResponseHelper.BadRequestError('The URL does not point to valid JSON file!'); + throw new BadRequestError('The URL does not point to valid JSON file!'); } // Do a very basic check if it is really a n8n-workflow-json @@ -183,7 +186,7 @@ workflowsController.get( typeof workflowData.connections !== 'object' || Array.isArray(workflowData.connections) ) { - throw new ResponseHelper.BadRequestError( + throw new BadRequestError( 'The data in the file does not seem to be a n8n workflow JSON file!', ); } @@ -221,7 +224,7 @@ workflowsController.get( workflowId, userId: req.user.id, }); - throw new ResponseHelper.NotFoundError( + throw new NotFoundError( 'Could not load the workflow - you can only access workflows owned by you', ); } @@ -271,7 +274,7 @@ workflowsController.delete( workflowId, userId: req.user.id, }); - throw new ResponseHelper.BadRequestError( + throw new BadRequestError( 'Could not delete the workflow - you can only remove workflows owned by you', ); } diff --git a/packages/cli/src/workflows/workflows.services.ee.ts b/packages/cli/src/workflows/workflows.services.ee.ts index 04448e76cbae8..f865e2f382a4e 100644 --- a/packages/cli/src/workflows/workflows.services.ee.ts +++ b/packages/cli/src/workflows/workflows.services.ee.ts @@ -1,6 +1,5 @@ import type { DeleteResult, EntityManager } from 'typeorm'; import { In, Not } from 'typeorm'; -import * as ResponseHelper from '@/ResponseHelper'; import * as WorkflowHelpers from '@/WorkflowHelpers'; import { SharedWorkflow } from '@db/entities/SharedWorkflow'; import type { User } from '@db/entities/User'; @@ -17,6 +16,8 @@ import { RoleService } from '@/services/role.service'; import Container from 'typedi'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; +import { NotFoundError } from '@/errors/response-errors/not-found.error'; export class EEWorkflowsService extends WorkflowsService { static async isOwned( @@ -170,7 +171,7 @@ export class EEWorkflowsService extends WorkflowsService { const previousVersion = await EEWorkflowsService.get({ id: workflowId }); if (!previousVersion) { - throw new ResponseHelper.NotFoundError('Workflow not found'); + throw new NotFoundError('Workflow not found'); } const allCredentials = await CredentialsService.getMany(user); @@ -183,9 +184,9 @@ export class EEWorkflowsService extends WorkflowsService { ); } catch (error) { if (error instanceof NodeOperationError) { - throw new ResponseHelper.BadRequestError(error.message); + throw new BadRequestError(error.message); } - throw new ResponseHelper.BadRequestError( + throw new BadRequestError( 'Invalid workflow credentials - make sure you have access to all credentials and try again.', ); } diff --git a/packages/cli/src/workflows/workflows.services.ts b/packages/cli/src/workflows/workflows.services.ts index 162ad2aaf5c7a..40ad9cede90d2 100644 --- a/packages/cli/src/workflows/workflows.services.ts +++ b/packages/cli/src/workflows/workflows.services.ts @@ -6,7 +6,6 @@ import { In, Like } from 'typeorm'; import pick from 'lodash/pick'; import { v4 as uuid } from 'uuid'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; -import * as ResponseHelper from '@/ResponseHelper'; import * as WorkflowHelpers from '@/WorkflowHelpers'; import config from '@/config'; import type { SharedWorkflow } from '@db/entities/SharedWorkflow'; @@ -34,6 +33,8 @@ import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee' import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { WorkflowTagMappingRepository } from '@db/repositories/workflowTagMapping.repository'; import { ExecutionRepository } from '@db/repositories/execution.repository'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; +import { NotFoundError } from '@/errors/response-errors/not-found.error'; export class WorkflowsService { static async getSharing( @@ -208,7 +209,7 @@ export class WorkflowsService { workflowId, userId: user.id, }); - throw new ResponseHelper.NotFoundError( + throw new NotFoundError( 'You do not have permission to update this workflow. Ask the owner to share it with you.', ); } @@ -220,7 +221,7 @@ export class WorkflowsService { workflow.versionId !== '' && workflow.versionId !== shared.workflow.versionId ) { - throw new ResponseHelper.BadRequestError( + throw new BadRequestError( 'Your most recent changes may be lost, because someone else just updated this workflow. Open this workflow in a new tab to see those new updates.', 100, ); @@ -330,7 +331,7 @@ export class WorkflowsService { }); if (updatedWorkflow === null) { - throw new ResponseHelper.BadRequestError( + throw new BadRequestError( `Workflow with ID "${workflowId}" could not be found to be updated.`, ); } @@ -368,7 +369,7 @@ export class WorkflowsService { message = message ?? (error as Error).message; // Now return the original error for UI to display - throw new ResponseHelper.BadRequestError(message); + throw new BadRequestError(message); } } diff --git a/packages/cli/test/unit/controllers/me.controller.test.ts b/packages/cli/test/unit/controllers/me.controller.test.ts index 1ea28820ff863..c27f071102e3a 100644 --- a/packages/cli/test/unit/controllers/me.controller.test.ts +++ b/packages/cli/test/unit/controllers/me.controller.test.ts @@ -6,7 +6,6 @@ import type { PublicUser } from '@/Interfaces'; import type { User } from '@db/entities/User'; import { MeController } from '@/controllers/me.controller'; import { AUTH_COOKIE_NAME } from '@/constants'; -import { BadRequestError } from '@/ResponseHelper'; import type { AuthenticatedRequest, MeRequest } from '@/requests'; import { UserService } from '@/services/user.service'; import { ExternalHooks } from '@/ExternalHooks'; @@ -14,6 +13,7 @@ import { InternalHooks } from '@/InternalHooks'; import { License } from '@/License'; import { badPasswords } from '../shared/testData'; import { mockInstance } from '../../shared/mocking'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; describe('MeController', () => { const externalHooks = mockInstance(ExternalHooks); diff --git a/packages/cli/test/unit/controllers/oAuth1Credential.controller.test.ts b/packages/cli/test/unit/controllers/oAuth1Credential.controller.test.ts index fb278f1616e5c..0ea169261cb4d 100644 --- a/packages/cli/test/unit/controllers/oAuth1Credential.controller.test.ts +++ b/packages/cli/test/unit/controllers/oAuth1Credential.controller.test.ts @@ -7,7 +7,6 @@ import { OAuth1CredentialController } from '@/controllers/oauth/oAuth1Credential import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import type { User } from '@db/entities/User'; import type { OAuthRequest } from '@/requests'; -import { BadRequestError, NotFoundError } from '@/ResponseHelper'; import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; import { ExternalHooks } from '@/ExternalHooks'; @@ -17,6 +16,8 @@ import { SecretsHelper } from '@/SecretsHelpers'; import { CredentialsHelper } from '@/CredentialsHelper'; import { mockInstance } from '../../shared/mocking'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; +import { NotFoundError } from '@/errors/response-errors/not-found.error'; describe('OAuth1CredentialController', () => { mockInstance(Logger); diff --git a/packages/cli/test/unit/controllers/oAuth2Credential.controller.test.ts b/packages/cli/test/unit/controllers/oAuth2Credential.controller.test.ts index 72c9d8bfe2158..3498f6a532c8a 100644 --- a/packages/cli/test/unit/controllers/oAuth2Credential.controller.test.ts +++ b/packages/cli/test/unit/controllers/oAuth2Credential.controller.test.ts @@ -9,7 +9,6 @@ import { OAuth2CredentialController } from '@/controllers/oauth/oAuth2Credential import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import type { User } from '@db/entities/User'; import type { OAuthRequest } from '@/requests'; -import { BadRequestError, NotFoundError } from '@/ResponseHelper'; import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; import { ExternalHooks } from '@/ExternalHooks'; @@ -19,6 +18,8 @@ import { SecretsHelper } from '@/SecretsHelpers'; import { CredentialsHelper } from '@/CredentialsHelper'; import { mockInstance } from '../../shared/mocking'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; +import { NotFoundError } from '@/errors/response-errors/not-found.error'; describe('OAuth2CredentialController', () => { mockInstance(Logger); diff --git a/packages/cli/test/unit/controllers/owner.controller.test.ts b/packages/cli/test/unit/controllers/owner.controller.test.ts index 099f3bf47a005..1f45e54bb7066 100644 --- a/packages/cli/test/unit/controllers/owner.controller.test.ts +++ b/packages/cli/test/unit/controllers/owner.controller.test.ts @@ -5,7 +5,6 @@ import type { IInternalHooksClass } from '@/Interfaces'; import type { User } from '@db/entities/User'; import type { SettingsRepository } from '@db/repositories/settings.repository'; import type { Config } from '@/config'; -import { BadRequestError } from '@/ResponseHelper'; import type { OwnerRequest } from '@/requests'; import { OwnerController } from '@/controllers/owner.controller'; import { AUTH_COOKIE_NAME } from '@/constants'; @@ -14,6 +13,7 @@ import { License } from '@/License'; import { mockInstance } from '../../shared/mocking'; import { badPasswords } from '../shared/testData'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; describe('OwnerController', () => { const config = mock(); diff --git a/packages/cli/test/unit/controllers/translation.controller.test.ts b/packages/cli/test/unit/controllers/translation.controller.test.ts index e23f196d0d8d6..3abaf7f933069 100644 --- a/packages/cli/test/unit/controllers/translation.controller.test.ts +++ b/packages/cli/test/unit/controllers/translation.controller.test.ts @@ -6,7 +6,7 @@ import { TranslationController, CREDENTIAL_TRANSLATIONS_DIR, } from '@/controllers/translation.controller'; -import { BadRequestError } from '@/ResponseHelper'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; describe('TranslationController', () => { const config = mock(); From 3ab3ec9da88f7b7ae07a98d7ef7c4f9892079048 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Tue, 28 Nov 2023 11:44:55 +0100 Subject: [PATCH 05/17] fix(editor): Allow owners and admins to share workflows and credentials they don't own (#7833) --- packages/@n8n/permissions/src/types.ts | 2 +- packages/cli/src/permissions/roles.ts | 1 + .../src/components/CredentialCard.vue | 2 +- .../CredentialEdit/CredentialSharing.ee.vue | 35 +++++++------------ .../src/components/WorkflowShareModal.ee.vue | 5 +-- packages/editor-ui/src/permissions.ts | 9 +++-- .../src/plugins/i18n/locales/en.json | 4 +-- packages/editor-ui/src/stores/users.store.ts | 3 +- 8 files changed, 29 insertions(+), 32 deletions(-) diff --git a/packages/@n8n/permissions/src/types.ts b/packages/@n8n/permissions/src/types.ts index edb4e4c099517..0a14440922d80 100644 --- a/packages/@n8n/permissions/src/types.ts +++ b/packages/@n8n/permissions/src/types.ts @@ -18,7 +18,7 @@ export type WildcardScope = `${Resource}:*` | '*'; export type WorkflowScope = ResourceScope<'workflow', DefaultOperations | 'share'>; export type TagScope = ResourceScope<'tag'>; export type UserScope = ResourceScope<'user'>; -export type CredentialScope = ResourceScope<'credential'>; +export type CredentialScope = ResourceScope<'credential', DefaultOperations | 'share'>; export type VariableScope = ResourceScope<'variable'>; export type SourceControlScope = ResourceScope<'sourceControl', 'pull' | 'push' | 'manage'>; export type ExternalSecretStoreScope = ResourceScope< diff --git a/packages/cli/src/permissions/roles.ts b/packages/cli/src/permissions/roles.ts index 95cabb03ca3f1..131406aee854d 100644 --- a/packages/cli/src/permissions/roles.ts +++ b/packages/cli/src/permissions/roles.ts @@ -17,6 +17,7 @@ export const ownerPermissions: Scope[] = [ 'credential:update', 'credential:delete', 'credential:list', + 'credential:share', 'variable:create', 'variable:read', 'variable:update', diff --git a/packages/editor-ui/src/components/CredentialCard.vue b/packages/editor-ui/src/components/CredentialCard.vue index 80ba4b5498f9a..a6671b82c4499 100644 --- a/packages/editor-ui/src/components/CredentialCard.vue +++ b/packages/editor-ui/src/components/CredentialCard.vue @@ -142,7 +142,7 @@ export default defineComponent({ }, async onAction(action: string) { if (action === CREDENTIAL_LIST_ITEM_ACTIONS.OPEN) { - await this.onClick(); + await this.onClick(new Event('click')); } else if (action === CREDENTIAL_LIST_ITEM_ACTIONS.DELETE) { const deleteConfirmed = await this.confirm( this.$locale.baseText( diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue index d0988a16ee657..062358cab1b32 100644 --- a/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue @@ -31,28 +31,18 @@ />
- - - + + {{ $locale.baseText('credentialEdit.credentialSharing.info.owner') }} - - {{ $locale.baseText('credentialEdit.credentialSharing.info.instanceOwner') }} + + {{ + $locale.baseText('credentialEdit.credentialSharing.info.sharee', { + interpolate: { credentialOwnerName }, + }) + }} + + + {{ $locale.baseText('credentialEdit.credentialSharing.info.reader') }} sharee.id === user.id, ); + const isOwner = this.credentialData.ownedBy.id === user.id; - return !isCurrentUser && !isAlreadySharedWithUser; + return !isCurrentUser && !isAlreadySharedWithUser && !isOwner; }); }, sharedWithList(): IUser[] { diff --git a/packages/editor-ui/src/components/WorkflowShareModal.ee.vue b/packages/editor-ui/src/components/WorkflowShareModal.ee.vue index dfb2b4d844d33..bd9ebc0f08f8d 100644 --- a/packages/editor-ui/src/components/WorkflowShareModal.ee.vue +++ b/packages/editor-ui/src/components/WorkflowShareModal.ee.vue @@ -23,7 +23,7 @@
- + {{ $locale.baseText('workflows.shareModal.info.sharee', { interpolate: { workflowOwnerName }, @@ -213,8 +213,9 @@ export default defineComponent({ const isAlreadySharedWithUser = (this.sharedWith || []).find( (sharee) => sharee.id === user.id, ); + const isOwner = this.workflow?.ownedBy?.id === user.id; - return !isCurrentUser && !isAlreadySharedWithUser; + return !isCurrentUser && !isAlreadySharedWithUser && !isOwner; }); }, sharedWithList(): Array> { diff --git a/packages/editor-ui/src/permissions.ts b/packages/editor-ui/src/permissions.ts index 47b1dee9d3700..995e9ad6f0769 100644 --- a/packages/editor-ui/src/permissions.ts +++ b/packages/editor-ui/src/permissions.ts @@ -64,6 +64,7 @@ export const parsePermissionsTable = ( export const getCredentialPermissions = (user: IUser | null, credential: ICredentialsResponse) => { const settingsStore = useSettingsStore(); + const rbacStore = useRBACStore(); const isSharingEnabled = settingsStore.isEnterpriseFeatureEnabled( EnterpriseEditionFeature.Sharing, ); @@ -77,10 +78,14 @@ export const getCredentialPermissions = (user: IUser | null, credential: ICreden name: UserRole.ResourceSharee, test: () => !!credential?.sharedWith?.find((sharee) => sharee.id === user?.id), }, + { name: 'read', test: () => rbacStore.hasScope('credential:read') }, { name: 'save', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] }, { name: 'updateName', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] }, { name: 'updateConnection', test: [UserRole.ResourceOwner] }, - { name: 'updateSharing', test: [UserRole.ResourceOwner] }, + { + name: 'updateSharing', + test: (permissions) => rbacStore.hasScope('credential:share') || !!permissions.isOwner, + }, { name: 'updateNodeAccess', test: [UserRole.ResourceOwner] }, { name: 'delete', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] }, { name: 'use', test: [UserRole.ResourceOwner, UserRole.ResourceSharee] }, @@ -104,7 +109,7 @@ export const getWorkflowPermissions = (user: IUser | null, workflow: IWorkflowDb }, { name: 'updateSharing', - test: (permissions) => rbacStore.hasScope('workflow:update') || !!permissions.isOwner, + test: (permissions) => rbacStore.hasScope('workflow:share') || !!permissions.isOwner, }, { name: 'delete', diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index b3dde531fc815..4635c040eef41 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -416,7 +416,7 @@ "credentialEdit.oAuthButton.connectMyAccount": "Connect my account", "credentialEdit.oAuthButton.signInWithGoogle": "Sign in with Google", "credentialEdit.credentialSharing.info.owner": "Sharing a credential allows people to use it in their workflows. They cannot access credential details.", - "credentialEdit.credentialSharing.info.instanceOwner": "You can view this credential because you are the instance owner (and rename or delete it too). To use it in a workflow, ask the credential owner to share it with you.", + "credentialEdit.credentialSharing.info.reader": "You can view this credential because you have permission to read and share (and rename or delete it too). To use it in a workflow, ask the credential owner to share it with you.", "credentialEdit.credentialSharing.info.sharee": "Only {credentialOwnerName} can change who this credential is shared with", "credentialEdit.credentialSharing.info.sharee.fallback": "the owner", "credentialEdit.credentialSharing.select.placeholder": "Add users...", @@ -1998,7 +1998,7 @@ "workflows.shareModal.changesHint": "You made changes", "workflows.shareModal.isDefaultUser.description": "You first need to set up your owner account to enable workflow sharing features.", "workflows.shareModal.isDefaultUser.button": "Go to settings", - "workflows.shareModal.info.sharee": "Only {workflowOwnerName} can change who this workflow is shared with", + "workflows.shareModal.info.sharee": "Only {workflowOwnerName} or users with workflow sharing permission can change who this workflow is shared with", "workflows.shareModal.info.sharee.fallback": "the owner", "workflows.roles.editor": "Editor", "workflows.concurrentChanges.confirmMessage.title": "Workflow was changed by someone else", diff --git a/packages/editor-ui/src/stores/users.store.ts b/packages/editor-ui/src/stores/users.store.ts index 0b2e1e787aac1..a05c67379ae0e 100644 --- a/packages/editor-ui/src/stores/users.store.ts +++ b/packages/editor-ui/src/stores/users.store.ts @@ -43,10 +43,9 @@ import { useRBACStore } from '@/stores/rbac.store'; import type { Scope, ScopeLevel } from '@n8n/permissions'; import { inviteUsers, acceptInvitation } from '@/api/invitation'; -const isDefaultUser = (user: IUserResponse | null) => - user?.isPending && user?.globalRole?.name === ROLE.Owner; const isPendingUser = (user: IUserResponse | null) => !!user?.isPending; const isInstanceOwner = (user: IUserResponse | null) => user?.globalRole?.name === ROLE.Owner; +const isDefaultUser = (user: IUserResponse | null) => isInstanceOwner(user) && isPendingUser(user); export const useUsersStore = defineStore(STORES.USERS, { state: (): IUsersState => ({ From 753cbc1e96b2758d32d7277527caa336be7fd6c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Tue, 28 Nov 2023 12:15:08 +0100 Subject: [PATCH 06/17] refactor(editor): Delete some barrel files and reduce circular dependencies (no-changelog) (#7838) --- .../src/components/N8nDatatable/Datatable.vue | 2 +- .../N8nKeyboardShortcut.vue | 2 +- .../N8nNodeCreatorNode/NodeCreatorNode.vue | 2 +- .../design-system/src/composables/index.ts | 2 - packages/design-system/src/main.ts | 2 +- packages/design-system/vite.config.mts | 1 + packages/editor-ui/src/App.vue | 26 ++++---- packages/editor-ui/src/Interface.ts | 2 +- .../editor-ui/src/__tests__/router.test.ts | 2 +- packages/editor-ui/src/api/cloudPlans.ts | 2 +- packages/editor-ui/src/api/communityNodes.ts | 2 +- packages/editor-ui/src/api/environments.ee.ts | 2 +- packages/editor-ui/src/api/eventbus.ee.ts | 2 +- .../editor-ui/src/api/externalSecrets.ee.ts | 2 +- packages/editor-ui/src/api/orchestration.ts | 2 +- packages/editor-ui/src/api/sourceControl.ts | 4 +- packages/editor-ui/src/api/sso.ts | 2 +- packages/editor-ui/src/api/tags.ts | 2 +- packages/editor-ui/src/api/workflowHistory.ts | 2 +- .../src/components/ActivationModal.vue | 2 +- .../src/components/BinaryDataDisplayEmbed.vue | 2 +- .../src/components/BreakpointsObserver.vue | 4 +- .../src/components/ChangePasswordModal.vue | 2 +- .../src/components/ChatEmbedModal.vue | 4 +- .../components/CodeNodeEditor/AskAI/AskAI.vue | 13 +++- .../CodeNodeEditor/CodeNodeEditor.vue | 5 +- .../completions/secrets.completions.ts | 2 +- .../completions/variables.completions.ts | 2 +- .../CommunityPackageInstallModal.vue | 2 +- .../CommunityPackageManageConfirmModal.vue | 2 +- .../src/components/ContactPromptModal.vue | 2 +- .../components/ContextMenu/ContextMenu.vue | 2 +- .../editor-ui/src/components/CopyInput.vue | 2 +- .../src/components/CredentialCard.vue | 2 +- .../CredentialEdit/AuthTypeSelector.vue | 2 +- .../CredentialEdit/CredentialConfig.vue | 2 +- .../CredentialEdit/CredentialEdit.vue | 11 ++-- .../CredentialEdit/CredentialSharing.ee.vue | 2 +- .../CredentialEdit/GoogleAuthButton.vue | 2 +- .../CredentialPicker/CredentialPicker.vue | 2 +- .../CredentialPicker/CredentialsDropdown.vue | 2 +- .../src/components/DebugPaywallModal.vue | 2 +- .../src/components/DeleteUserModal.vue | 2 +- .../components/DuplicateWorkflowDialog.vue | 4 +- .../src/components/Error/NodeErrorView.vue | 4 +- .../src/components/ExecutionFilter.vue | 4 +- .../src/components/ExecutionsList.vue | 8 ++- .../src/components/ExecutionsUsage.vue | 2 +- .../ExecutionsView/ExecutionPreview.vue | 3 +- .../ExecutionsView/ExecutionsList.vue | 6 +- .../__tests__/ExecutionPreview.test.ts | 3 +- .../ExternalSecretsProviderCard.ee.vue | 7 ++- ...rnalSecretsProviderConnectionSwitch.ee.vue | 6 +- .../ExternalSecretsProviderModal.ee.vue | 7 ++- .../src/components/ImportCurlModal.vue | 2 +- .../InlineExpressionEditorOutput.vue | 2 +- .../src/components/InlineNameEdit.vue | 2 +- .../src/components/InviteUsersModal.vue | 4 +- packages/editor-ui/src/components/Logo.vue | 3 +- .../src/components/MainHeader/MainHeader.vue | 4 +- .../components/MainHeader/WorkflowDetails.vue | 22 +++---- .../editor-ui/src/components/MainSidebar.vue | 22 +++---- .../components/MainSidebarSourceControl.vue | 5 +- .../src/components/MfaSetupModal.vue | 2 +- packages/editor-ui/src/components/Node.vue | 4 +- .../Node/NodeCreator/ItemTypes/NodeItem.vue | 5 +- .../NodeCreator/ItemTypes/SubcategoryItem.vue | 2 +- .../Node/NodeCreator/Modes/ActionsMode.vue | 2 +- .../Node/NodeCreator/Modes/NodesMode.vue | 3 +- .../Node/NodeCreator/NodeCreator.vue | 3 +- .../Node/NodeCreator/Panel/NodesListPanel.vue | 2 +- .../Node/NodeCreator/Panel/SearchBar.vue | 2 +- .../NodeCreator/composables/useActions.ts | 2 +- .../NodeCreator/composables/useViewStacks.ts | 4 +- .../src/components/Node/NodeCreator/utils.ts | 2 +- .../components/Node/NodeCreator/viewsData.ts | 4 +- .../src/components/NodeCredentials.vue | 4 +- .../src/components/NodeDetailsView.vue | 4 +- .../src/components/NodeExecuteButton.vue | 3 +- .../editor-ui/src/components/NodeList.vue | 2 +- .../editor-ui/src/components/NodeSettings.vue | 2 +- .../src/components/NodeSettingsTabs.vue | 2 +- .../editor-ui/src/components/NodeWebhooks.vue | 2 +- .../components/OnboardingCallSignupModal.vue | 2 +- .../editor-ui/src/components/PageAlert.vue | 4 +- .../src/components/ParameterInput.vue | 5 +- .../src/components/ParameterInputExpanded.vue | 2 +- .../src/components/ParameterInputFull.vue | 11 ++-- .../src/components/ParameterInputHint.vue | 2 +- .../src/components/ParameterInputList.vue | 6 +- .../src/components/ParameterInputWrapper.vue | 5 +- .../src/components/ParameterOptions.vue | 3 +- .../src/components/PersonalizationModal.vue | 6 +- .../ResourceLocator/ResourceLocator.vue | 8 +-- .../ResourceMapper/MappingFields.vue | 10 ++- .../ResourceMapper/MappingModeSelect.vue | 2 +- .../ResourceMapper/MatchingColumnsSelect.vue | 2 +- .../ResourceMapper/ResourceMapper.vue | 3 +- packages/editor-ui/src/components/RunData.vue | 6 +- .../RunDataAi/AiRunContentBlock.vue | 4 +- .../src/components/RunDataAi/RunDataAi.vue | 3 +- .../components/RunDataAi/RunDataAiContent.vue | 3 +- .../editor-ui/src/components/RunDataJson.vue | 5 +- .../src/components/RunDataJsonActions.vue | 6 +- .../src/components/RunDataSchema.vue | 5 +- .../src/components/RunDataSchemaItem.vue | 4 +- .../src/components/RunDataSearch.vue | 2 +- .../editor-ui/src/components/RunDataTable.vue | 4 +- .../editor-ui/src/components/SSOLogin.vue | 3 +- .../EventDestinationCard.ee.vue | 2 +- .../EventDestinationSettingsModal.ee.vue | 2 +- .../editor-ui/src/components/ShortenName.vue | 2 +- .../components/SourceControlPullModal.ee.vue | 6 +- .../components/SourceControlPushModal.ee.vue | 6 +- packages/editor-ui/src/components/Sticky.vue | 4 +- .../editor-ui/src/components/TagsDropdown.vue | 3 +- .../components/TagsManager/TagsManager.vue | 2 +- .../editor-ui/src/components/TemplateCard.vue | 3 +- .../src/components/TemplateDetails.vue | 4 +- packages/editor-ui/src/components/TimeAgo.vue | 2 +- .../editor-ui/src/components/TriggerPanel.vue | 2 +- .../editor-ui/src/components/ValueSurvey.vue | 2 +- .../editor-ui/src/components/VariablesRow.vue | 4 +- .../src/components/WorkerList.ee.vue | 9 +-- .../Workers/WorkerChartsAccordion.ee.vue | 2 +- .../Workers/WorkerNetAccordion.ee.vue | 4 +- .../src/components/WorkflowActivator.vue | 4 +- .../editor-ui/src/components/WorkflowCard.vue | 3 +- .../WorkflowHistoryContent.vue | 2 +- .../WorkflowHistory/WorkflowHistoryList.vue | 2 +- .../WorkflowHistoryListItem.vue | 2 +- .../WorkflowHistoryVersionRestoreModal.vue | 4 +- .../src/components/WorkflowLMChat.vue | 5 +- .../src/components/WorkflowPreview.vue | 3 +- .../src/components/WorkflowSettings.vue | 2 +- .../src/components/WorkflowShareModal.ee.vue | 3 +- .../__tests__/RunDataSearch.test.ts | 3 +- .../src/components/banners/BannerStack.vue | 2 +- .../banners/EmailConfirmationBanner.vue | 2 +- .../layouts/ResourcesListLayout.vue | 2 +- .../__tests__/useContextMenu.test.ts | 3 +- .../__tests__/useDataSchema.test.ts | 2 +- packages/editor-ui/src/composables/index.ts | 17 ----- .../src/composables/useCanvasMouseSelect.ts | 2 +- .../src/composables/useContextMenu.ts | 5 +- .../src/composables/useDataSchema.ts | 2 +- .../src/composables/useExecutionDebugging.ts | 8 ++- .../src/composables/useExternalHooks.ts | 2 +- .../src/composables/useHistoryHelper.ts | 4 +- .../src/composables/useTitleChange.ts | 2 +- .../editor-ui/src/composables/useToast.ts | 2 +- packages/editor-ui/src/hooks/index.ts | 4 -- .../mixins/__tests__/workflowHelpers.spec.ts | 3 +- .../editor-ui/src/mixins/externalHooks.ts | 2 +- .../editor-ui/src/mixins/genericHelpers.ts | 4 +- packages/editor-ui/src/mixins/newVersions.ts | 2 +- packages/editor-ui/src/mixins/nodeHelpers.ts | 4 +- packages/editor-ui/src/mixins/pinData.ts | 4 +- .../editor-ui/src/mixins/pushConnection.ts | 5 +- .../editor-ui/src/mixins/workflowActivate.ts | 2 +- .../editor-ui/src/mixins/workflowHelpers.ts | 7 ++- packages/editor-ui/src/mixins/workflowRun.ts | 2 +- .../completions/datatype.completions.ts | 5 +- .../completions/dollar.completions.ts | 2 +- packages/editor-ui/src/plugins/i18n/index.ts | 14 ++--- .../editor-ui/src/plugins/telemetry/index.ts | 9 +-- .../src/rbac/checks/__tests__/hasRole.test.ts | 2 +- packages/editor-ui/src/rbac/checks/hasRole.ts | 2 +- .../src/rbac/checks/isAuthenticated.ts | 2 +- .../rbac/checks/isEnterpriseFeatureEnabled.ts | 2 +- packages/editor-ui/src/rbac/checks/isGuest.ts | 2 +- .../rbac/middleware/__tests__/role.test.ts | 2 +- packages/editor-ui/src/router.ts | 11 ++-- .../src/stores/__tests__/environments.spec.ts | 2 +- .../editor-ui/src/stores/__tests__/ui.test.ts | 3 +- .../src/stores/__tests__/workflows.spec.ts | 4 +- .../src/stores/__tests__/workflows.test.ts | 2 +- .../editor-ui/src/stores/auditLogs.store.ts | 2 +- packages/editor-ui/src/stores/canvas.store.ts | 16 ++--- .../editor-ui/src/stores/credentials.store.ts | 2 +- packages/editor-ui/src/stores/index.ts | 31 ---------- .../editor-ui/src/stores/nodeTypes.store.ts | 2 +- .../editor-ui/src/stores/settings.store.ts | 9 ++- packages/editor-ui/src/stores/ui.store.ts | 5 +- packages/editor-ui/src/stores/users.store.ts | 2 +- .../editor-ui/src/stores/workflows.store.ts | 13 ++-- packages/editor-ui/src/types/externalHooks.ts | 4 +- .../src/utils/__tests__/htmlUtils.test.ts | 2 +- .../src/utils/__tests__/objectUtils.test.ts | 2 +- .../__tests__/sourceControlUtils.test.ts | 2 +- .../src/utils/__tests__/typesUtils.test.ts | 2 +- packages/editor-ui/src/utils/canvasUtils.ts | 21 +------ packages/editor-ui/src/utils/index.ts | 13 ---- .../editor-ui/src/utils/nodeTypesUtils.ts | 3 +- packages/editor-ui/src/utils/nodeViewUtils.ts | 23 ++++++- .../editor-ui/src/utils/pairedItemUtils.ts | 4 +- .../editor-ui/src/utils/telemetryUtils.ts | 2 +- .../src/views/ChangePasswordView.vue | 2 +- .../editor-ui/src/views/CredentialsView.vue | 2 +- .../src/views/ForgotMyPasswordView.vue | 2 +- packages/editor-ui/src/views/NodeView.vue | 62 +++++++++---------- .../editor-ui/src/views/SettingsApiView.vue | 3 +- .../editor-ui/src/views/SettingsAuditLogs.vue | 5 +- .../src/views/SettingsCommunityNodesView.vue | 2 +- .../src/views/SettingsExternalSecrets.vue | 6 +- .../editor-ui/src/views/SettingsLdapView.vue | 8 ++- .../src/views/SettingsLogStreamingView.vue | 14 ++--- .../src/views/SettingsPersonalView.vue | 3 +- .../src/views/SettingsSourceControl.vue | 8 ++- packages/editor-ui/src/views/SettingsSso.vue | 4 +- .../src/views/SettingsUsageAndPlan.vue | 6 +- .../editor-ui/src/views/SettingsUsersView.vue | 4 +- packages/editor-ui/src/views/SetupView.vue | 2 +- .../AppsRequiringCredsNotice.vue | 2 +- .../SetupWorkflowFromTemplateView.vue | 4 +- .../setupTemplate.store.ts | 12 ++-- packages/editor-ui/src/views/SigninView.vue | 5 +- packages/editor-ui/src/views/SignoutView.vue | 2 +- packages/editor-ui/src/views/SignupView.vue | 2 +- .../src/views/TemplatesCollectionView.vue | 2 +- .../src/views/TemplatesSearchView.vue | 4 +- .../src/views/TemplatesWorkflowView.vue | 2 +- .../editor-ui/src/views/VariablesView.vue | 17 ++--- .../editor-ui/src/views/WorkflowHistory.vue | 3 +- .../src/views/WorkflowOnboardingView.vue | 6 +- .../editor-ui/src/views/WorkflowsView.vue | 2 +- .../views/__tests__/SettingsAuditLogs.test.ts | 3 +- .../__tests__/SettingsSourceControl.test.ts | 3 +- .../src/views/__tests__/VariablesView.spec.ts | 4 +- 229 files changed, 539 insertions(+), 507 deletions(-) delete mode 100644 packages/design-system/src/composables/index.ts delete mode 100644 packages/editor-ui/src/composables/index.ts delete mode 100644 packages/editor-ui/src/hooks/index.ts delete mode 100644 packages/editor-ui/src/stores/index.ts delete mode 100644 packages/editor-ui/src/utils/index.ts diff --git a/packages/design-system/src/components/N8nDatatable/Datatable.vue b/packages/design-system/src/components/N8nDatatable/Datatable.vue index b776cc759e9af..14d1b216555b0 100644 --- a/packages/design-system/src/components/N8nDatatable/Datatable.vue +++ b/packages/design-system/src/components/N8nDatatable/Datatable.vue @@ -3,7 +3,7 @@ import type { PropType } from 'vue'; import { computed, defineComponent, ref, useCssModule } from 'vue'; import type { DatatableColumn, DatatableRow, DatatableRowDataType } from '../../types'; import { getValueByPath } from '../../utils'; -import { useI18n } from '../../composables'; +import { useI18n } from '../../composables/useI18n'; import N8nSelect from '../N8nSelect'; import N8nOption from '../N8nOption'; import N8nPagination from '../N8nPagination'; diff --git a/packages/design-system/src/components/N8nKeyboardShortcut/N8nKeyboardShortcut.vue b/packages/design-system/src/components/N8nKeyboardShortcut/N8nKeyboardShortcut.vue index 333299b1fbd50..8dc809755a1bf 100644 --- a/packages/design-system/src/components/N8nKeyboardShortcut/N8nKeyboardShortcut.vue +++ b/packages/design-system/src/components/N8nKeyboardShortcut/N8nKeyboardShortcut.vue @@ -1,6 +1,6 @@ + + diff --git a/packages/editor-ui/src/components/Node.vue b/packages/editor-ui/src/components/Node.vue index 63a6c88eb61f3..e1ff7b8c52f3b 100644 --- a/packages/editor-ui/src/components/Node.vue +++ b/packages/editor-ui/src/components/Node.vue @@ -169,6 +169,7 @@ import type { INodeOutputConfiguration, INodeTypeDescription, ITaskData, + NodeOperationError, } from 'n8n-workflow'; import { NodeConnectionType, NodeHelpers } from 'n8n-workflow'; @@ -392,7 +393,7 @@ export default defineComponent({ nodeExecutionStatus(): string { const nodeExecutionRunData = this.workflowsStore.getWorkflowRunData?.[this.name]; if (nodeExecutionRunData) { - return nodeExecutionRunData[0].executionStatus ?? ''; + return nodeExecutionRunData.filter(Boolean)[0].executionStatus ?? ''; } return ''; }, @@ -401,7 +402,7 @@ export default defineComponent({ const nodeExecutionRunData = this.workflowsStore.getWorkflowRunData?.[this.name]; if (nodeExecutionRunData) { nodeExecutionRunData.forEach((executionRunData) => { - if (executionRunData.error) { + if (executionRunData?.error) { issues.push( `${executionRunData.error.message}${ executionRunData.error.description ? ` (${executionRunData.error.description})` : '' @@ -426,7 +427,10 @@ export default defineComponent({ return this.node ? this.node.position : [0, 0]; }, showDisabledLinethrough(): boolean { - return !!(this.data.disabled && this.inputs.length === 1 && this.outputs.length === 1); + return ( + !this.isConfigurableNode && + !!(this.data.disabled && this.inputs.length === 1 && this.outputs.length === 1) + ); }, shortNodeType(): string { return this.$locale.shortNodeType(this.data.type); @@ -482,9 +486,15 @@ export default defineComponent({ borderColor = '--color-foreground-base'; } else if (!this.isExecuting) { if (this.hasIssues) { - borderColor = '--color-danger'; - returnStyles['border-width'] = '2px'; - returnStyles['border-style'] = 'solid'; + // Do not set red border if there is an issue with the configuration node + if ( + (this.nodeRunData?.[0]?.error as NodeOperationError)?.functionality !== + 'configuration-node' + ) { + borderColor = '--color-danger'; + returnStyles['border-width'] = '2px'; + returnStyles['border-style'] = 'solid'; + } } else if (this.waiting || this.showPinnedDataInfo) { borderColor = '--color-canvas-node-pinned-border'; } else if (this.nodeExecutionStatus === 'unknown') { @@ -608,6 +618,7 @@ export default defineComponent({ !this.isTriggerNode || this.isManualTypeNode || this.isScheduledGroup || + this.uiStore.isModalActive || dataItemsCount === 0 ) return; @@ -1333,6 +1344,10 @@ export default defineComponent({ z-index: 10; } + &.add-input-endpoint-error { + --endpoint-svg-color: var(--color-danger); + } + .add-input-endpoint-default { transition: transform var(--add-input-endpoint--transition-duration) ease; } diff --git a/packages/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue b/packages/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue index d485c3a214a62..35b92b93e0047 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue @@ -162,7 +162,7 @@ function onBackButton() { v-if="activeViewStack.info && !activeViewStack.search" :class="$style.info" :content="activeViewStack.info" - theme="info" + theme="warning" /> diff --git a/packages/editor-ui/src/components/Node/NodeCreator/composables/useViewStacks.ts b/packages/editor-ui/src/components/Node/NodeCreator/composables/useViewStacks.ts index bbd30c25ac1fe..62576cd1649ea 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/composables/useViewStacks.ts +++ b/packages/editor-ui/src/components/Node/NodeCreator/composables/useViewStacks.ts @@ -8,6 +8,7 @@ import type { SimplifiedNodeType, } from '@/Interface'; import { + AI_CODE_NODE_TYPE, AI_OTHERS_NODE_CREATOR_VIEW, DEFAULT_SUBCATEGORY, TRIGGER_NODE_CREATOR_VIEW, @@ -152,6 +153,9 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => { }, panelClass: relatedAIView?.properties.panelClass, baseFilter: (i: INodeCreateElement) => { + // AI Code node could have any connection type so we don't want to display it + // in the compatible connection view as it would be displayed in all of them + if (i.key === AI_CODE_NODE_TYPE) return false; const displayNode = nodesByConnectionType[connectionType].includes(i.key); // TODO: Filtering works currently fine for displaying compatible node when dropping diff --git a/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts b/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts index 8b55a869e6552..466ae9da1940a 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts +++ b/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts @@ -317,18 +317,6 @@ export function TriggerView(nodes: SimplifiedNodeType[]) { ], }; - const hasAINodes = (nodes ?? []).some((node) => node.codex?.categories?.includes(AI_SUBCATEGORY)); - if (hasAINodes) - view.items.push({ - key: AI_NODE_CREATOR_VIEW, - type: 'view', - properties: { - title: i18n.baseText('nodeCreator.aiPanel.langchainAiNodes'), - icon: 'robot', - description: i18n.baseText('nodeCreator.aiPanel.nodesForAi'), - }, - }); - return view; } diff --git a/packages/editor-ui/src/components/NodeDetailsView.vue b/packages/editor-ui/src/components/NodeDetailsView.vue index edfe58d41c27a..404cdd4d4a40b 100644 --- a/packages/editor-ui/src/components/NodeDetailsView.vue +++ b/packages/editor-ui/src/components/NodeDetailsView.vue @@ -8,6 +8,7 @@ width="auto" append-to-body data-test-id="ndv" + :data-has-output-connection="hasOutputConnection" > 0; + }, parentNodes(): string[] { if (this.activeNode) { return ( @@ -634,6 +646,9 @@ export default defineComponent({ nodeTypeSelected(nodeTypeName: string) { this.$emit('nodeTypeSelected', nodeTypeName); }, + async onSwitchSelectedNode(nodeTypeName: string) { + this.$emit('switchSelectedNode', nodeTypeName); + }, async close() { if (this.isDragging) { return; @@ -739,6 +754,10 @@ export default defineComponent({ diff --git a/packages/editor-ui/src/components/RunDataAi/RunDataAi.vue b/packages/editor-ui/src/components/RunDataAi/RunDataAi.vue index 3ee11d306762a..79014ba0dab19 100644 --- a/packages/editor-ui/src/components/RunDataAi/RunDataAi.vue +++ b/packages/editor-ui/src/components/RunDataAi/RunDataAi.vue @@ -8,6 +8,7 @@ :indent="12" @node-click="onItemClick" :expand-on-click-node="false" + data-test-id="lm-chat-logs-tree" >