mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
This change expands on the command channel communication introduced lately between the main instance(s) and the workers. The frontend gets a new menu entry "Workers" which will, when opened, trigger a regular call to getStatus from the workers. The workers then respond via their response channel to the backend, which then pushes the status to the frontend. This introduces the use of ChartJS for metrics. This feature is still in MVP state and thus disabled by default for the moment.
193 lines
5.5 KiB
TypeScript
193 lines
5.5 KiB
TypeScript
import { Request } from 'express';
|
|
import { Service } from 'typedi';
|
|
import { v4 as uuid } from 'uuid';
|
|
import config from '@/config';
|
|
import type { Role } from '@db/entities/Role';
|
|
import { RoleRepository } from '@db/repositories/role.repository';
|
|
import { SettingsRepository } from '@db/repositories/settings.repository';
|
|
import { UserRepository } from '@db/repositories/user.repository';
|
|
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
|
import { hashPassword } from '@/UserManagement/UserManagementHelper';
|
|
import { eventBus } from '@/eventbus/MessageEventBus/MessageEventBus';
|
|
import { License } from '@/License';
|
|
import { LICENSE_FEATURES, inE2ETests } from '@/constants';
|
|
import { NoAuthRequired, Patch, Post, RestController } from '@/decorators';
|
|
import type { UserSetupPayload } from '@/requests';
|
|
import type { BooleanLicenseFeature } from '@/Interfaces';
|
|
import { MfaService } from '@/Mfa/mfa.service';
|
|
|
|
if (!inE2ETests) {
|
|
console.error('E2E endpoints only allowed during E2E tests');
|
|
process.exit(1);
|
|
}
|
|
|
|
const tablesToTruncate = [
|
|
'auth_identity',
|
|
'auth_provider_sync_history',
|
|
'event_destinations',
|
|
'shared_workflow',
|
|
'shared_credentials',
|
|
'webhook_entity',
|
|
'workflows_tags',
|
|
'credentials_entity',
|
|
'tag_entity',
|
|
'workflow_statistics',
|
|
'workflow_entity',
|
|
'execution_entity',
|
|
'settings',
|
|
'installed_packages',
|
|
'installed_nodes',
|
|
'user',
|
|
'role',
|
|
'variables',
|
|
];
|
|
|
|
type ResetRequest = Request<
|
|
{},
|
|
{},
|
|
{
|
|
owner: UserSetupPayload;
|
|
members: UserSetupPayload[];
|
|
}
|
|
>;
|
|
|
|
@Service()
|
|
@NoAuthRequired()
|
|
@RestController('/e2e')
|
|
export class E2EController {
|
|
private enabledFeatures: Record<BooleanLicenseFeature, boolean> = {
|
|
[LICENSE_FEATURES.SHARING]: false,
|
|
[LICENSE_FEATURES.LDAP]: false,
|
|
[LICENSE_FEATURES.SAML]: false,
|
|
[LICENSE_FEATURES.LOG_STREAMING]: false,
|
|
[LICENSE_FEATURES.ADVANCED_EXECUTION_FILTERS]: false,
|
|
[LICENSE_FEATURES.SOURCE_CONTROL]: false,
|
|
[LICENSE_FEATURES.VARIABLES]: false,
|
|
[LICENSE_FEATURES.API_DISABLED]: false,
|
|
[LICENSE_FEATURES.EXTERNAL_SECRETS]: false,
|
|
[LICENSE_FEATURES.SHOW_NON_PROD_BANNER]: false,
|
|
[LICENSE_FEATURES.WORKFLOW_HISTORY]: false,
|
|
[LICENSE_FEATURES.DEBUG_IN_EDITOR]: false,
|
|
[LICENSE_FEATURES.BINARY_DATA_S3]: false,
|
|
[LICENSE_FEATURES.MULTIPLE_MAIN_INSTANCES]: false,
|
|
[LICENSE_FEATURES.WORKER_VIEW]: false,
|
|
};
|
|
|
|
constructor(
|
|
license: License,
|
|
private roleRepo: RoleRepository,
|
|
private settingsRepo: SettingsRepository,
|
|
private userRepo: UserRepository,
|
|
private workflowRunner: ActiveWorkflowRunner,
|
|
private mfaService: MfaService,
|
|
) {
|
|
license.isFeatureEnabled = (feature: BooleanLicenseFeature) =>
|
|
this.enabledFeatures[feature] ?? false;
|
|
}
|
|
|
|
@Post('/reset')
|
|
async reset(req: ResetRequest) {
|
|
this.resetFeatures();
|
|
await this.resetLogStreaming();
|
|
await this.removeActiveWorkflows();
|
|
await this.truncateAll();
|
|
await this.setupUserManagement(req.body.owner, req.body.members);
|
|
}
|
|
|
|
@Patch('/feature')
|
|
setFeature(req: Request<{}, {}, { feature: BooleanLicenseFeature; enabled: boolean }>) {
|
|
const { enabled, feature } = req.body;
|
|
this.enabledFeatures[feature] = enabled;
|
|
}
|
|
|
|
@Patch('/queue-mode')
|
|
async setQueueMode(req: Request<{}, {}, { enabled: boolean }>) {
|
|
const { enabled } = req.body;
|
|
config.set('executions.mode', enabled ? 'queue' : 'regular');
|
|
return { success: true, message: `Queue mode set to ${config.getEnv('executions.mode')}` };
|
|
}
|
|
|
|
private resetFeatures() {
|
|
for (const feature of Object.keys(this.enabledFeatures)) {
|
|
this.enabledFeatures[feature as BooleanLicenseFeature] = false;
|
|
}
|
|
}
|
|
|
|
private async removeActiveWorkflows() {
|
|
this.workflowRunner.removeAllQueuedWorkflowActivations();
|
|
await this.workflowRunner.removeAll();
|
|
}
|
|
|
|
private async resetLogStreaming() {
|
|
for (const id in eventBus.destinations) {
|
|
await eventBus.removeDestination(id, false);
|
|
}
|
|
}
|
|
|
|
private async truncateAll() {
|
|
for (const table of tablesToTruncate) {
|
|
try {
|
|
const { connection } = this.roleRepo.manager;
|
|
await connection.query(
|
|
`DELETE FROM ${table}; DELETE FROM sqlite_sequence WHERE name=${table};`,
|
|
);
|
|
} catch (error) {
|
|
console.warn('Dropping Table for E2E Reset error: ', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async setupUserManagement(owner: UserSetupPayload, members: UserSetupPayload[]) {
|
|
const roles: Array<[Role['name'], Role['scope']]> = [
|
|
['owner', 'global'],
|
|
['member', 'global'],
|
|
['owner', 'workflow'],
|
|
['owner', 'credential'],
|
|
['user', 'credential'],
|
|
['editor', 'workflow'],
|
|
];
|
|
|
|
const [{ id: globalOwnerRoleId }, { id: globalMemberRoleId }] = await this.roleRepo.save(
|
|
roles.map(([name, scope], index) => ({ name, scope, id: (index + 1).toString() })),
|
|
);
|
|
|
|
const instanceOwner = {
|
|
id: uuid(),
|
|
...owner,
|
|
password: await hashPassword(owner.password),
|
|
globalRoleId: globalOwnerRoleId,
|
|
};
|
|
|
|
if (owner?.mfaSecret && owner.mfaRecoveryCodes?.length) {
|
|
const { encryptedRecoveryCodes, encryptedSecret } =
|
|
this.mfaService.encryptSecretAndRecoveryCodes(owner.mfaSecret, owner.mfaRecoveryCodes);
|
|
instanceOwner.mfaSecret = encryptedSecret;
|
|
instanceOwner.mfaRecoveryCodes = encryptedRecoveryCodes;
|
|
}
|
|
|
|
const users = [];
|
|
|
|
users.push(instanceOwner);
|
|
|
|
for (const { password, ...payload } of members) {
|
|
users.push(
|
|
this.userRepo.create({
|
|
id: uuid(),
|
|
...payload,
|
|
password: await hashPassword(password),
|
|
globalRoleId: globalMemberRoleId,
|
|
}),
|
|
);
|
|
}
|
|
|
|
await this.userRepo.insert(users);
|
|
|
|
await this.settingsRepo.update(
|
|
{ key: 'userManagement.isInstanceOwnerSetUp' },
|
|
{ value: 'true' },
|
|
);
|
|
|
|
config.set('userManagement.isInstanceOwnerSetUp', true);
|
|
}
|
|
}
|