refactor(core): Use Dependency Injection for all Controller classes (no-changelog) (#8146)

## Review / Merge checklist
- [x] PR title and summary are descriptive
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2023-12-27 11:50:43 +01:00
committed by GitHub
parent 518a99e528
commit f69ddcd796
51 changed files with 209 additions and 522 deletions
+1 -2
View File
@@ -10,7 +10,6 @@ import { N8N_VERSION, inDevelopment, inTest } from '@/constants';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import * as Db from '@/Db';
import { N8nInstanceType } from '@/Interfaces';
import type { IExternalHooksClass } from '@/Interfaces';
import { ExternalHooks } from '@/ExternalHooks';
import { send, sendErrorResponse } from '@/ResponseHelper';
import { rawBodyReader, bodyParser, corsMiddleware } from '@/middlewares';
@@ -31,7 +30,7 @@ export abstract class AbstractServer {
readonly app: express.Application;
protected externalHooks: IExternalHooksClass;
protected externalHooks: ExternalHooks;
protected activeWorkflowRunner: ActiveWorkflowRunner;
+2 -6
View File
@@ -1,10 +1,6 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { Service } from 'typedi';
import type {
IExternalHooksClass,
IExternalHooksFileData,
IExternalHooksFunctions,
} from '@/Interfaces';
import type { IExternalHooksFileData, IExternalHooksFunctions } from '@/Interfaces';
import config from '@/config';
import { UserRepository } from '@db/repositories/user.repository';
import { CredentialsRepository } from '@db/repositories/credentials.repository';
@@ -13,7 +9,7 @@ import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { ApplicationError } from 'n8n-workflow';
@Service()
export class ExternalHooks implements IExternalHooksClass {
export class ExternalHooks {
externalHooks: {
[key: string]: Array<() => {}>;
} = {};
@@ -1,12 +1,10 @@
import { Authorized, Get, Post, RestController, RequireGlobalScope } from '@/decorators';
import { ExternalSecretsRequest } from '@/requests';
import { Response } from 'express';
import { Service } from 'typedi';
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()
@RestController('/external-secrets')
export class ExternalSecretsController {
+2 -134
View File
@@ -44,6 +44,7 @@ import type { CredentialsRepository } from '@db/repositories/credentials.reposit
import type { SettingsRepository } from '@db/repositories/settings.repository';
import type { UserRepository } from '@db/repositories/user.repository';
import type { WorkflowRepository } from '@db/repositories/workflow.repository';
import type { ExternalHooks } from './ExternalHooks';
import type { LICENSE_FEATURES, LICENSE_QUOTAS } from './constants';
import type { WorkflowWithSharingsAndCredentials } from './workflows/workflows.types';
import type { WorkerJobStatusSummary } from './services/orchestration/worker/types';
@@ -254,11 +255,6 @@ export interface IExternalHooksFunctions {
};
}
export interface IExternalHooksClass {
init(): Promise<void>;
run(hookName: string, hookParameters?: any[]): Promise<void>;
}
export type WebhookCORSRequest = Request & { method: 'OPTIONS' };
export type WebhookRequest = Request<{ path: string }> & {
@@ -326,134 +322,6 @@ export interface ITelemetryUserDeletionData {
migration_user_id?: string;
}
export interface IInternalHooksClass {
onN8nStop(): Promise<void>;
onServerStarted(
diagnosticInfo: IDiagnosticInfo,
firstWorkflowCreatedAt?: Date,
): Promise<unknown[]>;
onPersonalizationSurveySubmitted(userId: string, answers: Record<string, string>): Promise<void>;
onWorkflowCreated(user: User, workflow: IWorkflowBase, publicApi: boolean): Promise<void>;
onWorkflowDeleted(user: User, workflowId: string, publicApi: boolean): Promise<void>;
onWorkflowSaved(user: User, workflow: IWorkflowBase, publicApi: boolean): Promise<void>;
onWorkflowBeforeExecute(executionId: string, data: IWorkflowExecutionDataProcess): Promise<void>;
onWorkflowPostExecute(
executionId: string,
workflow: IWorkflowBase,
runData?: IRun,
userId?: string,
): Promise<void>;
onNodeBeforeExecute(
executionId: string,
workflow: IWorkflowBase,
nodeName: string,
): Promise<void>;
onNodePostExecute(executionId: string, workflow: IWorkflowBase, nodeName: string): Promise<void>;
onUserDeletion(userDeletionData: {
user: User;
telemetryData: ITelemetryUserDeletionData;
publicApi: boolean;
}): Promise<void>;
onUserInvite(userInviteData: {
user: User;
target_user_id: string[];
public_api: boolean;
email_sent: boolean;
invitee_role: string;
}): Promise<void>;
onUserRoleChange(userInviteData: {
user: User;
target_user_id: string;
public_api: boolean;
target_user_new_role: string;
}): Promise<void>;
onUserReinvite(userReinviteData: {
user: User;
target_user_id: string;
public_api: boolean;
}): Promise<void>;
onUserUpdate(userUpdateData: { user: User; fields_changed: string[] }): Promise<void>;
onUserInviteEmailClick(userInviteClickData: { inviter: User; invitee: User }): Promise<void>;
onUserPasswordResetEmailClick(userPasswordResetData: { user: User }): Promise<void>;
onUserTransactionalEmail(
userTransactionalEmailData: {
user_id: string;
message_type: 'Reset password' | 'New user invite' | 'Resend invite';
public_api: boolean;
},
user?: User,
): Promise<void>;
onEmailFailed(failedEmailData: {
user: User;
message_type: 'Reset password' | 'New user invite' | 'Resend invite';
public_api: boolean;
}): Promise<void>;
onUserCreatedCredentials(userCreatedCredentialsData: {
user: User;
credential_name: string;
credential_type: string;
credential_id: string;
public_api: boolean;
}): Promise<void>;
onUserSharedCredentials(userSharedCredentialsData: {
user: User;
credential_name: string;
credential_type: string;
credential_id: string;
user_id_sharer: string;
user_ids_sharees_added: string[];
sharees_removed: number | null;
}): Promise<void>;
onUserPasswordResetRequestClick(userPasswordResetData: { user: User }): Promise<void>;
onInstanceOwnerSetup(instanceOwnerSetupData: { user_id: string }, user?: User): Promise<void>;
onUserSignup(
user: User,
userSignupData: {
user_type: AuthProviderType;
was_disabled_ldap_user: boolean;
},
): Promise<void>;
onCommunityPackageInstallFinished(installationData: {
user: User;
input_string: string;
package_name: string;
success: boolean;
package_version?: string;
package_node_names?: string[];
package_author?: string;
package_author_email?: string;
failure_reason?: string;
}): Promise<void>;
onCommunityPackageUpdateFinished(updateData: {
user: User;
package_name: string;
package_version_current: string;
package_version_new: string;
package_node_names: string[];
package_author?: string;
package_author_email?: string;
}): Promise<void>;
onCommunityPackageDeleteFinished(deleteData: {
user: User;
package_name: string;
package_version?: string;
package_node_names?: string[];
package_author?: string;
package_author_email?: string;
}): Promise<void>;
onApiKeyCreated(apiKeyDeletedData: { user: User; public_api: boolean }): Promise<void>;
onApiKeyDeleted(apiKeyDeletedData: { user: User; public_api: boolean }): Promise<void>;
onVariableCreated(createData: { variable_type: string }): Promise<void>;
onExternalSecretsProviderSettingsSaved(saveData: {
user_id?: string;
vault_type: string;
is_valid: boolean;
is_new: boolean;
error_message?: string;
}): Promise<void>;
}
export interface IVersionNotificationSettings {
enabled: boolean;
endpoint: string;
@@ -839,7 +707,7 @@ export interface PublicUser {
export interface N8nApp {
app: Application;
restEndpoint: string;
externalHooks: IExternalHooksClass;
externalHooks: ExternalHooks;
activeWorkflowRunner: ActiveWorkflowRunner;
}
+1 -2
View File
@@ -13,7 +13,6 @@ import { TelemetryHelpers } from 'n8n-workflow';
import { get as pslGet } from 'psl';
import type {
IDiagnosticInfo,
IInternalHooksClass,
ITelemetryUserDeletionData,
IWorkflowDb,
IExecutionTrackProperties,
@@ -49,7 +48,7 @@ function userToPayload(user: User): {
}
@Service()
export class InternalHooks implements IInternalHooksClass {
export class InternalHooks {
constructor(
private telemetry: Telemetry,
private nodeTypes: NodeTypes,
+2 -2
View File
@@ -3,7 +3,7 @@ import { Container, Service } from 'typedi';
import path from 'path';
import fsPromises from 'fs/promises';
import type { DirectoryLoader, Types } from 'n8n-core';
import type { Class, DirectoryLoader, Types } from 'n8n-core';
import {
CUSTOM_EXTENSION_ENV,
InstanceSettings,
@@ -250,7 +250,7 @@ export class LoadNodesAndCredentials {
* Run a loader of source files of nodes and credentials in a directory.
*/
private async runDirectoryLoader<T extends DirectoryLoader>(
constructor: new (...args: ConstructorParameters<typeof DirectoryLoader>) => T,
constructor: Class<T, ConstructorParameters<typeof DirectoryLoader>>,
dir: string,
) {
const loader = new constructor(dir, this.excludeNodes, this.includeNodes);
+53 -125
View File
@@ -1,9 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
/* eslint-disable prefer-const */
/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { Container, Service } from 'typedi';
@@ -20,14 +17,9 @@ import type { ServeStaticOptions } from 'serve-static';
import type { FindManyOptions, FindOptionsWhere } from 'typeorm';
import { Not, In } from 'typeorm';
import { InstanceSettings } from 'n8n-core';
import { type Class, InstanceSettings } from 'n8n-core';
import type {
ICredentialTypes,
ExecutionStatus,
IExecutionsSummary,
IN8nUISettings,
} from 'n8n-workflow';
import type { ExecutionStatus, IExecutionsSummary, IN8nUISettings } from 'n8n-workflow';
import { jsonParse } from 'n8n-workflow';
// @ts-ignore
@@ -52,7 +44,6 @@ import { registerController } from '@/decorators';
import { AuthController } from '@/controllers/auth.controller';
import { BinaryDataController } from '@/controllers/binaryData.controller';
import { DynamicNodeParametersController } from '@/controllers/dynamicNodeParameters.controller';
import { LdapController } from '@/controllers/ldap.controller';
import { MeController } from '@/controllers/me.controller';
import { MFAController } from '@/controllers/mfa.controller';
import { NodeTypesController } from '@/controllers/nodeTypes.controller';
@@ -70,9 +61,7 @@ import { isApiEnabled, loadPublicApiVersions } from '@/PublicApi';
import type { ICredentialsOverwrite, IDiagnosticInfo, IExecutionsStopData } from '@/Interfaces';
import { ActiveExecutions } from '@/ActiveExecutions';
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { CredentialTypes } from '@/CredentialTypes';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { NodeTypes } from '@/NodeTypes';
import * as ResponseHelper from '@/ResponseHelper';
import { WaitTracker } from '@/WaitTracker';
import { toHttpNodeParameters } from '@/CurlConverterHelper';
@@ -91,7 +80,6 @@ import { getStatusUsingPreviousExecutionStatusMethod } from './executions/execut
import { SamlController } from './sso/saml/routes/saml.controller.ee';
import { SamlService } from './sso/saml/saml.service.ee';
import { VariablesController } from './environments/variables/variables.controller.ee';
import { LdapManager } from './Ldap/LdapManager.ee';
import {
isLdapCurrentAuthenticationMethod,
isSamlCurrentAuthenticationMethod,
@@ -101,16 +89,10 @@ import { SourceControlController } from '@/environments/sourceControl/sourceCont
import type { ExecutionEntity } from '@db/entities/ExecutionEntity';
import { ExecutionRepository } from '@db/repositories/execution.repository';
import { SettingsRepository } from '@db/repositories/settings.repository';
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { MfaService } from './Mfa/mfa.service';
import { handleMfaDisable, isMfaFeatureEnabled } from './Mfa/helpers';
import type { FrontendService } from './services/frontend.service';
import { RoleService } from './services/role.service';
import { UserService } from './services/user.service';
import { ActiveWorkflowsController } from './controllers/activeWorkflows.controller';
import { OrchestrationController } from './controllers/orchestration.controller';
import { WorkflowHistoryController } from './workflows/workflowHistory/workflowHistory.controller.ee';
@@ -120,7 +102,6 @@ import { RoleController } from './controllers/role.controller';
import { BadRequestError } from './errors/response-errors/bad-request.error';
import { NotFoundError } from './errors/response-errors/not-found.error';
import { MultiMainSetup } from './services/orchestration/main/MultiMainSetup.ee';
import { PasswordUtility } from './services/password.utility';
const exec = promisify(callbackExec);
@@ -136,16 +117,8 @@ export class Server extends AbstractServer {
private loadNodesAndCredentials: LoadNodesAndCredentials;
private nodeTypes: NodeTypes;
private credentialTypes: ICredentialTypes;
private frontendService?: FrontendService;
private postHog: PostHogClient;
private collaborationService: CollaborationService;
constructor() {
super('main');
@@ -159,8 +132,6 @@ export class Server extends AbstractServer {
async start() {
this.loadNodesAndCredentials = Container.get(LoadNodesAndCredentials);
this.credentialTypes = Container.get(CredentialTypes);
this.nodeTypes = Container.get(NodeTypes);
if (!config.getEnv('endpoints.disableUi')) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -169,7 +140,6 @@ export class Server extends AbstractServer {
this.activeExecutionsInstance = Container.get(ActiveExecutions);
this.waitTracker = Container.get(WaitTracker);
this.postHog = Container.get(PostHogClient);
this.presetCredentialsLoaded = false;
this.endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint');
@@ -241,101 +211,70 @@ export class Server extends AbstractServer {
.then(async (workflow) =>
Container.get(InternalHooks).onServerStarted(diagnosticInfo, workflow?.createdAt),
);
this.collaborationService = Container.get(CollaborationService);
Container.get(CollaborationService);
}
private async registerControllers(ignoredEndpoints: Readonly<string[]>) {
const { app, externalHooks, activeWorkflowRunner, nodeTypes, logger } = this;
const { app } = this;
setupAuthMiddlewares(app, ignoredEndpoints, this.restEndpoint);
const internalHooks = Container.get(InternalHooks);
const userService = Container.get(UserService);
const postHog = this.postHog;
const mfaService = Container.get(MfaService);
const controllers: object[] = [
new EventBusController(),
new EventBusControllerEE(),
Container.get(AuthController),
Container.get(LicenseController),
Container.get(OAuth1CredentialController),
Container.get(OAuth2CredentialController),
new OwnerController(
config,
logger,
internalHooks,
Container.get(SettingsRepository),
userService,
Container.get(PasswordUtility),
postHog,
),
Container.get(MeController),
Container.get(DynamicNodeParametersController),
new NodeTypesController(config, nodeTypes),
Container.get(PasswordResetController),
Container.get(TagsController),
new TranslationController(config, this.credentialTypes),
new UsersController(
logger,
externalHooks,
internalHooks,
Container.get(SharedCredentialsRepository),
Container.get(SharedWorkflowRepository),
activeWorkflowRunner,
Container.get(RoleService),
userService,
Container.get(License),
),
Container.get(SamlController),
Container.get(SourceControlController),
Container.get(WorkflowStatisticsController),
Container.get(ExternalSecretsController),
Container.get(OrchestrationController),
Container.get(WorkflowHistoryController),
Container.get(BinaryDataController),
Container.get(VariablesController),
new InvitationController(
config,
logger,
internalHooks,
externalHooks,
Container.get(UserService),
Container.get(License),
Container.get(PasswordUtility),
postHog,
),
Container.get(VariablesController),
Container.get(RoleController),
Container.get(ActiveWorkflowsController),
const controllers: Array<Class<object>> = [
EventBusController,
EventBusControllerEE,
AuthController,
LicenseController,
OAuth1CredentialController,
OAuth2CredentialController,
OwnerController,
MeController,
DynamicNodeParametersController,
NodeTypesController,
PasswordResetController,
TagsController,
TranslationController,
UsersController,
SamlController,
SourceControlController,
WorkflowStatisticsController,
ExternalSecretsController,
OrchestrationController,
WorkflowHistoryController,
BinaryDataController,
VariablesController,
InvitationController,
VariablesController,
RoleController,
ActiveWorkflowsController,
];
if (Container.get(MultiMainSetup).isEnabled) {
const { DebugController } = await import('./controllers/debug.controller');
controllers.push(Container.get(DebugController));
controllers.push(DebugController);
}
if (isLdapEnabled()) {
const { service, sync } = LdapManager.getInstance();
controllers.push(new LdapController(service, sync, internalHooks));
const { LdapController } = await require('@/controllers/ldap.controller');
controllers.push(LdapController);
}
if (config.getEnv('nodes.communityPackages.enabled')) {
const { CommunityPackagesController } = await import(
'@/controllers/communityPackages.controller'
);
controllers.push(Container.get(CommunityPackagesController));
controllers.push(CommunityPackagesController);
}
if (inE2ETests) {
const { E2EController } = await import('./controllers/e2e.controller');
controllers.push(Container.get(E2EController));
controllers.push(E2EController);
}
if (isMfaFeatureEnabled()) {
controllers.push(new MFAController(mfaService));
controllers.push(MFAController);
}
controllers.forEach((controller) => registerController(app, config, controller));
controllers.forEach((controller) => registerController(app, controller));
}
async configure(): Promise<void> {
@@ -356,7 +295,7 @@ export class Server extends AbstractServer {
await this.externalHooks.run('frontend.settings', [frontendService.getSettings()]);
}
await this.postHog.init();
await Container.get(PostHogClient).init();
const publicApiEndpoint = config.getEnv('publicApi.path');
const excludeEndpoints = config.getEnv('security.excludeEndpoints');
@@ -450,21 +389,16 @@ export class Server extends AbstractServer {
// ----------------------------------------
this.app.post(
`/${this.restEndpoint}/curl-to-json`,
ResponseHelper.send(
async (
req: CurlHelper.ToJson,
res: express.Response,
): Promise<{ [key: string]: string }> => {
const curlCommand = req.body.curlCommand ?? '';
ResponseHelper.send(async (req: CurlHelper.ToJson) => {
const curlCommand = req.body.curlCommand ?? '';
try {
const parameters = toHttpNodeParameters(curlCommand);
return ResponseHelper.flattenObject(parameters, 'parameters');
} catch (e) {
throw new BadRequestError('Invalid cURL command');
}
},
),
try {
const parameters = toHttpNodeParameters(curlCommand);
return ResponseHelper.flattenObject(parameters, 'parameters');
} catch (e) {
throw new BadRequestError('Invalid cURL command');
}
}),
);
// ----------------------------------------
@@ -687,9 +621,7 @@ export class Server extends AbstractServer {
// Returns all the available timezones
this.app.get(
`/${this.restEndpoint}/options/timezones`,
ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<object> => {
return timezones;
}),
ResponseHelper.send(async () => timezones),
);
// ----------------------------------------
@@ -701,13 +633,8 @@ export class Server extends AbstractServer {
this.app.get(
`/${this.restEndpoint}/settings`,
ResponseHelper.send(
async (req: express.Request, res: express.Response): Promise<IN8nUISettings> => {
void Container.get(InternalHooks).onFrontendSettingsAPI(
req.headers.sessionid as string,
);
return frontendService.getSettings();
},
async (req: express.Request): Promise<IN8nUISettings> =>
frontendService.getSettings(req.headers.sessionid as string),
),
);
}
@@ -766,6 +693,7 @@ export class Server extends AbstractServer {
};
const serveIcons: express.RequestHandler = async (req, res) => {
// eslint-disable-next-line prefer-const
let { scope, packageName } = req.params;
if (scope) packageName = `@${scope}/${packageName}`;
const filePath = this.loadNodesAndCredentials.resolveIcon(packageName, req.originalUrl);
+2 -2
View File
@@ -14,7 +14,7 @@ import { initErrorHandling } from '@/ErrorReporting';
import { ExternalHooks } from '@/ExternalHooks';
import { NodeTypes } from '@/NodeTypes';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import type { IExternalHooksClass, N8nInstanceType } from '@/Interfaces';
import type { N8nInstanceType } from '@/Interfaces';
import { InternalHooks } from '@/InternalHooks';
import { PostHogClient } from '@/posthog';
import { License } from '@/License';
@@ -27,7 +27,7 @@ import { ShutdownService } from '@/shutdown/Shutdown.service';
export abstract class BaseCommand extends Command {
protected logger = Container.get(Logger);
protected externalHooks?: IExternalHooksClass;
protected externalHooks?: ExternalHooks;
protected nodeTypes: NodeTypes;
-1
View File
@@ -73,4 +73,3 @@ setGlobalState({
// eslint-disable-next-line import/no-default-export
export default config;
export type Config = typeof config;
@@ -1,9 +1,7 @@
import { Service } from 'typedi';
import { Authorized, Get, RestController } from '@/decorators';
import { WorkflowRequest } from '@/requests';
import { ActiveWorkflowsService } from '@/services/activeWorkflows.service';
@Service()
@Authorized()
@RestController('/active-workflows')
export class ActiveWorkflowsController {
@@ -1,6 +1,5 @@
import validator from 'validator';
import { In } from 'typeorm';
import { Service } from 'typedi';
import { Authorized, Get, Post, RestController } from '@/decorators';
import { issueCookie, resolveJwt } from '@/auth/jwt';
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from '@/constants';
@@ -27,7 +26,6 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
import { ApplicationError } from 'n8n-workflow';
@Service()
@RestController()
export class AuthController {
constructor(
@@ -1,11 +1,9 @@
import { Service } from 'typedi';
import express from 'express';
import { BinaryDataService, FileNotFoundError, isValidNonDefaultMode } from 'n8n-core';
import { Get, RestController } from '@/decorators';
import { BinaryDataRequest } from '@/requests';
@RestController('/binary-data')
@Service()
export class BinaryDataController {
constructor(private readonly binaryDataService: BinaryDataService) {}
@@ -1,4 +1,3 @@
import { Service } from 'typedi';
import { Request, Response, NextFunction } from 'express';
import config from '@/config';
import {
@@ -42,14 +41,13 @@ export function isNpmError(error: unknown): error is { code: number; stdout: str
return typeof error === 'object' && error !== null && 'code' in error && 'stdout' in error;
}
@Service()
@Authorized()
@RestController('/community-packages')
export class CommunityPackagesController {
constructor(
private push: Push,
private internalHooks: InternalHooks,
private communityPackagesService: CommunityPackagesService,
private readonly push: Push,
private readonly internalHooks: InternalHooks,
private readonly communityPackagesService: CommunityPackagesService,
) {}
// TODO: move this into a new decorator `@IfConfig('executions.mode', 'queue')`
@@ -1,4 +1,3 @@
import { Service } from 'typedi';
import { Get, RestController } from '@/decorators';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee';
@@ -6,7 +5,6 @@ import { WorkflowRepository } from '@/databases/repositories/workflow.repository
import { In } from 'typeorm';
@RestController('/debug')
@Service()
export class DebugController {
constructor(
private readonly multiMainSetup: MultiMainSetup,
@@ -1,4 +1,3 @@
import { Service } from 'typedi';
import type { RequestHandler } from 'express';
import { NextFunction, Response } from 'express';
import type {
@@ -22,7 +21,6 @@ const assertMethodName: RequestHandler = (req, res, next) => {
next();
};
@Service()
@Authorized()
@RestController('/dynamic-node-parameters')
export class DynamicNodeParametersController {
+9 -16
View File
@@ -1,5 +1,4 @@
import { Request } from 'express';
import { Container, Service } from 'typedi';
import { v4 as uuid } from 'uuid';
import config from '@/config';
import type { Role } from '@db/entities/Role';
@@ -64,7 +63,6 @@ type PushRequest = Request<
}
>;
@Service()
@NoAuthRequired()
@RestController('/e2e')
export class E2EController {
@@ -89,12 +87,13 @@ export class E2EController {
constructor(
license: License,
private roleRepo: RoleRepository,
private settingsRepo: SettingsRepository,
private userRepo: UserRepository,
private workflowRunner: ActiveWorkflowRunner,
private mfaService: MfaService,
private cacheService: CacheService,
private readonly roleRepo: RoleRepository,
private readonly settingsRepo: SettingsRepository,
private readonly userRepo: UserRepository,
private readonly workflowRunner: ActiveWorkflowRunner,
private readonly mfaService: MfaService,
private readonly cacheService: CacheService,
private readonly push: Push,
private readonly passwordUtility: PasswordUtility,
) {
license.isFeatureEnabled = (feature: BooleanLicenseFeature) =>
@@ -112,14 +111,8 @@ export class E2EController {
}
@Post('/push')
async push(req: PushRequest) {
const pushInstance = Container.get(Push);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const sessionId = Object.keys(pushInstance.getBackend().connections as object)[0];
pushInstance.send(req.body.type, req.body.data, sessionId);
async pushSend(req: PushRequest) {
this.push.broadcast(req.body.type, req.body.data);
}
@Patch('/feature')
@@ -1,12 +1,11 @@
import { In } from 'typeorm';
import Container, { Service } from 'typedi';
import { Response } from 'express';
import config from '@/config';
import { Authorized, NoAuthRequired, Post, RequireGlobalScope, RestController } from '@/decorators';
import { issueCookie } from '@/auth/jwt';
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import { Response } from 'express';
import { UserRequest } from '@/requests';
import { Config } from '@/config';
import { IExternalHooksClass, IInternalHooksClass } from '@/Interfaces';
import { License } from '@/License';
import { UserService } from '@/services/user.service';
import { Logger } from '@/Logger';
@@ -17,20 +16,20 @@ 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';
import { InternalHooks } from '@/InternalHooks';
import { ExternalHooks } from '@/ExternalHooks';
@Service()
@Authorized()
@RestController('/invitations')
export class InvitationController {
constructor(
private readonly config: Config,
private readonly logger: Logger,
private readonly internalHooks: IInternalHooksClass,
private readonly externalHooks: IExternalHooksClass,
private readonly internalHooks: InternalHooks,
private readonly externalHooks: ExternalHooks,
private readonly userService: UserService,
private readonly license: License,
private readonly passwordUtility: PasswordUtility,
private readonly postHog?: PostHogClient,
private readonly postHog: PostHogClient,
) {}
/**
@@ -40,7 +39,7 @@ export class InvitationController {
@Post('/')
@RequireGlobalScope('user:create')
async inviteUser(req: UserRequest.Invite) {
const isWithinUsersLimit = Container.get(License).isWithinUsersLimit();
const isWithinUsersLimit = this.license.isWithinUsersLimit();
if (isSamlLicensedAndEnabled()) {
this.logger.debug(
@@ -58,7 +57,7 @@ export class InvitationController {
throw new UnauthorizedError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED);
}
if (!this.config.getEnv('userManagement.isInstanceOwnerSetUp')) {
if (!config.getEnv('userManagement.isInstanceOwnerSetUp')) {
this.logger.debug(
'Request to send email invite(s) to user(s) failed because the owner account is not set up',
);
@@ -1,8 +1,9 @@
import pick from 'lodash/pick';
import { Authorized, Get, Post, Put, RestController, RequireGlobalScope } from '@/decorators';
import { getLdapConfig, getLdapSynchronizations, updateLdapConfig } from '@/Ldap/helpers';
import { LdapService } from '@/Ldap/LdapService.ee';
import { LdapSync } from '@/Ldap/LdapSync.ee';
import { LdapManager } from '@/Ldap/LdapManager.ee';
import type { LdapService } from '@/Ldap/LdapService.ee';
import type { LdapSync } from '@/Ldap/LdapSync.ee';
import { LdapConfiguration } from '@/Ldap/types';
import { NON_SENSIBLE_LDAP_CONFIG_PROPERTIES } from '@/Ldap/constants';
import { InternalHooks } from '@/InternalHooks';
@@ -11,11 +12,15 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@Authorized()
@RestController('/ldap')
export class LdapController {
constructor(
private ldapService: LdapService,
private ldapSync: LdapSync,
private internalHooks: InternalHooks,
) {}
private ldapService: LdapService;
private ldapSync: LdapSync;
constructor(private readonly internalHooks: InternalHooks) {
const { service, sync } = LdapManager.getInstance();
this.ldapService = service;
this.ldapSync = sync;
}
@Get('/config')
@RequireGlobalScope('ldap:manage')
@@ -1,7 +1,6 @@
import validator from 'validator';
import { plainToInstance } from 'class-transformer';
import { Response } from 'express';
import { Service } from 'typedi';
import { randomBytes } from 'crypto';
import { Authorized, Delete, Get, Patch, Post, RestController } from '@/decorators';
import { PasswordUtility } from '@/services/password.utility';
@@ -22,7 +21,6 @@ import { ExternalHooks } from '@/ExternalHooks';
import { InternalHooks } from '@/InternalHooks';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@Service()
@Authorized()
@RestController('/me')
export class MeController {
@@ -1,10 +1,8 @@
import { Service } from 'typedi';
import { Authorized, Delete, Get, Post, RestController } from '@/decorators';
import { AuthenticatedRequest, MFA } from '@/requests';
import { MfaService } from '@/Mfa/mfa.service';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@Service()
@Authorized()
@RestController('/mfa')
export class MFAController {
@@ -3,22 +3,19 @@ import get from 'lodash/get';
import { Request } from 'express';
import type { INodeTypeDescription, INodeTypeNameVersion } from 'n8n-workflow';
import { Authorized, Post, RestController } from '@/decorators';
import { Config } from '@/config';
import config from '@/config';
import { NodeTypes } from '@/NodeTypes';
@Authorized()
@RestController('/node-types')
export class NodeTypesController {
constructor(
private readonly config: Config,
private readonly nodeTypes: NodeTypes,
) {}
constructor(private readonly nodeTypes: NodeTypes) {}
@Post('/')
async getNodeInfo(req: Request) {
const nodeInfos = get(req, 'body.nodeInfos', []) as INodeTypeNameVersion[];
const defaultLocale = this.config.getEnv('defaultLocale');
const defaultLocale = config.getEnv('defaultLocale');
if (defaultLocale === 'en') {
return nodeInfos.reduce<INodeTypeDescription[]>((acc, { name, version }) => {
@@ -1,4 +1,3 @@
import { Service } from 'typedi';
import { Response } from 'express';
import type { AxiosRequestConfig } from 'axios';
import axios from 'axios';
@@ -30,7 +29,6 @@ const algorithmMap = {
/* eslint-enable */
} as const;
@Service()
@Authorized()
@RestController('/oauth1-credential')
export class OAuth1CredentialController extends AbstractOAuthController {
@@ -1,4 +1,3 @@
import { Service } from 'typedi';
import type { ClientOAuth2Options } from '@n8n/client-oauth2';
import { ClientOAuth2 } from '@n8n/client-oauth2';
import Csrf from 'csrf';
@@ -31,7 +30,6 @@ interface CsrfStateParam {
token: string;
}
@Service()
@Authorized()
@RestController('/oauth2-credential')
export class OAuth2CredentialController extends AbstractOAuthController {
@@ -1,12 +1,10 @@
import { Authorized, Post, RestController, RequireGlobalScope } from '@/decorators';
import { OrchestrationRequest } from '@/requests';
import { Service } from 'typedi';
import { SingleMainSetup } from '@/services/orchestration/main/SingleMainSetup';
import { License } from '../License';
import { License } from '@/License';
@Authorized()
@RestController('/orchestration')
@Service()
export class OrchestrationController {
constructor(
private readonly singleMainSetup: SingleMainSetup,
@@ -1,29 +1,29 @@
import validator from 'validator';
import { Response } from 'express';
import config from '@/config';
import { validateEntity } from '@/GenericHelpers';
import { Authorized, Post, RestController } from '@/decorators';
import { PasswordUtility } from '@/services/password.utility';
import { issueCookie } from '@/auth/jwt';
import { Response } from 'express';
import { Config } from '@/config';
import { OwnerRequest } from '@/requests';
import { IInternalHooksClass } from '@/Interfaces';
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';
import { InternalHooks } from '@/InternalHooks';
@Authorized(['global', 'owner'])
@RestController('/owner')
export class OwnerController {
constructor(
private readonly config: Config,
private readonly logger: Logger,
private readonly internalHooks: IInternalHooksClass,
private readonly internalHooks: InternalHooks,
private readonly settingsRepository: SettingsRepository,
private readonly userService: UserService,
private readonly passwordUtility: PasswordUtility,
private readonly postHog?: PostHogClient,
private readonly postHog: PostHogClient,
) {}
/**
@@ -35,7 +35,7 @@ export class OwnerController {
const { email, firstName, lastName, password } = req.body;
const { id: userId, globalRole } = req.user;
if (this.config.getEnv('userManagement.isInstanceOwnerSetUp')) {
if (config.getEnv('userManagement.isInstanceOwnerSetUp')) {
this.logger.debug(
'Request to claim instance ownership failed because instance owner already exists',
{
@@ -94,7 +94,7 @@ export class OwnerController {
{ value: JSON.stringify(true) },
);
this.config.set('userManagement.isInstanceOwnerSetUp', true);
config.set('userManagement.isInstanceOwnerSetUp', true);
this.logger.debug('Setting isInstanceOwnerSetUp updated successfully', { userId });
@@ -1,6 +1,5 @@
import { Response } from 'express';
import { rateLimit } from 'express-rate-limit';
import { Service } from 'typedi';
import { IsNull, Not } from 'typeorm';
import validator from 'validator';
@@ -31,7 +30,6 @@ const throttle = rateLimit({
message: { message: 'Too many requests' },
});
@Service()
@RestController()
export class PasswordResetController {
constructor(
@@ -1,9 +1,7 @@
import { License } from '@/License';
import { Get, RestController } from '@/decorators';
import { RoleService } from '@/services/role.service';
import { Service } from 'typedi';
@Service()
@RestController('/roles')
export class RoleController {
constructor(
@@ -12,16 +12,14 @@ import {
} from '@/decorators';
import { TagService } from '@/services/tag.service';
import { TagsRequest } from '@/requests';
import { Service } from 'typedi';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@Authorized()
@RestController('/tags')
@Service()
export class TagsController {
private config = config;
constructor(private tagService: TagService) {}
constructor(private readonly tagService: TagService) {}
// TODO: move this into a new decorator `@IfEnabled('workflowTagsDisabled')`
@Middleware()
@@ -1,12 +1,12 @@
import type { Request } from 'express';
import { ICredentialTypes } from 'n8n-workflow';
import { join } from 'path';
import { access } from 'fs/promises';
import { Authorized, Get, RestController } from '@/decorators';
import { Config } from '@/config';
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';
import { CredentialTypes } from '@/CredentialTypes';
export const CREDENTIAL_TRANSLATIONS_DIR = 'n8n-nodes-base/dist/credentials/translations';
export const NODE_HEADERS_PATH = join(NODES_BASE_DIR, 'dist/nodes/headers');
@@ -18,10 +18,7 @@ export declare namespace TranslationRequest {
@Authorized()
@RestController('/')
export class TranslationController {
constructor(
private config: Config,
private credentialTypes: ICredentialTypes,
) {}
constructor(private readonly credentialTypes: CredentialTypes) {}
@Get('/credential-translation')
async getCredentialTranslation(req: TranslationRequest.Credential) {
@@ -30,7 +27,7 @@ export class TranslationController {
if (!this.credentialTypes.recognizes(credentialType))
throw new BadRequestError(`Invalid Credential type: "${credentialType}"`);
const defaultLocale = this.config.getEnv('defaultLocale');
const defaultLocale = config.getEnv('defaultLocale');
const translationPath = join(
CREDENTIAL_TRANSLATIONS_DIR,
defaultLocale,
@@ -6,7 +6,6 @@ import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import { RequireGlobalScope, Authorized, Delete, Get, RestController, Patch } from '@/decorators';
import { ListQuery, UserRequest, UserSettingsUpdatePayload } from '@/requests';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { IExternalHooksClass, IInternalHooksClass } from '@/Interfaces';
import type { PublicUser, ITelemetryUserDeletionData } from '@/Interfaces';
import { AuthIdentity } from '@db/entities/AuthIdentity';
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
@@ -20,14 +19,16 @@ 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';
import { License } from '@/License';
import { ExternalHooks } from '@/ExternalHooks';
import { InternalHooks } from '@/InternalHooks';
@Authorized()
@RestController('/users')
export class UsersController {
constructor(
private readonly logger: Logger,
private readonly externalHooks: IExternalHooksClass,
private readonly internalHooks: IInternalHooksClass,
private readonly externalHooks: ExternalHooks,
private readonly internalHooks: InternalHooks,
private readonly sharedCredentialsRepository: SharedCredentialsRepository,
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
private readonly activeWorkflowRunner: ActiveWorkflowRunner,
@@ -1,4 +1,3 @@
import { Service } from 'typedi';
import { Response, NextFunction } from 'express';
import { Get, Middleware, RestController } from '@/decorators';
import type { WorkflowStatistics } from '@db/entities/WorkflowStatistics';
@@ -18,12 +17,11 @@ interface WorkflowStatisticsData<T> {
manualError: T;
}
@Service()
@RestController('/workflow-stats')
export class WorkflowStatisticsController {
constructor(
private sharedWorkflowRepository: SharedWorkflowRepository,
private workflowStatisticsRepository: WorkflowStatisticsRepository,
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
private readonly workflowStatisticsRepository: WorkflowStatisticsRepository,
private readonly logger: Logger,
) {}
@@ -7,6 +7,7 @@ import {
UpdateDateColumn,
} from 'typeorm';
import config from '@/config';
import type { Class } from 'n8n-core';
import { generateNanoId } from '../utils/generators';
const dbType = config.getEnv('database.type');
@@ -27,9 +28,7 @@ const tsColumnOptions: ColumnOptions = {
type: datetimeColumnType,
};
type Constructor<T> = new (...args: any[]) => T;
function mixinStringId<T extends Constructor<{}>>(base: T) {
function mixinStringId<T extends Class<{}, any[]>>(base: T) {
class Derived extends base {
@PrimaryColumn('varchar')
id: string;
@@ -44,7 +43,7 @@ function mixinStringId<T extends Constructor<{}>>(base: T) {
return Derived;
}
function mixinTimestamps<T extends Constructor<{}>>(base: T) {
function mixinTimestamps<T extends Class<{}, any[]>>(base: T) {
class Derived extends base {
@CreateDateColumn(tsColumnOptions)
createdAt: Date;
@@ -24,6 +24,7 @@ import { type ServiceClass, ShutdownService } from '@/shutdown/Shutdown.service'
export const OnShutdown =
(priority = 100): MethodDecorator =>
(prototype, propertyKey, descriptor) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const serviceClass = prototype.constructor as ServiceClass;
const methodName = String(propertyKey);
// TODO: assert that serviceClass is decorated with @Service
@@ -1,7 +1,10 @@
import { Service } from 'typedi';
import { CONTROLLER_BASE_PATH } from './constants';
export const RestController =
(basePath: `/${string}` = '/'): ClassDecorator =>
(target: object) => {
Reflect.defineMetadata(CONTROLLER_BASE_PATH, basePath, target);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return Service()(target);
};
@@ -1,6 +1,9 @@
import { Container } from 'typedi';
import { Router } from 'express';
import type { Application, Request, Response, RequestHandler } from 'express';
import type { Config } from '@/config';
import type { Class } from 'n8n-core';
import config from '@/config';
import type { AuthenticatedRequest } from '@/requests';
import { send } from '@/ResponseHelper'; // TODO: move `ResponseHelper.send` to this file
import {
@@ -21,7 +24,7 @@ import type {
ScopeMetadata,
} from './types';
import type { BooleanLicenseFeature } from '@/Interfaces';
import Container from 'typedi';
import { License } from '@/License';
import type { Scope } from '@n8n/permissions';
import { ApplicationError } from 'n8n-workflow';
@@ -81,9 +84,8 @@ const authFreeRoutes: string[] = [];
export const canSkipAuth = (method: string, path: string): boolean =>
authFreeRoutes.includes(`${method.toLowerCase()} ${path}`);
export const registerController = (app: Application, config: Config, cObj: object) => {
const controller = cObj as Controller;
const controllerClass = controller.constructor;
export const registerController = (app: Application, controllerClass: Class<object>) => {
const controller = Container.get(controllerClass as Class<Controller>);
const controllerBasePath = Reflect.getMetadata(CONTROLLER_BASE_PATH, controllerClass) as
| string
| undefined;
@@ -10,7 +10,6 @@ export const SOURCE_CONTROL_SSH_FOLDER = 'ssh';
export const SOURCE_CONTROL_SSH_KEY_NAME = 'key';
export const SOURCE_CONTROL_DEFAULT_BRANCH = 'main';
export const SOURCE_CONTROL_ORIGIN = 'origin';
export const SOURCE_CONTROL_API_ROOT = 'source-control';
export const SOURCE_CONTROL_README = `
# n8n Source Control
`;
@@ -1,4 +1,3 @@
import { Container, Service } from 'typedi';
import type { PullResult } from 'simple-git';
import express from 'express';
import { Authorized, Get, Post, Patch, RestController, RequireGlobalScope } from '@/decorators';
@@ -11,20 +10,20 @@ import { SourceControlRequest } from './types/requests';
import { SourceControlPreferencesService } from './sourceControlPreferences.service.ee';
import type { SourceControlPreferences } from './types/sourceControlPreferences';
import type { SourceControlledFile } from './types/sourceControlledFile';
import { SOURCE_CONTROL_API_ROOT, SOURCE_CONTROL_DEFAULT_BRANCH } from './constants';
import { SOURCE_CONTROL_DEFAULT_BRANCH } from './constants';
import type { ImportResult } from './types/importResult';
import { InternalHooks } from '../../InternalHooks';
import { InternalHooks } from '@/InternalHooks';
import { getRepoType } from './sourceControlHelper.ee';
import { SourceControlGetStatus } from './types/sourceControlGetStatus';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@Service()
@Authorized()
@RestController(`/${SOURCE_CONTROL_API_ROOT}`)
@RestController('/source-control')
export class SourceControlController {
constructor(
private sourceControlService: SourceControlService,
private sourceControlPreferencesService: SourceControlPreferencesService,
private readonly sourceControlService: SourceControlService,
private readonly sourceControlPreferencesService: SourceControlPreferencesService,
private readonly internalHooks: InternalHooks,
) {}
@Authorized('none')
@@ -85,7 +84,7 @@ export class SourceControlController {
const resultingPreferences = this.sourceControlPreferencesService.getPreferences();
// #region Tracking Information
// located in controller so as to not call this multiple times when updating preferences
void Container.get(InternalHooks).onSourceControlSettingsUpdated({
void this.internalHooks.onSourceControlSettingsUpdated({
branch_name: resultingPreferences.branchName,
connected: resultingPreferences.connected,
read_only_instance: resultingPreferences.branchReadOnly,
@@ -130,7 +129,7 @@ export class SourceControlController {
}
await this.sourceControlService.init();
const resultingPreferences = this.sourceControlPreferencesService.getPreferences();
void Container.get(InternalHooks).onSourceControlSettingsUpdated({
void this.internalHooks.onSourceControlSettingsUpdated({
branch_name: resultingPreferences.branchName,
connected: resultingPreferences.connected,
read_only_instance: resultingPreferences.branchReadOnly,
@@ -1,5 +1,3 @@
import { Service } from 'typedi';
import { VariablesRequest } from '@/requests';
import {
Authorized,
@@ -17,11 +15,10 @@ 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()
@RestController('/variables')
export class VariablesController {
constructor(private variablesService: VariablesService) {}
constructor(private readonly variablesService: VariablesService) {}
@Get('/')
@RequireGlobalScope('variable:list')
@@ -1,9 +1,7 @@
import { Service } from 'typedi';
import { Authorized, Get, Post, RequireGlobalScope, RestController } from '@/decorators';
import { LicenseRequest } from '@/requests';
import { LicenseService } from './license.service';
@Service()
@Authorized()
@RestController('/license')
export class LicenseController {
@@ -30,6 +30,7 @@ import { UserManagementMailer } from '@/UserManagement/email';
import type { CommunityPackagesService } from '@/services/communityPackages.service';
import { Logger } from '@/Logger';
import { UrlService } from './url.service';
import { InternalHooks } from '@/InternalHooks';
@Service()
export class FrontendService {
@@ -46,6 +47,7 @@ export class FrontendService {
private readonly mailer: UserManagementMailer,
private readonly instanceSettings: InstanceSettings,
private readonly urlService: UrlService,
private readonly internalHooks: InternalHooks,
) {
loadNodesAndCredentials.addPostProcessor(async () => this.generateTypes());
void this.generateTypes();
@@ -218,7 +220,9 @@ export class FrontendService {
this.writeStaticJSON('credentials', credentials);
}
getSettings(): IN8nUISettings {
getSettings(sessionId?: string): IN8nUISettings {
void this.internalHooks.onFrontendSettingsAPI(sessionId);
const restEndpoint = config.getEnv('endpoints.rest');
// Update all urls, in case `WEBHOOK_URL` was updated by `--tunnel`
@@ -1,10 +1,10 @@
import { Container, Service } from 'typedi';
import { ApplicationError, ErrorReporterProxy, assert } from 'n8n-workflow';
import type { Class } from 'n8n-core';
import { Logger } from '@/Logger';
export interface ServiceClass {
new (): Record<string, () => Promise<void> | void>;
}
type HandlerFn = () => Promise<void> | void;
export type ServiceClass = Class<Record<string, HandlerFn>>;
export interface ShutdownHandler {
serviceClass: ServiceClass;
@@ -1,5 +1,4 @@
import express from 'express';
import { Container, Service } from 'typedi';
import {
Authorized,
Get,
@@ -36,13 +35,13 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { AuthError } from '@/errors/response-errors/auth.error';
import { UrlService } from '@/services/url.service';
@Service()
@Authorized()
@RestController('/sso/saml')
export class SamlController {
constructor(
private readonly samlService: SamlService,
private readonly urlService: UrlService,
private readonly internalHooks: InternalHooks,
) {}
@NoAuthRequired()
@@ -142,7 +141,7 @@ export class SamlController {
}
}
if (loginResult.authenticatedUser) {
void Container.get(InternalHooks).onUserLoginSuccess({
void this.internalHooks.onUserLoginSuccess({
user: loginResult.authenticatedUser,
authenticationMethod: 'saml',
});
@@ -159,7 +158,7 @@ export class SamlController {
return res.status(202).send(loginResult.attributes);
}
}
void Container.get(InternalHooks).onUserLoginFailed({
void this.internalHooks.onUserLoginFailed({
user: loginResult.attributes.email ?? 'unknown',
authenticationMethod: 'saml',
});
@@ -168,7 +167,7 @@ export class SamlController {
if (isConnectionTestRequest(req)) {
return res.send(getSamlConnectionTestFailedView((error as Error).message));
}
void Container.get(InternalHooks).onUserLoginFailed({
void this.internalHooks.onUserLoginFailed({
user: 'unknown',
authenticationMethod: 'saml',
});
@@ -1,6 +1,5 @@
import { Authorized, RestController, Get, Middleware } from '@/decorators';
import { WorkflowHistoryRequest } from '@/requests';
import { Service } from 'typedi';
import { WorkflowHistoryService } from './workflowHistory.service.ee';
import { Request, Response, NextFunction } from 'express';
import { isWorkflowHistoryEnabled, isWorkflowHistoryLicensed } from './workflowHistoryHelper.ee';
@@ -12,7 +11,6 @@ import { WorkflowHistoryVersionNotFoundError } from '@/errors/workflow-history-v
const DEFAULT_TAKE = 20;
@Service()
@Authorized()
@RestController('/workflow-history')
export class WorkflowHistoryController {
@@ -1,22 +1,18 @@
import { Container } from 'typedi';
import type { SuperAgentTest } from 'supertest';
import { SOURCE_CONTROL_API_ROOT } from '@/environments/sourceControl/constants';
import * as utils from '../shared/utils/';
import type { User } from '@db/entities/User';
import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper';
import Container from 'typedi';
import config from '@/config';
import { SourceControlPreferencesService } from '@/environments/sourceControl/sourceControlPreferences.service.ee';
import { SourceControlService } from '@/environments/sourceControl/sourceControl.service.ee';
import type { SourceControlledFile } from '@/environments/sourceControl/types/sourceControlledFile';
import { getGlobalMemberRole, getGlobalOwnerRole } from '../shared/db/roles';
import * as utils from '../shared/utils/';
import { getGlobalOwnerRole } from '../shared/db/roles';
import { createUser } from '../shared/db/users';
let authOwnerAgent: SuperAgentTest;
let authMemberAgent: SuperAgentTest;
let owner: User;
let member: User;
const sharingSpy = jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(true);
const testServer = utils.setupTestServer({
endpointGroups: ['sourceControl', 'license', 'auth'],
@@ -25,11 +21,8 @@ const testServer = utils.setupTestServer({
beforeAll(async () => {
const globalOwnerRole = await getGlobalOwnerRole();
const globalMemberRole = await getGlobalMemberRole();
owner = await createUser({ globalRole: globalOwnerRole });
member = await createUser({ globalRole: globalMemberRole });
authOwnerAgent = testServer.authAgentFor(owner);
authMemberAgent = testServer.authAgentFor(member);
Container.get(SourceControlPreferencesService).isSourceControlConnected = () => true;
});
@@ -37,7 +30,7 @@ beforeAll(async () => {
describe('GET /sourceControl/preferences', () => {
test('should return Source Control preferences', async () => {
await authOwnerAgent
.get(`/${SOURCE_CONTROL_API_ROOT}/preferences`)
.get('/source-control/preferences')
.expect(200)
.expect((res) => {
return 'repositoryUrl' in res.body && 'branchName' in res.body;
@@ -60,7 +53,7 @@ describe('GET /sourceControl/preferences', () => {
] as SourceControlledFile[];
};
await authOwnerAgent
.get(`/${SOURCE_CONTROL_API_ROOT}/get-status`)
.get('/source-control/get-status')
.query({ direction: 'push', preferLocalVersion: 'true', verbose: 'false' })
.expect(200)
.expect((res) => {
@@ -73,7 +66,7 @@ describe('GET /sourceControl/preferences', () => {
test('refreshing key pairsshould return new rsa key', async () => {
config.set('sourceControl.defaultKeyPairType', 'rsa');
await authOwnerAgent
.post(`/${SOURCE_CONTROL_API_ROOT}/generate-key-pair`)
.post('/source-control/generate-key-pair')
.send()
.expect(200)
.expect((res) => {
@@ -1,6 +1,7 @@
import type { DataSourceOptions as ConnectionOptions, Repository } from 'typeorm';
import { DataSource as Connection } from 'typeorm';
import { Container } from 'typedi';
import type { Class } from 'n8n-core';
import config from '@/config';
import * as Db from '@/Db';
@@ -116,7 +117,7 @@ const repositories = [
*/
export async function truncate(names: Array<(typeof repositories)[number]>) {
for (const name of names) {
const RepositoryClass: { new (): Repository<any> } = (
const RepositoryClass: Class<Repository<object>> = (
await import(`@db/repositories/${name.charAt(0).toLowerCase() + name.slice(1)}.repository`)
)[`${name}Repository`];
await Container.get(RepositoryClass).delete({});
@@ -14,14 +14,13 @@ import { rawBodyReader, bodyParser, setupAuthMiddlewares } from '@/middlewares';
import { PostHogClient } from '@/posthog';
import { License } from '@/License';
import { Logger } from '@/Logger';
import { InternalHooks } from '@/InternalHooks';
import { mockInstance } from '../../../shared/mocking';
import * as testDb from '../../shared/testDb';
import { AUTHLESS_ENDPOINTS, PUBLIC_API_REST_PATH_SEGMENT, REST_PATH_SEGMENT } from '../constants';
import type { SetupProps, TestServer } from '../types';
import { InternalHooks } from '@/InternalHooks';
import { LicenseMocker } from '../license';
import { PasswordUtility } from '@/services/password.utility';
/**
* Plugin to prefix a path segment into a request URL pathname.
@@ -76,7 +75,7 @@ export const setupTestServer = ({
app.use(cookieParser());
// Mock all telemetry and logging
const logger = mockInstance(Logger);
mockInstance(Logger);
mockInstance(InternalHooks);
mockInstance(PostHogClient);
@@ -140,12 +139,12 @@ export const setupTestServer = ({
const { VariablesController } = await import(
'@/environments/variables/variables.controller.ee'
);
registerController(app, config, Container.get(VariablesController));
registerController(app, VariablesController);
break;
case 'license':
const { LicenseController } = await import('@/license/license.controller');
registerController(app, config, Container.get(LicenseController));
registerController(app, LicenseController);
break;
case 'metrics':
@@ -156,166 +155,108 @@ export const setupTestServer = ({
case 'eventBus':
const { EventBusController } = await import('@/eventbus/eventBus.controller');
const { EventBusControllerEE } = await import('@/eventbus/eventBus.controller.ee');
registerController(app, config, new EventBusController());
registerController(app, config, new EventBusControllerEE());
registerController(app, EventBusController);
registerController(app, EventBusControllerEE);
break;
case 'auth':
const { AuthController } = await import('@/controllers/auth.controller');
registerController(app, config, Container.get(AuthController));
registerController(app, AuthController);
break;
case 'mfa':
const { MFAController } = await import('@/controllers/mfa.controller');
registerController(app, config, Container.get(MFAController));
registerController(app, MFAController);
break;
case 'ldap':
const { LdapManager } = await import('@/Ldap/LdapManager.ee');
const { handleLdapInit } = await import('@/Ldap/helpers');
const { LdapController } = await import('@/controllers/ldap.controller');
testServer.license.enable('feat:ldap');
await handleLdapInit();
const { service, sync } = LdapManager.getInstance();
registerController(
app,
config,
new LdapController(service, sync, Container.get(InternalHooks)),
);
registerController(app, LdapController);
break;
case 'saml':
const { setSamlLoginEnabled } = await import('@/sso/saml/samlHelpers');
const { SamlController } = await import('@/sso/saml/routes/saml.controller.ee');
await setSamlLoginEnabled(true);
registerController(app, config, Container.get(SamlController));
registerController(app, SamlController);
break;
case 'sourceControl':
const { SourceControlController } = await import(
'@/environments/sourceControl/sourceControl.controller.ee'
);
registerController(app, config, Container.get(SourceControlController));
registerController(app, SourceControlController);
break;
case 'community-packages':
const { CommunityPackagesController } = await import(
'@/controllers/communityPackages.controller'
);
registerController(app, config, Container.get(CommunityPackagesController));
registerController(app, CommunityPackagesController);
break;
case 'me':
const { MeController } = await import('@/controllers/me.controller');
registerController(app, config, Container.get(MeController));
registerController(app, MeController);
break;
case 'passwordReset':
const { PasswordResetController } = await import(
'@/controllers/passwordReset.controller'
);
registerController(app, config, Container.get(PasswordResetController));
registerController(app, PasswordResetController);
break;
case 'owner':
const { UserService } = await import('@/services/user.service');
const { SettingsRepository } = await import('@db/repositories/settings.repository');
const { OwnerController } = await import('@/controllers/owner.controller');
registerController(
app,
config,
new OwnerController(
config,
logger,
Container.get(InternalHooks),
Container.get(SettingsRepository),
Container.get(UserService),
Container.get(PasswordUtility),
),
);
registerController(app, OwnerController);
break;
case 'users':
const { SharedCredentialsRepository } = await import(
'@db/repositories/sharedCredentials.repository'
);
const { SharedWorkflowRepository } = await import(
'@db/repositories/sharedWorkflow.repository'
);
const { ActiveWorkflowRunner } = await import('@/ActiveWorkflowRunner');
const { UserService: US } = await import('@/services/user.service');
const { ExternalHooks: EH } = await import('@/ExternalHooks');
const { RoleService: RS } = await import('@/services/role.service');
const { UsersController } = await import('@/controllers/users.controller');
registerController(
app,
config,
new UsersController(
logger,
Container.get(EH),
Container.get(InternalHooks),
Container.get(SharedCredentialsRepository),
Container.get(SharedWorkflowRepository),
Container.get(ActiveWorkflowRunner),
Container.get(RS),
Container.get(US),
Container.get(License),
),
);
registerController(app, UsersController);
break;
case 'invitations':
const { InvitationController } = await import('@/controllers/invitation.controller');
const { ExternalHooks: EHS } = await import('@/ExternalHooks');
const { UserService: USE } = await import('@/services/user.service');
registerController(
app,
config,
new InvitationController(
config,
logger,
Container.get(InternalHooks),
Container.get(EHS),
Container.get(USE),
Container.get(License),
Container.get(PasswordUtility),
),
);
registerController(app, InvitationController);
break;
case 'tags':
const { TagsController } = await import('@/controllers/tags.controller');
registerController(app, config, Container.get(TagsController));
registerController(app, TagsController);
break;
case 'externalSecrets':
const { ExternalSecretsController } = await import(
'@/ExternalSecrets/ExternalSecrets.controller.ee'
);
registerController(app, config, Container.get(ExternalSecretsController));
registerController(app, ExternalSecretsController);
break;
case 'workflowHistory':
const { WorkflowHistoryController } = await import(
'@/workflows/workflowHistory/workflowHistory.controller.ee'
);
registerController(app, config, Container.get(WorkflowHistoryController));
registerController(app, WorkflowHistoryController);
break;
case 'binaryData':
const { BinaryDataController } = await import('@/controllers/binaryData.controller');
registerController(app, config, Container.get(BinaryDataController));
registerController(app, BinaryDataController);
break;
case 'role':
const { RoleController } = await import('@/controllers/role.controller');
registerController(app, config, Container.get(RoleController));
registerController(app, RoleController);
break;
case 'debug':
const { DebugController } = await import('@/controllers/debug.controller');
registerController(app, config, Container.get(DebugController));
registerController(app, DebugController);
break;
}
}
+3 -2
View File
@@ -1,12 +1,13 @@
import { Container } from 'typedi';
import { mock } from 'jest-mock-extended';
import type { DeepPartial } from 'ts-essentials';
import type { Class } from 'n8n-core';
export const mockInstance = <T>(
ctor: new (...args: unknown[]) => T,
serviceClass: Class<T>,
data: DeepPartial<T> | undefined = undefined,
) => {
const instance = mock<T>(data);
Container.set(ctor, instance);
Container.set(serviceClass, instance);
return instance;
};
@@ -1,10 +1,9 @@
import type { CookieOptions, Response } from 'express';
import { anyObject, captor, mock } from 'jest-mock-extended';
import jwt from 'jsonwebtoken';
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 config from '@/config';
import type { OwnerRequest } from '@/requests';
import { OwnerController } from '@/controllers/owner.controller';
import { AUTH_COOKIE_NAME } from '@/constants';
@@ -16,32 +15,33 @@ import { badPasswords } from '../shared/testData';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { PasswordUtility } from '@/services/password.utility';
import Container from 'typedi';
import type { InternalHooks } from '@/InternalHooks';
describe('OwnerController', () => {
const config = mock<Config>();
const internalHooks = mock<IInternalHooksClass>();
const configGetSpy = jest.spyOn(config, 'getEnv');
const internalHooks = mock<InternalHooks>();
const userService = mockInstance(UserService);
const settingsRepository = mock<SettingsRepository>();
mockInstance(License).isWithinUsersLimit.mockReturnValue(true);
const controller = new OwnerController(
config,
mock(),
internalHooks,
settingsRepository,
userService,
Container.get(PasswordUtility),
mock(),
);
describe('setupOwner', () => {
it('should throw a BadRequestError if the instance owner is already setup', async () => {
config.getEnv.calledWith('userManagement.isInstanceOwnerSetUp').mockReturnValue(true);
configGetSpy.mockReturnValue(true);
await expect(controller.setupOwner(mock(), mock())).rejects.toThrowError(
new BadRequestError('Instance owner already setup'),
);
});
it('should throw a BadRequestError if the email is invalid', async () => {
config.getEnv.calledWith('userManagement.isInstanceOwnerSetUp').mockReturnValue(false);
configGetSpy.mockReturnValue(false);
const req = mock<OwnerRequest.Post>({ body: { email: 'invalid email' } });
await expect(controller.setupOwner(req, mock())).rejects.toThrowError(
new BadRequestError('Invalid email address'),
@@ -51,7 +51,7 @@ describe('OwnerController', () => {
describe('should throw if the password is invalid', () => {
Object.entries(badPasswords).forEach(([password, errorMessage]) => {
it(password, async () => {
config.getEnv.calledWith('userManagement.isInstanceOwnerSetUp').mockReturnValue(false);
configGetSpy.mockReturnValue(false);
const req = mock<OwnerRequest.Post>({ body: { email: 'valid@email.com', password } });
await expect(controller.setupOwner(req, mock())).rejects.toThrowError(
new BadRequestError(errorMessage),
@@ -61,7 +61,7 @@ describe('OwnerController', () => {
});
it('should throw a BadRequestError if firstName & lastName are missing ', async () => {
config.getEnv.calledWith('userManagement.isInstanceOwnerSetUp').mockReturnValue(false);
configGetSpy.mockReturnValue(false);
const req = mock<OwnerRequest.Post>({
body: { email: 'valid@email.com', password: 'NewPassword123', firstName: '', lastName: '' },
});
@@ -86,7 +86,7 @@ describe('OwnerController', () => {
user,
});
const res = mock<Response>();
config.getEnv.calledWith('userManagement.isInstanceOwnerSetUp').mockReturnValue(false);
configGetSpy.mockReturnValue(false);
userService.save.calledWith(anyObject()).mockResolvedValue(user);
jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token');
@@ -1,6 +1,6 @@
import { mock } from 'jest-mock-extended';
import type { ICredentialTypes } from 'n8n-workflow';
import type { Config } from '@/config';
import config from '@/config';
import type { TranslationRequest } from '@/controllers/translation.controller';
import {
TranslationController,
@@ -9,9 +9,9 @@ import {
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
describe('TranslationController', () => {
const config = mock<Config>();
const configGetSpy = jest.spyOn(config, 'getEnv');
const credentialTypes = mock<ICredentialTypes>();
const controller = new TranslationController(config, credentialTypes);
const controller = new TranslationController(credentialTypes);
describe('getCredentialTranslation', () => {
it('should throw 400 on invalid credential types', async () => {
@@ -27,7 +27,7 @@ describe('TranslationController', () => {
it('should return translation json on valid credential types', async () => {
const credentialType = 'credential-type';
const req = mock<TranslationRequest.Credential>({ query: { credentialType } });
config.getEnv.calledWith('defaultLocale').mockReturnValue('de');
configGetSpy.mockReturnValue('de');
credentialTypes.recognizes.calledWith(credentialType).mockReturnValue(true);
const response = { translation: 'string' };
jest.mock(`${CREDENTIAL_TRANSLATIONS_DIR}/de/credential-type.json`, () => response, {
+2
View File
@@ -5,6 +5,8 @@ import type {
ValidationResult,
} from 'n8n-workflow';
export type Class<T = object, A extends unknown[] = unknown[]> = new (...args: A) => T;
export interface IProcessMessage {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any;
+2 -1
View File
@@ -3,9 +3,10 @@ import { mock } from 'jest-mock-extended';
import { Duplex } from 'stream';
import type { DeepPartial } from 'ts-essentials';
import type { Class } from '@/Interfaces';
export const mockInstance = <T>(
constructor: new (...args: unknown[]) => T,
constructor: Class<T>,
data: DeepPartial<T> | undefined = undefined,
) => {
const instance = mock<T>(data);