feat(core): Email recipients on resource shared (#8408)
This commit is contained in:
@@ -648,7 +648,12 @@ export class InternalHooks {
|
||||
|
||||
async onUserTransactionalEmail(userTransactionalEmailData: {
|
||||
user_id: string;
|
||||
message_type: 'Reset password' | 'New user invite' | 'Resend invite';
|
||||
message_type:
|
||||
| 'Reset password'
|
||||
| 'New user invite'
|
||||
| 'Resend invite'
|
||||
| 'Workflow shared'
|
||||
| 'Credentials shared';
|
||||
public_api: boolean;
|
||||
}): Promise<void> {
|
||||
return await this.telemetry.track(
|
||||
@@ -737,7 +742,12 @@ export class InternalHooks {
|
||||
|
||||
async onEmailFailed(failedEmailData: {
|
||||
user: User;
|
||||
message_type: 'Reset password' | 'New user invite' | 'Resend invite';
|
||||
message_type:
|
||||
| 'Reset password'
|
||||
| 'New user invite'
|
||||
| 'Resend invite'
|
||||
| 'Workflow shared'
|
||||
| 'Credentials shared';
|
||||
public_api: boolean;
|
||||
}): Promise<void> {
|
||||
void Promise.all([
|
||||
|
||||
@@ -9,7 +9,7 @@ import { NodeMailer } from './NodeMailer';
|
||||
import { ApplicationError } from 'n8n-workflow';
|
||||
|
||||
type Template = HandlebarsTemplateDelegate<unknown>;
|
||||
type TemplateName = 'invite' | 'passwordReset';
|
||||
type TemplateName = 'invite' | 'passwordReset' | 'workflowShared' | 'credentialsShared';
|
||||
|
||||
const templates: Partial<Record<TemplateName, Template>> = {};
|
||||
|
||||
@@ -81,4 +81,50 @@ export class UserManagementMailer {
|
||||
// No error, just say no email was sent.
|
||||
return result ?? { emailSent: false };
|
||||
}
|
||||
|
||||
async notifyWorkflowShared({
|
||||
recipientEmails,
|
||||
workflowName,
|
||||
baseUrl,
|
||||
workflowId,
|
||||
sharerFirstName,
|
||||
}: {
|
||||
recipientEmails: string[];
|
||||
workflowName: string;
|
||||
baseUrl: string;
|
||||
workflowId: string;
|
||||
sharerFirstName: string;
|
||||
}) {
|
||||
const populateTemplate = await getTemplate('workflowShared', 'workflowShared.html');
|
||||
|
||||
const result = await this.mailer?.sendMail({
|
||||
emailRecipients: recipientEmails,
|
||||
subject: `${sharerFirstName} has shared an n8n workflow with you`,
|
||||
body: populateTemplate({ workflowName, workflowUrl: `${baseUrl}/workflow/${workflowId}` }),
|
||||
});
|
||||
|
||||
return result ?? { emailSent: false };
|
||||
}
|
||||
|
||||
async notifyCredentialsShared({
|
||||
sharerFirstName,
|
||||
credentialsName,
|
||||
recipientEmails,
|
||||
baseUrl,
|
||||
}: {
|
||||
sharerFirstName: string;
|
||||
credentialsName: string;
|
||||
recipientEmails: string[];
|
||||
baseUrl: string;
|
||||
}) {
|
||||
const populateTemplate = await getTemplate('credentialsShared', 'credentialsShared.html');
|
||||
|
||||
const result = await this.mailer?.sendMail({
|
||||
emailRecipients: recipientEmails,
|
||||
subject: `${sharerFirstName} has shared an n8n credential with you`,
|
||||
body: populateTemplate({ credentialsName, credentialsListUrl: `${baseUrl}/credentials` }),
|
||||
});
|
||||
|
||||
return result ?? { emailSent: false };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<p>Hi there,</p>
|
||||
<p><b>"{{ credentialsName }}" credential</b> has been shared with you.</p>
|
||||
<p>To view all the credentials you have access to within n8n, click the following link:</p>
|
||||
<p><a href="{{ credentialsListUrl }}" target="_blank">{{ credentialsListUrl }}</a></p>
|
||||
@@ -0,0 +1,4 @@
|
||||
<p>Hi there,</p>
|
||||
<p><b>"{{ workflowName }}" workflow</b> has been shared with you.</p>
|
||||
<p>To access the workflow, click the following link:</p>
|
||||
<p><a href="{{ workflowUrl }}" target="_blank">{{ workflowUrl }}</a></p>
|
||||
@@ -852,6 +852,18 @@ export const schema = {
|
||||
default: '',
|
||||
env: 'N8N_UM_EMAIL_TEMPLATES_PWRESET',
|
||||
},
|
||||
workflowShared: {
|
||||
doc: 'Overrides default HTML template for notifying that a workflow was shared (use full path)',
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'N8N_UM_EMAIL_TEMPLATES_WORKFLOW_SHARED',
|
||||
},
|
||||
credentialsShared: {
|
||||
doc: 'Overrides default HTML template for notifying that credentials were shared (use full path)',
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'N8N_UM_EMAIL_TEMPLATES_CREDENTIALS_SHARED',
|
||||
},
|
||||
},
|
||||
},
|
||||
authenticationMethod: {
|
||||
|
||||
@@ -15,6 +15,11 @@ import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
|
||||
import { CredentialsRepository } from '@/databases/repositories/credentials.repository';
|
||||
import * as utils from '@/utils';
|
||||
import { UserRepository } from '@/databases/repositories/user.repository';
|
||||
import { UserManagementMailer } from '@/UserManagement/email';
|
||||
import { UrlService } from '@/services/url.service';
|
||||
import { Logger } from '@/Logger';
|
||||
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
||||
|
||||
export const EECredentialsController = express.Router();
|
||||
|
||||
@@ -185,5 +190,37 @@ EECredentialsController.put(
|
||||
user_ids_sharees_added: newShareeIds,
|
||||
sharees_removed: amountRemoved,
|
||||
});
|
||||
|
||||
const recipients = await Container.get(UserRepository).getEmailsByIds(newShareeIds);
|
||||
|
||||
if (recipients.length === 0) return;
|
||||
|
||||
try {
|
||||
await Container.get(UserManagementMailer).notifyCredentialsShared({
|
||||
sharerFirstName: req.user.firstName,
|
||||
credentialsName: credential.name,
|
||||
recipientEmails: recipients.map(({ email }) => email),
|
||||
baseUrl: Container.get(UrlService).getInstanceBaseUrl(),
|
||||
});
|
||||
} catch (error) {
|
||||
void Container.get(InternalHooks).onEmailFailed({
|
||||
user: req.user,
|
||||
message_type: 'Credentials shared',
|
||||
public_api: false,
|
||||
});
|
||||
if (error instanceof Error) {
|
||||
throw new InternalServerError(`Please contact your administrator: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
Container.get(Logger).info('Sent credentials shared email successfully', {
|
||||
sharerId: req.user.id,
|
||||
});
|
||||
|
||||
void Container.get(InternalHooks).onUserTransactionalEmail({
|
||||
user_id: req.user.id,
|
||||
message_type: 'Credentials shared',
|
||||
public_api: false,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -84,4 +84,14 @@ export class UserRepository extends Repository<User> {
|
||||
|
||||
return findManyOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get emails of users who have completed setup, by user IDs.
|
||||
*/
|
||||
async getEmailsByIds(userIds: string[]) {
|
||||
return await this.find({
|
||||
select: ['email'],
|
||||
where: { id: In(userIds), password: Not(IsNull()) },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ import { WorkflowRequest } from './workflow.request';
|
||||
import { EnterpriseWorkflowService } from './workflow.service.ee';
|
||||
import { WorkflowExecutionService } from './workflowExecution.service';
|
||||
import { WorkflowSharingService } from './workflowSharing.service';
|
||||
import { UserManagementMailer } from '@/UserManagement/email';
|
||||
import { UrlService } from '@/services/url.service';
|
||||
|
||||
@Service()
|
||||
@Authorized()
|
||||
@@ -62,6 +64,8 @@ export class WorkflowsController {
|
||||
private readonly workflowSharingService: WorkflowSharingService,
|
||||
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
|
||||
private readonly userRepository: UserRepository,
|
||||
private readonly mailer: UserManagementMailer,
|
||||
private readonly urlService: UrlService,
|
||||
) {}
|
||||
|
||||
@Post('/')
|
||||
@@ -401,5 +405,36 @@ export class WorkflowsController {
|
||||
});
|
||||
|
||||
void this.internalHooks.onWorkflowSharingUpdate(workflowId, req.user.id, shareWithIds);
|
||||
|
||||
const recipients = await this.userRepository.getEmailsByIds(newShareeIds);
|
||||
|
||||
if (recipients.length === 0) return;
|
||||
|
||||
try {
|
||||
await this.mailer.notifyWorkflowShared({
|
||||
recipientEmails: recipients.map(({ email }) => email),
|
||||
workflowName: workflow.name,
|
||||
workflowId,
|
||||
sharerFirstName: req.user.firstName,
|
||||
baseUrl: this.urlService.getInstanceBaseUrl(),
|
||||
});
|
||||
} catch (error) {
|
||||
void this.internalHooks.onEmailFailed({
|
||||
user: req.user,
|
||||
message_type: 'Workflow shared',
|
||||
public_api: false,
|
||||
});
|
||||
if (error instanceof Error) {
|
||||
throw new InternalServerError(`Please contact your administrator: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info('Sent workflow shared email successfully', { sharerId: req.user.id });
|
||||
|
||||
void this.internalHooks.onUserTransactionalEmail({
|
||||
user_id: req.user.id,
|
||||
message_type: 'Workflow shared',
|
||||
public_api: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user