1
1
mirror of https://github.com/n8n-io/n8n.git synced 2024-09-19 17:07:18 +03:00

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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 209 additions and 522 deletions

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;

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<() => {}>;
} = {};

View File

@ -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 {

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;
}

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,

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

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

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;

View File

@ -73,4 +73,3 @@ setGlobalState({
// eslint-disable-next-line import/no-default-export
export default config;
export type Config = typeof config;

View File

@ -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 {

View File

@ -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(

View File

@ -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) {}

View File

@ -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')`

View File

@ -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,

View File

@ -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 {

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')

View File

@ -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',
);

View File

@ -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')

View File

@ -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 {

View File

@ -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 {

View File

@ -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 }) => {

View File

@ -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 {

View File

@ -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 {

View File

@ -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,

View File

@ -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 });

View File

@ -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(

View File

@ -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(

View File

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

View File

@ -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,

View File

@ -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,

View File

@ -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,
) {}

View File

@ -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;

View File

@ -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

View File

@ -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);
};

View File

@ -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;

View File

@ -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
`;

View File

@ -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,

View File

@ -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')

View File

@ -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 {

View File

@ -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`

View File

@ -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;

View File

@ -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',
});

View File

@ -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 {

View File

@ -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) => {

View File

@ -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({});

View File

@ -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;
}
}

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;
};

View File

@ -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');

View File

@ -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, {

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;

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