Skip to content

Commit

Permalink
refactor(core): Auto-register controllers at startup (no-changelog) (n…
Browse files Browse the repository at this point in the history
  • Loading branch information
netroy authored and adrian-martinez-onestic committed Jun 20, 2024
1 parent a48cdb5 commit 6fcc56f
Show file tree
Hide file tree
Showing 15 changed files with 433 additions and 473 deletions.
232 changes: 94 additions & 138 deletions packages/cli/src/Server.ts
Original file line number Diff line number Diff line change
@@ -1,77 +1,71 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { Container, Service } from 'typedi';
import { exec as callbackExec } from 'child_process';
import { access as fsAccess } from 'fs/promises';
import { promisify } from 'util';
import cookieParser from 'cookie-parser';
import express from 'express';
import helmet from 'helmet';
import { type Class, InstanceSettings } from 'n8n-core';
import { InstanceSettings } from 'n8n-core';
import type { IN8nUISettings } from 'n8n-workflow';

// @ts-ignore
// @ts-expect-error missing types
import timezones from 'google-timezones-json';

import config from '@/config';
import { Queue } from '@/Queue';

import { WorkflowsController } from '@/workflows/workflows.controller';
import { EDITOR_UI_DIST_DIR, inDevelopment, inE2ETests, N8N_VERSION, Time } from '@/constants';
import { CredentialsController } from '@/credentials/credentials.controller';
import {
EDITOR_UI_DIST_DIR,
inDevelopment,
inE2ETests,
inProduction,
N8N_VERSION,
Time,
} from '@/constants';
import type { APIRequest } from '@/requests';
import { registerController } from '@/decorators';
import { AuthController } from '@/controllers/auth.controller';
import { BinaryDataController } from '@/controllers/binaryData.controller';
import { CurlController } from '@/controllers/curl.controller';
import { DynamicNodeParametersController } from '@/controllers/dynamicNodeParameters.controller';
import { MeController } from '@/controllers/me.controller';
import { MFAController } from '@/controllers/mfa.controller';
import { NodeTypesController } from '@/controllers/nodeTypes.controller';
import { OAuth1CredentialController } from '@/controllers/oauth/oAuth1Credential.controller';
import { OAuth2CredentialController } from '@/controllers/oauth/oAuth2Credential.controller';
import { OwnerController } from '@/controllers/owner.controller';
import { PasswordResetController } from '@/controllers/passwordReset.controller';
import { TagsController } from '@/controllers/tags.controller';
import { TranslationController } from '@/controllers/translation.controller';
import { UsersController } from '@/controllers/users.controller';
import { WorkflowStatisticsController } from '@/controllers/workflowStatistics.controller';
import { ExternalSecretsController } from '@/ExternalSecrets/ExternalSecrets.controller.ee';
import { ExecutionsController } from '@/executions/executions.controller';
import { ControllerRegistry } from '@/decorators';
import { isApiEnabled, loadPublicApiVersions } from '@/PublicApi';
import type { ICredentialsOverwrite } from '@/Interfaces';
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import * as ResponseHelper from '@/ResponseHelper';
import { EventBusController } from '@/eventbus/eventBus.controller';
import { LicenseController } from '@/license/license.controller';
import { setupPushServer, setupPushHandler } from '@/push';
import { isLdapEnabled } from './Ldap/helpers';
import { AbstractServer } from './AbstractServer';
import { PostHogClient } from './posthog';
import { isLdapEnabled } from '@/Ldap/helpers';
import { AbstractServer } from '@/AbstractServer';
import { PostHogClient } from '@/posthog';
import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus';
import { InternalHooks } from './InternalHooks';
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 { SourceControlService } from '@/environments/sourceControl/sourceControl.service.ee';
import { SourceControlController } from '@/environments/sourceControl/sourceControl.controller.ee';
import { AIController } from '@/controllers/ai.controller';

import { handleMfaDisable, isMfaFeatureEnabled } from './Mfa/helpers';
import type { FrontendService } from './services/frontend.service';
import { ActiveWorkflowsController } from './controllers/activeWorkflows.controller';
import { OrchestrationController } from './controllers/orchestration.controller';
import { WorkflowHistoryController } from './workflows/workflowHistory/workflowHistory.controller.ee';
import { InvitationController } from './controllers/invitation.controller';
// import { CollaborationService } from './collaboration/collaboration.service';
import { InternalHooks } from '@/InternalHooks';
import { handleMfaDisable, isMfaFeatureEnabled } from '@/Mfa/helpers';
import type { FrontendService } from '@/services/frontend.service';
import { OrchestrationService } from '@/services/orchestration.service';
import { ProjectController } from './controllers/project.controller';
import { RoleController } from './controllers/role.controller';
import { UserSettingsController } from './controllers/userSettings.controller';

import '@/controllers/activeWorkflows.controller';
import '@/controllers/ai.controller';
import '@/controllers/auth.controller';
import '@/controllers/binaryData.controller';
import '@/controllers/curl.controller';
import '@/controllers/dynamicNodeParameters.controller';
import '@/controllers/invitation.controller';
import '@/controllers/me.controller';
import '@/controllers/nodeTypes.controller';
import '@/controllers/oauth/oAuth1Credential.controller';
import '@/controllers/oauth/oAuth2Credential.controller';
import '@/controllers/orchestration.controller';
import '@/controllers/owner.controller';
import '@/controllers/passwordReset.controller';
import '@/controllers/project.controller';
import '@/controllers/role.controller';
import '@/controllers/tags.controller';
import '@/controllers/translation.controller';
import '@/controllers/users.controller';
import '@/controllers/userSettings.controller';
import '@/controllers/workflowStatistics.controller';
import '@/credentials/credentials.controller';
import '@/eventbus/eventBus.controller';
import '@/executions/executions.controller';
import '@/ExternalSecrets/ExternalSecrets.controller.ee';
import '@/license/license.controller';
import '@/workflows/workflowHistory/workflowHistory.controller.ee';
import '@/workflows/workflows.controller';

const exec = promisify(callbackExec);

Expand All @@ -81,23 +75,23 @@ export class Server extends AbstractServer {

private presetCredentialsLoaded: boolean;

private loadNodesAndCredentials: LoadNodesAndCredentials;

private frontendService?: FrontendService;

constructor() {
constructor(
private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
private readonly orchestrationService: OrchestrationService,
private readonly postHogClient: PostHogClient,
) {
super('main');

this.testWebhooksEnabled = true;
this.webhooksEnabled = !config.getEnv('endpoints.disableProductionWebhooksOnMainProcess');
}

async start() {
this.loadNodesAndCredentials = Container.get(LoadNodesAndCredentials);

if (!config.getEnv('endpoints.disableUi')) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
this.frontendService = Container.get(require('@/services/frontend.service').FrontendService);
const { FrontendService } = await import('@/services/frontend.service');
this.frontendService = Container.get(FrontendService);
}

this.presetCredentialsLoaded = false;
Expand All @@ -111,84 +105,62 @@ export class Server extends AbstractServer {
}

void Container.get(InternalHooks).onServerStarted();
// Container.get(CollaborationService);
}

private async registerControllers() {
const { app } = this;

const controllers: Array<Class<object>> = [
EventBusController,
AuthController,
LicenseController,
OAuth1CredentialController,
OAuth2CredentialController,
OwnerController,
MeController,
DynamicNodeParametersController,
NodeTypesController,
PasswordResetController,
TagsController,
TranslationController,
UsersController,
SamlController,
SourceControlController,
WorkflowStatisticsController,
ExternalSecretsController,
OrchestrationController,
WorkflowHistoryController,
BinaryDataController,
VariablesController,
InvitationController,
VariablesController,
ActiveWorkflowsController,
WorkflowsController,
ExecutionsController,
CredentialsController,
AIController,
ProjectController,
RoleController,
CurlController,
UserSettingsController,
];

if (
process.env.NODE_ENV !== 'production' &&
Container.get(OrchestrationService).isMultiMainSetupEnabled
) {
const { DebugController } = await import('@/controllers/debug.controller');
controllers.push(DebugController);
private async registerAdditionalControllers() {
if (!inProduction && this.orchestrationService.isMultiMainSetupEnabled) {
await import('@/controllers/debug.controller');
}

if (isLdapEnabled()) {
const { LdapService } = await import('@/Ldap/ldap.service');
const { LdapController } = await require('@/Ldap/ldap.controller');
await import('@/Ldap/ldap.controller');
await Container.get(LdapService).init();
controllers.push(LdapController);
}

if (config.getEnv('nodes.communityPackages.enabled')) {
const { CommunityPackagesController } = await import(
'@/controllers/communityPackages.controller'
);
controllers.push(CommunityPackagesController);
await import('@/controllers/communityPackages.controller');
}

if (inE2ETests) {
const { E2EController } = await import('./controllers/e2e.controller');
controllers.push(E2EController);
await import('@/controllers/e2e.controller');
}

if (isMfaFeatureEnabled()) {
controllers.push(MFAController);
await import('@/controllers/mfa.controller');
}

if (!config.getEnv('endpoints.disableUi')) {
const { CtaController } = await import('@/controllers/cta.controller');
controllers.push(CtaController);
await import('@/controllers/cta.controller');
}

controllers.forEach((controller) => registerController(app, controller));
// ----------------------------------------
// SAML
// ----------------------------------------

// initialize SamlService if it is licensed, even if not enabled, to
// set up the initial environment
try {
const { SamlService } = await import('@/sso/saml/saml.service.ee');
await Container.get(SamlService).init();
await import('@/sso/saml/routes/saml.controller.ee');
} catch (error) {
this.logger.warn(`SAML initialization failed: ${(error as Error).message}`);
}

// ----------------------------------------
// Source Control
// ----------------------------------------
try {
const { SourceControlService } = await import(
'@/environments/sourceControl/sourceControl.service.ee'
);
await Container.get(SourceControlService).init();
await import('@/environments/sourceControl/sourceControl.controller.ee');
await import('@/environments/variables/variables.controller.ee');
} catch (error) {
this.logger.warn(`Source Control initialization failed: ${(error as Error).message}`);
}
}

async configure(): Promise<void> {
Expand All @@ -209,7 +181,7 @@ export class Server extends AbstractServer {
await this.externalHooks.run('frontend.settings', [frontendService.getSettings()]);
}

await Container.get(PostHogClient).init();
await this.postHogClient.init();

const publicApiEndpoint = config.getEnv('publicApi.path');

Expand Down Expand Up @@ -238,33 +210,16 @@ export class Server extends AbstractServer {
setupPushHandler(restEndpoint, app);

if (config.getEnv('executions.mode') === 'queue') {
const { Queue } = await import('@/Queue');
await Container.get(Queue).init();
}

await handleMfaDisable();

await this.registerControllers();

// ----------------------------------------
// SAML
// ----------------------------------------

// initialize SamlService if it is licensed, even if not enabled, to
// set up the initial environment
try {
await Container.get(SamlService).init();
} catch (error) {
this.logger.warn(`SAML initialization failed: ${error.message}`);
}
await this.registerAdditionalControllers();

// ----------------------------------------
// Source Control
// ----------------------------------------
try {
await Container.get(SourceControlService).init();
} catch (error) {
this.logger.warn(`Source Control initialization failed: ${error.message}`);
}
// register all known controllers
Container.get(ControllerRegistry).activate(app);

// ----------------------------------------
// Options
Expand All @@ -273,6 +228,7 @@ export class Server extends AbstractServer {
// Returns all the available timezones
this.app.get(
`/${this.restEndpoint}/options/timezones`,
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
ResponseHelper.send(async () => timezones),
);

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class AuthController {
) {}

/** Log in a user */
@Post('/login', { skipAuth: true, rateLimit: {} })
@Post('/login', { skipAuth: true, rateLimit: true })
async login(req: LoginRequest, res: Response): Promise<PublicUser | undefined> {
const { email, password, mfaToken, mfaRecoveryCode } = req.body;
if (!email) throw new ApplicationError('Email is required to log in');
Expand Down
18 changes: 7 additions & 11 deletions packages/cli/src/decorators/Licensed.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import type { BooleanLicenseFeature } from '@/Interfaces';
import type { LicenseMetadata } from './types';
import { CONTROLLER_LICENSE_FEATURES } from './constants';
import { getRouteMetadata } from './controller.registry';
import type { Controller } from './types';

export const Licensed = (features: BooleanLicenseFeature | BooleanLicenseFeature[]) => {
// eslint-disable-next-line @typescript-eslint/ban-types
return (target: Function | object, handlerName?: string) => {
const controllerClass = handlerName ? target.constructor : target;
const license = (Reflect.getMetadata(CONTROLLER_LICENSE_FEATURES, controllerClass) ??
{}) as LicenseMetadata;
license[handlerName ?? '*'] = Array.isArray(features) ? features : [features];
Reflect.defineMetadata(CONTROLLER_LICENSE_FEATURES, license, controllerClass);
export const Licensed =
(licenseFeature: BooleanLicenseFeature): MethodDecorator =>
(target, handlerName) => {
const routeMetadata = getRouteMetadata(target.constructor as Controller, String(handlerName));
routeMetadata.licenseFeature = licenseFeature;
};
};
11 changes: 4 additions & 7 deletions packages/cli/src/decorators/Middleware.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { CONTROLLER_MIDDLEWARES } from './constants';
import type { MiddlewareMetadata } from './types';
import { getControllerMetadata } from './controller.registry';
import type { Controller } from './types';

export const Middleware = (): MethodDecorator => (target, handlerName) => {
const controllerClass = target.constructor;
const middlewares = (Reflect.getMetadata(CONTROLLER_MIDDLEWARES, controllerClass) ??
[]) as MiddlewareMetadata[];
middlewares.push({ handlerName: String(handlerName) });
Reflect.defineMetadata(CONTROLLER_MIDDLEWARES, middlewares, controllerClass);
const metadata = getControllerMetadata(target.constructor as Controller);
metadata.middlewares.push(String(handlerName));
};
8 changes: 5 additions & 3 deletions packages/cli/src/decorators/RestController.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Service } from 'typedi';
import { CONTROLLER_BASE_PATH } from './constants';
import { getControllerMetadata } from './controller.registry';
import type { Controller } from './types';

export const RestController =
(basePath: `/${string}` = '/'): ClassDecorator =>
(target: object) => {
Reflect.defineMetadata(CONTROLLER_BASE_PATH, basePath, target);
(target) => {
const metadata = getControllerMetadata(target as unknown as Controller);
metadata.basePath = basePath;
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return Service()(target);
};
Loading

0 comments on commit 6fcc56f

Please sign in to comment.