2022-08-02 01:40:57 -07:00
|
|
|
import express from 'express';
|
|
|
|
import { PublicInstalledPackage } from 'n8n-workflow';
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2022-11-09 06:25:00 -08:00
|
|
|
import config from '@/config';
|
|
|
|
import { InternalHooksManager } from '@/InternalHooksManager';
|
|
|
|
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
|
|
|
import * as Push from '@/Push';
|
|
|
|
import * as ResponseHelper from '@/ResponseHelper';
|
2022-08-02 01:40:57 -07:00
|
|
|
|
2022-08-03 09:10:59 -07:00
|
|
|
import {
|
2022-08-02 01:40:57 -07:00
|
|
|
checkNpmPackageStatus,
|
2022-08-30 02:54:50 -07:00
|
|
|
executeCommand,
|
2022-08-02 01:40:57 -07:00
|
|
|
hasPackageLoaded,
|
|
|
|
isClientError,
|
|
|
|
isNpmError,
|
2022-08-30 02:54:50 -07:00
|
|
|
matchMissingPackages,
|
|
|
|
matchPackagesWithUpdates,
|
|
|
|
parseNpmPackageName,
|
|
|
|
removePackageFromMissingList,
|
|
|
|
sanitizeNpmPackageName,
|
2022-11-09 06:25:00 -08:00
|
|
|
} from '@/CommunityNodes/helpers';
|
2022-08-02 01:40:57 -07:00
|
|
|
import {
|
|
|
|
findInstalledPackage,
|
2022-08-30 02:54:50 -07:00
|
|
|
getAllInstalledPackages,
|
2022-08-02 01:40:57 -07:00
|
|
|
isPackageInstalled,
|
2022-11-09 06:25:00 -08:00
|
|
|
} from '@/CommunityNodes/packageModel';
|
2022-08-30 02:54:50 -07:00
|
|
|
import {
|
|
|
|
RESPONSE_ERROR_MESSAGES,
|
|
|
|
STARTER_TEMPLATE_NAME,
|
|
|
|
UNKNOWN_FAILURE_REASON,
|
2022-11-09 06:25:00 -08:00
|
|
|
} from '@/constants';
|
|
|
|
import { isAuthenticatedRequest } from '@/UserManagement/UserManagementHelper';
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2022-11-09 06:25:00 -08:00
|
|
|
import { InstalledPackages } from '@db/entities/InstalledPackages';
|
|
|
|
import type { CommunityPackages } from '@/Interfaces';
|
|
|
|
import type { NodeRequest } from '@/requests';
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
const { PACKAGE_NOT_INSTALLED, PACKAGE_NAME_NOT_PROVIDED } = RESPONSE_ERROR_MESSAGES;
|
|
|
|
|
|
|
|
export const nodesController = express.Router();
|
2022-07-20 07:24:03 -07:00
|
|
|
|
|
|
|
nodesController.use((req, res, next) => {
|
|
|
|
if (!isAuthenticatedRequest(req) || req.user.globalRole.name !== 'owner') {
|
|
|
|
res.status(403).json({ status: 'error', message: 'Unauthorized' });
|
|
|
|
return;
|
|
|
|
}
|
2022-08-02 01:40:57 -07:00
|
|
|
|
2022-07-20 07:24:03 -07:00
|
|
|
next();
|
|
|
|
});
|
|
|
|
|
|
|
|
nodesController.use((req, res, next) => {
|
|
|
|
if (config.getEnv('executions.mode') === 'queue' && req.method !== 'GET') {
|
|
|
|
res.status(400).json({
|
|
|
|
status: 'error',
|
|
|
|
message: 'Package management is disabled when running in "queue" mode',
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
2022-08-02 01:40:57 -07:00
|
|
|
|
2022-07-20 07:24:03 -07:00
|
|
|
next();
|
|
|
|
});
|
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
/**
|
|
|
|
* POST /nodes
|
|
|
|
*
|
|
|
|
* Install an n8n community package
|
|
|
|
*/
|
2022-07-20 07:24:03 -07:00
|
|
|
nodesController.post(
|
|
|
|
'/',
|
|
|
|
ResponseHelper.send(async (req: NodeRequest.Post) => {
|
|
|
|
const { name } = req.body;
|
2022-08-02 01:40:57 -07:00
|
|
|
|
|
|
|
if (!name) {
|
2022-11-22 05:00:36 -08:00
|
|
|
throw new ResponseHelper.BadRequestError(PACKAGE_NAME_NOT_PROVIDED);
|
2022-08-02 01:40:57 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
let parsed: CommunityPackages.ParsedPackageName;
|
|
|
|
|
2022-07-20 07:24:03 -07:00
|
|
|
try {
|
2022-08-02 01:40:57 -07:00
|
|
|
parsed = parseNpmPackageName(name);
|
2022-07-20 07:24:03 -07:00
|
|
|
} catch (error) {
|
2022-11-22 05:00:36 -08:00
|
|
|
throw new ResponseHelper.BadRequestError(
|
2022-08-02 01:40:57 -07:00
|
|
|
error instanceof Error ? error.message : 'Failed to parse package name',
|
2022-08-03 09:10:59 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (parsed.packageName === STARTER_TEMPLATE_NAME) {
|
2022-11-22 05:00:36 -08:00
|
|
|
throw new ResponseHelper.BadRequestError(
|
2022-08-03 09:10:59 -07:00
|
|
|
[
|
|
|
|
`Package "${parsed.packageName}" is only a template`,
|
|
|
|
'Please enter an actual package to install',
|
|
|
|
].join('.'),
|
2022-08-02 01:40:57 -07:00
|
|
|
);
|
2022-07-20 07:24:03 -07:00
|
|
|
}
|
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
const isInstalled = await isPackageInstalled(parsed.packageName);
|
|
|
|
const hasLoaded = hasPackageLoaded(name);
|
|
|
|
|
|
|
|
if (isInstalled && hasLoaded) {
|
2022-11-22 05:00:36 -08:00
|
|
|
throw new ResponseHelper.BadRequestError(
|
2022-08-02 01:40:57 -07:00
|
|
|
[
|
|
|
|
`Package "${parsed.packageName}" is already installed`,
|
|
|
|
'To update it, click the corresponding button in the UI',
|
|
|
|
].join('.'),
|
2022-07-20 07:24:03 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
const packageStatus = await checkNpmPackageStatus(name);
|
|
|
|
|
2022-07-20 07:24:03 -07:00
|
|
|
if (packageStatus.status !== 'OK') {
|
2022-11-22 05:00:36 -08:00
|
|
|
throw new ResponseHelper.BadRequestError(
|
2022-08-02 01:40:57 -07:00
|
|
|
`Package "${name}" is banned so it cannot be installed`,
|
2022-07-20 07:24:03 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
let installedPackage: InstalledPackages;
|
|
|
|
|
2022-07-20 07:24:03 -07:00
|
|
|
try {
|
2022-08-02 01:40:57 -07:00
|
|
|
installedPackage = await LoadNodesAndCredentials().loadNpmModule(
|
|
|
|
parsed.packageName,
|
|
|
|
parsed.version,
|
2022-07-20 07:24:03 -07:00
|
|
|
);
|
|
|
|
} catch (error) {
|
2022-08-02 01:40:57 -07:00
|
|
|
const errorMessage = error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON;
|
2022-07-20 07:24:03 -07:00
|
|
|
|
|
|
|
void InternalHooksManager.getInstance().onCommunityPackageInstallFinished({
|
|
|
|
user_id: req.user.id,
|
|
|
|
input_string: name,
|
2022-08-02 01:40:57 -07:00
|
|
|
package_name: parsed.packageName,
|
2022-07-20 07:24:03 -07:00
|
|
|
success: false,
|
2022-08-02 01:40:57 -07:00
|
|
|
package_version: parsed.version,
|
2022-07-20 07:24:03 -07:00
|
|
|
failure_reason: errorMessage,
|
|
|
|
});
|
2022-08-02 01:40:57 -07:00
|
|
|
|
|
|
|
const message = [`Error loading package "${name}"`, errorMessage].join(':');
|
|
|
|
|
|
|
|
const clientError = error instanceof Error ? isClientError(error) : false;
|
|
|
|
|
2022-11-22 05:00:36 -08:00
|
|
|
throw new ResponseHelper[clientError ? 'BadRequestError' : 'InternalServerError'](message);
|
2022-07-20 07:24:03 -07:00
|
|
|
}
|
2022-08-02 01:40:57 -07:00
|
|
|
|
|
|
|
if (!hasLoaded) removePackageFromMissingList(name);
|
|
|
|
|
|
|
|
const pushInstance = Push.getInstance();
|
|
|
|
|
|
|
|
// broadcast to connected frontends that node list has been updated
|
|
|
|
installedPackage.installedNodes.forEach((node) => {
|
|
|
|
pushInstance.send('reloadNodeType', {
|
|
|
|
name: node.type,
|
|
|
|
version: node.latestVersion,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
void InternalHooksManager.getInstance().onCommunityPackageInstallFinished({
|
|
|
|
user_id: req.user.id,
|
|
|
|
input_string: name,
|
|
|
|
package_name: parsed.packageName,
|
|
|
|
success: true,
|
|
|
|
package_version: parsed.version,
|
|
|
|
package_node_names: installedPackage.installedNodes.map((node) => node.name),
|
|
|
|
package_author: installedPackage.authorName,
|
|
|
|
package_author_email: installedPackage.authorEmail,
|
|
|
|
});
|
|
|
|
|
|
|
|
return installedPackage;
|
2022-07-20 07:24:03 -07:00
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
/**
|
|
|
|
* GET /nodes
|
|
|
|
*
|
|
|
|
* Retrieve list of installed n8n community packages
|
|
|
|
*/
|
2022-07-20 07:24:03 -07:00
|
|
|
nodesController.get(
|
|
|
|
'/',
|
|
|
|
ResponseHelper.send(async (): Promise<PublicInstalledPackage[]> => {
|
2022-08-02 01:40:57 -07:00
|
|
|
const installedPackages = await getAllInstalledPackages();
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
if (installedPackages.length === 0) return [];
|
|
|
|
|
|
|
|
let pendingUpdates: CommunityPackages.AvailableUpdates | undefined;
|
2022-07-20 07:24:03 -07:00
|
|
|
|
|
|
|
try {
|
2022-08-02 01:40:57 -07:00
|
|
|
const command = ['npm', 'outdated', '--json'].join(' ');
|
|
|
|
await executeCommand(command, { doNotHandleError: true });
|
2022-07-20 07:24:03 -07:00
|
|
|
} catch (error) {
|
2022-08-02 01:40:57 -07:00
|
|
|
// when there are updates, npm exits with code 1
|
|
|
|
// when there are no updates, command succeeds
|
|
|
|
// https://github.com/npm/rfcs/issues/473
|
|
|
|
|
|
|
|
if (isNpmError(error) && error.code === 1) {
|
|
|
|
pendingUpdates = JSON.parse(error.stdout) as CommunityPackages.AvailableUpdates;
|
2022-07-20 07:24:03 -07:00
|
|
|
}
|
|
|
|
}
|
2022-08-02 01:40:57 -07:00
|
|
|
|
|
|
|
let hydratedPackages = matchPackagesWithUpdates(installedPackages, pendingUpdates);
|
|
|
|
|
2022-07-20 07:24:03 -07:00
|
|
|
try {
|
2022-08-02 01:40:57 -07:00
|
|
|
const missingPackages = config.get('nodes.packagesMissing') as string | undefined;
|
|
|
|
|
|
|
|
if (missingPackages) {
|
|
|
|
hydratedPackages = matchMissingPackages(hydratedPackages, missingPackages);
|
2022-07-20 07:24:03 -07:00
|
|
|
}
|
2022-08-02 01:40:57 -07:00
|
|
|
} catch (_) {
|
2022-07-20 07:24:03 -07:00
|
|
|
// Do nothing if setting is missing
|
|
|
|
}
|
2022-08-02 01:40:57 -07:00
|
|
|
|
2022-07-20 07:24:03 -07:00
|
|
|
return hydratedPackages;
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
/**
|
|
|
|
* DELETE /nodes
|
|
|
|
*
|
|
|
|
* Uninstall an installed n8n community package
|
|
|
|
*/
|
2022-07-20 07:24:03 -07:00
|
|
|
nodesController.delete(
|
|
|
|
'/',
|
|
|
|
ResponseHelper.send(async (req: NodeRequest.Delete) => {
|
2022-08-30 02:54:50 -07:00
|
|
|
const { name } = req.query;
|
2022-08-02 01:40:57 -07:00
|
|
|
|
2022-07-20 07:24:03 -07:00
|
|
|
if (!name) {
|
2022-11-22 05:00:36 -08:00
|
|
|
throw new ResponseHelper.BadRequestError(PACKAGE_NAME_NOT_PROVIDED);
|
2022-07-20 07:24:03 -07:00
|
|
|
}
|
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
try {
|
|
|
|
sanitizeNpmPackageName(name);
|
|
|
|
} catch (error) {
|
|
|
|
const message = error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON;
|
|
|
|
|
2022-11-22 05:00:36 -08:00
|
|
|
throw new ResponseHelper.BadRequestError(message);
|
2022-08-02 01:40:57 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
const installedPackage = await findInstalledPackage(name);
|
2022-07-20 07:24:03 -07:00
|
|
|
|
|
|
|
if (!installedPackage) {
|
2022-11-22 05:00:36 -08:00
|
|
|
throw new ResponseHelper.BadRequestError(PACKAGE_NOT_INSTALLED);
|
2022-07-20 07:24:03 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2022-08-02 01:40:57 -07:00
|
|
|
await LoadNodesAndCredentials().removeNpmModule(name, installedPackage);
|
|
|
|
} catch (error) {
|
|
|
|
const message = [
|
|
|
|
`Error removing package "${name}"`,
|
|
|
|
error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON,
|
|
|
|
].join(':');
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2022-11-22 05:00:36 -08:00
|
|
|
throw new ResponseHelper.InternalServerError(message);
|
2022-08-02 01:40:57 -07:00
|
|
|
}
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
const pushInstance = Push.getInstance();
|
|
|
|
|
|
|
|
// broadcast to connected frontends that node list has been updated
|
|
|
|
installedPackage.installedNodes.forEach((node) => {
|
|
|
|
pushInstance.send('removeNodeType', {
|
|
|
|
name: node.type,
|
|
|
|
version: node.latestVersion,
|
2022-07-20 07:24:03 -07:00
|
|
|
});
|
2022-08-02 01:40:57 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
void InternalHooksManager.getInstance().onCommunityPackageDeleteFinished({
|
|
|
|
user_id: req.user.id,
|
|
|
|
package_name: name,
|
|
|
|
package_version: installedPackage.installedVersion,
|
|
|
|
package_node_names: installedPackage.installedNodes.map((node) => node.name),
|
|
|
|
package_author: installedPackage.authorName,
|
|
|
|
package_author_email: installedPackage.authorEmail,
|
|
|
|
});
|
2022-07-20 07:24:03 -07:00
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
/**
|
|
|
|
* PATCH /nodes
|
|
|
|
*
|
|
|
|
* Update an installed n8n community package
|
|
|
|
*/
|
2022-07-20 07:24:03 -07:00
|
|
|
nodesController.patch(
|
|
|
|
'/',
|
|
|
|
ResponseHelper.send(async (req: NodeRequest.Update) => {
|
|
|
|
const { name } = req.body;
|
2022-08-02 01:40:57 -07:00
|
|
|
|
2022-07-20 07:24:03 -07:00
|
|
|
if (!name) {
|
2022-11-22 05:00:36 -08:00
|
|
|
throw new ResponseHelper.BadRequestError(PACKAGE_NAME_NOT_PROVIDED);
|
2022-07-20 07:24:03 -07:00
|
|
|
}
|
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
const previouslyInstalledPackage = await findInstalledPackage(name);
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
if (!previouslyInstalledPackage) {
|
2022-11-22 05:00:36 -08:00
|
|
|
throw new ResponseHelper.BadRequestError(PACKAGE_NOT_INSTALLED);
|
2022-07-20 07:24:03 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
const newInstalledPackage = await LoadNodesAndCredentials().updateNpmModule(
|
2022-08-02 01:40:57 -07:00
|
|
|
parseNpmPackageName(name).packageName,
|
|
|
|
previouslyInstalledPackage,
|
2022-07-20 07:24:03 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
const pushInstance = Push.getInstance();
|
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
// broadcast to connected frontends that node list has been updated
|
|
|
|
previouslyInstalledPackage.installedNodes.forEach((node) => {
|
2022-07-20 07:24:03 -07:00
|
|
|
pushInstance.send('removeNodeType', {
|
2022-08-02 01:40:57 -07:00
|
|
|
name: node.type,
|
|
|
|
version: node.latestVersion,
|
2022-07-20 07:24:03 -07:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
newInstalledPackage.installedNodes.forEach((node) => {
|
2022-07-20 07:24:03 -07:00
|
|
|
pushInstance.send('reloadNodeType', {
|
2022-08-02 01:40:57 -07:00
|
|
|
name: node.name,
|
|
|
|
version: node.latestVersion,
|
2022-07-20 07:24:03 -07:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
void InternalHooksManager.getInstance().onCommunityPackageUpdateFinished({
|
|
|
|
user_id: req.user.id,
|
|
|
|
package_name: name,
|
2022-08-02 01:40:57 -07:00
|
|
|
package_version_current: previouslyInstalledPackage.installedVersion,
|
2022-07-20 07:24:03 -07:00
|
|
|
package_version_new: newInstalledPackage.installedVersion,
|
|
|
|
package_node_names: newInstalledPackage.installedNodes.map((node) => node.name),
|
|
|
|
package_author: newInstalledPackage.authorName,
|
|
|
|
package_author_email: newInstalledPackage.authorEmail,
|
|
|
|
});
|
|
|
|
|
|
|
|
return newInstalledPackage;
|
|
|
|
} catch (error) {
|
2022-08-02 01:40:57 -07:00
|
|
|
previouslyInstalledPackage.installedNodes.forEach((node) => {
|
2022-07-20 07:24:03 -07:00
|
|
|
const pushInstance = Push.getInstance();
|
|
|
|
pushInstance.send('removeNodeType', {
|
2022-08-02 01:40:57 -07:00
|
|
|
name: node.type,
|
|
|
|
version: node.latestVersion,
|
2022-07-20 07:24:03 -07:00
|
|
|
});
|
|
|
|
});
|
2022-08-02 01:40:57 -07:00
|
|
|
|
|
|
|
const message = [
|
|
|
|
`Error removing package "${name}"`,
|
|
|
|
error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON,
|
|
|
|
].join(':');
|
|
|
|
|
2022-11-22 05:00:36 -08:00
|
|
|
throw new ResponseHelper.InternalServerError(message);
|
2022-07-20 07:24:03 -07:00
|
|
|
}
|
|
|
|
}),
|
|
|
|
);
|