mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
refactor(core): Convert more routes to use the decorator pattern (no-changelog) (#5611)
* move nodeTypes api to a controller class * move tags api to a controller class * move LDAP routes to a controller class * move nodes routes to a controller class
This commit is contained in:
parent
493f7a1c92
commit
356e916194
|
@ -1,77 +0,0 @@
|
||||||
import express from 'express';
|
|
||||||
import { LdapManager } from '../LdapManager.ee';
|
|
||||||
import { getLdapConfig, getLdapSynchronizations, updateLdapConfig } from '../helpers';
|
|
||||||
import type { LdapConfiguration } from '../types';
|
|
||||||
import pick from 'lodash.pick';
|
|
||||||
import { NON_SENSIBLE_LDAP_CONFIG_PROPERTIES } from '../constants';
|
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
|
||||||
import { Container } from 'typedi';
|
|
||||||
|
|
||||||
export const ldapController = express.Router();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /ldap/config
|
|
||||||
*/
|
|
||||||
ldapController.get('/config', async (req: express.Request, res: express.Response) => {
|
|
||||||
const data = await getLdapConfig();
|
|
||||||
return res.status(200).json({ data });
|
|
||||||
});
|
|
||||||
/**
|
|
||||||
* POST /ldap/test-connection
|
|
||||||
*/
|
|
||||||
ldapController.post('/test-connection', async (req: express.Request, res: express.Response) => {
|
|
||||||
try {
|
|
||||||
await LdapManager.getInstance().service.testConnection();
|
|
||||||
} catch (error) {
|
|
||||||
const errorObject = error as { message: string };
|
|
||||||
return res.status(400).json({ message: errorObject.message });
|
|
||||||
}
|
|
||||||
return res.status(200).json();
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PUT /ldap/config
|
|
||||||
*/
|
|
||||||
ldapController.put('/config', async (req: LdapConfiguration.Update, res: express.Response) => {
|
|
||||||
try {
|
|
||||||
await updateLdapConfig(req.body);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Error) {
|
|
||||||
return res.status(400).json({ message: e.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await getLdapConfig();
|
|
||||||
|
|
||||||
void Container.get(InternalHooks).onUserUpdatedLdapSettings({
|
|
||||||
user_id: req.user.id,
|
|
||||||
...pick(data, NON_SENSIBLE_LDAP_CONFIG_PROPERTIES),
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).json({ data });
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /ldap/sync
|
|
||||||
*/
|
|
||||||
ldapController.post('/sync', async (req: LdapConfiguration.Sync, res: express.Response) => {
|
|
||||||
const runType = req.body.type;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await LdapManager.getInstance().sync.run(runType);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Error) {
|
|
||||||
return res.status(400).json({ message: e.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res.status(200).json({});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /ldap/sync
|
|
||||||
*/
|
|
||||||
ldapController.get('/sync', async (req: LdapConfiguration.GetSync, res: express.Response) => {
|
|
||||||
const { page = '0', perPage = '20' } = req.query;
|
|
||||||
const data = await getLdapSynchronizations(parseInt(page, 10), parseInt(perPage, 10));
|
|
||||||
return res.status(200).json({ data });
|
|
||||||
});
|
|
|
@ -59,7 +59,6 @@ import config from '@/config';
|
||||||
import * as Queue from '@/Queue';
|
import * as Queue from '@/Queue';
|
||||||
import { getSharedWorkflowIds } from '@/WorkflowHelpers';
|
import { getSharedWorkflowIds } from '@/WorkflowHelpers';
|
||||||
|
|
||||||
import { nodesController } from '@/api/nodes.api';
|
|
||||||
import { workflowsController } from '@/workflows/workflows.controller';
|
import { workflowsController } from '@/workflows/workflows.controller';
|
||||||
import {
|
import {
|
||||||
EDITOR_UI_DIST_DIR,
|
EDITOR_UI_DIST_DIR,
|
||||||
|
@ -83,16 +82,18 @@ import type {
|
||||||
import { registerController } from '@/decorators';
|
import { registerController } from '@/decorators';
|
||||||
import {
|
import {
|
||||||
AuthController,
|
AuthController,
|
||||||
|
LdapController,
|
||||||
MeController,
|
MeController,
|
||||||
|
NodesController,
|
||||||
|
NodeTypesController,
|
||||||
OwnerController,
|
OwnerController,
|
||||||
PasswordResetController,
|
PasswordResetController,
|
||||||
|
TagsController,
|
||||||
TranslationController,
|
TranslationController,
|
||||||
UsersController,
|
UsersController,
|
||||||
} from '@/controllers';
|
} from '@/controllers';
|
||||||
|
|
||||||
import { executionsController } from '@/executions/executions.controller';
|
import { executionsController } from '@/executions/executions.controller';
|
||||||
import { nodeTypesController } from '@/api/nodeTypes.api';
|
|
||||||
import { tagsController } from '@/api/tags.api';
|
|
||||||
import { workflowStatsController } from '@/api/workflowStats.api';
|
import { workflowStatsController } from '@/api/workflowStats.api';
|
||||||
import { loadPublicApiVersions } from '@/PublicApi';
|
import { loadPublicApiVersions } from '@/PublicApi';
|
||||||
import {
|
import {
|
||||||
|
@ -134,7 +135,6 @@ import { licenseController } from './license/license.controller';
|
||||||
import { Push, setupPushServer, setupPushHandler } from '@/push';
|
import { Push, setupPushServer, setupPushHandler } from '@/push';
|
||||||
import { setupAuthMiddlewares } from './middlewares';
|
import { setupAuthMiddlewares } from './middlewares';
|
||||||
import { initEvents } from './events';
|
import { initEvents } from './events';
|
||||||
import { ldapController } from './Ldap/routes/ldap.controller.ee';
|
|
||||||
import { getLdapLoginLabel, isLdapEnabled, isLdapLoginEnabled } from './Ldap/helpers';
|
import { getLdapLoginLabel, isLdapEnabled, isLdapLoginEnabled } from './Ldap/helpers';
|
||||||
import { AbstractServer } from './AbstractServer';
|
import { AbstractServer } from './AbstractServer';
|
||||||
import { configureMetrics } from './metrics';
|
import { configureMetrics } from './metrics';
|
||||||
|
@ -152,6 +152,7 @@ import { getSamlLoginLabel, isSamlLoginEnabled, isSamlLicensed } from './sso/sam
|
||||||
import { samlControllerPublic } from './sso/saml/routes/saml.controller.public.ee';
|
import { samlControllerPublic } from './sso/saml/routes/saml.controller.public.ee';
|
||||||
import { SamlService } from './sso/saml/saml.service.ee';
|
import { SamlService } from './sso/saml/saml.service.ee';
|
||||||
import { samlControllerProtected } from './sso/saml/routes/saml.controller.protected.ee';
|
import { samlControllerProtected } from './sso/saml/routes/saml.controller.protected.ee';
|
||||||
|
import { LdapManager } from './Ldap/LdapManager.ee';
|
||||||
|
|
||||||
const exec = promisify(callbackExec);
|
const exec = promisify(callbackExec);
|
||||||
|
|
||||||
|
@ -371,7 +372,7 @@ class Server extends AbstractServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerControllers(ignoredEndpoints: Readonly<string[]>) {
|
private registerControllers(ignoredEndpoints: Readonly<string[]>) {
|
||||||
const { app, externalHooks, activeWorkflowRunner } = this;
|
const { app, externalHooks, activeWorkflowRunner, nodeTypes } = this;
|
||||||
const repositories = Db.collections;
|
const repositories = Db.collections;
|
||||||
setupAuthMiddlewares(app, ignoredEndpoints, this.restEndpoint, repositories.User);
|
setupAuthMiddlewares(app, ignoredEndpoints, this.restEndpoint, repositories.User);
|
||||||
|
|
||||||
|
@ -380,11 +381,13 @@ class Server extends AbstractServer {
|
||||||
const mailer = getMailerInstance();
|
const mailer = getMailerInstance();
|
||||||
const postHog = this.postHog;
|
const postHog = this.postHog;
|
||||||
|
|
||||||
const controllers = [
|
const controllers: object[] = [
|
||||||
new AuthController({ config, internalHooks, repositories, logger, postHog }),
|
new AuthController({ config, internalHooks, repositories, logger, postHog }),
|
||||||
new OwnerController({ config, internalHooks, repositories, logger }),
|
new OwnerController({ config, internalHooks, repositories, logger }),
|
||||||
new MeController({ externalHooks, internalHooks, repositories, logger }),
|
new MeController({ externalHooks, internalHooks, repositories, logger }),
|
||||||
|
new NodeTypesController({ config, nodeTypes }),
|
||||||
new PasswordResetController({ config, externalHooks, internalHooks, repositories, logger }),
|
new PasswordResetController({ config, externalHooks, internalHooks, repositories, logger }),
|
||||||
|
new TagsController({ config, repositories, externalHooks }),
|
||||||
new TranslationController(config, this.credentialTypes),
|
new TranslationController(config, this.credentialTypes),
|
||||||
new UsersController({
|
new UsersController({
|
||||||
config,
|
config,
|
||||||
|
@ -397,6 +400,18 @@ class Server extends AbstractServer {
|
||||||
postHog,
|
postHog,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (isLdapEnabled()) {
|
||||||
|
const { service, sync } = LdapManager.getInstance();
|
||||||
|
controllers.push(new LdapController(service, sync, internalHooks));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.getEnv('nodes.communityPackages.enabled')) {
|
||||||
|
controllers.push(
|
||||||
|
new NodesController(config, this.loadNodesAndCredentials, this.push, internalHooks),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
controllers.forEach((controller) => registerController(app, config, controller));
|
controllers.forEach((controller) => registerController(app, config, controller));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -482,13 +497,6 @@ class Server extends AbstractServer {
|
||||||
|
|
||||||
this.app.use(`/${this.restEndpoint}/credentials`, credentialsController);
|
this.app.use(`/${this.restEndpoint}/credentials`, credentialsController);
|
||||||
|
|
||||||
// ----------------------------------------
|
|
||||||
// Packages and nodes management
|
|
||||||
// ----------------------------------------
|
|
||||||
if (config.getEnv('nodes.communityPackages.enabled')) {
|
|
||||||
this.app.use(`/${this.restEndpoint}/nodes`, nodesController);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// Workflow
|
// Workflow
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
@ -504,18 +512,6 @@ class Server extends AbstractServer {
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
this.app.use(`/${this.restEndpoint}/workflow-stats`, workflowStatsController);
|
this.app.use(`/${this.restEndpoint}/workflow-stats`, workflowStatsController);
|
||||||
|
|
||||||
// ----------------------------------------
|
|
||||||
// Tags
|
|
||||||
// ----------------------------------------
|
|
||||||
this.app.use(`/${this.restEndpoint}/tags`, tagsController);
|
|
||||||
|
|
||||||
// ----------------------------------------
|
|
||||||
// LDAP
|
|
||||||
// ----------------------------------------
|
|
||||||
if (isLdapEnabled()) {
|
|
||||||
this.app.use(`/${this.restEndpoint}/ldap`, ldapController);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// SAML
|
// SAML
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
@ -534,7 +530,6 @@ class Server extends AbstractServer {
|
||||||
this.app.use(`/${this.restEndpoint}/sso/saml`, samlControllerProtected);
|
this.app.use(`/${this.restEndpoint}/sso/saml`, samlControllerProtected);
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
|
||||||
// Returns parameter values which normally get loaded from an external API or
|
// Returns parameter values which normally get loaded from an external API or
|
||||||
// get generated dynamically
|
// get generated dynamically
|
||||||
this.app.get(
|
this.app.get(
|
||||||
|
@ -645,12 +640,6 @@ class Server extends AbstractServer {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ----------------------------------------
|
|
||||||
// Node-Types
|
|
||||||
// ----------------------------------------
|
|
||||||
|
|
||||||
this.app.use(`/${this.restEndpoint}/node-types`, nodeTypesController);
|
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// Active Workflows
|
// Active Workflows
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
|
|
@ -1,110 +0,0 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */
|
|
||||||
/* eslint-disable @typescript-eslint/no-invalid-void-type */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
||||||
/* eslint-disable no-param-reassign */
|
|
||||||
|
|
||||||
import express from 'express';
|
|
||||||
|
|
||||||
import * as Db from '@/Db';
|
|
||||||
import { ExternalHooks } from '@/ExternalHooks';
|
|
||||||
import type { ITagWithCountDb } from '@/Interfaces';
|
|
||||||
import * as ResponseHelper from '@/ResponseHelper';
|
|
||||||
import config from '@/config';
|
|
||||||
import * as TagHelpers from '@/TagHelpers';
|
|
||||||
import { validateEntity } from '@/GenericHelpers';
|
|
||||||
import { TagEntity } from '@db/entities/TagEntity';
|
|
||||||
import type { TagsRequest } from '@/requests';
|
|
||||||
import { Container } from 'typedi';
|
|
||||||
|
|
||||||
export const tagsController = express.Router();
|
|
||||||
|
|
||||||
const workflowsEnabledMiddleware: express.RequestHandler = (req, res, next) => {
|
|
||||||
if (config.getEnv('workflowTagsDisabled')) {
|
|
||||||
throw new ResponseHelper.BadRequestError('Workflow tags are disabled');
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Retrieves all tags, with or without usage count
|
|
||||||
tagsController.get(
|
|
||||||
'/',
|
|
||||||
workflowsEnabledMiddleware,
|
|
||||||
ResponseHelper.send(async (req: express.Request): Promise<TagEntity[] | ITagWithCountDb[]> => {
|
|
||||||
if (req.query.withUsageCount === 'true') {
|
|
||||||
const tablePrefix = config.getEnv('database.tablePrefix');
|
|
||||||
return TagHelpers.getTagsWithCountDb(tablePrefix);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Db.collections.Tag.find({ select: ['id', 'name', 'createdAt', 'updatedAt'] });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Creates a tag
|
|
||||||
tagsController.post(
|
|
||||||
'/',
|
|
||||||
workflowsEnabledMiddleware,
|
|
||||||
ResponseHelper.send(async (req: express.Request): Promise<TagEntity | void> => {
|
|
||||||
const newTag = new TagEntity();
|
|
||||||
newTag.name = req.body.name.trim();
|
|
||||||
|
|
||||||
await Container.get(ExternalHooks).run('tag.beforeCreate', [newTag]);
|
|
||||||
|
|
||||||
await validateEntity(newTag);
|
|
||||||
const tag = await Db.collections.Tag.save(newTag);
|
|
||||||
|
|
||||||
await Container.get(ExternalHooks).run('tag.afterCreate', [tag]);
|
|
||||||
|
|
||||||
return tag;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Updates a tag
|
|
||||||
tagsController.patch(
|
|
||||||
'/:id(\\d+)',
|
|
||||||
workflowsEnabledMiddleware,
|
|
||||||
ResponseHelper.send(async (req: express.Request): Promise<TagEntity | void> => {
|
|
||||||
const { name } = req.body;
|
|
||||||
const { id } = req.params;
|
|
||||||
|
|
||||||
const newTag = new TagEntity();
|
|
||||||
// @ts-ignore
|
|
||||||
newTag.id = id;
|
|
||||||
newTag.name = name.trim();
|
|
||||||
|
|
||||||
await Container.get(ExternalHooks).run('tag.beforeUpdate', [newTag]);
|
|
||||||
|
|
||||||
await validateEntity(newTag);
|
|
||||||
const tag = await Db.collections.Tag.save(newTag);
|
|
||||||
|
|
||||||
await Container.get(ExternalHooks).run('tag.afterUpdate', [tag]);
|
|
||||||
|
|
||||||
return tag;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
tagsController.delete(
|
|
||||||
'/:id(\\d+)',
|
|
||||||
workflowsEnabledMiddleware,
|
|
||||||
ResponseHelper.send(async (req: TagsRequest.Delete): Promise<boolean> => {
|
|
||||||
if (
|
|
||||||
config.getEnv('userManagement.isInstanceOwnerSetUp') === true &&
|
|
||||||
req.user.globalRole.name !== 'owner'
|
|
||||||
) {
|
|
||||||
throw new ResponseHelper.UnauthorizedError(
|
|
||||||
'You are not allowed to perform this action',
|
|
||||||
'Only owners can remove tags',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const id = req.params.id;
|
|
||||||
|
|
||||||
await Container.get(ExternalHooks).run('tag.beforeDelete', [id]);
|
|
||||||
|
|
||||||
await Db.collections.Tag.delete({ id });
|
|
||||||
|
|
||||||
await Container.get(ExternalHooks).run('tag.afterDelete', [id]);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
);
|
|
|
@ -1,6 +1,10 @@
|
||||||
export { AuthController } from './auth.controller';
|
export { AuthController } from './auth.controller';
|
||||||
|
export { LdapController } from './ldap.controller';
|
||||||
export { MeController } from './me.controller';
|
export { MeController } from './me.controller';
|
||||||
|
export { NodesController } from './nodes.controller';
|
||||||
|
export { NodeTypesController } from './nodeTypes.controller';
|
||||||
export { OwnerController } from './owner.controller';
|
export { OwnerController } from './owner.controller';
|
||||||
export { PasswordResetController } from './passwordReset.controller';
|
export { PasswordResetController } from './passwordReset.controller';
|
||||||
|
export { TagsController } from './tags.controller';
|
||||||
export { TranslationController } from './translation.controller';
|
export { TranslationController } from './translation.controller';
|
||||||
export { UsersController } from './users.controller';
|
export { UsersController } from './users.controller';
|
||||||
|
|
65
packages/cli/src/controllers/ldap.controller.ts
Normal file
65
packages/cli/src/controllers/ldap.controller.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import pick from 'lodash.pick';
|
||||||
|
import { Get, Post, Put, RestController } from '@/decorators';
|
||||||
|
import { getLdapConfig, getLdapSynchronizations, updateLdapConfig } from '@/Ldap/helpers';
|
||||||
|
import { LdapService } from '@/Ldap/LdapService.ee';
|
||||||
|
import { LdapSync } from '@/Ldap/LdapSync.ee';
|
||||||
|
import { LdapConfiguration } from '@/Ldap/types';
|
||||||
|
import { BadRequestError } from '@/ResponseHelper';
|
||||||
|
import { NON_SENSIBLE_LDAP_CONFIG_PROPERTIES } from '@/Ldap/constants';
|
||||||
|
import { InternalHooks } from '@/InternalHooks';
|
||||||
|
|
||||||
|
@RestController('/ldap')
|
||||||
|
export class LdapController {
|
||||||
|
constructor(
|
||||||
|
private ldapService: LdapService,
|
||||||
|
private ldapSync: LdapSync,
|
||||||
|
private internalHooks: InternalHooks,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get('/config')
|
||||||
|
async getConfig() {
|
||||||
|
return getLdapConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/test-connection')
|
||||||
|
async testConnection() {
|
||||||
|
try {
|
||||||
|
await this.ldapService.testConnection();
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestError((error as { message: string }).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('/config')
|
||||||
|
async updateConfig(req: LdapConfiguration.Update) {
|
||||||
|
try {
|
||||||
|
await updateLdapConfig(req.body);
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestError((error as { message: string }).message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await getLdapConfig();
|
||||||
|
|
||||||
|
void this.internalHooks.onUserUpdatedLdapSettings({
|
||||||
|
user_id: req.user.id,
|
||||||
|
...pick(data, NON_SENSIBLE_LDAP_CONFIG_PROPERTIES),
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/sync')
|
||||||
|
async getLdapSync(req: LdapConfiguration.GetSync) {
|
||||||
|
const { page = '0', perPage = '20' } = req.query;
|
||||||
|
return getLdapSynchronizations(parseInt(page, 10), parseInt(perPage, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/sync')
|
||||||
|
async syncLdap(req: LdapConfiguration.Sync) {
|
||||||
|
try {
|
||||||
|
await this.ldapSync.run(req.body.type);
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestError((error as { message: string }).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,39 +1,43 @@
|
||||||
import express from 'express';
|
|
||||||
import { readFile } from 'fs/promises';
|
import { readFile } from 'fs/promises';
|
||||||
import get from 'lodash.get';
|
import get from 'lodash.get';
|
||||||
|
import { Request } from 'express';
|
||||||
import type { INodeTypeDescription, INodeTypeNameVersion } from 'n8n-workflow';
|
import type { INodeTypeDescription, INodeTypeNameVersion } from 'n8n-workflow';
|
||||||
|
import { Post, RestController } from '@/decorators';
|
||||||
import config from '@/config';
|
|
||||||
import { NodeTypes } from '@/NodeTypes';
|
|
||||||
import * as ResponseHelper from '@/ResponseHelper';
|
|
||||||
import { getNodeTranslationPath } from '@/TranslationHelpers';
|
import { getNodeTranslationPath } from '@/TranslationHelpers';
|
||||||
import { Container } from 'typedi';
|
import type { Config } from '@/config';
|
||||||
|
import type { NodeTypes } from '@/NodeTypes';
|
||||||
|
|
||||||
export const nodeTypesController = express.Router();
|
@RestController('/node-types')
|
||||||
|
export class NodeTypesController {
|
||||||
|
private readonly config: Config;
|
||||||
|
|
||||||
// Returns node information based on node names and versions
|
private readonly nodeTypes: NodeTypes;
|
||||||
nodeTypesController.post(
|
|
||||||
'/',
|
constructor({ config, nodeTypes }: { config: Config; nodeTypes: NodeTypes }) {
|
||||||
ResponseHelper.send(async (req: express.Request): Promise<INodeTypeDescription[]> => {
|
this.config = config;
|
||||||
|
this.nodeTypes = nodeTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/')
|
||||||
|
async getNodeInfo(req: Request) {
|
||||||
const nodeInfos = get(req, 'body.nodeInfos', []) as INodeTypeNameVersion[];
|
const nodeInfos = get(req, 'body.nodeInfos', []) as INodeTypeNameVersion[];
|
||||||
|
|
||||||
const defaultLocale = config.getEnv('defaultLocale');
|
const defaultLocale = this.config.getEnv('defaultLocale');
|
||||||
|
|
||||||
if (defaultLocale === 'en') {
|
if (defaultLocale === 'en') {
|
||||||
return nodeInfos.reduce<INodeTypeDescription[]>((acc, { name, version }) => {
|
return nodeInfos.reduce<INodeTypeDescription[]>((acc, { name, version }) => {
|
||||||
const { description } = Container.get(NodeTypes).getByNameAndVersion(name, version);
|
const { description } = this.nodeTypes.getByNameAndVersion(name, version);
|
||||||
acc.push(description);
|
acc.push(description);
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function populateTranslation(
|
const populateTranslation = async (
|
||||||
name: string,
|
name: string,
|
||||||
version: number,
|
version: number,
|
||||||
nodeTypes: INodeTypeDescription[],
|
nodeTypes: INodeTypeDescription[],
|
||||||
) {
|
) => {
|
||||||
const { description, sourcePath } = Container.get(NodeTypes).getWithSourcePath(name, version);
|
const { description, sourcePath } = this.nodeTypes.getWithSourcePath(name, version);
|
||||||
const translationPath = await getNodeTranslationPath({
|
const translationPath = await getNodeTranslationPath({
|
||||||
nodeSourcePath: sourcePath,
|
nodeSourcePath: sourcePath,
|
||||||
longNodeType: description.name,
|
longNodeType: description.name,
|
||||||
|
@ -44,12 +48,12 @@ nodeTypesController.post(
|
||||||
const translation = await readFile(translationPath, 'utf8');
|
const translation = await readFile(translationPath, 'utf8');
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
description.translation = JSON.parse(translation);
|
description.translation = JSON.parse(translation);
|
||||||
} catch (error) {
|
} catch {
|
||||||
// ignore - no translation exists at path
|
// ignore - no translation exists at path
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeTypes.push(description);
|
nodeTypes.push(description);
|
||||||
}
|
};
|
||||||
|
|
||||||
const nodeTypes: INodeTypeDescription[] = [];
|
const nodeTypes: INodeTypeDescription[] = [];
|
||||||
|
|
||||||
|
@ -60,5 +64,5 @@ nodeTypesController.post(
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
||||||
return nodeTypes;
|
return nodeTypes;
|
||||||
}),
|
}
|
||||||
);
|
}
|
|
@ -1,10 +1,12 @@
|
||||||
import express from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import type { PublicInstalledPackage } from 'n8n-workflow';
|
import {
|
||||||
|
RESPONSE_ERROR_MESSAGES,
|
||||||
import config from '@/config';
|
STARTER_TEMPLATE_NAME,
|
||||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
UNKNOWN_FAILURE_REASON,
|
||||||
import * as ResponseHelper from '@/ResponseHelper';
|
} from '@/constants';
|
||||||
|
import { Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators';
|
||||||
|
import { NodeRequest } from '@/requests';
|
||||||
|
import { BadRequestError, InternalServerError } from '@/ResponseHelper';
|
||||||
import {
|
import {
|
||||||
checkNpmPackageStatus,
|
checkNpmPackageStatus,
|
||||||
executeCommand,
|
executeCommand,
|
||||||
|
@ -22,57 +24,50 @@ import {
|
||||||
getAllInstalledPackages,
|
getAllInstalledPackages,
|
||||||
isPackageInstalled,
|
isPackageInstalled,
|
||||||
} from '@/CommunityNodes/packageModel';
|
} from '@/CommunityNodes/packageModel';
|
||||||
import {
|
|
||||||
RESPONSE_ERROR_MESSAGES,
|
|
||||||
STARTER_TEMPLATE_NAME,
|
|
||||||
UNKNOWN_FAILURE_REASON,
|
|
||||||
} from '@/constants';
|
|
||||||
import { isAuthenticatedRequest } from '@/UserManagement/UserManagementHelper';
|
|
||||||
|
|
||||||
import type { InstalledPackages } from '@db/entities/InstalledPackages';
|
import type { InstalledPackages } from '@db/entities/InstalledPackages';
|
||||||
import type { CommunityPackages } from '@/Interfaces';
|
import type { CommunityPackages } from '@/Interfaces';
|
||||||
import type { NodeRequest } from '@/requests';
|
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
import { Push } from '@/push';
|
|
||||||
import { Container } from 'typedi';
|
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
import { InternalHooks } from '@/InternalHooks';
|
||||||
|
import { Push } from '@/push';
|
||||||
|
import { Config } from '@/config';
|
||||||
|
import { isAuthenticatedRequest } from '@/UserManagement/UserManagementHelper';
|
||||||
|
|
||||||
const { PACKAGE_NOT_INSTALLED, PACKAGE_NAME_NOT_PROVIDED } = RESPONSE_ERROR_MESSAGES;
|
const { PACKAGE_NOT_INSTALLED, PACKAGE_NAME_NOT_PROVIDED } = RESPONSE_ERROR_MESSAGES;
|
||||||
|
|
||||||
export const nodesController = express.Router();
|
@RestController('/nodes')
|
||||||
|
export class NodesController {
|
||||||
|
constructor(
|
||||||
|
private config: Config,
|
||||||
|
private loadNodesAndCredentials: LoadNodesAndCredentials,
|
||||||
|
private push: Push,
|
||||||
|
private internalHooks: InternalHooks,
|
||||||
|
) {}
|
||||||
|
|
||||||
nodesController.use((req, res, next) => {
|
// TODO: move this into a new decorator `@Authorized`
|
||||||
if (!isAuthenticatedRequest(req) || req.user.globalRole.name !== 'owner') {
|
@Middleware()
|
||||||
res.status(403).json({ status: 'error', message: 'Unauthorized' });
|
checkIfOwner(req: Request, res: Response, next: NextFunction) {
|
||||||
return;
|
if (!isAuthenticatedRequest(req) || req.user.globalRole.name !== 'owner')
|
||||||
|
res.status(403).json({ status: 'error', message: 'Unauthorized' });
|
||||||
|
else next();
|
||||||
}
|
}
|
||||||
|
|
||||||
next();
|
// TODO: move this into a new decorator `@IfConfig('executions.mode', 'queue')`
|
||||||
});
|
@Middleware()
|
||||||
|
checkIfCommunityNodesEnabled(req: Request, res: Response, next: NextFunction) {
|
||||||
nodesController.use((req, res, next) => {
|
if (this.config.getEnv('executions.mode') === 'queue' && req.method !== 'GET')
|
||||||
if (config.getEnv('executions.mode') === 'queue' && req.method !== 'GET') {
|
res.status(400).json({
|
||||||
res.status(400).json({
|
status: 'error',
|
||||||
status: 'error',
|
message: 'Package management is disabled when running in "queue" mode',
|
||||||
message: 'Package management is disabled when running in "queue" mode',
|
});
|
||||||
});
|
else next();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
next();
|
@Post('/')
|
||||||
});
|
async installPackage(req: NodeRequest.Post) {
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /nodes
|
|
||||||
*
|
|
||||||
* Install an n8n community package
|
|
||||||
*/
|
|
||||||
nodesController.post(
|
|
||||||
'/',
|
|
||||||
ResponseHelper.send(async (req: NodeRequest.Post) => {
|
|
||||||
const { name } = req.body;
|
const { name } = req.body;
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
throw new ResponseHelper.BadRequestError(PACKAGE_NAME_NOT_PROVIDED);
|
throw new BadRequestError(PACKAGE_NAME_NOT_PROVIDED);
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsed: CommunityPackages.ParsedPackageName;
|
let parsed: CommunityPackages.ParsedPackageName;
|
||||||
|
@ -80,13 +75,13 @@ nodesController.post(
|
||||||
try {
|
try {
|
||||||
parsed = parseNpmPackageName(name);
|
parsed = parseNpmPackageName(name);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new ResponseHelper.BadRequestError(
|
throw new BadRequestError(
|
||||||
error instanceof Error ? error.message : 'Failed to parse package name',
|
error instanceof Error ? error.message : 'Failed to parse package name',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.packageName === STARTER_TEMPLATE_NAME) {
|
if (parsed.packageName === STARTER_TEMPLATE_NAME) {
|
||||||
throw new ResponseHelper.BadRequestError(
|
throw new BadRequestError(
|
||||||
[
|
[
|
||||||
`Package "${parsed.packageName}" is only a template`,
|
`Package "${parsed.packageName}" is only a template`,
|
||||||
'Please enter an actual package to install',
|
'Please enter an actual package to install',
|
||||||
|
@ -98,7 +93,7 @@ nodesController.post(
|
||||||
const hasLoaded = hasPackageLoaded(name);
|
const hasLoaded = hasPackageLoaded(name);
|
||||||
|
|
||||||
if (isInstalled && hasLoaded) {
|
if (isInstalled && hasLoaded) {
|
||||||
throw new ResponseHelper.BadRequestError(
|
throw new BadRequestError(
|
||||||
[
|
[
|
||||||
`Package "${parsed.packageName}" is already installed`,
|
`Package "${parsed.packageName}" is already installed`,
|
||||||
'To update it, click the corresponding button in the UI',
|
'To update it, click the corresponding button in the UI',
|
||||||
|
@ -109,22 +104,19 @@ nodesController.post(
|
||||||
const packageStatus = await checkNpmPackageStatus(name);
|
const packageStatus = await checkNpmPackageStatus(name);
|
||||||
|
|
||||||
if (packageStatus.status !== 'OK') {
|
if (packageStatus.status !== 'OK') {
|
||||||
throw new ResponseHelper.BadRequestError(
|
throw new BadRequestError(`Package "${name}" is banned so it cannot be installed`);
|
||||||
`Package "${name}" is banned so it cannot be installed`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let installedPackage: InstalledPackages;
|
let installedPackage: InstalledPackages;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
installedPackage = await Container.get(LoadNodesAndCredentials).loadNpmModule(
|
installedPackage = await this.loadNodesAndCredentials.loadNpmModule(
|
||||||
parsed.packageName,
|
parsed.packageName,
|
||||||
parsed.version,
|
parsed.version,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON;
|
const errorMessage = error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON;
|
||||||
|
|
||||||
void Container.get(InternalHooks).onCommunityPackageInstallFinished({
|
void this.internalHooks.onCommunityPackageInstallFinished({
|
||||||
user: req.user,
|
user: req.user,
|
||||||
input_string: name,
|
input_string: name,
|
||||||
package_name: parsed.packageName,
|
package_name: parsed.packageName,
|
||||||
|
@ -136,23 +128,20 @@ nodesController.post(
|
||||||
const message = [`Error loading package "${name}"`, errorMessage].join(':');
|
const message = [`Error loading package "${name}"`, errorMessage].join(':');
|
||||||
|
|
||||||
const clientError = error instanceof Error ? isClientError(error) : false;
|
const clientError = error instanceof Error ? isClientError(error) : false;
|
||||||
|
throw new (clientError ? BadRequestError : InternalServerError)(message);
|
||||||
throw new ResponseHelper[clientError ? 'BadRequestError' : 'InternalServerError'](message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasLoaded) removePackageFromMissingList(name);
|
if (!hasLoaded) removePackageFromMissingList(name);
|
||||||
|
|
||||||
const pushInstance = Container.get(Push);
|
|
||||||
|
|
||||||
// broadcast to connected frontends that node list has been updated
|
// broadcast to connected frontends that node list has been updated
|
||||||
installedPackage.installedNodes.forEach((node) => {
|
installedPackage.installedNodes.forEach((node) => {
|
||||||
pushInstance.send('reloadNodeType', {
|
this.push.send('reloadNodeType', {
|
||||||
name: node.type,
|
name: node.type,
|
||||||
version: node.latestVersion,
|
version: node.latestVersion,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
void Container.get(InternalHooks).onCommunityPackageInstallFinished({
|
void this.internalHooks.onCommunityPackageInstallFinished({
|
||||||
user: req.user,
|
user: req.user,
|
||||||
input_string: name,
|
input_string: name,
|
||||||
package_name: parsed.packageName,
|
package_name: parsed.packageName,
|
||||||
|
@ -164,17 +153,10 @@ nodesController.post(
|
||||||
});
|
});
|
||||||
|
|
||||||
return installedPackage;
|
return installedPackage;
|
||||||
}),
|
}
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
@Get('/')
|
||||||
* GET /nodes
|
async getInstalledPackages() {
|
||||||
*
|
|
||||||
* Retrieve list of installed n8n community packages
|
|
||||||
*/
|
|
||||||
nodesController.get(
|
|
||||||
'/',
|
|
||||||
ResponseHelper.send(async (): Promise<PublicInstalledPackage[]> => {
|
|
||||||
const installedPackages = await getAllInstalledPackages();
|
const installedPackages = await getAllInstalledPackages();
|
||||||
|
|
||||||
if (installedPackages.length === 0) return [];
|
if (installedPackages.length === 0) return [];
|
||||||
|
@ -188,7 +170,6 @@ nodesController.get(
|
||||||
// when there are updates, npm exits with code 1
|
// when there are updates, npm exits with code 1
|
||||||
// when there are no updates, command succeeds
|
// when there are no updates, command succeeds
|
||||||
// https://github.com/npm/rfcs/issues/473
|
// https://github.com/npm/rfcs/issues/473
|
||||||
|
|
||||||
if (isNpmError(error) && error.code === 1) {
|
if (isNpmError(error) && error.code === 1) {
|
||||||
pendingUpdates = JSON.parse(error.stdout) as CommunityPackages.AvailableUpdates;
|
pendingUpdates = JSON.parse(error.stdout) as CommunityPackages.AvailableUpdates;
|
||||||
}
|
}
|
||||||
|
@ -197,31 +178,21 @@ nodesController.get(
|
||||||
let hydratedPackages = matchPackagesWithUpdates(installedPackages, pendingUpdates);
|
let hydratedPackages = matchPackagesWithUpdates(installedPackages, pendingUpdates);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const missingPackages = config.get('nodes.packagesMissing') as string | undefined;
|
const missingPackages = this.config.get('nodes.packagesMissing') as string | undefined;
|
||||||
|
|
||||||
if (missingPackages) {
|
if (missingPackages) {
|
||||||
hydratedPackages = matchMissingPackages(hydratedPackages, missingPackages);
|
hydratedPackages = matchMissingPackages(hydratedPackages, missingPackages);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {}
|
||||||
// Do nothing if setting is missing
|
|
||||||
}
|
|
||||||
|
|
||||||
return hydratedPackages;
|
return hydratedPackages;
|
||||||
}),
|
}
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
@Delete('/')
|
||||||
* DELETE /nodes
|
async uninstallPackage(req: NodeRequest.Delete) {
|
||||||
*
|
|
||||||
* Uninstall an installed n8n community package
|
|
||||||
*/
|
|
||||||
nodesController.delete(
|
|
||||||
'/',
|
|
||||||
ResponseHelper.send(async (req: NodeRequest.Delete) => {
|
|
||||||
const { name } = req.query;
|
const { name } = req.query;
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
throw new ResponseHelper.BadRequestError(PACKAGE_NAME_NOT_PROVIDED);
|
throw new BadRequestError(PACKAGE_NAME_NOT_PROVIDED);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -229,37 +200,35 @@ nodesController.delete(
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON;
|
const message = error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON;
|
||||||
|
|
||||||
throw new ResponseHelper.BadRequestError(message);
|
throw new BadRequestError(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const installedPackage = await findInstalledPackage(name);
|
const installedPackage = await findInstalledPackage(name);
|
||||||
|
|
||||||
if (!installedPackage) {
|
if (!installedPackage) {
|
||||||
throw new ResponseHelper.BadRequestError(PACKAGE_NOT_INSTALLED);
|
throw new BadRequestError(PACKAGE_NOT_INSTALLED);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Container.get(LoadNodesAndCredentials).removeNpmModule(name, installedPackage);
|
await this.loadNodesAndCredentials.removeNpmModule(name, installedPackage);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = [
|
const message = [
|
||||||
`Error removing package "${name}"`,
|
`Error removing package "${name}"`,
|
||||||
error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON,
|
error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON,
|
||||||
].join(':');
|
].join(':');
|
||||||
|
|
||||||
throw new ResponseHelper.InternalServerError(message);
|
throw new InternalServerError(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pushInstance = Container.get(Push);
|
|
||||||
|
|
||||||
// broadcast to connected frontends that node list has been updated
|
// broadcast to connected frontends that node list has been updated
|
||||||
installedPackage.installedNodes.forEach((node) => {
|
installedPackage.installedNodes.forEach((node) => {
|
||||||
pushInstance.send('removeNodeType', {
|
this.push.send('removeNodeType', {
|
||||||
name: node.type,
|
name: node.type,
|
||||||
version: node.latestVersion,
|
version: node.latestVersion,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
void Container.get(InternalHooks).onCommunityPackageDeleteFinished({
|
void this.internalHooks.onCommunityPackageDeleteFinished({
|
||||||
user: req.user,
|
user: req.user,
|
||||||
package_name: name,
|
package_name: name,
|
||||||
package_version: installedPackage.installedVersion,
|
package_version: installedPackage.installedVersion,
|
||||||
|
@ -267,53 +236,44 @@ nodesController.delete(
|
||||||
package_author: installedPackage.authorName,
|
package_author: installedPackage.authorName,
|
||||||
package_author_email: installedPackage.authorEmail,
|
package_author_email: installedPackage.authorEmail,
|
||||||
});
|
});
|
||||||
}),
|
}
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
@Patch('/')
|
||||||
* PATCH /nodes
|
async updatePackage(req: NodeRequest.Update) {
|
||||||
*
|
|
||||||
* Update an installed n8n community package
|
|
||||||
*/
|
|
||||||
nodesController.patch(
|
|
||||||
'/',
|
|
||||||
ResponseHelper.send(async (req: NodeRequest.Update) => {
|
|
||||||
const { name } = req.body;
|
const { name } = req.body;
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
throw new ResponseHelper.BadRequestError(PACKAGE_NAME_NOT_PROVIDED);
|
throw new BadRequestError(PACKAGE_NAME_NOT_PROVIDED);
|
||||||
}
|
}
|
||||||
|
|
||||||
const previouslyInstalledPackage = await findInstalledPackage(name);
|
const previouslyInstalledPackage = await findInstalledPackage(name);
|
||||||
|
|
||||||
if (!previouslyInstalledPackage) {
|
if (!previouslyInstalledPackage) {
|
||||||
throw new ResponseHelper.BadRequestError(PACKAGE_NOT_INSTALLED);
|
throw new BadRequestError(PACKAGE_NOT_INSTALLED);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newInstalledPackage = await Container.get(LoadNodesAndCredentials).updateNpmModule(
|
const newInstalledPackage = await this.loadNodesAndCredentials.updateNpmModule(
|
||||||
parseNpmPackageName(name).packageName,
|
parseNpmPackageName(name).packageName,
|
||||||
previouslyInstalledPackage,
|
previouslyInstalledPackage,
|
||||||
);
|
);
|
||||||
|
|
||||||
const pushInstance = Container.get(Push);
|
|
||||||
|
|
||||||
// broadcast to connected frontends that node list has been updated
|
// broadcast to connected frontends that node list has been updated
|
||||||
previouslyInstalledPackage.installedNodes.forEach((node) => {
|
previouslyInstalledPackage.installedNodes.forEach((node) => {
|
||||||
pushInstance.send('removeNodeType', {
|
this.push.send('removeNodeType', {
|
||||||
name: node.type,
|
name: node.type,
|
||||||
version: node.latestVersion,
|
version: node.latestVersion,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
newInstalledPackage.installedNodes.forEach((node) => {
|
newInstalledPackage.installedNodes.forEach((node) => {
|
||||||
pushInstance.send('reloadNodeType', {
|
this.push.send('reloadNodeType', {
|
||||||
name: node.name,
|
name: node.name,
|
||||||
version: node.latestVersion,
|
version: node.latestVersion,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
void Container.get(InternalHooks).onCommunityPackageUpdateFinished({
|
void this.internalHooks.onCommunityPackageUpdateFinished({
|
||||||
user: req.user,
|
user: req.user,
|
||||||
package_name: name,
|
package_name: name,
|
||||||
package_version_current: previouslyInstalledPackage.installedVersion,
|
package_version_current: previouslyInstalledPackage.installedVersion,
|
||||||
|
@ -326,8 +286,7 @@ nodesController.patch(
|
||||||
return newInstalledPackage;
|
return newInstalledPackage;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
previouslyInstalledPackage.installedNodes.forEach((node) => {
|
previouslyInstalledPackage.installedNodes.forEach((node) => {
|
||||||
const pushInstance = Container.get(Push);
|
this.push.send('removeNodeType', {
|
||||||
pushInstance.send('removeNodeType', {
|
|
||||||
name: node.type,
|
name: node.type,
|
||||||
version: node.latestVersion,
|
version: node.latestVersion,
|
||||||
});
|
});
|
||||||
|
@ -338,7 +297,7 @@ nodesController.patch(
|
||||||
error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON,
|
error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON,
|
||||||
].join(':');
|
].join(':');
|
||||||
|
|
||||||
throw new ResponseHelper.InternalServerError(message);
|
throw new InternalServerError(message);
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
);
|
}
|
102
packages/cli/src/controllers/tags.controller.ts
Normal file
102
packages/cli/src/controllers/tags.controller.ts
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import type { Repository } from 'typeorm';
|
||||||
|
import type { Config } from '@/config';
|
||||||
|
import { Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators';
|
||||||
|
import type { IDatabaseCollections, IExternalHooksClass, ITagWithCountDb } from '@/Interfaces';
|
||||||
|
import { TagEntity } from '@db/entities/TagEntity';
|
||||||
|
import { getTagsWithCountDb } from '@/TagHelpers';
|
||||||
|
import { validateEntity } from '@/GenericHelpers';
|
||||||
|
import { BadRequestError, UnauthorizedError } from '@/ResponseHelper';
|
||||||
|
import { TagsRequest } from '@/requests';
|
||||||
|
|
||||||
|
@RestController('/tags')
|
||||||
|
export class TagsController {
|
||||||
|
private config: Config;
|
||||||
|
|
||||||
|
private externalHooks: IExternalHooksClass;
|
||||||
|
|
||||||
|
private tagsRepository: Repository<TagEntity>;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
config,
|
||||||
|
externalHooks,
|
||||||
|
repositories,
|
||||||
|
}: {
|
||||||
|
config: Config;
|
||||||
|
externalHooks: IExternalHooksClass;
|
||||||
|
repositories: Pick<IDatabaseCollections, 'Tag'>;
|
||||||
|
}) {
|
||||||
|
this.config = config;
|
||||||
|
this.externalHooks = externalHooks;
|
||||||
|
this.tagsRepository = repositories.Tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: move this into a new decorator `@IfEnabled('workflowTagsDisabled')`
|
||||||
|
@Middleware()
|
||||||
|
workflowsEnabledMiddleware(req: Request, res: Response, next: NextFunction) {
|
||||||
|
if (this.config.getEnv('workflowTagsDisabled'))
|
||||||
|
throw new BadRequestError('Workflow tags are disabled');
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieves all tags, with or without usage count
|
||||||
|
@Get('/')
|
||||||
|
async getAll(req: TagsRequest.GetAll): Promise<TagEntity[] | ITagWithCountDb[]> {
|
||||||
|
const { withUsageCount } = req.query;
|
||||||
|
if (withUsageCount === 'true') {
|
||||||
|
const tablePrefix = this.config.getEnv('database.tablePrefix');
|
||||||
|
return getTagsWithCountDb(tablePrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.tagsRepository.find({ select: ['id', 'name', 'createdAt', 'updatedAt'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a tag
|
||||||
|
@Post('/')
|
||||||
|
async createTag(req: TagsRequest.Create): Promise<TagEntity> {
|
||||||
|
const newTag = new TagEntity();
|
||||||
|
newTag.name = req.body.name.trim();
|
||||||
|
|
||||||
|
await this.externalHooks.run('tag.beforeCreate', [newTag]);
|
||||||
|
await validateEntity(newTag);
|
||||||
|
|
||||||
|
const tag = await this.tagsRepository.save(newTag);
|
||||||
|
await this.externalHooks.run('tag.afterCreate', [tag]);
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates a tag
|
||||||
|
@Patch('/:id(\\d+)')
|
||||||
|
async updateTag(req: TagsRequest.Update): Promise<TagEntity> {
|
||||||
|
const { name } = req.body;
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const newTag = new TagEntity();
|
||||||
|
newTag.id = id;
|
||||||
|
newTag.name = name.trim();
|
||||||
|
|
||||||
|
await this.externalHooks.run('tag.beforeUpdate', [newTag]);
|
||||||
|
await validateEntity(newTag);
|
||||||
|
|
||||||
|
const tag = await this.tagsRepository.save(newTag);
|
||||||
|
await this.externalHooks.run('tag.afterUpdate', [tag]);
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('/:id(\\d+)')
|
||||||
|
async deleteTag(req: TagsRequest.Delete) {
|
||||||
|
const isInstanceOwnerSetUp = this.config.getEnv('userManagement.isInstanceOwnerSetUp');
|
||||||
|
if (isInstanceOwnerSetUp && req.user.globalRole.name !== 'owner') {
|
||||||
|
throw new UnauthorizedError(
|
||||||
|
'You are not allowed to perform this action',
|
||||||
|
'Only owners can remove tags',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const { id } = req.params;
|
||||||
|
await this.externalHooks.run('tag.beforeDelete', [id]);
|
||||||
|
|
||||||
|
await this.tagsRepository.delete({ id });
|
||||||
|
await this.externalHooks.run('tag.afterDelete', [id]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
11
packages/cli/src/decorators/Middleware.ts
Normal file
11
packages/cli/src/decorators/Middleware.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { CONTROLLER_MIDDLEWARES } from './constants';
|
||||||
|
import type { MiddlewareMetadata } from './types';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
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);
|
||||||
|
};
|
|
@ -15,5 +15,6 @@ const RouteFactory =
|
||||||
|
|
||||||
export const Get = RouteFactory('get');
|
export const Get = RouteFactory('get');
|
||||||
export const Post = RouteFactory('post');
|
export const Post = RouteFactory('post');
|
||||||
|
export const Put = RouteFactory('put');
|
||||||
export const Patch = RouteFactory('patch');
|
export const Patch = RouteFactory('patch');
|
||||||
export const Delete = RouteFactory('delete');
|
export const Delete = RouteFactory('delete');
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export const CONTROLLER_ROUTES = 'CONTROLLER_ROUTES';
|
export const CONTROLLER_ROUTES = 'CONTROLLER_ROUTES';
|
||||||
export const CONTROLLER_BASE_PATH = 'CONTROLLER_BASE_PATH';
|
export const CONTROLLER_BASE_PATH = 'CONTROLLER_BASE_PATH';
|
||||||
|
export const CONTROLLER_MIDDLEWARES = 'CONTROLLER_MIDDLEWARES';
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export { RestController } from './RestController';
|
export { RestController } from './RestController';
|
||||||
export { Get, Post, Patch, Delete } from './Route';
|
export { Get, Post, Put, Patch, Delete } from './Route';
|
||||||
|
export { Middleware } from './Middleware';
|
||||||
export { registerController } from './registerController';
|
export { registerController } from './registerController';
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import type { Config } from '@/config';
|
import type { Config } from '@/config';
|
||||||
import { CONTROLLER_BASE_PATH, CONTROLLER_ROUTES } from './constants';
|
import { CONTROLLER_BASE_PATH, CONTROLLER_MIDDLEWARES, CONTROLLER_ROUTES } from './constants';
|
||||||
import { send } from '@/ResponseHelper'; // TODO: move `ResponseHelper.send` to this file
|
import { send } from '@/ResponseHelper'; // TODO: move `ResponseHelper.send` to this file
|
||||||
import type { Application, Request, Response } from 'express';
|
import type { Application, Request, Response, RequestHandler } from 'express';
|
||||||
import type { Controller, RouteMetadata } from './types';
|
import type { Controller, MiddlewareMetadata, RouteMetadata } from './types';
|
||||||
|
|
||||||
export const registerController = (app: Application, config: Config, controller: object) => {
|
export const registerController = (app: Application, config: Config, controller: object) => {
|
||||||
const controllerClass = controller.constructor;
|
const controllerClass = controller.constructor;
|
||||||
|
@ -20,9 +20,17 @@ export const registerController = (app: Application, config: Config, controller:
|
||||||
const restBasePath = config.getEnv('endpoints.rest');
|
const restBasePath = config.getEnv('endpoints.rest');
|
||||||
const prefix = `/${[restBasePath, controllerBasePath].join('/')}`.replace(/\/+/g, '/');
|
const prefix = `/${[restBasePath, controllerBasePath].join('/')}`.replace(/\/+/g, '/');
|
||||||
|
|
||||||
|
const middlewares = (
|
||||||
|
(Reflect.getMetadata(CONTROLLER_MIDDLEWARES, controllerClass) ?? []) as MiddlewareMetadata[]
|
||||||
|
).map(
|
||||||
|
({ handlerName }) =>
|
||||||
|
(controller as Controller)[handlerName].bind(controller) as RequestHandler,
|
||||||
|
);
|
||||||
|
|
||||||
routes.forEach(({ method, path, handlerName }) => {
|
routes.forEach(({ method, path, handlerName }) => {
|
||||||
router[method](
|
router[method](
|
||||||
path,
|
path,
|
||||||
|
...middlewares,
|
||||||
send(async (req: Request, res: Response) =>
|
send(async (req: Request, res: Response) =>
|
||||||
(controller as Controller)[handlerName](req, res),
|
(controller as Controller)[handlerName](req, res),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
|
|
||||||
export type Method = 'get' | 'post' | 'patch' | 'delete';
|
export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete';
|
||||||
|
|
||||||
|
export interface MiddlewareMetadata {
|
||||||
|
handlerName: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RouteMetadata {
|
export interface RouteMetadata {
|
||||||
method: Method;
|
method: Method;
|
||||||
|
|
|
@ -3,7 +3,6 @@ export * from './CredentialsHelper';
|
||||||
export * from './CredentialTypes';
|
export * from './CredentialTypes';
|
||||||
export * from './CredentialsOverwrites';
|
export * from './CredentialsOverwrites';
|
||||||
export * from './Interfaces';
|
export * from './Interfaces';
|
||||||
export * from './NodeTypes';
|
|
||||||
export * from './WaitingWebhooks';
|
export * from './WaitingWebhooks';
|
||||||
export * from './WorkflowCredentials';
|
export * from './WorkflowCredentials';
|
||||||
export * from './WorkflowRunner';
|
export * from './WorkflowRunner';
|
||||||
|
|
|
@ -18,7 +18,7 @@ import {
|
||||||
} from '@/UserManagement/UserManagementHelper';
|
} from '@/UserManagement/UserManagementHelper';
|
||||||
import type { Repository } from 'typeorm';
|
import type { Repository } from 'typeorm';
|
||||||
import type { User } from '@db/entities/User';
|
import type { User } from '@db/entities/User';
|
||||||
import { SamlUrls } from '../sso/saml/constants';
|
import { SamlUrls } from '@/sso/saml/constants';
|
||||||
|
|
||||||
const jwtFromRequest = (req: Request) => {
|
const jwtFromRequest = (req: Request) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
|
|
@ -330,6 +330,9 @@ export type NodeListSearchRequest = AuthenticatedRequest<
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
|
||||||
export declare namespace TagsRequest {
|
export declare namespace TagsRequest {
|
||||||
|
type GetAll = AuthenticatedRequest<{}, {}, {}, { withUsageCount: string }>;
|
||||||
|
type Create = AuthenticatedRequest<{}, {}, { name: string }>;
|
||||||
|
type Update = AuthenticatedRequest<{ id: string }, {}, { name: string }>;
|
||||||
type Delete = AuthenticatedRequest<{ id: string }>;
|
type Delete = AuthenticatedRequest<{ id: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,13 +49,11 @@ beforeAll(async () => {
|
||||||
|
|
||||||
authAgent = utils.createAuthAgent(app);
|
authAgent = utils.createAuthAgent(app);
|
||||||
|
|
||||||
config.set(LDAP_ENABLED, true);
|
|
||||||
defaultLdapConfig.bindingAdminPassword = await encryptPassword(
|
defaultLdapConfig.bindingAdminPassword = await encryptPassword(
|
||||||
defaultLdapConfig.bindingAdminPassword,
|
defaultLdapConfig.bindingAdminPassword,
|
||||||
);
|
);
|
||||||
|
|
||||||
utils.initConfigFile();
|
utils.initConfigFile();
|
||||||
await utils.initLdapManager();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|
|
@ -31,11 +31,8 @@ import { DeepPartial } from 'ts-essentials';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||||
import { CredentialTypes } from '@/CredentialTypes';
|
|
||||||
import { ExternalHooks } from '@/ExternalHooks';
|
import { ExternalHooks } from '@/ExternalHooks';
|
||||||
import { NodeTypes } from '@/NodeTypes';
|
|
||||||
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
||||||
import { nodesController } from '@/api/nodes.api';
|
|
||||||
import { workflowsController } from '@/workflows/workflows.controller';
|
import { workflowsController } from '@/workflows/workflows.controller';
|
||||||
import { AUTH_COOKIE_NAME, NODE_PACKAGE_PREFIX } from '@/constants';
|
import { AUTH_COOKIE_NAME, NODE_PACKAGE_PREFIX } from '@/constants';
|
||||||
import { credentialsController } from '@/credentials/credentials.controller';
|
import { credentialsController } from '@/credentials/credentials.controller';
|
||||||
|
@ -65,7 +62,9 @@ import { eventBusRouter } from '@/eventbus/eventBusRoutes';
|
||||||
import { registerController } from '@/decorators';
|
import { registerController } from '@/decorators';
|
||||||
import {
|
import {
|
||||||
AuthController,
|
AuthController,
|
||||||
|
LdapController,
|
||||||
MeController,
|
MeController,
|
||||||
|
NodesController,
|
||||||
OwnerController,
|
OwnerController,
|
||||||
PasswordResetController,
|
PasswordResetController,
|
||||||
UsersController,
|
UsersController,
|
||||||
|
@ -74,11 +73,13 @@ import { setupAuthMiddlewares } from '@/middlewares';
|
||||||
import * as testDb from '../shared/testDb';
|
import * as testDb from '../shared/testDb';
|
||||||
|
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { handleLdapInit } from '@/Ldap/helpers';
|
|
||||||
import { ldapController } from '@/Ldap/routes/ldap.controller.ee';
|
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
import { InternalHooks } from '@/InternalHooks';
|
||||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
import { PostHogClient } from '@/posthog';
|
import { PostHogClient } from '@/posthog';
|
||||||
|
import { LdapManager } from '@/Ldap/LdapManager.ee';
|
||||||
|
import { LDAP_ENABLED } from '@/Ldap/constants';
|
||||||
|
import { handleLdapInit } from '@/Ldap/helpers';
|
||||||
|
import { Push } from '@/push';
|
||||||
|
|
||||||
export const mockInstance = <T>(
|
export const mockInstance = <T>(
|
||||||
ctor: new (...args: any[]) => T,
|
ctor: new (...args: any[]) => T,
|
||||||
|
@ -155,10 +156,8 @@ export async function initTestServer({
|
||||||
const map: Record<string, express.Router | express.Router[] | any> = {
|
const map: Record<string, express.Router | express.Router[] | any> = {
|
||||||
credentials: { controller: credentialsController, path: 'credentials' },
|
credentials: { controller: credentialsController, path: 'credentials' },
|
||||||
workflows: { controller: workflowsController, path: 'workflows' },
|
workflows: { controller: workflowsController, path: 'workflows' },
|
||||||
nodes: { controller: nodesController, path: 'nodes' },
|
|
||||||
license: { controller: licenseController, path: 'license' },
|
license: { controller: licenseController, path: 'license' },
|
||||||
eventBus: { controller: eventBusRouter, path: 'eventbus' },
|
eventBus: { controller: eventBusRouter, path: 'eventbus' },
|
||||||
ldap: { controller: ldapController, path: 'ldap' },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (enablePublicAPI) {
|
if (enablePublicAPI) {
|
||||||
|
@ -190,6 +189,27 @@ export async function initTestServer({
|
||||||
new AuthController({ config, logger, internalHooks, repositories }),
|
new AuthController({ config, logger, internalHooks, repositories }),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case 'ldap':
|
||||||
|
config.set(LDAP_ENABLED, true);
|
||||||
|
await handleLdapInit();
|
||||||
|
const { service, sync } = LdapManager.getInstance();
|
||||||
|
registerController(
|
||||||
|
testServer.app,
|
||||||
|
config,
|
||||||
|
new LdapController(service, sync, internalHooks),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'nodes':
|
||||||
|
registerController(
|
||||||
|
testServer.app,
|
||||||
|
config,
|
||||||
|
new NodesController(
|
||||||
|
config,
|
||||||
|
Container.get(LoadNodesAndCredentials),
|
||||||
|
Container.get(Push),
|
||||||
|
internalHooks,
|
||||||
|
),
|
||||||
|
);
|
||||||
case 'me':
|
case 'me':
|
||||||
registerController(
|
registerController(
|
||||||
testServer.app,
|
testServer.app,
|
||||||
|
@ -246,15 +266,7 @@ const classifyEndpointGroups = (endpointGroups: EndpointGroup[]) => {
|
||||||
const routerEndpoints: EndpointGroup[] = [];
|
const routerEndpoints: EndpointGroup[] = [];
|
||||||
const functionEndpoints: EndpointGroup[] = [];
|
const functionEndpoints: EndpointGroup[] = [];
|
||||||
|
|
||||||
const ROUTER_GROUP = [
|
const ROUTER_GROUP = ['credentials', 'workflows', 'publicApi', 'eventBus', 'license'];
|
||||||
'credentials',
|
|
||||||
'nodes',
|
|
||||||
'workflows',
|
|
||||||
'publicApi',
|
|
||||||
'ldap',
|
|
||||||
'eventBus',
|
|
||||||
'license',
|
|
||||||
];
|
|
||||||
|
|
||||||
endpointGroups.forEach((group) =>
|
endpointGroups.forEach((group) =>
|
||||||
(ROUTER_GROUP.includes(group) ? routerEndpoints : functionEndpoints).push(group),
|
(ROUTER_GROUP.includes(group) ? routerEndpoints : functionEndpoints).push(group),
|
||||||
|
@ -320,13 +332,6 @@ export async function initCredentialsTypes(): Promise<void> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize LDAP manager.
|
|
||||||
*/
|
|
||||||
export async function initLdapManager(): Promise<void> {
|
|
||||||
await handleLdapInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize node types.
|
* Initialize node types.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in a new issue