mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 09:36:44 +00:00
refactor(core): Decouple event bus from internal hooks (no-changelog) (#9724)
This commit is contained in:
@@ -3,13 +3,11 @@ import { snakeCase } from 'change-case';
|
|||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import { get as pslGet } from 'psl';
|
import { get as pslGet } from 'psl';
|
||||||
import type {
|
import type {
|
||||||
AuthenticationMethod,
|
|
||||||
ExecutionStatus,
|
ExecutionStatus,
|
||||||
INodesGraphResult,
|
INodesGraphResult,
|
||||||
IRun,
|
IRun,
|
||||||
ITelemetryTrackProperties,
|
ITelemetryTrackProperties,
|
||||||
IWorkflowBase,
|
IWorkflowBase,
|
||||||
WorkflowExecuteMode,
|
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { TelemetryHelpers } from 'n8n-workflow';
|
import { TelemetryHelpers } from 'n8n-workflow';
|
||||||
import { InstanceSettings } from 'n8n-core';
|
import { InstanceSettings } from 'n8n-core';
|
||||||
@@ -18,17 +16,13 @@ import config from '@/config';
|
|||||||
import { N8N_VERSION } from '@/constants';
|
import { N8N_VERSION } from '@/constants';
|
||||||
import type { AuthProviderType } from '@db/entities/AuthIdentity';
|
import type { AuthProviderType } from '@db/entities/AuthIdentity';
|
||||||
import type { GlobalRole, User } from '@db/entities/User';
|
import type { GlobalRole, User } from '@db/entities/User';
|
||||||
import type { ExecutionMetadata } from '@db/entities/ExecutionMetadata';
|
|
||||||
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
|
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
|
||||||
import { WorkflowRepository } from '@db/repositories/workflow.repository';
|
import { WorkflowRepository } from '@db/repositories/workflow.repository';
|
||||||
import type { EventPayloadWorkflow } from '@/eventbus';
|
|
||||||
import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus';
|
|
||||||
import { determineFinalExecutionStatus } from '@/executionLifecycleHooks/shared/sharedHookFunctions';
|
import { determineFinalExecutionStatus } from '@/executionLifecycleHooks/shared/sharedHookFunctions';
|
||||||
import type {
|
import type {
|
||||||
ITelemetryUserDeletionData,
|
ITelemetryUserDeletionData,
|
||||||
IWorkflowDb,
|
IWorkflowDb,
|
||||||
IExecutionTrackProperties,
|
IExecutionTrackProperties,
|
||||||
IWorkflowExecutionDataProcess,
|
|
||||||
} from '@/Interfaces';
|
} from '@/Interfaces';
|
||||||
import { License } from '@/License';
|
import { License } from '@/License';
|
||||||
import { EventsService } from '@/services/events.service';
|
import { EventsService } from '@/services/events.service';
|
||||||
@@ -38,22 +32,7 @@ import type { Project } from '@db/entities/Project';
|
|||||||
import type { ProjectRole } from '@db/entities/ProjectRelation';
|
import type { ProjectRole } from '@db/entities/ProjectRelation';
|
||||||
import { ProjectRelationRepository } from './databases/repositories/projectRelation.repository';
|
import { ProjectRelationRepository } from './databases/repositories/projectRelation.repository';
|
||||||
import { SharedCredentialsRepository } from './databases/repositories/sharedCredentials.repository';
|
import { SharedCredentialsRepository } from './databases/repositories/sharedCredentials.repository';
|
||||||
|
import { MessageEventBus } from './eventbus/MessageEventBus/MessageEventBus';
|
||||||
function userToPayload(user: User): {
|
|
||||||
userId: string;
|
|
||||||
_email: string;
|
|
||||||
_firstName: string;
|
|
||||||
_lastName: string;
|
|
||||||
globalRole: GlobalRole;
|
|
||||||
} {
|
|
||||||
return {
|
|
||||||
userId: user.id,
|
|
||||||
_email: user.email,
|
|
||||||
_firstName: user.firstName,
|
|
||||||
_lastName: user.lastName,
|
|
||||||
globalRole: user.role,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class InternalHooks {
|
export class InternalHooks {
|
||||||
@@ -64,10 +43,10 @@ export class InternalHooks {
|
|||||||
private readonly workflowRepository: WorkflowRepository,
|
private readonly workflowRepository: WorkflowRepository,
|
||||||
eventsService: EventsService,
|
eventsService: EventsService,
|
||||||
private readonly instanceSettings: InstanceSettings,
|
private readonly instanceSettings: InstanceSettings,
|
||||||
private readonly eventBus: MessageEventBus,
|
|
||||||
private readonly license: License,
|
private readonly license: License,
|
||||||
private readonly projectRelationRepository: ProjectRelationRepository,
|
private readonly projectRelationRepository: ProjectRelationRepository,
|
||||||
private readonly sharedCredentialsRepository: SharedCredentialsRepository,
|
private readonly sharedCredentialsRepository: SharedCredentialsRepository,
|
||||||
|
private readonly _eventBus: MessageEventBus, // needed until we decouple telemetry
|
||||||
) {
|
) {
|
||||||
eventsService.on(
|
eventsService.on(
|
||||||
'telemetry.onFirstProductionWorkflowSuccess',
|
'telemetry.onFirstProductionWorkflowSuccess',
|
||||||
@@ -177,41 +156,23 @@ export class InternalHooks {
|
|||||||
publicApi: boolean,
|
publicApi: boolean,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
||||||
void Promise.all([
|
|
||||||
this.eventBus.sendAuditEvent({
|
void this.telemetry.track('User created workflow', {
|
||||||
eventName: 'n8n.audit.workflow.created',
|
user_id: user.id,
|
||||||
payload: {
|
workflow_id: workflow.id,
|
||||||
...userToPayload(user),
|
node_graph_string: JSON.stringify(nodeGraph),
|
||||||
workflowId: workflow.id,
|
public_api: publicApi,
|
||||||
workflowName: workflow.name,
|
project_id: project.id,
|
||||||
},
|
project_type: project.type,
|
||||||
}),
|
});
|
||||||
this.telemetry.track('User created workflow', {
|
|
||||||
user_id: user.id,
|
|
||||||
workflow_id: workflow.id,
|
|
||||||
node_graph_string: JSON.stringify(nodeGraph),
|
|
||||||
public_api: publicApi,
|
|
||||||
project_id: project.id,
|
|
||||||
project_type: project.type,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onWorkflowDeleted(user: User, workflowId: string, publicApi: boolean): Promise<void> {
|
async onWorkflowDeleted(user: User, workflowId: string, publicApi: boolean): Promise<void> {
|
||||||
void Promise.all([
|
void this.telemetry.track('User deleted workflow', {
|
||||||
this.eventBus.sendAuditEvent({
|
user_id: user.id,
|
||||||
eventName: 'n8n.audit.workflow.deleted',
|
workflow_id: workflowId,
|
||||||
payload: {
|
public_api: publicApi,
|
||||||
...userToPayload(user),
|
});
|
||||||
workflowId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.telemetry.track('User deleted workflow', {
|
|
||||||
user_id: user.id,
|
|
||||||
workflow_id: workflowId,
|
|
||||||
public_api: publicApi,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onWorkflowSaved(user: User, workflow: IWorkflowDb, publicApi: boolean): Promise<void> {
|
async onWorkflowSaved(user: User, workflow: IWorkflowDb, publicApi: boolean): Promise<void> {
|
||||||
@@ -247,127 +208,22 @@ export class InternalHooks {
|
|||||||
(note) => note.overlapping,
|
(note) => note.overlapping,
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
void Promise.all([
|
void this.telemetry.track('User saved workflow', {
|
||||||
this.eventBus.sendAuditEvent({
|
user_id: user.id,
|
||||||
eventName: 'n8n.audit.workflow.updated',
|
workflow_id: workflow.id,
|
||||||
payload: {
|
node_graph_string: JSON.stringify(nodeGraph),
|
||||||
...userToPayload(user),
|
notes_count_overlapping: overlappingCount,
|
||||||
workflowId: workflow.id,
|
notes_count_non_overlapping: notesCount - overlappingCount,
|
||||||
workflowName: workflow.name,
|
version_cli: N8N_VERSION,
|
||||||
},
|
num_tags: workflow.tags?.length ?? 0,
|
||||||
}),
|
public_api: publicApi,
|
||||||
this.telemetry.track('User saved workflow', {
|
sharing_role: userRole,
|
||||||
user_id: user.id,
|
|
||||||
workflow_id: workflow.id,
|
|
||||||
node_graph_string: JSON.stringify(nodeGraph),
|
|
||||||
notes_count_overlapping: overlappingCount,
|
|
||||||
notes_count_non_overlapping: notesCount - overlappingCount,
|
|
||||||
version_cli: N8N_VERSION,
|
|
||||||
num_tags: workflow.tags?.length ?? 0,
|
|
||||||
public_api: publicApi,
|
|
||||||
sharing_role: userRole,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onNodeBeforeExecute(
|
|
||||||
executionId: string,
|
|
||||||
workflow: IWorkflowBase,
|
|
||||||
nodeName: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const nodeInWorkflow = workflow.nodes.find((node) => node.name === nodeName);
|
|
||||||
void this.eventBus.sendNodeEvent({
|
|
||||||
eventName: 'n8n.node.started',
|
|
||||||
payload: {
|
|
||||||
executionId,
|
|
||||||
nodeName,
|
|
||||||
workflowId: workflow.id?.toString(),
|
|
||||||
workflowName: workflow.name,
|
|
||||||
nodeType: nodeInWorkflow?.type,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async onNodePostExecute(
|
|
||||||
executionId: string,
|
|
||||||
workflow: IWorkflowBase,
|
|
||||||
nodeName: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const nodeInWorkflow = workflow.nodes.find((node) => node.name === nodeName);
|
|
||||||
void this.eventBus.sendNodeEvent({
|
|
||||||
eventName: 'n8n.node.finished',
|
|
||||||
payload: {
|
|
||||||
executionId,
|
|
||||||
nodeName,
|
|
||||||
workflowId: workflow.id?.toString(),
|
|
||||||
workflowName: workflow.name,
|
|
||||||
nodeType: nodeInWorkflow?.type,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async onWorkflowBeforeExecute(
|
|
||||||
executionId: string,
|
|
||||||
data: IWorkflowExecutionDataProcess | IWorkflowBase,
|
|
||||||
): Promise<void> {
|
|
||||||
let payload: EventPayloadWorkflow;
|
|
||||||
// this hook is called slightly differently depending on whether it's from a worker or the main instance
|
|
||||||
// in the worker context, meaning in queue mode, only IWorkflowBase is available
|
|
||||||
if ('executionData' in data) {
|
|
||||||
payload = {
|
|
||||||
executionId,
|
|
||||||
userId: data.userId ?? undefined,
|
|
||||||
workflowId: data.workflowData.id?.toString(),
|
|
||||||
isManual: data.executionMode === 'manual',
|
|
||||||
workflowName: data.workflowData.name,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
payload = {
|
|
||||||
executionId,
|
|
||||||
userId: undefined,
|
|
||||||
workflowId: (data as IWorkflowBase).id?.toString(),
|
|
||||||
isManual: false,
|
|
||||||
workflowName: (data as IWorkflowBase).name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
void this.eventBus.sendWorkflowEvent({
|
|
||||||
eventName: 'n8n.workflow.started',
|
|
||||||
payload,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async onWorkflowCrashed(
|
|
||||||
executionId: string,
|
|
||||||
executionMode: WorkflowExecuteMode,
|
|
||||||
workflowData?: IWorkflowBase,
|
|
||||||
executionMetadata?: ExecutionMetadata[],
|
|
||||||
): Promise<void> {
|
|
||||||
let metaData;
|
|
||||||
try {
|
|
||||||
if (executionMetadata) {
|
|
||||||
metaData = executionMetadata.reduce((acc, meta) => {
|
|
||||||
return { ...acc, [meta.key]: meta.value };
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
void Promise.all([
|
|
||||||
this.eventBus.sendWorkflowEvent({
|
|
||||||
eventName: 'n8n.workflow.crashed',
|
|
||||||
payload: {
|
|
||||||
executionId,
|
|
||||||
isManual: executionMode === 'manual',
|
|
||||||
workflowId: workflowData?.id?.toString(),
|
|
||||||
workflowName: workflowData?.name,
|
|
||||||
metaData,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line complexity
|
// eslint-disable-next-line complexity
|
||||||
async onWorkflowPostExecute(
|
async onWorkflowPostExecute(
|
||||||
executionId: string,
|
_executionId: string,
|
||||||
workflow: IWorkflowBase,
|
workflow: IWorkflowBase,
|
||||||
runData?: IRun,
|
runData?: IRun,
|
||||||
userId?: string,
|
userId?: string,
|
||||||
@@ -505,36 +361,6 @@ export class InternalHooks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sharedEventPayload: EventPayloadWorkflow = {
|
|
||||||
executionId,
|
|
||||||
success: telemetryProperties.success,
|
|
||||||
userId: telemetryProperties.user_id,
|
|
||||||
workflowId: workflow.id,
|
|
||||||
isManual: telemetryProperties.is_manual,
|
|
||||||
workflowName: workflow.name,
|
|
||||||
metaData: runData?.data?.resultData?.metadata,
|
|
||||||
};
|
|
||||||
let event;
|
|
||||||
if (telemetryProperties.success) {
|
|
||||||
event = this.eventBus.sendWorkflowEvent({
|
|
||||||
eventName: 'n8n.workflow.success',
|
|
||||||
payload: sharedEventPayload,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
event = this.eventBus.sendWorkflowEvent({
|
|
||||||
eventName: 'n8n.workflow.failed',
|
|
||||||
payload: {
|
|
||||||
...sharedEventPayload,
|
|
||||||
lastNodeExecuted: runData?.data.resultData.lastNodeExecuted,
|
|
||||||
errorNodeType: telemetryProperties.error_node_type,
|
|
||||||
errorNodeId: telemetryProperties.error_node_id?.toString(),
|
|
||||||
errorMessage: telemetryProperties.error_message?.toString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
promises.push(event);
|
|
||||||
|
|
||||||
void Promise.all([...promises, this.telemetry.trackWorkflowExecution(telemetryProperties)]);
|
void Promise.all([...promises, this.telemetry.trackWorkflowExecution(telemetryProperties)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,19 +389,11 @@ export class InternalHooks {
|
|||||||
telemetryData: ITelemetryUserDeletionData;
|
telemetryData: ITelemetryUserDeletionData;
|
||||||
publicApi: boolean;
|
publicApi: boolean;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
void Promise.all([
|
void this.telemetry.track('User deleted user', {
|
||||||
this.eventBus.sendAuditEvent({
|
...userDeletionData.telemetryData,
|
||||||
eventName: 'n8n.audit.user.deleted',
|
user_id: userDeletionData.user.id,
|
||||||
payload: {
|
public_api: userDeletionData.publicApi,
|
||||||
...userToPayload(userDeletionData.user),
|
});
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.telemetry.track('User deleted user', {
|
|
||||||
...userDeletionData.telemetryData,
|
|
||||||
user_id: userDeletionData.user.id,
|
|
||||||
public_api: userDeletionData.publicApi,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onUserInvite(userInviteData: {
|
async onUserInvite(userInviteData: {
|
||||||
@@ -585,23 +403,13 @@ export class InternalHooks {
|
|||||||
email_sent: boolean;
|
email_sent: boolean;
|
||||||
invitee_role: string;
|
invitee_role: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
void Promise.all([
|
void this.telemetry.track('User invited new user', {
|
||||||
this.eventBus.sendAuditEvent({
|
user_id: userInviteData.user.id,
|
||||||
eventName: 'n8n.audit.user.invited',
|
target_user_id: userInviteData.target_user_id,
|
||||||
payload: {
|
public_api: userInviteData.public_api,
|
||||||
...userToPayload(userInviteData.user),
|
email_sent: userInviteData.email_sent,
|
||||||
targetUserId: userInviteData.target_user_id,
|
invitee_role: userInviteData.invitee_role,
|
||||||
},
|
});
|
||||||
}),
|
|
||||||
|
|
||||||
this.telemetry.track('User invited new user', {
|
|
||||||
user_id: userInviteData.user.id,
|
|
||||||
target_user_id: userInviteData.target_user_id,
|
|
||||||
public_api: userInviteData.public_api,
|
|
||||||
email_sent: userInviteData.email_sent,
|
|
||||||
invitee_role: userInviteData.invitee_role,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onUserRoleChange(userRoleChangeData: {
|
async onUserRoleChange(userRoleChangeData: {
|
||||||
@@ -615,27 +423,6 @@ export class InternalHooks {
|
|||||||
void this.telemetry.track('User changed role', { user_id: user.id, ...rest });
|
void this.telemetry.track('User changed role', { user_id: user.id, ...rest });
|
||||||
}
|
}
|
||||||
|
|
||||||
async onUserReinvite(userReinviteData: {
|
|
||||||
user: User;
|
|
||||||
target_user_id: string;
|
|
||||||
public_api: boolean;
|
|
||||||
}): Promise<void> {
|
|
||||||
void Promise.all([
|
|
||||||
this.eventBus.sendAuditEvent({
|
|
||||||
eventName: 'n8n.audit.user.reinvited',
|
|
||||||
payload: {
|
|
||||||
...userToPayload(userReinviteData.user),
|
|
||||||
targetUserId: userReinviteData.target_user_id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.telemetry.track('User resent new user invite email', {
|
|
||||||
user_id: userReinviteData.user.id,
|
|
||||||
target_user_id: userReinviteData.target_user_id,
|
|
||||||
public_api: userReinviteData.public_api,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onUserRetrievedUser(userRetrievedData: {
|
async onUserRetrievedUser(userRetrievedData: {
|
||||||
user_id: string;
|
user_id: string;
|
||||||
public_api: boolean;
|
public_api: boolean;
|
||||||
@@ -679,55 +466,25 @@ export class InternalHooks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onUserUpdate(userUpdateData: { user: User; fields_changed: string[] }): Promise<void> {
|
async onUserUpdate(userUpdateData: { user: User; fields_changed: string[] }): Promise<void> {
|
||||||
void Promise.all([
|
void this.telemetry.track('User changed personal settings', {
|
||||||
this.eventBus.sendAuditEvent({
|
user_id: userUpdateData.user.id,
|
||||||
eventName: 'n8n.audit.user.updated',
|
fields_changed: userUpdateData.fields_changed,
|
||||||
payload: {
|
});
|
||||||
...userToPayload(userUpdateData.user),
|
|
||||||
fieldsChanged: userUpdateData.fields_changed,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.telemetry.track('User changed personal settings', {
|
|
||||||
user_id: userUpdateData.user.id,
|
|
||||||
fields_changed: userUpdateData.fields_changed,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onUserInviteEmailClick(userInviteClickData: {
|
async onUserInviteEmailClick(userInviteClickData: {
|
||||||
inviter: User;
|
inviter: User;
|
||||||
invitee: User;
|
invitee: User;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
void Promise.all([
|
void this.telemetry.track('User clicked invite link from email', {
|
||||||
this.eventBus.sendAuditEvent({
|
user_id: userInviteClickData.invitee.id,
|
||||||
eventName: 'n8n.audit.user.invitation.accepted',
|
});
|
||||||
payload: {
|
|
||||||
invitee: {
|
|
||||||
...userToPayload(userInviteClickData.invitee),
|
|
||||||
},
|
|
||||||
inviter: {
|
|
||||||
...userToPayload(userInviteClickData.inviter),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.telemetry.track('User clicked invite link from email', {
|
|
||||||
user_id: userInviteClickData.invitee.id,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onUserPasswordResetEmailClick(userPasswordResetData: { user: User }): Promise<void> {
|
async onUserPasswordResetEmailClick(userPasswordResetData: { user: User }): Promise<void> {
|
||||||
void Promise.all([
|
void this.telemetry.track('User clicked password reset link from email', {
|
||||||
this.eventBus.sendAuditEvent({
|
user_id: userPasswordResetData.user.id,
|
||||||
eventName: 'n8n.audit.user.reset',
|
});
|
||||||
payload: {
|
|
||||||
...userToPayload(userPasswordResetData.user),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.telemetry.track('User clicked password reset link from email', {
|
|
||||||
user_id: userPasswordResetData.user.id,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onUserTransactionalEmail(userTransactionalEmailData: {
|
async onUserTransactionalEmail(userTransactionalEmailData: {
|
||||||
@@ -756,47 +513,23 @@ export class InternalHooks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onApiKeyDeleted(apiKeyDeletedData: { user: User; public_api: boolean }): Promise<void> {
|
async onApiKeyDeleted(apiKeyDeletedData: { user: User; public_api: boolean }): Promise<void> {
|
||||||
void Promise.all([
|
void this.telemetry.track('API key deleted', {
|
||||||
this.eventBus.sendAuditEvent({
|
user_id: apiKeyDeletedData.user.id,
|
||||||
eventName: 'n8n.audit.user.api.deleted',
|
public_api: apiKeyDeletedData.public_api,
|
||||||
payload: {
|
});
|
||||||
...userToPayload(apiKeyDeletedData.user),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.telemetry.track('API key deleted', {
|
|
||||||
user_id: apiKeyDeletedData.user.id,
|
|
||||||
public_api: apiKeyDeletedData.public_api,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onApiKeyCreated(apiKeyCreatedData: { user: User; public_api: boolean }): Promise<void> {
|
async onApiKeyCreated(apiKeyCreatedData: { user: User; public_api: boolean }): Promise<void> {
|
||||||
void Promise.all([
|
void this.telemetry.track('API key created', {
|
||||||
this.eventBus.sendAuditEvent({
|
user_id: apiKeyCreatedData.user.id,
|
||||||
eventName: 'n8n.audit.user.api.created',
|
public_api: apiKeyCreatedData.public_api,
|
||||||
payload: {
|
});
|
||||||
...userToPayload(apiKeyCreatedData.user),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.telemetry.track('API key created', {
|
|
||||||
user_id: apiKeyCreatedData.user.id,
|
|
||||||
public_api: apiKeyCreatedData.public_api,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onUserPasswordResetRequestClick(userPasswordResetData: { user: User }): Promise<void> {
|
async onUserPasswordResetRequestClick(userPasswordResetData: { user: User }): Promise<void> {
|
||||||
void Promise.all([
|
void this.telemetry.track('User requested password reset while logged out', {
|
||||||
this.eventBus.sendAuditEvent({
|
user_id: userPasswordResetData.user.id,
|
||||||
eventName: 'n8n.audit.user.reset.requested',
|
});
|
||||||
payload: {
|
|
||||||
...userToPayload(userPasswordResetData.user),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.telemetry.track('User requested password reset while logged out', {
|
|
||||||
user_id: userPasswordResetData.user.id,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onInstanceOwnerSetup(instanceOwnerSetupData: { user_id: string }): Promise<void> {
|
async onInstanceOwnerSetup(instanceOwnerSetupData: { user_id: string }): Promise<void> {
|
||||||
@@ -810,18 +543,10 @@ export class InternalHooks {
|
|||||||
was_disabled_ldap_user: boolean;
|
was_disabled_ldap_user: boolean;
|
||||||
},
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
void Promise.all([
|
void this.telemetry.track('User signed up', {
|
||||||
this.eventBus.sendAuditEvent({
|
user_id: user.id,
|
||||||
eventName: 'n8n.audit.user.signedup',
|
...userSignupData,
|
||||||
payload: {
|
});
|
||||||
...userToPayload(user),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.telemetry.track('User signed up', {
|
|
||||||
user_id: user.id,
|
|
||||||
...userSignupData,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onEmailFailed(failedEmailData: {
|
async onEmailFailed(failedEmailData: {
|
||||||
@@ -834,50 +559,9 @@ export class InternalHooks {
|
|||||||
| 'Credentials shared';
|
| 'Credentials shared';
|
||||||
public_api: boolean;
|
public_api: boolean;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
void Promise.all([
|
void this.telemetry.track('Instance failed to send transactional email to user', {
|
||||||
this.eventBus.sendAuditEvent({
|
user_id: failedEmailData.user.id,
|
||||||
eventName: 'n8n.audit.user.email.failed',
|
});
|
||||||
payload: {
|
|
||||||
messageType: failedEmailData.message_type,
|
|
||||||
...userToPayload(failedEmailData.user),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.telemetry.track('Instance failed to send transactional email to user', {
|
|
||||||
user_id: failedEmailData.user.id,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onUserLoginSuccess(userLoginData: {
|
|
||||||
user: User;
|
|
||||||
authenticationMethod: AuthenticationMethod;
|
|
||||||
}): Promise<void> {
|
|
||||||
void Promise.all([
|
|
||||||
this.eventBus.sendAuditEvent({
|
|
||||||
eventName: 'n8n.audit.user.login.success',
|
|
||||||
payload: {
|
|
||||||
authenticationMethod: userLoginData.authenticationMethod,
|
|
||||||
...userToPayload(userLoginData.user),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onUserLoginFailed(userLoginData: {
|
|
||||||
user: string;
|
|
||||||
authenticationMethod: AuthenticationMethod;
|
|
||||||
reason?: string;
|
|
||||||
}): Promise<void> {
|
|
||||||
void Promise.all([
|
|
||||||
this.eventBus.sendAuditEvent({
|
|
||||||
eventName: 'n8n.audit.user.login.failed',
|
|
||||||
payload: {
|
|
||||||
authenticationMethod: userLoginData.authenticationMethod,
|
|
||||||
user: userLoginData.user,
|
|
||||||
reason: userLoginData.reason,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -894,25 +578,14 @@ export class InternalHooks {
|
|||||||
const project = await this.sharedCredentialsRepository.findCredentialOwningProject(
|
const project = await this.sharedCredentialsRepository.findCredentialOwningProject(
|
||||||
userCreatedCredentialsData.credential_id,
|
userCreatedCredentialsData.credential_id,
|
||||||
);
|
);
|
||||||
void Promise.all([
|
void this.telemetry.track('User created credentials', {
|
||||||
this.eventBus.sendAuditEvent({
|
user_id: userCreatedCredentialsData.user.id,
|
||||||
eventName: 'n8n.audit.user.credentials.created',
|
credential_type: userCreatedCredentialsData.credential_type,
|
||||||
payload: {
|
credential_id: userCreatedCredentialsData.credential_id,
|
||||||
...userToPayload(userCreatedCredentialsData.user),
|
instance_id: this.instanceSettings.instanceId,
|
||||||
credentialName: userCreatedCredentialsData.credential_name,
|
project_id: project?.id,
|
||||||
credentialType: userCreatedCredentialsData.credential_type,
|
project_type: project?.type,
|
||||||
credentialId: userCreatedCredentialsData.credential_id,
|
});
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.telemetry.track('User created credentials', {
|
|
||||||
user_id: userCreatedCredentialsData.user.id,
|
|
||||||
credential_type: userCreatedCredentialsData.credential_type,
|
|
||||||
credential_id: userCreatedCredentialsData.credential_id,
|
|
||||||
instance_id: this.instanceSettings.instanceId,
|
|
||||||
project_id: project?.id,
|
|
||||||
project_type: project?.type,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onUserSharedCredentials(userSharedCredentialsData: {
|
async onUserSharedCredentials(userSharedCredentialsData: {
|
||||||
@@ -924,29 +597,15 @@ export class InternalHooks {
|
|||||||
user_ids_sharees_added: string[];
|
user_ids_sharees_added: string[];
|
||||||
sharees_removed: number | null;
|
sharees_removed: number | null;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
void Promise.all([
|
void this.telemetry.track('User updated cred sharing', {
|
||||||
this.eventBus.sendAuditEvent({
|
user_id: userSharedCredentialsData.user.id,
|
||||||
eventName: 'n8n.audit.user.credentials.shared',
|
credential_type: userSharedCredentialsData.credential_type,
|
||||||
payload: {
|
credential_id: userSharedCredentialsData.credential_id,
|
||||||
...userToPayload(userSharedCredentialsData.user),
|
user_id_sharer: userSharedCredentialsData.user_id_sharer,
|
||||||
credentialName: userSharedCredentialsData.credential_name,
|
user_ids_sharees_added: userSharedCredentialsData.user_ids_sharees_added,
|
||||||
credentialType: userSharedCredentialsData.credential_type,
|
sharees_removed: userSharedCredentialsData.sharees_removed,
|
||||||
credentialId: userSharedCredentialsData.credential_id,
|
instance_id: this.instanceSettings.instanceId,
|
||||||
userIdSharer: userSharedCredentialsData.user_id_sharer,
|
});
|
||||||
userIdsShareesAdded: userSharedCredentialsData.user_ids_sharees_added,
|
|
||||||
shareesRemoved: userSharedCredentialsData.sharees_removed,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.telemetry.track('User updated cred sharing', {
|
|
||||||
user_id: userSharedCredentialsData.user.id,
|
|
||||||
credential_type: userSharedCredentialsData.credential_type,
|
|
||||||
credential_id: userSharedCredentialsData.credential_id,
|
|
||||||
user_id_sharer: userSharedCredentialsData.user_id_sharer,
|
|
||||||
user_ids_sharees_added: userSharedCredentialsData.user_ids_sharees_added,
|
|
||||||
sharees_removed: userSharedCredentialsData.sharees_removed,
|
|
||||||
instance_id: this.instanceSettings.instanceId,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onUserUpdatedCredentials(userUpdatedCredentialsData: {
|
async onUserUpdatedCredentials(userUpdatedCredentialsData: {
|
||||||
@@ -955,22 +614,11 @@ export class InternalHooks {
|
|||||||
credential_type: string;
|
credential_type: string;
|
||||||
credential_id: string;
|
credential_id: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
void Promise.all([
|
void this.telemetry.track('User updated credentials', {
|
||||||
this.eventBus.sendAuditEvent({
|
user_id: userUpdatedCredentialsData.user.id,
|
||||||
eventName: 'n8n.audit.user.credentials.updated',
|
credential_type: userUpdatedCredentialsData.credential_type,
|
||||||
payload: {
|
credential_id: userUpdatedCredentialsData.credential_id,
|
||||||
...userToPayload(userUpdatedCredentialsData.user),
|
});
|
||||||
credentialName: userUpdatedCredentialsData.credential_name,
|
|
||||||
credentialType: userUpdatedCredentialsData.credential_type,
|
|
||||||
credentialId: userUpdatedCredentialsData.credential_id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.telemetry.track('User updated credentials', {
|
|
||||||
user_id: userUpdatedCredentialsData.user.id,
|
|
||||||
credential_type: userUpdatedCredentialsData.credential_type,
|
|
||||||
credential_id: userUpdatedCredentialsData.credential_id,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onUserDeletedCredentials(userUpdatedCredentialsData: {
|
async onUserDeletedCredentials(userUpdatedCredentialsData: {
|
||||||
@@ -979,23 +627,12 @@ export class InternalHooks {
|
|||||||
credential_type: string;
|
credential_type: string;
|
||||||
credential_id: string;
|
credential_id: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
void Promise.all([
|
void this.telemetry.track('User deleted credentials', {
|
||||||
this.eventBus.sendAuditEvent({
|
user_id: userUpdatedCredentialsData.user.id,
|
||||||
eventName: 'n8n.audit.user.credentials.deleted',
|
credential_type: userUpdatedCredentialsData.credential_type,
|
||||||
payload: {
|
credential_id: userUpdatedCredentialsData.credential_id,
|
||||||
...userToPayload(userUpdatedCredentialsData.user),
|
instance_id: this.instanceSettings.instanceId,
|
||||||
credentialName: userUpdatedCredentialsData.credential_name,
|
});
|
||||||
credentialType: userUpdatedCredentialsData.credential_type,
|
|
||||||
credentialId: userUpdatedCredentialsData.credential_id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.telemetry.track('User deleted credentials', {
|
|
||||||
user_id: userUpdatedCredentialsData.user.id,
|
|
||||||
credential_type: userUpdatedCredentialsData.credential_type,
|
|
||||||
credential_id: userUpdatedCredentialsData.credential_id,
|
|
||||||
instance_id: this.instanceSettings.instanceId,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1013,33 +650,17 @@ export class InternalHooks {
|
|||||||
package_author_email?: string;
|
package_author_email?: string;
|
||||||
failure_reason?: string;
|
failure_reason?: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
void Promise.all([
|
void this.telemetry.track('cnr package install finished', {
|
||||||
this.eventBus.sendAuditEvent({
|
user_id: installationData.user.id,
|
||||||
eventName: 'n8n.audit.package.installed',
|
input_string: installationData.input_string,
|
||||||
payload: {
|
package_name: installationData.package_name,
|
||||||
...userToPayload(installationData.user),
|
success: installationData.success,
|
||||||
inputString: installationData.input_string,
|
package_version: installationData.package_version,
|
||||||
packageName: installationData.package_name,
|
package_node_names: installationData.package_node_names,
|
||||||
success: installationData.success,
|
package_author: installationData.package_author,
|
||||||
packageVersion: installationData.package_version,
|
package_author_email: installationData.package_author_email,
|
||||||
packageNodeNames: installationData.package_node_names,
|
failure_reason: installationData.failure_reason,
|
||||||
packageAuthor: installationData.package_author,
|
});
|
||||||
packageAuthorEmail: installationData.package_author_email,
|
|
||||||
failureReason: installationData.failure_reason,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.telemetry.track('cnr package install finished', {
|
|
||||||
user_id: installationData.user.id,
|
|
||||||
input_string: installationData.input_string,
|
|
||||||
package_name: installationData.package_name,
|
|
||||||
success: installationData.success,
|
|
||||||
package_version: installationData.package_version,
|
|
||||||
package_node_names: installationData.package_node_names,
|
|
||||||
package_author: installationData.package_author,
|
|
||||||
package_author_email: installationData.package_author_email,
|
|
||||||
failure_reason: installationData.failure_reason,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onCommunityPackageUpdateFinished(updateData: {
|
async onCommunityPackageUpdateFinished(updateData: {
|
||||||
@@ -1051,29 +672,15 @@ export class InternalHooks {
|
|||||||
package_author?: string;
|
package_author?: string;
|
||||||
package_author_email?: string;
|
package_author_email?: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
void Promise.all([
|
void this.telemetry.track('cnr package updated', {
|
||||||
this.eventBus.sendAuditEvent({
|
user_id: updateData.user.id,
|
||||||
eventName: 'n8n.audit.package.updated',
|
package_name: updateData.package_name,
|
||||||
payload: {
|
package_version_current: updateData.package_version_current,
|
||||||
...userToPayload(updateData.user),
|
package_version_new: updateData.package_version_new,
|
||||||
packageName: updateData.package_name,
|
package_node_names: updateData.package_node_names,
|
||||||
packageVersionCurrent: updateData.package_version_current,
|
package_author: updateData.package_author,
|
||||||
packageVersionNew: updateData.package_version_new,
|
package_author_email: updateData.package_author_email,
|
||||||
packageNodeNames: updateData.package_node_names,
|
});
|
||||||
packageAuthor: updateData.package_author,
|
|
||||||
packageAuthorEmail: updateData.package_author_email,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.telemetry.track('cnr package updated', {
|
|
||||||
user_id: updateData.user.id,
|
|
||||||
package_name: updateData.package_name,
|
|
||||||
package_version_current: updateData.package_version_current,
|
|
||||||
package_version_new: updateData.package_version_new,
|
|
||||||
package_node_names: updateData.package_node_names,
|
|
||||||
package_author: updateData.package_author,
|
|
||||||
package_author_email: updateData.package_author_email,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onCommunityPackageDeleteFinished(deleteData: {
|
async onCommunityPackageDeleteFinished(deleteData: {
|
||||||
@@ -1084,27 +691,14 @@ export class InternalHooks {
|
|||||||
package_author?: string;
|
package_author?: string;
|
||||||
package_author_email?: string;
|
package_author_email?: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
void Promise.all([
|
void this.telemetry.track('cnr package deleted', {
|
||||||
this.eventBus.sendAuditEvent({
|
user_id: deleteData.user.id,
|
||||||
eventName: 'n8n.audit.package.deleted',
|
package_name: deleteData.package_name,
|
||||||
payload: {
|
package_version: deleteData.package_version,
|
||||||
...userToPayload(deleteData.user),
|
package_node_names: deleteData.package_node_names,
|
||||||
packageName: deleteData.package_name,
|
package_author: deleteData.package_author,
|
||||||
packageVersion: deleteData.package_version,
|
package_author_email: deleteData.package_author_email,
|
||||||
packageNodeNames: deleteData.package_node_names,
|
});
|
||||||
packageAuthor: deleteData.package_author,
|
|
||||||
packageAuthorEmail: deleteData.package_author_email,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.telemetry.track('cnr package deleted', {
|
|
||||||
user_id: deleteData.user.id,
|
|
||||||
package_name: deleteData.package_name,
|
|
||||||
package_version: deleteData.package_version,
|
|
||||||
package_node_names: deleteData.package_node_names,
|
|
||||||
package_author: deleteData.package_author,
|
|
||||||
package_author_email: deleteData.package_author_email,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onLdapSyncFinished(data: {
|
async onLdapSyncFinished(data: {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { CredentialsRepository } from '@db/repositories/credentials.repository';
|
|||||||
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
|
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
|
||||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
import { InternalHooks } from '@/InternalHooks';
|
||||||
|
import { EventRelay } from '@/eventbus/event-relay.service';
|
||||||
|
|
||||||
export async function getCredentials(credentialId: string): Promise<ICredentialsDb | null> {
|
export async function getCredentials(credentialId: string): Promise<ICredentialsDb | null> {
|
||||||
return await Container.get(CredentialsRepository).findOneBy({ id: credentialId });
|
return await Container.get(CredentialsRepository).findOneBy({ id: credentialId });
|
||||||
@@ -59,6 +60,12 @@ export async function saveCredential(
|
|||||||
credential_id: credential.id,
|
credential_id: credential.id,
|
||||||
public_api: true,
|
public_api: true,
|
||||||
});
|
});
|
||||||
|
Container.get(EventRelay).emit('credentials-created', {
|
||||||
|
user,
|
||||||
|
credentialName: credential.name,
|
||||||
|
credentialType: credential.type,
|
||||||
|
credentialId: credential.id,
|
||||||
|
});
|
||||||
|
|
||||||
return await Db.transaction(async (transactionManager) => {
|
return await Db.transaction(async (transactionManager) => {
|
||||||
const savedCredential = await transactionManager.save<CredentialsEntity>(credential);
|
const savedCredential = await transactionManager.save<CredentialsEntity>(credential);
|
||||||
@@ -95,6 +102,12 @@ export async function removeCredential(
|
|||||||
credential_type: credentials.type,
|
credential_type: credentials.type,
|
||||||
credential_id: credentials.id,
|
credential_id: credentials.id,
|
||||||
});
|
});
|
||||||
|
Container.get(EventRelay).emit('credentials-deleted', {
|
||||||
|
user,
|
||||||
|
credentialName: credentials.name,
|
||||||
|
credentialType: credentials.type,
|
||||||
|
credentialId: credentials.id,
|
||||||
|
});
|
||||||
return await Container.get(CredentialsRepository).remove(credentials);
|
return await Container.get(CredentialsRepository).remove(credentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { SharedWorkflowRepository } from '@/databases/repositories/sharedWorkflo
|
|||||||
import { TagRepository } from '@/databases/repositories/tag.repository';
|
import { TagRepository } from '@/databases/repositories/tag.repository';
|
||||||
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
||||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||||
|
import { EventRelay } from '@/eventbus/event-relay.service';
|
||||||
|
|
||||||
export = {
|
export = {
|
||||||
createWorkflow: [
|
createWorkflow: [
|
||||||
@@ -56,6 +57,10 @@ export = {
|
|||||||
|
|
||||||
await Container.get(ExternalHooks).run('workflow.afterCreate', [createdWorkflow]);
|
await Container.get(ExternalHooks).run('workflow.afterCreate', [createdWorkflow]);
|
||||||
void Container.get(InternalHooks).onWorkflowCreated(req.user, createdWorkflow, project, true);
|
void Container.get(InternalHooks).onWorkflowCreated(req.user, createdWorkflow, project, true);
|
||||||
|
Container.get(EventRelay).emit('workflow-created', {
|
||||||
|
workflow: createdWorkflow,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
|
||||||
return res.json(createdWorkflow);
|
return res.json(createdWorkflow);
|
||||||
},
|
},
|
||||||
@@ -233,6 +238,11 @@ export = {
|
|||||||
|
|
||||||
await Container.get(ExternalHooks).run('workflow.afterUpdate', [updateData]);
|
await Container.get(ExternalHooks).run('workflow.afterUpdate', [updateData]);
|
||||||
void Container.get(InternalHooks).onWorkflowSaved(req.user, updateData, true);
|
void Container.get(InternalHooks).onWorkflowSaved(req.user, updateData, true);
|
||||||
|
Container.get(EventRelay).emit('workflow-saved', {
|
||||||
|
user: req.user,
|
||||||
|
workflowId: updateData.id,
|
||||||
|
workflowName: updateData.name,
|
||||||
|
});
|
||||||
|
|
||||||
return res.json(updatedWorkflow);
|
return res.json(updatedWorkflow);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { toError } from '@/utils';
|
|||||||
|
|
||||||
import type { InviteEmailData, PasswordResetData, SendEmailResult } from './Interfaces';
|
import type { InviteEmailData, PasswordResetData, SendEmailResult } from './Interfaces';
|
||||||
import { NodeMailer } from './NodeMailer';
|
import { NodeMailer } from './NodeMailer';
|
||||||
|
import { EventRelay } from '@/eventbus/event-relay.service';
|
||||||
|
|
||||||
type Template = HandlebarsTemplateDelegate<unknown>;
|
type Template = HandlebarsTemplateDelegate<unknown>;
|
||||||
type TemplateName = 'invite' | 'passwordReset' | 'workflowShared' | 'credentialsShared';
|
type TemplateName = 'invite' | 'passwordReset' | 'workflowShared' | 'credentialsShared';
|
||||||
@@ -144,6 +145,10 @@ export class UserManagementMailer {
|
|||||||
message_type: 'Workflow shared',
|
message_type: 'Workflow shared',
|
||||||
public_api: false,
|
public_api: false,
|
||||||
});
|
});
|
||||||
|
Container.get(EventRelay).emit('email-failed', {
|
||||||
|
user: sharer,
|
||||||
|
messageType: 'Workflow shared',
|
||||||
|
});
|
||||||
|
|
||||||
const error = toError(e);
|
const error = toError(e);
|
||||||
|
|
||||||
@@ -199,6 +204,10 @@ export class UserManagementMailer {
|
|||||||
message_type: 'Credentials shared',
|
message_type: 'Credentials shared',
|
||||||
public_api: false,
|
public_api: false,
|
||||||
});
|
});
|
||||||
|
Container.get(EventRelay).emit('email-failed', {
|
||||||
|
user: sharer,
|
||||||
|
messageType: 'Credentials shared',
|
||||||
|
});
|
||||||
|
|
||||||
const error = toError(e);
|
const error = toError(e);
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ import { WorkflowRepository } from './databases/repositories/workflow.repository
|
|||||||
import { UrlService } from './services/url.service';
|
import { UrlService } from './services/url.service';
|
||||||
import { WorkflowExecutionService } from './workflows/workflowExecution.service';
|
import { WorkflowExecutionService } from './workflows/workflowExecution.service';
|
||||||
import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus';
|
import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus';
|
||||||
|
import { EventRelay } from './eventbus/event-relay.service';
|
||||||
|
|
||||||
const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');
|
const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');
|
||||||
|
|
||||||
@@ -392,17 +393,21 @@ export function hookFunctionsPreExecute(): IWorkflowExecuteHooks {
|
|||||||
*/
|
*/
|
||||||
function hookFunctionsSave(): IWorkflowExecuteHooks {
|
function hookFunctionsSave(): IWorkflowExecuteHooks {
|
||||||
const logger = Container.get(Logger);
|
const logger = Container.get(Logger);
|
||||||
const internalHooks = Container.get(InternalHooks);
|
|
||||||
const eventsService = Container.get(EventsService);
|
const eventsService = Container.get(EventsService);
|
||||||
|
const eventRelay = Container.get(EventRelay);
|
||||||
return {
|
return {
|
||||||
nodeExecuteBefore: [
|
nodeExecuteBefore: [
|
||||||
async function (this: WorkflowHooks, nodeName: string): Promise<void> {
|
async function (this: WorkflowHooks, nodeName: string): Promise<void> {
|
||||||
void internalHooks.onNodeBeforeExecute(this.executionId, this.workflowData, nodeName);
|
const { executionId, workflowData: workflow } = this;
|
||||||
|
|
||||||
|
eventRelay.emit('node-pre-execute', { executionId, workflow, nodeName });
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
nodeExecuteAfter: [
|
nodeExecuteAfter: [
|
||||||
async function (this: WorkflowHooks, nodeName: string): Promise<void> {
|
async function (this: WorkflowHooks, nodeName: string): Promise<void> {
|
||||||
void internalHooks.onNodePostExecute(this.executionId, this.workflowData, nodeName);
|
const { executionId, workflowData: workflow } = this;
|
||||||
|
|
||||||
|
eventRelay.emit('node-post-execute', { executionId, workflow, nodeName });
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
workflowExecuteBefore: [],
|
workflowExecuteBefore: [],
|
||||||
@@ -541,20 +546,27 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
|
|||||||
const logger = Container.get(Logger);
|
const logger = Container.get(Logger);
|
||||||
const internalHooks = Container.get(InternalHooks);
|
const internalHooks = Container.get(InternalHooks);
|
||||||
const eventsService = Container.get(EventsService);
|
const eventsService = Container.get(EventsService);
|
||||||
|
const eventRelay = Container.get(EventRelay);
|
||||||
return {
|
return {
|
||||||
nodeExecuteBefore: [
|
nodeExecuteBefore: [
|
||||||
async function (this: WorkflowHooks, nodeName: string): Promise<void> {
|
async function (this: WorkflowHooks, nodeName: string): Promise<void> {
|
||||||
void internalHooks.onNodeBeforeExecute(this.executionId, this.workflowData, nodeName);
|
const { executionId, workflowData: workflow } = this;
|
||||||
|
|
||||||
|
eventRelay.emit('node-pre-execute', { executionId, workflow, nodeName });
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
nodeExecuteAfter: [
|
nodeExecuteAfter: [
|
||||||
async function (this: WorkflowHooks, nodeName: string): Promise<void> {
|
async function (this: WorkflowHooks, nodeName: string): Promise<void> {
|
||||||
void internalHooks.onNodePostExecute(this.executionId, this.workflowData, nodeName);
|
const { executionId, workflowData: workflow } = this;
|
||||||
|
|
||||||
|
eventRelay.emit('node-post-execute', { executionId, workflow, nodeName });
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
workflowExecuteBefore: [
|
workflowExecuteBefore: [
|
||||||
async function (): Promise<void> {
|
async function (): Promise<void> {
|
||||||
void internalHooks.onWorkflowBeforeExecute(this.executionId, this.workflowData);
|
const { executionId, workflowData } = this;
|
||||||
|
|
||||||
|
eventRelay.emit('workflow-pre-execute', { executionId, data: workflowData });
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
workflowExecuteAfter: [
|
workflowExecuteAfter: [
|
||||||
@@ -622,9 +634,17 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
|
|||||||
eventsService.emit('workflowExecutionCompleted', this.workflowData, fullRunData);
|
eventsService.emit('workflowExecutionCompleted', this.workflowData, fullRunData);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async function (this: WorkflowHooks, fullRunData: IRun): Promise<void> {
|
async function (this: WorkflowHooks, runData: IRun): Promise<void> {
|
||||||
// send tracking and event log events, but don't wait for them
|
const { executionId, workflowData: workflow } = this;
|
||||||
void internalHooks.onWorkflowPostExecute(this.executionId, this.workflowData, fullRunData);
|
|
||||||
|
void internalHooks.onWorkflowPostExecute(executionId, workflow, runData);
|
||||||
|
eventRelay.emit('workflow-post-execute', {
|
||||||
|
workflowId: workflow.id,
|
||||||
|
workflowName: workflow.name,
|
||||||
|
executionId,
|
||||||
|
success: runData.status === 'success',
|
||||||
|
isManual: runData.mode === 'manual',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
async function (this: WorkflowHooks, fullRunData: IRun) {
|
async function (this: WorkflowHooks, fullRunData: IRun) {
|
||||||
const externalHooks = Container.get(ExternalHooks);
|
const externalHooks = Container.get(ExternalHooks);
|
||||||
@@ -765,6 +785,7 @@ async function executeWorkflow(
|
|||||||
|
|
||||||
const nodeTypes = Container.get(NodeTypes);
|
const nodeTypes = Container.get(NodeTypes);
|
||||||
const activeExecutions = Container.get(ActiveExecutions);
|
const activeExecutions = Container.get(ActiveExecutions);
|
||||||
|
const eventRelay = Container.get(EventRelay);
|
||||||
|
|
||||||
const workflowData =
|
const workflowData =
|
||||||
options.loadedWorkflowData ??
|
options.loadedWorkflowData ??
|
||||||
@@ -792,7 +813,7 @@ async function executeWorkflow(
|
|||||||
executionId = options.parentExecutionId ?? (await activeExecutions.add(runData));
|
executionId = options.parentExecutionId ?? (await activeExecutions.add(runData));
|
||||||
}
|
}
|
||||||
|
|
||||||
void internalHooks.onWorkflowBeforeExecute(executionId || '', runData);
|
Container.get(EventRelay).emit('workflow-pre-execute', { executionId, data: runData });
|
||||||
|
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
@@ -905,6 +926,14 @@ async function executeWorkflow(
|
|||||||
await externalHooks.run('workflow.postExecute', [data, workflowData, executionId]);
|
await externalHooks.run('workflow.postExecute', [data, workflowData, executionId]);
|
||||||
|
|
||||||
void internalHooks.onWorkflowPostExecute(executionId, workflowData, data, additionalData.userId);
|
void internalHooks.onWorkflowPostExecute(executionId, workflowData, data, additionalData.userId);
|
||||||
|
eventRelay.emit('workflow-post-execute', {
|
||||||
|
workflowId: workflowData.id,
|
||||||
|
workflowName: workflowData.name,
|
||||||
|
executionId,
|
||||||
|
success: data.status === 'success',
|
||||||
|
isManual: data.mode === 'manual',
|
||||||
|
userId: additionalData.userId,
|
||||||
|
});
|
||||||
|
|
||||||
// subworkflow either finished, or is in status waiting due to a wait node, both cases are considered successes here
|
// subworkflow either finished, or is in status waiting due to a wait node, both cases are considered successes here
|
||||||
if (data.finished === true || data.status === 'waiting') {
|
if (data.finished === true || data.status === 'waiting') {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import { PermissionChecker } from '@/UserManagement/PermissionChecker';
|
|||||||
import { InternalHooks } from '@/InternalHooks';
|
import { InternalHooks } from '@/InternalHooks';
|
||||||
import { Logger } from '@/Logger';
|
import { Logger } from '@/Logger';
|
||||||
import { WorkflowStaticDataService } from '@/workflows/workflowStaticData.service';
|
import { WorkflowStaticDataService } from '@/workflows/workflowStaticData.service';
|
||||||
|
import { EventRelay } from './eventbus/event-relay.service';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class WorkflowRunner {
|
export class WorkflowRunner {
|
||||||
@@ -52,6 +53,7 @@ export class WorkflowRunner {
|
|||||||
private readonly workflowStaticDataService: WorkflowStaticDataService,
|
private readonly workflowStaticDataService: WorkflowStaticDataService,
|
||||||
private readonly nodeTypes: NodeTypes,
|
private readonly nodeTypes: NodeTypes,
|
||||||
private readonly permissionChecker: PermissionChecker,
|
private readonly permissionChecker: PermissionChecker,
|
||||||
|
private readonly eventRelay: EventRelay,
|
||||||
) {
|
) {
|
||||||
if (this.executionsMode === 'queue') {
|
if (this.executionsMode === 'queue') {
|
||||||
this.jobQueue = Container.get(Queue);
|
this.jobQueue = Container.get(Queue);
|
||||||
@@ -145,7 +147,7 @@ export class WorkflowRunner {
|
|||||||
await this.enqueueExecution(executionId, data, loadStaticData, realtime);
|
await this.enqueueExecution(executionId, data, loadStaticData, realtime);
|
||||||
} else {
|
} else {
|
||||||
await this.runMainProcess(executionId, data, loadStaticData, executionId);
|
await this.runMainProcess(executionId, data, loadStaticData, executionId);
|
||||||
void Container.get(InternalHooks).onWorkflowBeforeExecute(executionId, data);
|
this.eventRelay.emit('workflow-pre-execute', { executionId, data });
|
||||||
}
|
}
|
||||||
|
|
||||||
// only run these when not in queue mode or when the execution is manual,
|
// only run these when not in queue mode or when the execution is manual,
|
||||||
@@ -164,6 +166,14 @@ export class WorkflowRunner {
|
|||||||
executionData,
|
executionData,
|
||||||
data.userId,
|
data.userId,
|
||||||
);
|
);
|
||||||
|
this.eventRelay.emit('workflow-post-execute', {
|
||||||
|
workflowId: data.workflowData.id,
|
||||||
|
workflowName: data.workflowData.name,
|
||||||
|
executionId,
|
||||||
|
success: executionData?.status === 'success',
|
||||||
|
isManual: data.executionMode === 'manual',
|
||||||
|
userId: data.userId,
|
||||||
|
});
|
||||||
if (this.externalHooks.exists('workflow.postExecute')) {
|
if (this.externalHooks.exists('workflow.postExecute')) {
|
||||||
try {
|
try {
|
||||||
await this.externalHooks.run('workflow.postExecute', [
|
await this.externalHooks.run('workflow.postExecute', [
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
updateLdapUserOnLocalDb,
|
updateLdapUserOnLocalDb,
|
||||||
} from '@/Ldap/helpers';
|
} from '@/Ldap/helpers';
|
||||||
import type { User } from '@db/entities/User';
|
import type { User } from '@db/entities/User';
|
||||||
|
import { EventRelay } from '@/eventbus/event-relay.service';
|
||||||
|
|
||||||
export const handleLdapLogin = async (
|
export const handleLdapLogin = async (
|
||||||
loginId: string,
|
loginId: string,
|
||||||
@@ -54,6 +55,7 @@ export const handleLdapLogin = async (
|
|||||||
user_type: 'ldap',
|
user_type: 'ldap',
|
||||||
was_disabled_ldap_user: false,
|
was_disabled_ldap_user: false,
|
||||||
});
|
});
|
||||||
|
Container.get(EventRelay).emit('user-signed-up', { user });
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
|||||||
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
|
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
|
||||||
import { ApplicationError } from 'n8n-workflow';
|
import { ApplicationError } from 'n8n-workflow';
|
||||||
import { UserRepository } from '@/databases/repositories/user.repository';
|
import { UserRepository } from '@/databases/repositories/user.repository';
|
||||||
|
import { EventRelay } from '@/eventbus/event-relay.service';
|
||||||
|
|
||||||
@RestController()
|
@RestController()
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
@@ -35,6 +36,7 @@ export class AuthController {
|
|||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
private readonly license: License,
|
private readonly license: License,
|
||||||
private readonly userRepository: UserRepository,
|
private readonly userRepository: UserRepository,
|
||||||
|
private readonly eventRelay: EventRelay,
|
||||||
private readonly postHog?: PostHogClient,
|
private readonly postHog?: PostHogClient,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -90,16 +92,17 @@ export class AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.authService.issueCookie(res, user, req.browserId);
|
this.authService.issueCookie(res, user, req.browserId);
|
||||||
void this.internalHooks.onUserLoginSuccess({
|
|
||||||
|
this.eventRelay.emit('user-logged-in', {
|
||||||
user,
|
user,
|
||||||
authenticationMethod: usedAuthenticationMethod,
|
authenticationMethod: usedAuthenticationMethod,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await this.userService.toPublic(user, { posthog: this.postHog, withScopes: true });
|
return await this.userService.toPublic(user, { posthog: this.postHog, withScopes: true });
|
||||||
}
|
}
|
||||||
void this.internalHooks.onUserLoginFailed({
|
this.eventRelay.emit('user-login-failed', {
|
||||||
user: email,
|
|
||||||
authenticationMethod: usedAuthenticationMethod,
|
authenticationMethod: usedAuthenticationMethod,
|
||||||
|
userEmail: email,
|
||||||
reason: 'wrong credentials',
|
reason: 'wrong credentials',
|
||||||
});
|
});
|
||||||
throw new AuthError('Wrong username or password. Do you have caps lock on?');
|
throw new AuthError('Wrong username or password. Do you have caps lock on?');
|
||||||
@@ -177,6 +180,7 @@ export class AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void this.internalHooks.onUserInviteEmailClick({ inviter, invitee });
|
void this.internalHooks.onUserInviteEmailClick({ inviter, invitee });
|
||||||
|
this.eventRelay.emit('user-invite-email-click', { inviter, invitee });
|
||||||
|
|
||||||
const { firstName, lastName } = inviter;
|
const { firstName, lastName } = inviter;
|
||||||
return { inviter: { firstName, lastName } };
|
return { inviter: { firstName, lastName } };
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Push } from '@/push';
|
|||||||
import { CommunityPackagesService } from '@/services/communityPackages.service';
|
import { CommunityPackagesService } from '@/services/communityPackages.service';
|
||||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||||
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
||||||
|
import { EventRelay } from '@/eventbus/event-relay.service';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
PACKAGE_NOT_INSTALLED,
|
PACKAGE_NOT_INSTALLED,
|
||||||
@@ -38,6 +39,7 @@ export class CommunityPackagesController {
|
|||||||
private readonly push: Push,
|
private readonly push: Push,
|
||||||
private readonly internalHooks: InternalHooks,
|
private readonly internalHooks: InternalHooks,
|
||||||
private readonly communityPackagesService: CommunityPackagesService,
|
private readonly communityPackagesService: CommunityPackagesService,
|
||||||
|
private readonly eventRelay: EventRelay,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// TODO: move this into a new decorator `@IfConfig('executions.mode', 'queue')`
|
// TODO: move this into a new decorator `@IfConfig('executions.mode', 'queue')`
|
||||||
@@ -114,6 +116,14 @@ export class CommunityPackagesController {
|
|||||||
package_version: parsed.version,
|
package_version: parsed.version,
|
||||||
failure_reason: errorMessage,
|
failure_reason: errorMessage,
|
||||||
});
|
});
|
||||||
|
this.eventRelay.emit('community-package-installed', {
|
||||||
|
user: req.user,
|
||||||
|
inputString: name,
|
||||||
|
packageName: parsed.packageName,
|
||||||
|
success: false,
|
||||||
|
packageVersion: parsed.version,
|
||||||
|
failureReason: errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
let message = [`Error loading package "${name}" `, errorMessage].join(':');
|
let message = [`Error loading package "${name}" `, errorMessage].join(':');
|
||||||
if (error instanceof Error && error.cause instanceof Error) {
|
if (error instanceof Error && error.cause instanceof Error) {
|
||||||
@@ -144,6 +154,16 @@ export class CommunityPackagesController {
|
|||||||
package_author: installedPackage.authorName,
|
package_author: installedPackage.authorName,
|
||||||
package_author_email: installedPackage.authorEmail,
|
package_author_email: installedPackage.authorEmail,
|
||||||
});
|
});
|
||||||
|
this.eventRelay.emit('community-package-installed', {
|
||||||
|
user: req.user,
|
||||||
|
inputString: name,
|
||||||
|
packageName: parsed.packageName,
|
||||||
|
success: true,
|
||||||
|
packageVersion: parsed.version,
|
||||||
|
packageNodeNames: installedPackage.installedNodes.map((node) => node.name),
|
||||||
|
packageAuthor: installedPackage.authorName,
|
||||||
|
packageAuthorEmail: installedPackage.authorEmail,
|
||||||
|
});
|
||||||
|
|
||||||
return installedPackage;
|
return installedPackage;
|
||||||
}
|
}
|
||||||
@@ -233,6 +253,14 @@ export class CommunityPackagesController {
|
|||||||
package_author: installedPackage.authorName,
|
package_author: installedPackage.authorName,
|
||||||
package_author_email: installedPackage.authorEmail,
|
package_author_email: installedPackage.authorEmail,
|
||||||
});
|
});
|
||||||
|
this.eventRelay.emit('community-package-deleted', {
|
||||||
|
user: req.user,
|
||||||
|
packageName: name,
|
||||||
|
packageVersion: installedPackage.installedVersion,
|
||||||
|
packageNodeNames: installedPackage.installedNodes.map((node) => node.name),
|
||||||
|
packageAuthor: installedPackage.authorName,
|
||||||
|
packageAuthorEmail: installedPackage.authorEmail,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('/')
|
@Patch('/')
|
||||||
@@ -281,6 +309,15 @@ export class CommunityPackagesController {
|
|||||||
package_author: newInstalledPackage.authorName,
|
package_author: newInstalledPackage.authorName,
|
||||||
package_author_email: newInstalledPackage.authorEmail,
|
package_author_email: newInstalledPackage.authorEmail,
|
||||||
});
|
});
|
||||||
|
this.eventRelay.emit('community-package-updated', {
|
||||||
|
user: req.user,
|
||||||
|
packageName: name,
|
||||||
|
packageVersionCurrent: previouslyInstalledPackage.installedVersion,
|
||||||
|
packageVersionNew: newInstalledPackage.installedVersion,
|
||||||
|
packageNodeNames: newInstalledPackage.installedNodes.map((n) => n.name),
|
||||||
|
packageAuthor: newInstalledPackage.authorName,
|
||||||
|
packageAuthorEmail: newInstalledPackage.authorEmail,
|
||||||
|
});
|
||||||
|
|
||||||
return newInstalledPackage;
|
return newInstalledPackage;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
|||||||
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
|
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
import { InternalHooks } from '@/InternalHooks';
|
||||||
import { ExternalHooks } from '@/ExternalHooks';
|
import { ExternalHooks } from '@/ExternalHooks';
|
||||||
|
import { EventRelay } from '@/eventbus/event-relay.service';
|
||||||
|
|
||||||
@RestController('/invitations')
|
@RestController('/invitations')
|
||||||
export class InvitationController {
|
export class InvitationController {
|
||||||
@@ -31,6 +32,7 @@ export class InvitationController {
|
|||||||
private readonly passwordUtility: PasswordUtility,
|
private readonly passwordUtility: PasswordUtility,
|
||||||
private readonly userRepository: UserRepository,
|
private readonly userRepository: UserRepository,
|
||||||
private readonly postHog: PostHogClient,
|
private readonly postHog: PostHogClient,
|
||||||
|
private readonly eventRelay: EventRelay,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -170,6 +172,7 @@ export class InvitationController {
|
|||||||
user_type: 'email',
|
user_type: 'email',
|
||||||
was_disabled_ldap_user: false,
|
was_disabled_ldap_user: false,
|
||||||
});
|
});
|
||||||
|
this.eventRelay.emit('user-signed-up', { user: updatedUser });
|
||||||
|
|
||||||
const publicInvitee = await this.userService.toPublic(invitee);
|
const publicInvitee = await this.userService.toPublic(invitee);
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { InternalHooks } from '@/InternalHooks';
|
|||||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||||
import { UserRepository } from '@/databases/repositories/user.repository';
|
import { UserRepository } from '@/databases/repositories/user.repository';
|
||||||
import { isApiEnabled } from '@/PublicApi';
|
import { isApiEnabled } from '@/PublicApi';
|
||||||
|
import { EventRelay } from '@/eventbus/event-relay.service';
|
||||||
|
|
||||||
export const isApiEnabledMiddleware: RequestHandler = (_, res, next) => {
|
export const isApiEnabledMiddleware: RequestHandler = (_, res, next) => {
|
||||||
if (isApiEnabled()) {
|
if (isApiEnabled()) {
|
||||||
@@ -42,6 +43,7 @@ export class MeController {
|
|||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
private readonly passwordUtility: PasswordUtility,
|
private readonly passwordUtility: PasswordUtility,
|
||||||
private readonly userRepository: UserRepository,
|
private readonly userRepository: UserRepository,
|
||||||
|
private readonly eventRelay: EventRelay,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -96,11 +98,9 @@ export class MeController {
|
|||||||
|
|
||||||
this.authService.issueCookie(res, user, req.browserId);
|
this.authService.issueCookie(res, user, req.browserId);
|
||||||
|
|
||||||
const updatedKeys = Object.keys(payload);
|
const fieldsChanged = Object.keys(payload);
|
||||||
void this.internalHooks.onUserUpdate({
|
void this.internalHooks.onUserUpdate({ user, fields_changed: fieldsChanged });
|
||||||
user,
|
this.eventRelay.emit('user-updated', { user, fieldsChanged });
|
||||||
fields_changed: updatedKeys,
|
|
||||||
});
|
|
||||||
|
|
||||||
const publicUser = await this.userService.toPublic(user);
|
const publicUser = await this.userService.toPublic(user);
|
||||||
|
|
||||||
@@ -149,10 +149,8 @@ export class MeController {
|
|||||||
|
|
||||||
this.authService.issueCookie(res, updatedUser, req.browserId);
|
this.authService.issueCookie(res, updatedUser, req.browserId);
|
||||||
|
|
||||||
void this.internalHooks.onUserUpdate({
|
void this.internalHooks.onUserUpdate({ user: updatedUser, fields_changed: ['password'] });
|
||||||
user: updatedUser,
|
this.eventRelay.emit('user-updated', { user: updatedUser, fieldsChanged: ['password'] });
|
||||||
fields_changed: ['password'],
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.externalHooks.run('user.password.update', [updatedUser.email, updatedUser.password]);
|
await this.externalHooks.run('user.password.update', [updatedUser.email, updatedUser.password]);
|
||||||
|
|
||||||
@@ -200,10 +198,8 @@ export class MeController {
|
|||||||
|
|
||||||
await this.userService.update(req.user.id, { apiKey });
|
await this.userService.update(req.user.id, { apiKey });
|
||||||
|
|
||||||
void this.internalHooks.onApiKeyCreated({
|
void this.internalHooks.onApiKeyCreated({ user: req.user, public_api: false });
|
||||||
user: req.user,
|
this.eventRelay.emit('api-key-created', { user: req.user });
|
||||||
public_api: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { apiKey };
|
return { apiKey };
|
||||||
}
|
}
|
||||||
@@ -223,10 +219,8 @@ export class MeController {
|
|||||||
async deleteAPIKey(req: AuthenticatedRequest) {
|
async deleteAPIKey(req: AuthenticatedRequest) {
|
||||||
await this.userService.update(req.user.id, { apiKey: null });
|
await this.userService.update(req.user.id, { apiKey: null });
|
||||||
|
|
||||||
void this.internalHooks.onApiKeyDeleted({
|
void this.internalHooks.onApiKeyDeleted({ user: req.user, public_api: false });
|
||||||
user: req.user,
|
this.eventRelay.emit('api-key-deleted', { user: req.user });
|
||||||
public_api: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
|
|||||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||||
import { UnprocessableRequestError } from '@/errors/response-errors/unprocessable.error';
|
import { UnprocessableRequestError } from '@/errors/response-errors/unprocessable.error';
|
||||||
import { UserRepository } from '@/databases/repositories/user.repository';
|
import { UserRepository } from '@/databases/repositories/user.repository';
|
||||||
|
import { EventRelay } from '@/eventbus/event-relay.service';
|
||||||
|
|
||||||
@RestController()
|
@RestController()
|
||||||
export class PasswordResetController {
|
export class PasswordResetController {
|
||||||
@@ -36,6 +37,7 @@ export class PasswordResetController {
|
|||||||
private readonly license: License,
|
private readonly license: License,
|
||||||
private readonly passwordUtility: PasswordUtility,
|
private readonly passwordUtility: PasswordUtility,
|
||||||
private readonly userRepository: UserRepository,
|
private readonly userRepository: UserRepository,
|
||||||
|
private readonly eventRelay: EventRelay,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -123,6 +125,7 @@ export class PasswordResetController {
|
|||||||
message_type: 'Reset password',
|
message_type: 'Reset password',
|
||||||
public_api: false,
|
public_api: false,
|
||||||
});
|
});
|
||||||
|
this.eventRelay.emit('email-failed', { user, messageType: 'Reset password' });
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
throw new InternalServerError(`Please contact your administrator: ${error.message}`);
|
throw new InternalServerError(`Please contact your administrator: ${error.message}`);
|
||||||
}
|
}
|
||||||
@@ -136,6 +139,7 @@ export class PasswordResetController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
void this.internalHooks.onUserPasswordResetRequestClick({ user });
|
void this.internalHooks.onUserPasswordResetRequestClick({ user });
|
||||||
|
this.eventRelay.emit('user-password-reset-request-click', { user });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -168,6 +172,7 @@ export class PasswordResetController {
|
|||||||
|
|
||||||
this.logger.info('Reset-password token resolved successfully', { userId: user.id });
|
this.logger.info('Reset-password token resolved successfully', { userId: user.id });
|
||||||
void this.internalHooks.onUserPasswordResetEmailClick({ user });
|
void this.internalHooks.onUserPasswordResetEmailClick({ user });
|
||||||
|
this.eventRelay.emit('user-password-reset-email-click', { user });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -210,10 +215,8 @@ export class PasswordResetController {
|
|||||||
|
|
||||||
this.authService.issueCookie(res, user, req.browserId);
|
this.authService.issueCookie(res, user, req.browserId);
|
||||||
|
|
||||||
void this.internalHooks.onUserUpdate({
|
void this.internalHooks.onUserUpdate({ user, fields_changed: ['password'] });
|
||||||
user,
|
this.eventRelay.emit('user-updated', { user, fieldsChanged: ['password'] });
|
||||||
fields_changed: ['password'],
|
|
||||||
});
|
|
||||||
|
|
||||||
// if this user used to be an LDAP users
|
// if this user used to be an LDAP users
|
||||||
const ldapIdentity = user?.authIdentities?.find((i) => i.providerType === 'ldap');
|
const ldapIdentity = user?.authIdentities?.find((i) => i.providerType === 'ldap');
|
||||||
@@ -222,6 +225,7 @@ export class PasswordResetController {
|
|||||||
user_type: 'email',
|
user_type: 'email',
|
||||||
was_disabled_ldap_user: true,
|
was_disabled_ldap_user: true,
|
||||||
});
|
});
|
||||||
|
this.eventRelay.emit('user-signed-up', { user });
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.externalHooks.run('user.password.update', [user.email, passwordHash]);
|
await this.externalHooks.run('user.password.update', [user.email, passwordHash]);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { Project } from '@/databases/entities/Project';
|
|||||||
import { WorkflowService } from '@/workflows/workflow.service';
|
import { WorkflowService } from '@/workflows/workflow.service';
|
||||||
import { CredentialsService } from '@/credentials/credentials.service';
|
import { CredentialsService } from '@/credentials/credentials.service';
|
||||||
import { ProjectService } from '@/services/project.service';
|
import { ProjectService } from '@/services/project.service';
|
||||||
|
import { EventRelay } from '@/eventbus/event-relay.service';
|
||||||
|
|
||||||
@RestController('/users')
|
@RestController('/users')
|
||||||
export class UsersController {
|
export class UsersController {
|
||||||
@@ -44,6 +45,7 @@ export class UsersController {
|
|||||||
private readonly workflowService: WorkflowService,
|
private readonly workflowService: WorkflowService,
|
||||||
private readonly credentialsService: CredentialsService,
|
private readonly credentialsService: CredentialsService,
|
||||||
private readonly projectService: ProjectService,
|
private readonly projectService: ProjectService,
|
||||||
|
private readonly eventRelay: EventRelay,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
static ERROR_MESSAGES = {
|
static ERROR_MESSAGES = {
|
||||||
@@ -256,6 +258,7 @@ export class UsersController {
|
|||||||
telemetryData,
|
telemetryData,
|
||||||
publicApi: false,
|
publicApi: false,
|
||||||
});
|
});
|
||||||
|
this.eventRelay.emit('user-deleted', { user: req.user });
|
||||||
|
|
||||||
await this.externalHooks.run('user.deleted', [await this.userService.toPublic(userToDelete)]);
|
await this.externalHooks.run('user.deleted', [await this.userService.toPublic(userToDelete)]);
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { In } from '@n8n/typeorm';
|
|||||||
import { SharedCredentials } from '@/databases/entities/SharedCredentials';
|
import { SharedCredentials } from '@/databases/entities/SharedCredentials';
|
||||||
import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository';
|
import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { EventRelay } from '@/eventbus/event-relay.service';
|
||||||
|
|
||||||
@RestController('/credentials')
|
@RestController('/credentials')
|
||||||
export class CredentialsController {
|
export class CredentialsController {
|
||||||
@@ -42,6 +43,7 @@ export class CredentialsController {
|
|||||||
private readonly userManagementMailer: UserManagementMailer,
|
private readonly userManagementMailer: UserManagementMailer,
|
||||||
private readonly sharedCredentialsRepository: SharedCredentialsRepository,
|
private readonly sharedCredentialsRepository: SharedCredentialsRepository,
|
||||||
private readonly projectRelationRepository: ProjectRelationRepository,
|
private readonly projectRelationRepository: ProjectRelationRepository,
|
||||||
|
private readonly eventRelay: EventRelay,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get('/', { middlewares: listQueryMiddleware })
|
@Get('/', { middlewares: listQueryMiddleware })
|
||||||
@@ -164,6 +166,12 @@ export class CredentialsController {
|
|||||||
credential_id: credential.id,
|
credential_id: credential.id,
|
||||||
public_api: false,
|
public_api: false,
|
||||||
});
|
});
|
||||||
|
this.eventRelay.emit('credentials-created', {
|
||||||
|
user: req.user,
|
||||||
|
credentialName: newCredential.name,
|
||||||
|
credentialType: credential.type,
|
||||||
|
credentialId: credential.id,
|
||||||
|
});
|
||||||
|
|
||||||
const scopes = await this.credentialsService.getCredentialScopes(req.user, credential.id);
|
const scopes = await this.credentialsService.getCredentialScopes(req.user, credential.id);
|
||||||
|
|
||||||
@@ -218,6 +226,12 @@ export class CredentialsController {
|
|||||||
credential_type: credential.type,
|
credential_type: credential.type,
|
||||||
credential_id: credential.id,
|
credential_id: credential.id,
|
||||||
});
|
});
|
||||||
|
this.eventRelay.emit('credentials-updated', {
|
||||||
|
user: req.user,
|
||||||
|
credentialName: credential.name,
|
||||||
|
credentialType: credential.type,
|
||||||
|
credentialId: credential.id,
|
||||||
|
});
|
||||||
|
|
||||||
const scopes = await this.credentialsService.getCredentialScopes(req.user, credential.id);
|
const scopes = await this.credentialsService.getCredentialScopes(req.user, credential.id);
|
||||||
|
|
||||||
@@ -253,6 +267,12 @@ export class CredentialsController {
|
|||||||
credential_type: credential.type,
|
credential_type: credential.type,
|
||||||
credential_id: credential.id,
|
credential_id: credential.id,
|
||||||
});
|
});
|
||||||
|
this.eventRelay.emit('credentials-deleted', {
|
||||||
|
user: req.user,
|
||||||
|
credentialName: credential.name,
|
||||||
|
credentialType: credential.type,
|
||||||
|
credentialId: credential.id,
|
||||||
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -321,6 +341,15 @@ export class CredentialsController {
|
|||||||
user_ids_sharees_added: newShareeIds,
|
user_ids_sharees_added: newShareeIds,
|
||||||
sharees_removed: amountRemoved,
|
sharees_removed: amountRemoved,
|
||||||
});
|
});
|
||||||
|
this.eventRelay.emit('credentials-shared', {
|
||||||
|
user: req.user,
|
||||||
|
credentialName: credential.name,
|
||||||
|
credentialType: credential.type,
|
||||||
|
credentialId: credential.id,
|
||||||
|
userIdSharer: req.user.id,
|
||||||
|
userIdsShareesRemoved: newShareeIds,
|
||||||
|
shareesRemoved: amountRemoved,
|
||||||
|
});
|
||||||
|
|
||||||
const projectsRelations = await this.projectRelationRepository.findBy({
|
const projectsRelations = await this.projectRelationRepository.findBy({
|
||||||
projectId: In(newShareeIds),
|
projectId: In(newShareeIds),
|
||||||
|
|||||||
50
packages/cli/src/decorators/Redactable.ts
Normal file
50
packages/cli/src/decorators/Redactable.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { RedactableError } from '@/errors/redactable.error';
|
||||||
|
import type { UserLike } from '@/eventbus/event.types';
|
||||||
|
|
||||||
|
function toRedactable(userLike: UserLike) {
|
||||||
|
return {
|
||||||
|
userId: userLike.id,
|
||||||
|
_email: userLike.email,
|
||||||
|
_firstName: userLike.firstName,
|
||||||
|
_lastName: userLike.lastName,
|
||||||
|
globalRole: userLike.role,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldName = 'user' | 'inviter' | 'invitee';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark redactable properties in a `{ user: UserLike }` field in an `AuditEventRelay`
|
||||||
|
* method arg. These properties will be later redacted by the log streaming
|
||||||
|
* destination based on user prefs. Only for `n8n.audit.*` logs.
|
||||||
|
*
|
||||||
|
* Also transform `id` to `userId` and `role` to `globalRole`.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* { id: '123'; email: 'test@example.com', role: 'some-role' } ->
|
||||||
|
* { userId: '123'; _email: 'test@example.com', globalRole: 'some-role' }
|
||||||
|
*/
|
||||||
|
export const Redactable =
|
||||||
|
(fieldName: FieldName = 'user'): MethodDecorator =>
|
||||||
|
(_target, _propertyName, propertyDescriptor: PropertyDescriptor) => {
|
||||||
|
const originalMethod = propertyDescriptor.value as Function;
|
||||||
|
|
||||||
|
type MethodArgs = Array<{ [fieldName: string]: UserLike }>;
|
||||||
|
|
||||||
|
propertyDescriptor.value = function (...args: MethodArgs) {
|
||||||
|
const index = args.findIndex((arg) => arg[fieldName] !== undefined);
|
||||||
|
|
||||||
|
if (index === -1) throw new RedactableError(fieldName, args.toString());
|
||||||
|
|
||||||
|
const userLike = args[index]?.[fieldName];
|
||||||
|
|
||||||
|
// @ts-expect-error Transformation
|
||||||
|
if (userLike) args[index][fieldName] = toRedactable(userLike);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
return originalMethod.apply(this, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
return propertyDescriptor;
|
||||||
|
};
|
||||||
9
packages/cli/src/errors/redactable.error.ts
Normal file
9
packages/cli/src/errors/redactable.error.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { ApplicationError } from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class RedactableError extends ApplicationError {
|
||||||
|
constructor(fieldName: string, args: string) {
|
||||||
|
super(
|
||||||
|
`Failed to find "${fieldName}" property in argument "${args.toString()}". Please set the decorator \`@Redactable()\` only on \`AuditEventRelay\` methods where the argument contains a "${fieldName}" property.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { mock } from 'jest-mock-extended';
|
||||||
|
import { AuditEventRelay } from '../audit-event-relay.service';
|
||||||
|
import type { MessageEventBus } from '../MessageEventBus/MessageEventBus';
|
||||||
|
import type { Event } from '../event.types';
|
||||||
|
import type { EventRelay } from '../event-relay.service';
|
||||||
|
|
||||||
|
describe('AuditorService', () => {
|
||||||
|
const eventBus = mock<MessageEventBus>();
|
||||||
|
const eventRelay = mock<EventRelay>();
|
||||||
|
const auditor = new AuditEventRelay(eventRelay, eventBus);
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle `user-deleted` event', () => {
|
||||||
|
const arg: Event['user-deleted'] = {
|
||||||
|
user: {
|
||||||
|
id: '123',
|
||||||
|
email: 'john@n8n.io',
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
role: 'some-role',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error Private method
|
||||||
|
auditor.userDeleted(arg);
|
||||||
|
|
||||||
|
expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({
|
||||||
|
eventName: 'n8n.audit.user.deleted',
|
||||||
|
payload: {
|
||||||
|
userId: '123',
|
||||||
|
_email: 'john@n8n.io',
|
||||||
|
_firstName: 'John',
|
||||||
|
_lastName: 'Doe',
|
||||||
|
globalRole: 'some-role',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle `user-invite-email-click` event', () => {
|
||||||
|
const arg: Event['user-invite-email-click'] = {
|
||||||
|
inviter: {
|
||||||
|
id: '123',
|
||||||
|
email: 'john@n8n.io',
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
role: 'some-role',
|
||||||
|
},
|
||||||
|
invitee: {
|
||||||
|
id: '456',
|
||||||
|
email: 'jane@n8n.io',
|
||||||
|
firstName: 'Jane',
|
||||||
|
lastName: 'Doe',
|
||||||
|
role: 'some-other-role',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error Private method
|
||||||
|
auditor.userInviteEmailClick(arg);
|
||||||
|
|
||||||
|
expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({
|
||||||
|
eventName: 'n8n.audit.user.invitation.accepted',
|
||||||
|
payload: {
|
||||||
|
inviter: {
|
||||||
|
userId: '123',
|
||||||
|
_email: 'john@n8n.io',
|
||||||
|
_firstName: 'John',
|
||||||
|
_lastName: 'Doe',
|
||||||
|
globalRole: 'some-role',
|
||||||
|
},
|
||||||
|
invitee: {
|
||||||
|
userId: '456',
|
||||||
|
_email: 'jane@n8n.io',
|
||||||
|
_firstName: 'Jane',
|
||||||
|
_lastName: 'Doe',
|
||||||
|
globalRole: 'some-other-role',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
340
packages/cli/src/eventbus/audit-event-relay.service.ts
Normal file
340
packages/cli/src/eventbus/audit-event-relay.service.ts
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
import { Service } from 'typedi';
|
||||||
|
import { MessageEventBus } from './MessageEventBus/MessageEventBus';
|
||||||
|
import { Redactable } from '@/decorators/Redactable';
|
||||||
|
import { EventRelay } from './event-relay.service';
|
||||||
|
import type { Event } from './event.types';
|
||||||
|
import type { IWorkflowBase } from 'n8n-workflow';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class AuditEventRelay {
|
||||||
|
constructor(
|
||||||
|
private readonly eventRelay: EventRelay,
|
||||||
|
private readonly eventBus: MessageEventBus,
|
||||||
|
) {
|
||||||
|
this.setupHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupHandlers() {
|
||||||
|
this.eventRelay.on('workflow-created', (event) => this.workflowCreated(event));
|
||||||
|
this.eventRelay.on('workflow-deleted', (event) => this.workflowDeleted(event));
|
||||||
|
this.eventRelay.on('workflow-saved', (event) => this.workflowSaved(event));
|
||||||
|
this.eventRelay.on('workflow-pre-execute', (event) => this.workflowPreExecute(event));
|
||||||
|
this.eventRelay.on('workflow-post-execute', (event) => this.workflowPostExecute(event));
|
||||||
|
this.eventRelay.on('node-pre-execute', (event) => this.nodePreExecute(event));
|
||||||
|
this.eventRelay.on('node-post-execute', (event) => this.nodePostExecute(event));
|
||||||
|
this.eventRelay.on('user-deleted', (event) => this.userDeleted(event));
|
||||||
|
this.eventRelay.on('user-invited', (event) => this.userInvited(event));
|
||||||
|
this.eventRelay.on('user-reinvited', (event) => this.userReinvited(event));
|
||||||
|
this.eventRelay.on('user-updated', (event) => this.userUpdated(event));
|
||||||
|
this.eventRelay.on('user-signed-up', (event) => this.userSignedUp(event));
|
||||||
|
this.eventRelay.on('user-logged-in', (event) => this.userLoggedIn(event));
|
||||||
|
this.eventRelay.on('user-login-failed', (event) => this.userLoginFailed(event));
|
||||||
|
this.eventRelay.on('user-invite-email-click', (event) => this.userInviteEmailClick(event));
|
||||||
|
this.eventRelay.on('user-password-reset-email-click', (event) =>
|
||||||
|
this.userPasswordResetEmailClick(event),
|
||||||
|
);
|
||||||
|
this.eventRelay.on('user-password-reset-request-click', (event) =>
|
||||||
|
this.userPasswordResetRequestClick(event),
|
||||||
|
);
|
||||||
|
this.eventRelay.on('api-key-created', (event) => this.apiKeyCreated(event));
|
||||||
|
this.eventRelay.on('api-key-deleted', (event) => this.apiKeyDeleted(event));
|
||||||
|
this.eventRelay.on('email-failed', (event) => this.emailFailed(event));
|
||||||
|
this.eventRelay.on('credentials-created', (event) => this.credentialsCreated(event));
|
||||||
|
this.eventRelay.on('credentials-deleted', (event) => this.credentialsDeleted(event));
|
||||||
|
this.eventRelay.on('credentials-shared', (event) => this.credentialsShared(event));
|
||||||
|
this.eventRelay.on('credentials-updated', (event) => this.credentialsUpdated(event));
|
||||||
|
this.eventRelay.on('credentials-deleted', (event) => this.credentialsDeleted(event));
|
||||||
|
this.eventRelay.on('community-package-installed', (event) =>
|
||||||
|
this.communityPackageInstalled(event),
|
||||||
|
);
|
||||||
|
this.eventRelay.on('community-package-updated', (event) => this.communityPackageUpdated(event));
|
||||||
|
this.eventRelay.on('community-package-deleted', (event) => this.communityPackageDeleted(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Workflow
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Redactable()
|
||||||
|
private workflowCreated({ user, workflow }: Event['workflow-created']) {
|
||||||
|
void this.eventBus.sendAuditEvent({
|
||||||
|
eventName: 'n8n.audit.workflow.created',
|
||||||
|
payload: {
|
||||||
|
...user,
|
||||||
|
workflowId: workflow.id,
|
||||||
|
workflowName: workflow.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Redactable()
|
||||||
|
private workflowDeleted({ user, workflowId }: Event['workflow-deleted']) {
|
||||||
|
void this.eventBus.sendAuditEvent({
|
||||||
|
eventName: 'n8n.audit.workflow.deleted',
|
||||||
|
payload: { ...user, workflowId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Redactable()
|
||||||
|
private workflowSaved({ user, workflowId, workflowName }: Event['workflow-saved']) {
|
||||||
|
void this.eventBus.sendAuditEvent({
|
||||||
|
eventName: 'n8n.audit.workflow.updated',
|
||||||
|
payload: {
|
||||||
|
...user,
|
||||||
|
workflowId,
|
||||||
|
workflowName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private workflowPreExecute({ data, executionId }: Event['workflow-pre-execute']) {
|
||||||
|
const payload =
|
||||||
|
'executionData' in data
|
||||||
|
? {
|
||||||
|
executionId,
|
||||||
|
userId: data.userId,
|
||||||
|
workflowId: data.workflowData.id,
|
||||||
|
isManual: data.executionMode === 'manual',
|
||||||
|
workflowName: data.workflowData.name,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
executionId,
|
||||||
|
userId: undefined,
|
||||||
|
workflowId: (data as IWorkflowBase).id,
|
||||||
|
isManual: false,
|
||||||
|
workflowName: (data as IWorkflowBase).name,
|
||||||
|
};
|
||||||
|
|
||||||
|
void this.eventBus.sendWorkflowEvent({
|
||||||
|
eventName: 'n8n.workflow.started',
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private workflowPostExecute(event: Event['workflow-post-execute']) {
|
||||||
|
void this.eventBus.sendWorkflowEvent({
|
||||||
|
eventName: 'n8n.workflow.success',
|
||||||
|
payload: event,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node
|
||||||
|
*/
|
||||||
|
|
||||||
|
private nodePreExecute({ workflow, executionId, nodeName }: Event['node-pre-execute']) {
|
||||||
|
void this.eventBus.sendNodeEvent({
|
||||||
|
eventName: 'n8n.node.started',
|
||||||
|
payload: {
|
||||||
|
workflowId: workflow.id,
|
||||||
|
workflowName: workflow.name,
|
||||||
|
executionId,
|
||||||
|
nodeType: workflow.nodes.find((n) => n.name === nodeName)?.type,
|
||||||
|
nodeName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private nodePostExecute({ workflow, executionId, nodeName }: Event['node-post-execute']) {
|
||||||
|
void this.eventBus.sendNodeEvent({
|
||||||
|
eventName: 'n8n.node.finished',
|
||||||
|
payload: {
|
||||||
|
workflowId: workflow.id,
|
||||||
|
workflowName: workflow.name,
|
||||||
|
executionId,
|
||||||
|
nodeType: workflow.nodes.find((n) => n.name === nodeName)?.type,
|
||||||
|
nodeName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Redactable()
|
||||||
|
private userDeleted({ user }: Event['user-deleted']) {
|
||||||
|
void this.eventBus.sendAuditEvent({
|
||||||
|
eventName: 'n8n.audit.user.deleted',
|
||||||
|
payload: user,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Redactable()
|
||||||
|
private userInvited({ user, targetUserId }: Event['user-invited']) {
|
||||||
|
void this.eventBus.sendAuditEvent({
|
||||||
|
eventName: 'n8n.audit.user.invited',
|
||||||
|
payload: { ...user, targetUserId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Redactable()
|
||||||
|
private userReinvited({ user, targetUserId }: Event['user-reinvited']) {
|
||||||
|
void this.eventBus.sendAuditEvent({
|
||||||
|
eventName: 'n8n.audit.user.reinvited',
|
||||||
|
payload: { ...user, targetUserId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Redactable()
|
||||||
|
private userUpdated({ user, fieldsChanged }: Event['user-updated']) {
|
||||||
|
void this.eventBus.sendAuditEvent({
|
||||||
|
eventName: 'n8n.audit.user.updated',
|
||||||
|
payload: { ...user, fieldsChanged },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Redactable()
|
||||||
|
private userSignedUp({ user }: Event['user-signed-up']) {
|
||||||
|
void this.eventBus.sendAuditEvent({
|
||||||
|
eventName: 'n8n.audit.user.signedup',
|
||||||
|
payload: user,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Redactable()
|
||||||
|
private userLoggedIn({ user, authenticationMethod }: Event['user-logged-in']) {
|
||||||
|
void this.eventBus.sendAuditEvent({
|
||||||
|
eventName: 'n8n.audit.user.login.success',
|
||||||
|
payload: { ...user, authenticationMethod },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private userLoginFailed(
|
||||||
|
event: Event['user-login-failed'] /* exception: no `UserLike` to redact */,
|
||||||
|
) {
|
||||||
|
void this.eventBus.sendAuditEvent({
|
||||||
|
eventName: 'n8n.audit.user.login.failed',
|
||||||
|
payload: event,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Redactable('inviter')
|
||||||
|
@Redactable('invitee')
|
||||||
|
private userInviteEmailClick(event: Event['user-invite-email-click']) {
|
||||||
|
void this.eventBus.sendAuditEvent({
|
||||||
|
eventName: 'n8n.audit.user.invitation.accepted',
|
||||||
|
payload: event,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Redactable()
|
||||||
|
private userPasswordResetEmailClick({ user }: Event['user-password-reset-email-click']) {
|
||||||
|
void this.eventBus.sendAuditEvent({
|
||||||
|
eventName: 'n8n.audit.user.reset',
|
||||||
|
payload: user,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Redactable()
|
||||||
|
private userPasswordResetRequestClick({ user }: Event['user-password-reset-request-click']) {
|
||||||
|
void this.eventBus.sendAuditEvent({
|
||||||
|
eventName: 'n8n.audit.user.reset.requested',
|
||||||
|
payload: user,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API key
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Redactable()
|
||||||
|
private apiKeyCreated({ user }: Event['api-key-created']) {
|
||||||
|
void this.eventBus.sendAuditEvent({
|
||||||
|
eventName: 'n8n.audit.user.api.created',
|
||||||
|
payload: user,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Redactable()
|
||||||
|
private apiKeyDeleted({ user }: Event['api-key-deleted']) {
|
||||||
|
void this.eventBus.sendAuditEvent({
|
||||||
|
eventName: 'n8n.audit.user.api.deleted',
|
||||||
|
payload: user,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emailing
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Redactable()
|
||||||
|
private emailFailed({ user, messageType }: Event['email-failed']) {
|
||||||
|
void this.eventBus.sendAuditEvent({
|
||||||
|
eventName: 'n8n.audit.user.email.failed',
|
||||||
|
payload: { ...user, messageType },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Credentials
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Redactable()
|
||||||
|
private credentialsCreated({ user, ...rest }: Event['credentials-created']) {
|
||||||
|
void this.eventBus.sendAuditEvent({
|
||||||
|
eventName: 'n8n.audit.user.credentials.created',
|
||||||
|
payload: { ...user, ...rest },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Redactable()
|
||||||
|
private credentialsDeleted({ user, ...rest }: Event['credentials-deleted']) {
|
||||||
|
void this.eventBus.sendAuditEvent({
|
||||||
|
eventName: 'n8n.audit.user.credentials.deleted',
|
||||||
|
payload: { ...user, ...rest },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Redactable()
|
||||||
|
private credentialsShared({ user, ...rest }: Event['credentials-shared']) {
|
||||||
|
void this.eventBus.sendAuditEvent({
|
||||||
|
eventName: 'n8n.audit.user.credentials.shared',
|
||||||
|
payload: { ...user, ...rest },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Redactable()
|
||||||
|
private credentialsUpdated({ user, ...rest }: Event['credentials-updated']) {
|
||||||
|
void this.eventBus.sendAuditEvent({
|
||||||
|
eventName: 'n8n.audit.user.credentials.updated',
|
||||||
|
payload: { ...user, ...rest },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Community package
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Redactable()
|
||||||
|
private communityPackageInstalled({ user, ...rest }: Event['community-package-installed']) {
|
||||||
|
void this.eventBus.sendAuditEvent({
|
||||||
|
eventName: 'n8n.audit.package.installed',
|
||||||
|
payload: { ...user, ...rest },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Redactable()
|
||||||
|
private communityPackageUpdated({ user, ...rest }: Event['community-package-updated']) {
|
||||||
|
void this.eventBus.sendAuditEvent({
|
||||||
|
eventName: 'n8n.audit.package.updated',
|
||||||
|
payload: { ...user, ...rest },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Redactable()
|
||||||
|
private communityPackageDeleted({ user, ...rest }: Event['community-package-deleted']) {
|
||||||
|
void this.eventBus.sendAuditEvent({
|
||||||
|
eventName: 'n8n.audit.package.deleted',
|
||||||
|
payload: { ...user, ...rest },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
16
packages/cli/src/eventbus/event-relay.service.ts
Normal file
16
packages/cli/src/eventbus/event-relay.service.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { EventEmitter } from 'node:events';
|
||||||
|
import { Service } from 'typedi';
|
||||||
|
import type { Event } from './event.types';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class EventRelay extends EventEmitter {
|
||||||
|
emit<K extends keyof Event>(eventName: K, arg: Event[K]) {
|
||||||
|
super.emit(eventName, arg);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
on<K extends keyof Event>(eventName: K, handler: (arg: Event[K]) => void) {
|
||||||
|
super.on(eventName, handler);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
185
packages/cli/src/eventbus/event.types.ts
Normal file
185
packages/cli/src/eventbus/event.types.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import type { AuthenticationMethod, IWorkflowBase } from 'n8n-workflow';
|
||||||
|
import type { IWorkflowExecutionDataProcess } from '@/Interfaces';
|
||||||
|
|
||||||
|
export type UserLike = {
|
||||||
|
id: string;
|
||||||
|
email?: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
role: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Events sent by services and consumed by relays, e.g. `AuditEventRelay`.
|
||||||
|
*/
|
||||||
|
export type Event = {
|
||||||
|
'workflow-created': {
|
||||||
|
user: UserLike;
|
||||||
|
workflow: IWorkflowBase;
|
||||||
|
};
|
||||||
|
|
||||||
|
'workflow-deleted': {
|
||||||
|
user: UserLike;
|
||||||
|
workflowId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
'workflow-saved': {
|
||||||
|
user: UserLike;
|
||||||
|
workflowId: string;
|
||||||
|
workflowName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
'workflow-pre-execute': {
|
||||||
|
executionId: string;
|
||||||
|
data: IWorkflowExecutionDataProcess /* main process */ | IWorkflowBase /* worker */;
|
||||||
|
};
|
||||||
|
|
||||||
|
'workflow-post-execute': {
|
||||||
|
executionId: string;
|
||||||
|
success: boolean;
|
||||||
|
userId?: string;
|
||||||
|
workflowId: string;
|
||||||
|
isManual: boolean;
|
||||||
|
workflowName: string;
|
||||||
|
metadata?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
'node-pre-execute': {
|
||||||
|
executionId: string;
|
||||||
|
workflow: IWorkflowBase;
|
||||||
|
nodeName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
'node-post-execute': {
|
||||||
|
executionId: string;
|
||||||
|
workflow: IWorkflowBase;
|
||||||
|
nodeName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
'user-deleted': {
|
||||||
|
user: UserLike;
|
||||||
|
};
|
||||||
|
|
||||||
|
'user-invited': {
|
||||||
|
user: UserLike;
|
||||||
|
targetUserId: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
'user-reinvited': {
|
||||||
|
user: UserLike;
|
||||||
|
targetUserId: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
'user-updated': {
|
||||||
|
user: UserLike;
|
||||||
|
fieldsChanged: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
'user-signed-up': {
|
||||||
|
user: UserLike;
|
||||||
|
};
|
||||||
|
|
||||||
|
'user-logged-in': {
|
||||||
|
user: UserLike;
|
||||||
|
authenticationMethod: AuthenticationMethod;
|
||||||
|
};
|
||||||
|
|
||||||
|
'user-login-failed': {
|
||||||
|
userEmail: string;
|
||||||
|
authenticationMethod: AuthenticationMethod;
|
||||||
|
reason?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
'user-invite-email-click': {
|
||||||
|
inviter: UserLike;
|
||||||
|
invitee: UserLike;
|
||||||
|
};
|
||||||
|
|
||||||
|
'user-password-reset-email-click': {
|
||||||
|
user: UserLike;
|
||||||
|
};
|
||||||
|
|
||||||
|
'user-password-reset-request-click': {
|
||||||
|
user: UserLike;
|
||||||
|
};
|
||||||
|
|
||||||
|
'api-key-created': {
|
||||||
|
user: UserLike;
|
||||||
|
};
|
||||||
|
|
||||||
|
'api-key-deleted': {
|
||||||
|
user: UserLike;
|
||||||
|
};
|
||||||
|
|
||||||
|
'email-failed': {
|
||||||
|
user: UserLike;
|
||||||
|
messageType:
|
||||||
|
| 'Reset password'
|
||||||
|
| 'New user invite'
|
||||||
|
| 'Resend invite'
|
||||||
|
| 'Workflow shared'
|
||||||
|
| 'Credentials shared';
|
||||||
|
};
|
||||||
|
|
||||||
|
'credentials-created': {
|
||||||
|
user: UserLike;
|
||||||
|
credentialName: string;
|
||||||
|
credentialType: string;
|
||||||
|
credentialId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
'credentials-shared': {
|
||||||
|
user: UserLike;
|
||||||
|
credentialName: string;
|
||||||
|
credentialType: string;
|
||||||
|
credentialId: string;
|
||||||
|
userIdSharer: string;
|
||||||
|
userIdsShareesRemoved: string[];
|
||||||
|
shareesRemoved: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
'credentials-updated': {
|
||||||
|
user: UserLike;
|
||||||
|
credentialName: string;
|
||||||
|
credentialType: string;
|
||||||
|
credentialId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
'credentials-deleted': {
|
||||||
|
user: UserLike;
|
||||||
|
credentialName: string;
|
||||||
|
credentialType: string;
|
||||||
|
credentialId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
'community-package-installed': {
|
||||||
|
user: UserLike;
|
||||||
|
inputString: string;
|
||||||
|
packageName: string;
|
||||||
|
success: boolean;
|
||||||
|
packageVersion?: string;
|
||||||
|
packageNodeNames?: string[];
|
||||||
|
packageAuthor?: string;
|
||||||
|
packageAuthorEmail?: string;
|
||||||
|
failureReason?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
'community-package-updated': {
|
||||||
|
user: UserLike;
|
||||||
|
packageName: string;
|
||||||
|
packageVersionCurrent: string;
|
||||||
|
packageVersionNew: string;
|
||||||
|
packageNodeNames: string[];
|
||||||
|
packageAuthor?: string;
|
||||||
|
packageAuthorEmail?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
'community-package-deleted': {
|
||||||
|
user: UserLike;
|
||||||
|
packageName: string;
|
||||||
|
packageVersion: string;
|
||||||
|
packageNodeNames: string[];
|
||||||
|
packageAuthor?: string;
|
||||||
|
packageAuthorEmail?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -20,6 +20,8 @@ import { NodeCrashedError } from '@/errors/node-crashed.error';
|
|||||||
import { WorkflowCrashedError } from '@/errors/workflow-crashed.error';
|
import { WorkflowCrashedError } from '@/errors/workflow-crashed.error';
|
||||||
import { EventMessageNode } from '@/eventbus/EventMessageClasses/EventMessageNode';
|
import { EventMessageNode } from '@/eventbus/EventMessageClasses/EventMessageNode';
|
||||||
import { EventMessageWorkflow } from '@/eventbus/EventMessageClasses/EventMessageWorkflow';
|
import { EventMessageWorkflow } from '@/eventbus/EventMessageClasses/EventMessageWorkflow';
|
||||||
|
|
||||||
|
import type { EventRelay } from '@/eventbus/event-relay.service';
|
||||||
import type { EventMessageTypes as EventMessage } from '@/eventbus/EventMessageClasses';
|
import type { EventMessageTypes as EventMessage } from '@/eventbus/EventMessageClasses';
|
||||||
import type { Logger } from '@/Logger';
|
import type { Logger } from '@/Logger';
|
||||||
|
|
||||||
@@ -191,6 +193,7 @@ describe('ExecutionRecoveryService', () => {
|
|||||||
push,
|
push,
|
||||||
executionRepository,
|
executionRepository,
|
||||||
orchestrationService,
|
orchestrationService,
|
||||||
|
mock<EventRelay>(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import config from '@/config';
|
|||||||
import { OnShutdown } from '@/decorators/OnShutdown';
|
import { OnShutdown } from '@/decorators/OnShutdown';
|
||||||
import type { QueueRecoverySettings } from './execution.types';
|
import type { QueueRecoverySettings } from './execution.types';
|
||||||
import { OrchestrationService } from '@/services/orchestration.service';
|
import { OrchestrationService } from '@/services/orchestration.service';
|
||||||
|
import { EventRelay } from '@/eventbus/event-relay.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for recovering key properties in executions.
|
* Service for recovering key properties in executions.
|
||||||
@@ -27,6 +28,7 @@ export class ExecutionRecoveryService {
|
|||||||
private readonly push: Push,
|
private readonly push: Push,
|
||||||
private readonly executionRepository: ExecutionRepository,
|
private readonly executionRepository: ExecutionRepository,
|
||||||
private readonly orchestrationService: OrchestrationService,
|
private readonly orchestrationService: OrchestrationService,
|
||||||
|
private readonly eventRelay: EventRelay,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -284,6 +286,14 @@ export class ExecutionRecoveryService {
|
|||||||
status: execution.status,
|
status: execution.status,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.eventRelay.emit('workflow-post-execute', {
|
||||||
|
workflowId: execution.workflowData.id,
|
||||||
|
workflowName: execution.workflowData.name,
|
||||||
|
executionId: execution.id,
|
||||||
|
success: execution.status === 'success',
|
||||||
|
isManual: execution.mode === 'manual',
|
||||||
|
});
|
||||||
|
|
||||||
const externalHooks = getWorkflowHooksMain(
|
const externalHooks = getWorkflowHooksMain(
|
||||||
{
|
{
|
||||||
userId: '',
|
userId: '',
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { InternalHooks } from '@/InternalHooks';
|
|||||||
import { UrlService } from '@/services/url.service';
|
import { UrlService } from '@/services/url.service';
|
||||||
import type { UserRequest } from '@/requests';
|
import type { UserRequest } from '@/requests';
|
||||||
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
||||||
|
import { EventRelay } from '@/eventbus/event-relay.service';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
@@ -20,6 +21,7 @@ export class UserService {
|
|||||||
private readonly userRepository: UserRepository,
|
private readonly userRepository: UserRepository,
|
||||||
private readonly mailer: UserManagementMailer,
|
private readonly mailer: UserManagementMailer,
|
||||||
private readonly urlService: UrlService,
|
private readonly urlService: UrlService,
|
||||||
|
private readonly eventRelay: EventRelay,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async update(userId: string, data: Partial<User>) {
|
async update(userId: string, data: Partial<User>) {
|
||||||
@@ -156,6 +158,10 @@ export class UserService {
|
|||||||
email_sent: result.emailSent,
|
email_sent: result.emailSent,
|
||||||
invitee_role: role, // same role for all invited users
|
invitee_role: role, // same role for all invited users
|
||||||
});
|
});
|
||||||
|
this.eventRelay.emit('user-invited', {
|
||||||
|
user: owner,
|
||||||
|
targetUserId: Object.values(toInviteUsers),
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
void Container.get(InternalHooks).onEmailFailed({
|
void Container.get(InternalHooks).onEmailFailed({
|
||||||
@@ -163,6 +169,7 @@ export class UserService {
|
|||||||
message_type: 'New user invite',
|
message_type: 'New user invite',
|
||||||
public_api: false,
|
public_api: false,
|
||||||
});
|
});
|
||||||
|
this.eventRelay.emit('email-failed', { user: owner, messageType: 'New user invite' });
|
||||||
this.logger.error('Failed to send email', {
|
this.logger.error('Failed to send email', {
|
||||||
userId: owner.id,
|
userId: owner.id,
|
||||||
inviteAcceptUrl,
|
inviteAcceptUrl,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import url from 'url';
|
|||||||
import { Get, Post, RestController, GlobalScope } from '@/decorators';
|
import { Get, Post, RestController, GlobalScope } from '@/decorators';
|
||||||
import { AuthService } from '@/auth/auth.service';
|
import { AuthService } from '@/auth/auth.service';
|
||||||
import { AuthenticatedRequest } from '@/requests';
|
import { AuthenticatedRequest } from '@/requests';
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
|
||||||
import querystring from 'querystring';
|
import querystring from 'querystring';
|
||||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||||
import { AuthError } from '@/errors/response-errors/auth.error';
|
import { AuthError } from '@/errors/response-errors/auth.error';
|
||||||
@@ -28,6 +27,7 @@ import {
|
|||||||
import { SamlService } from '../saml.service.ee';
|
import { SamlService } from '../saml.service.ee';
|
||||||
import { SamlConfiguration } from '../types/requests';
|
import { SamlConfiguration } from '../types/requests';
|
||||||
import { getInitSSOFormView } from '../views/initSsoPost';
|
import { getInitSSOFormView } from '../views/initSsoPost';
|
||||||
|
import { EventRelay } from '@/eventbus/event-relay.service';
|
||||||
|
|
||||||
@RestController('/sso/saml')
|
@RestController('/sso/saml')
|
||||||
export class SamlController {
|
export class SamlController {
|
||||||
@@ -35,7 +35,7 @@ export class SamlController {
|
|||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
private readonly samlService: SamlService,
|
private readonly samlService: SamlService,
|
||||||
private readonly urlService: UrlService,
|
private readonly urlService: UrlService,
|
||||||
private readonly internalHooks: InternalHooks,
|
private readonly eventRelay: EventRelay,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get('/metadata', { skipAuth: true })
|
@Get('/metadata', { skipAuth: true })
|
||||||
@@ -126,10 +126,11 @@ export class SamlController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (loginResult.authenticatedUser) {
|
if (loginResult.authenticatedUser) {
|
||||||
void this.internalHooks.onUserLoginSuccess({
|
this.eventRelay.emit('user-logged-in', {
|
||||||
user: loginResult.authenticatedUser,
|
user: loginResult.authenticatedUser,
|
||||||
authenticationMethod: 'saml',
|
authenticationMethod: 'saml',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Only sign in user if SAML is enabled, otherwise treat as test connection
|
// Only sign in user if SAML is enabled, otherwise treat as test connection
|
||||||
if (isSamlLicensedAndEnabled()) {
|
if (isSamlLicensedAndEnabled()) {
|
||||||
this.authService.issueCookie(res, loginResult.authenticatedUser, req.browserId);
|
this.authService.issueCookie(res, loginResult.authenticatedUser, req.browserId);
|
||||||
@@ -143,8 +144,8 @@ export class SamlController {
|
|||||||
return res.status(202).send(loginResult.attributes);
|
return res.status(202).send(loginResult.attributes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void this.internalHooks.onUserLoginFailed({
|
this.eventRelay.emit('user-login-failed', {
|
||||||
user: loginResult.attributes.email ?? 'unknown',
|
userEmail: loginResult.attributes.email ?? 'unknown',
|
||||||
authenticationMethod: 'saml',
|
authenticationMethod: 'saml',
|
||||||
});
|
});
|
||||||
throw new AuthError('SAML Authentication failed');
|
throw new AuthError('SAML Authentication failed');
|
||||||
@@ -152,8 +153,8 @@ export class SamlController {
|
|||||||
if (isConnectionTestRequest(req)) {
|
if (isConnectionTestRequest(req)) {
|
||||||
return res.send(getSamlConnectionTestFailedView((error as Error).message));
|
return res.send(getSamlConnectionTestFailedView((error as Error).message));
|
||||||
}
|
}
|
||||||
void this.internalHooks.onUserLoginFailed({
|
this.eventRelay.emit('user-login-failed', {
|
||||||
user: 'unknown',
|
userEmail: 'unknown',
|
||||||
authenticationMethod: 'saml',
|
authenticationMethod: 'saml',
|
||||||
});
|
});
|
||||||
throw new AuthError('SAML Authentication failed: ' + (error as Error).message);
|
throw new AuthError('SAML Authentication failed: ' + (error as Error).message);
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import type { Scope } from '@n8n/permissions';
|
|||||||
import type { EntityManager } from '@n8n/typeorm';
|
import type { EntityManager } from '@n8n/typeorm';
|
||||||
import { In } from '@n8n/typeorm';
|
import { In } from '@n8n/typeorm';
|
||||||
import { SharedWorkflow } from '@/databases/entities/SharedWorkflow';
|
import { SharedWorkflow } from '@/databases/entities/SharedWorkflow';
|
||||||
|
import { EventRelay } from '@/eventbus/event-relay.service';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class WorkflowService {
|
export class WorkflowService {
|
||||||
@@ -51,6 +52,7 @@ export class WorkflowService {
|
|||||||
private readonly workflowSharingService: WorkflowSharingService,
|
private readonly workflowSharingService: WorkflowSharingService,
|
||||||
private readonly projectService: ProjectService,
|
private readonly projectService: ProjectService,
|
||||||
private readonly executionRepository: ExecutionRepository,
|
private readonly executionRepository: ExecutionRepository,
|
||||||
|
private readonly eventRelay: EventRelay,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getMany(user: User, options?: ListQuery.Options, includeScopes?: boolean) {
|
async getMany(user: User, options?: ListQuery.Options, includeScopes?: boolean) {
|
||||||
@@ -216,6 +218,11 @@ export class WorkflowService {
|
|||||||
|
|
||||||
await this.externalHooks.run('workflow.afterUpdate', [updatedWorkflow]);
|
await this.externalHooks.run('workflow.afterUpdate', [updatedWorkflow]);
|
||||||
void Container.get(InternalHooks).onWorkflowSaved(user, updatedWorkflow, false);
|
void Container.get(InternalHooks).onWorkflowSaved(user, updatedWorkflow, false);
|
||||||
|
this.eventRelay.emit('workflow-saved', {
|
||||||
|
user,
|
||||||
|
workflowId: updatedWorkflow.id,
|
||||||
|
workflowName: updatedWorkflow.name,
|
||||||
|
});
|
||||||
|
|
||||||
if (updatedWorkflow.active) {
|
if (updatedWorkflow.active) {
|
||||||
// When the workflow is supposed to be active add it again
|
// When the workflow is supposed to be active add it again
|
||||||
@@ -274,6 +281,7 @@ export class WorkflowService {
|
|||||||
await this.binaryDataService.deleteMany(idsForDeletion);
|
await this.binaryDataService.deleteMany(idsForDeletion);
|
||||||
|
|
||||||
void Container.get(InternalHooks).onWorkflowDeleted(user, workflowId, false);
|
void Container.get(InternalHooks).onWorkflowDeleted(user, workflowId, false);
|
||||||
|
this.eventRelay.emit('workflow-deleted', { user, workflowId });
|
||||||
await this.externalHooks.run('workflow.afterDelete', [workflowId]);
|
await this.externalHooks.run('workflow.afterDelete', [workflowId]);
|
||||||
|
|
||||||
return workflow;
|
return workflow;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import { In, type FindOptionsRelations } from '@n8n/typeorm';
|
|||||||
import type { Project } from '@/databases/entities/Project';
|
import type { Project } from '@/databases/entities/Project';
|
||||||
import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository';
|
import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { EventRelay } from '@/eventbus/event-relay.service';
|
||||||
|
|
||||||
@RestController('/workflows')
|
@RestController('/workflows')
|
||||||
export class WorkflowsController {
|
export class WorkflowsController {
|
||||||
@@ -64,6 +65,7 @@ export class WorkflowsController {
|
|||||||
private readonly projectRepository: ProjectRepository,
|
private readonly projectRepository: ProjectRepository,
|
||||||
private readonly projectService: ProjectService,
|
private readonly projectService: ProjectService,
|
||||||
private readonly projectRelationRepository: ProjectRelationRepository,
|
private readonly projectRelationRepository: ProjectRelationRepository,
|
||||||
|
private readonly eventRelay: EventRelay,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post('/')
|
@Post('/')
|
||||||
@@ -175,6 +177,7 @@ export class WorkflowsController {
|
|||||||
|
|
||||||
await this.externalHooks.run('workflow.afterCreate', [savedWorkflow]);
|
await this.externalHooks.run('workflow.afterCreate', [savedWorkflow]);
|
||||||
void this.internalHooks.onWorkflowCreated(req.user, newWorkflow, project!, false);
|
void this.internalHooks.onWorkflowCreated(req.user, newWorkflow, project!, false);
|
||||||
|
this.eventRelay.emit('workflow-created', { user: req.user, workflow: newWorkflow });
|
||||||
|
|
||||||
const scopes = await this.workflowService.getWorkflowScopes(req.user, savedWorkflow.id);
|
const scopes = await this.workflowService.getWorkflowScopes(req.user, savedWorkflow.id);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
import { Container } from 'typedi';
|
|
||||||
import type { AuthenticationMethod } from 'n8n-workflow';
|
|
||||||
|
|
||||||
import type { User } from '@db/entities/User';
|
import type { User } from '@db/entities/User';
|
||||||
import { setSamlLoginEnabled } from '@/sso/saml/samlHelpers';
|
import { setSamlLoginEnabled } from '@/sso/saml/samlHelpers';
|
||||||
import { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '@/sso/ssoHelpers';
|
import { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '@/sso/ssoHelpers';
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
|
||||||
import { SamlService } from '@/sso/saml/saml.service.ee';
|
|
||||||
import type { SamlUserAttributes } from '@/sso/saml/types/samlUserAttributes';
|
|
||||||
|
|
||||||
import { randomEmail, randomName, randomValidPassword } from '../shared/random';
|
import { randomEmail, randomName, randomValidPassword } from '../shared/random';
|
||||||
import * as utils from '../shared/utils/';
|
import * as utils from '../shared/utils/';
|
||||||
@@ -266,89 +260,3 @@ describe('Check endpoint permissions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('SAML login flow', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await enableSaml(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should trigger onUserLoginSuccess hook', async () => {
|
|
||||||
const mockedHandleSamlLogin = jest.spyOn(Container.get(SamlService), 'handleSamlLogin');
|
|
||||||
|
|
||||||
mockedHandleSamlLogin.mockImplementation(
|
|
||||||
async (): Promise<{
|
|
||||||
authenticatedUser: User;
|
|
||||||
attributes: SamlUserAttributes;
|
|
||||||
onboardingRequired: false;
|
|
||||||
}> => {
|
|
||||||
return {
|
|
||||||
authenticatedUser: someUser,
|
|
||||||
attributes: {
|
|
||||||
email: someUser.email,
|
|
||||||
firstName: someUser.firstName,
|
|
||||||
lastName: someUser.lastName,
|
|
||||||
userPrincipalName: someUser.email,
|
|
||||||
},
|
|
||||||
onboardingRequired: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const mockedHookOnUserLoginSuccess = jest.spyOn(
|
|
||||||
Container.get(InternalHooks),
|
|
||||||
'onUserLoginSuccess',
|
|
||||||
);
|
|
||||||
mockedHookOnUserLoginSuccess.mockImplementation(
|
|
||||||
async (userLoginData: { user: User; authenticationMethod: AuthenticationMethod }) => {
|
|
||||||
expect(userLoginData.authenticationMethod).toEqual('saml');
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await authOwnerAgent.post('/sso/saml/acs').expect(302);
|
|
||||||
expect(mockedHookOnUserLoginSuccess).toBeCalled();
|
|
||||||
mockedHookOnUserLoginSuccess.mockRestore();
|
|
||||||
mockedHandleSamlLogin.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should trigger onUserLoginFailed hook', async () => {
|
|
||||||
const mockedHandleSamlLogin = jest.spyOn(Container.get(SamlService), 'handleSamlLogin');
|
|
||||||
|
|
||||||
mockedHandleSamlLogin.mockImplementation(
|
|
||||||
async (): Promise<{
|
|
||||||
authenticatedUser: User | undefined;
|
|
||||||
attributes: SamlUserAttributes;
|
|
||||||
onboardingRequired: false;
|
|
||||||
}> => {
|
|
||||||
return {
|
|
||||||
authenticatedUser: undefined,
|
|
||||||
attributes: {
|
|
||||||
email: someUser.email,
|
|
||||||
firstName: someUser.firstName,
|
|
||||||
lastName: someUser.lastName,
|
|
||||||
userPrincipalName: someUser.email,
|
|
||||||
},
|
|
||||||
onboardingRequired: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const mockedHookOnUserLoginFailed = jest.spyOn(
|
|
||||||
Container.get(InternalHooks),
|
|
||||||
'onUserLoginFailed',
|
|
||||||
);
|
|
||||||
mockedHookOnUserLoginFailed.mockImplementation(
|
|
||||||
async (userLoginData: {
|
|
||||||
user: string;
|
|
||||||
authenticationMethod: AuthenticationMethod;
|
|
||||||
reason?: string;
|
|
||||||
}) => {
|
|
||||||
expect(userLoginData.authenticationMethod).toEqual('saml');
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await authOwnerAgent.post('/sso/saml/acs').expect(401);
|
|
||||||
expect(mockedHookOnUserLoginFailed).toBeCalled();
|
|
||||||
mockedHookOnUserLoginFailed.mockRestore();
|
|
||||||
mockedHandleSamlLogin.mockRestore();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ beforeAll(async () => {
|
|||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
|
mock(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ describe('InternalHooks', () => {
|
|||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
|
||||||
license,
|
license,
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
|
|||||||
Reference in New Issue
Block a user