mirror of
https://github.com/n8n-io/n8n.git
synced 2024-09-19 22:37:31 -07:00
[N8N-4355] Use safer templating for UserManagement emails (#3893)
This commit is contained in:
parent
dc8f8b7874
commit
c65458c154
1
package-lock.json
generated
1
package-lock.json
generated
|
@ -53645,6 +53645,7 @@
|
|||
"fast-glob": "^3.2.5",
|
||||
"flatted": "^3.2.4",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"handlebars": "4.7.7",
|
||||
"inquirer": "^7.0.1",
|
||||
"json-diff": "^0.5.4",
|
||||
"jsonschema": "^1.4.1",
|
||||
|
|
|
@ -129,6 +129,7 @@
|
|||
"fast-glob": "^3.2.5",
|
||||
"flatted": "^3.2.4",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"handlebars": "4.7.7",
|
||||
"inquirer": "^7.0.1",
|
||||
"json-diff": "^0.5.4",
|
||||
"jsonschema": "^1.4.1",
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
/* eslint-disable import/no-cycle */
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { IDataObject } from 'n8n-workflow';
|
||||
import Handlebars from 'handlebars';
|
||||
import { existsSync } from 'fs';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { join as pathJoin } from 'path';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { GenericHelpers } from '../..';
|
||||
import * as config from '../../../config';
|
||||
import {
|
||||
|
@ -12,35 +13,33 @@ import {
|
|||
} from './Interfaces';
|
||||
import { NodeMailer } from './NodeMailer';
|
||||
|
||||
// TODO: make function fully async (remove sync functions)
|
||||
async function getTemplate(configKeyName: string, defaultFilename: string) {
|
||||
const templateOverride = (await GenericHelpers.getConfigValue(
|
||||
`userManagement.emails.templates.${configKeyName}`,
|
||||
)) as string;
|
||||
type Template = HandlebarsTemplateDelegate<unknown>;
|
||||
type TemplateName = 'invite' | 'passwordReset';
|
||||
|
||||
let template;
|
||||
if (templateOverride && existsSync(templateOverride)) {
|
||||
template = readFileSync(templateOverride, {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
} else {
|
||||
template = readFileSync(pathJoin(__dirname, `templates/${defaultFilename}`), {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
const templates: Partial<Record<TemplateName, Template>> = {};
|
||||
|
||||
async function getTemplate(
|
||||
templateName: TemplateName,
|
||||
defaultFilename = `${templateName}.html`,
|
||||
): Promise<Template> {
|
||||
let template = templates[templateName];
|
||||
if (!template) {
|
||||
const templateOverride = (await GenericHelpers.getConfigValue(
|
||||
`userManagement.emails.templates.${templateName}`,
|
||||
)) as string;
|
||||
|
||||
let markup;
|
||||
if (templateOverride && existsSync(templateOverride)) {
|
||||
markup = await readFile(templateOverride, 'utf-8');
|
||||
} else {
|
||||
markup = await readFile(pathJoin(__dirname, `templates/${defaultFilename}`), 'utf-8');
|
||||
}
|
||||
template = Handlebars.compile(markup);
|
||||
templates[templateName] = template;
|
||||
}
|
||||
return template;
|
||||
}
|
||||
|
||||
function replaceStrings(template: string, data: IDataObject) {
|
||||
let output = template;
|
||||
const keys = Object.keys(data);
|
||||
keys.forEach((key) => {
|
||||
const regex = new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'g');
|
||||
output = output.replace(regex, data[key] as string);
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
export class UserManagementMailer {
|
||||
private mailer: UserManagementMailerImplementation | undefined;
|
||||
|
||||
|
@ -58,13 +57,13 @@ export class UserManagementMailer {
|
|||
}
|
||||
|
||||
async invite(inviteEmailData: InviteEmailData): Promise<SendEmailResult> {
|
||||
let template = await getTemplate('invite', 'invite.html');
|
||||
template = replaceStrings(template, inviteEmailData);
|
||||
if (!this.mailer) return Promise.reject();
|
||||
|
||||
const result = await this.mailer?.sendMail({
|
||||
const template = await getTemplate('invite');
|
||||
const result = await this.mailer.sendMail({
|
||||
emailRecipients: inviteEmailData.email,
|
||||
subject: 'You have been invited to n8n',
|
||||
body: template,
|
||||
body: template(inviteEmailData),
|
||||
});
|
||||
|
||||
// If mailer does not exist it means mail has been disabled.
|
||||
|
@ -72,14 +71,13 @@ export class UserManagementMailer {
|
|||
}
|
||||
|
||||
async passwordReset(passwordResetData: PasswordResetData): Promise<SendEmailResult> {
|
||||
let template = await getTemplate('passwordReset', 'passwordReset.html');
|
||||
template = replaceStrings(template, passwordResetData);
|
||||
if (!this.mailer) return Promise.reject();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const result = await this.mailer?.sendMail({
|
||||
const template = await getTemplate('passwordReset');
|
||||
const result = await this.mailer.sendMail({
|
||||
emailRecipients: passwordResetData.email,
|
||||
subject: 'n8n password reset',
|
||||
body: template,
|
||||
body: template(passwordResetData),
|
||||
});
|
||||
|
||||
// If mailer does not exist it means mail has been disabled.
|
||||
|
|
Loading…
Reference in a new issue