Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SMTP fixes for user management backend #2937

Merged
merged 27 commits into from
Mar 7, 2022
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
45c465e
:fire: Remove `UM_` from SMTP env vars
ivov Mar 3, 2022
8e9dc2a
:fire: Remove SMTP host default value
ivov Mar 3, 2022
abb9ab5
:zap: Update sender value
ivov Mar 3, 2022
5873009
:zap: Update invite template
ivov Mar 3, 2022
e8d090f
:zap: Update password reset template
ivov Mar 3, 2022
1354bf3
:zap: Update `N8N_EMAIL_MODE` default value
ivov Mar 3, 2022
4d32422
:fire: Remove `EMAIL` from all SMTP vars
ivov Mar 3, 2022
6372ec7
:sparkles: Implement `verifyConnection()`
ivov Mar 3, 2022
d2ab5f4
:twisted_rightwards_arrows: Merge master
ivov Mar 4, 2022
7f98c85
:truck: Reposition comment
ivov Mar 4, 2022
307629b
:pencil2: Fix typo
ivov Mar 4, 2022
8144956
:pencil2: Minor env var documentation improvements
ivov Mar 4, 2022
0f9acff
:art: Fix spacing
ivov Mar 4, 2022
342e22f
:art: Fix spacing
ivov Mar 4, 2022
f18d645
:card_file_box: Remove SMTP settings cache
ivov Mar 4, 2022
fadc5b3
:zap: Adjust log message
ivov Mar 4, 2022
d9a5d58
:zap: Update error message
ivov Mar 4, 2022
4b06cfa
:pencil2: Fix template typo
ivov Mar 4, 2022
4ee55e7
:pencil2: Adjust wording
ivov Mar 4, 2022
1ceed5e
:zap: Interpolate email into success toast
ivov Mar 4, 2022
2dcc4ad
:pencil2: Adjust base message in `verifyConnection()`
ivov Mar 4, 2022
14a0575
:zap: Verify connection on password reset
ivov Mar 4, 2022
e4e8a32
:zap: Bring up POST /users SMTP check
ivov Mar 4, 2022
7e3079b
:twisted_rightwards_arrows: Merge parent branch
ivov Mar 4, 2022
b0c074a
:bug: remove cookie if cookie is not valid
BHesseldieck Mar 7, 2022
7a91c21
:zap: verify connection on instantiation
BHesseldieck Mar 7, 2022
5af4ca2
🔀 Merge branch 'user-management-backend' into n8n-3098-smtp-fixes
BHesseldieck Mar 7, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 0 additions & 33 deletions packages/cli/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { BinaryDataManager, IBinaryDataConfig, TUNNEL_SUBDOMAIN_ENV, UserSetting
import { Command, flags } from '@oclif/command';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as Redis from 'ioredis';
import { AES, enc } from 'crypto-js';

import { IDataObject, LoggerProxy } from 'n8n-workflow';
import { createHash } from 'crypto';
Expand Down Expand Up @@ -219,38 +218,6 @@ export class Start extends Command {
throw new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY);
}

if (config.get('userManagement.emails.mode') === 'smtp') {
const { auth, ...rest } = config.get('userManagement.emails.smtp');

const encryptedAuth = {
user: auth.user,
pass: AES.encrypt(auth.pass, encryptionKey).toString(),
};

await Db.collections.Settings!.save({
key: 'userManagement.emails.smtp',
value: JSON.stringify({ ...rest, auth: encryptedAuth }),
loadOnStartup: false,
});
} else {
// If we don't have SMTP settings, try loading from db.
const smtpSetting = await Db.collections.Settings!.findOne({
key: 'userManagement.emails.smtp',
});

if (smtpSetting) {
const { auth, ...rest } = JSON.parse(smtpSetting.value) as SmtpConfig;

const decryptedAuth = {
user: auth.user,
pass: AES.decrypt(auth.pass, encryptionKey).toString(enc.Utf8),
};

config.set('userManagement.emails.mode', 'smtp');
config.set('userManagement.emails.smtp', { ...rest, auth: decryptedAuth });
}
}

// Load settings from database and set them to config.
const databaseSettings = await Db.collections.Settings!.find({ loadOnStartup: true });
databaseSettings.forEach((setting) => {
Expand Down
36 changes: 18 additions & 18 deletions packages/cli/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,47 +598,47 @@ const config = convict({
mode: {
doc: 'How to send emails',
format: ['', 'smtp'],
default: '',
env: 'N8N_UM_EMAIL_MODE',
default: 'smtp',
env: 'N8N_MODE',
},
smtp: {
host: {
doc: 'SMTP server host',
format: String,
default: 'smtp.gmail.com',
env: 'N8N_UM_EMAIL_SMTP_HOST',
format: String, // e.g. 'smtp.gmail.com'
default: '',
env: 'N8N_SMTP_HOST',
},
port: {
doc: 'SMTP Server port',
doc: 'SMTP server port',
format: Number,
default: 465,
env: 'N8N_UM_EMAIL_SMTP_PORT',
env: 'N8N_SMTP_PORT',
},
secure: {
doc: 'Whether or not to use SSL',
doc: 'Whether or not to use SSL for SMTP',
format: Boolean,
default: true,
env: 'N8N_UM_EMAIL_SMTP_SSL',
env: 'N8N_SMTP_SSL',
},
auth: {
user: {
doc: 'SMTP Login username',
format: String,
default: 'youremail@gmail.com',
env: 'N8N_UM_EMAIL_SMTP_USER',
doc: 'SMTP login username',
format: String, // e.g.'you@gmail.com'
default: '',
env: 'N8N_SMTP_USER',
},
pass: {
doc: 'SMTP Login password',
doc: 'SMTP login password',
format: String,
default: 'my-super-password',
env: 'N8N_UM_EMAIL_SMTP_PASS',
default: '',
env: 'N8N_SMTP_PASS',
},
},
sender: {
doc: 'How to display sender name',
format: String,
default: '"n8n rocks" <n8n@n8n.io>',
env: 'N8N_UM_EMAIL_SMTP_SENDER',
default: '',
env: 'N8N_SMTP_SENDER',
},
},
templates: {
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/UserManagement/email/Interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export interface UserManagementMailerImplementation {
sendMail: (mailData: MailData) => Promise<SendEmailResult>;
verifyConnection: () => Promise<void>;
}

export type InviteEmailData = {
Expand Down
29 changes: 28 additions & 1 deletion packages/cli/src/UserManagement/email/NodeMailer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,37 @@ export class NodeMailer implements UserManagementMailerImplementation {
});
}

async verifyConnection(): Promise<void> {
const host = config.get('userManagement.emails.smtp.host') as string;
const user = config.get('userManagement.emails.smtp.auth.user') as string;
const pass = config.get('userManagement.emails.smtp.auth.pass') as string;

return new Promise((resolve, reject) => {
this.transport.verify((error: Error) => {
if (!error) resolve();

const message = [];

if (!host) message.push('SMTP host not defined (N8N_SMTP_HOST).');
if (!user) message.push('SMTP user not defined (N8N_SMTP_USER).');
if (!pass) message.push('SMTP pass not defined (N8N_SMTP_PASS).');

reject(new Error(message.join(' ')));
});
});
}

async sendMail(mailData: MailData): Promise<SendEmailResult> {
let sender = config.get('userManagement.emails.smtp.sender');
const user = config.get('userManagement.emails.smtp.auth.user') as string;

if (!sender && user.includes('@')) {
sender = user;
}

try {
await this.transport.sendMail({
from: config.get('userManagement.emails.smtp.sender'),
from: sender,
to: mailData.emailRecipients,
subject: mailData.subject,
text: mailData.textOnly,
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/UserManagement/email/UserManagementMailer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ export class UserManagementMailer {
}
}

async verifyConnection(): Promise<void> {
if (!this.mailer) return Promise.reject();

return this.mailer.verifyConnection();
}

async invite(inviteEmailData: InviteEmailData): Promise<SendEmailResult> {
let template = await getTemplate('invite', 'invite.html');
template = replaceStrings(template, inviteEmailData);
Expand Down
6 changes: 2 additions & 4 deletions packages/cli/src/UserManagement/email/templates/invite.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
<p>Hi there,</p>
<p>You have been invited to join n8n {{domain}}.</p>
<p>Please click on the following link, or paste it into your browser to complete the process. The link is valid for 2 hours.</p>
<p>You have been invited to join n8n ({{ domain }}).</p>
<p>To accept, click the following link. It is valid for 2 hours.</p>
<p><a href="{{ inviteAcceptUrl }}" target="_blank">{{ inviteAcceptUrl }}</a></p>
<br>
<p>Thanks!</p>
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
<p>Hi {{firstName}},</p>
<p>You are receiving this because you (or someone else) requested a password reset for your account {{email}} on n8n {{domain}}.</p>
<p>Please click on the following link, or paste it into your browser to complete the process:</p>
<p>Somebody asked to reset your password on n8n ({{ domain }}).</p>
<p>If it was not you, you can safely ignore this email.</p>
<p>Click the following link to choose a new password:</p>
<a href="{{ passwordResetUrl }}">{{ passwordResetUrl }}</a>
<br><br>
<p>If you received this in error, you can safely ignore it.</p>
<p>Contact your n8n instance owner if you did not request to reset your password.</p>
<br>
<p>Thanks!</p>
16 changes: 15 additions & 1 deletion packages/cli/src/UserManagement/routes/passwordReset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,21 @@ export function passwordResetNamespace(this: N8nApp): void {
url.searchParams.append('userId', id);
url.searchParams.append('token', resetPasswordToken);

await UserManagementMailer.getInstance().passwordReset({
const mailer = UserManagementMailer.getInstance();

try {
await mailer.verifyConnection();
} catch (error) {
if (error instanceof Error) {
throw new ResponseHelper.ResponseError(
`Please contact your administrator: ${error.message}`,
undefined,
500,
);
}
}

await mailer.passwordReset({
email,
firstName,
lastName,
Expand Down
29 changes: 26 additions & 3 deletions packages/cli/src/UserManagement/routes/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ export function usersNamespace(this: N8nApp): void {
);
}

const mailer = getInstance();

try {
await mailer.verifyConnection();
} catch (error) {
if (error instanceof Error) {
throw new ResponseHelper.ResponseError(
`There is a problem with your SMTP setup: ${error.message}`,
undefined,
500,
);
}
}

if (!config.get('userManagement.isInstanceOwnerSetUp')) {
Logger.debug(
'Request to send email invite(s) to user(s) failed because emailing was not set up',
Expand Down Expand Up @@ -131,7 +145,7 @@ export function usersNamespace(this: N8nApp): void {
throw new ResponseHelper.ResponseError('An error occurred during user creation');
}

Logger.info('Created user shells successfully', { userId: req.user.id });
Logger.info('Created user shell(s) successfully', { userId: req.user.id });
Logger.verbose(total > 1 ? `${total} user shells created` : `1 user shell created`, {
userShells: createUsers,
});
Expand All @@ -141,7 +155,6 @@ export function usersNamespace(this: N8nApp): void {
const usersPendingSetup = Object.entries(createUsers).filter(([email, id]) => id && email);

// send invite email to new or not yet setup users
const mailer = getInstance();

const emailingResults = await Promise.all(
usersPendingSetup.map(async ([email, id]) => {
Expand Down Expand Up @@ -455,7 +468,17 @@ export function usersNamespace(this: N8nApp): void {
const baseUrl = getInstanceBaseUrl();
const inviteAcceptUrl = `${baseUrl}/signup?inviterId=${req.user.id}&inviteeId=${reinvitee.id}`;

const result = await getInstance().invite({
const mailer = getInstance();

try {
await mailer.verifyConnection();
} catch (error) {
if (error instanceof Error) {
throw new ResponseHelper.ResponseError(error.message, undefined, 500);
}
}

const result = await mailer.invite({
email: reinvitee.email,
inviteAcceptUrl,
domain: baseUrl,
Expand Down
4 changes: 2 additions & 2 deletions packages/editor-ui/src/plugins/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1097,7 +1097,6 @@
},
"BASIC_INFORMATION": "Basic Information",
"CHANGE_PASSWORD": "Change Password",
"CHECK_INBOX_AND_SPAM": "Please check your inbox (and perhaps your spam folder)",
"CONFIRM_DATA_HANDLING_AFTER_DELETION": "What should we do with their data?",
"CONFIRM_USER_DELETION": "Are you sure you want to delete this invited user?",
"CURRENT_PASSWORD": "Current password",
Expand All @@ -1114,6 +1113,7 @@
"FINISH_ACCOUNT_SETUP": "Finish account setup",
"FIRST_NAME": "First name",
"FORGOT_MY_PASSWORD": "Forgot my password",
"FORGOT_PASSWORD_SUCCESS_MESSAGE": "We’ve emailed {email} (if there’s a matching account)",
"GET_RECOVERY_LINK": "Email me a recovery link",
"GO_BACK": "Go back",
"INVALID_EMAIL_ERROR": "{email} is not a valid email",
Expand Down Expand Up @@ -1175,7 +1175,7 @@
"TRANSFERRED_TO_USER": "Transferred to {user}",
"TRANSFER_WORKFLOWS_AND_CREDENTIALS": "Transfer their workflows and credentials to another user",
"USERS": "Users",
"USERS_INVITED_ERROR": "Users could not be invited",
"USERS_INVITED_ERROR": "Could not invite users",
"USERS_INVITED_SUCCESS": "Users invited",
"USER_DELETE_ERROR": "Problem while deleting user",
"USER_DELETE_SUCCESS": "User deleted",
Expand Down
7 changes: 5 additions & 2 deletions packages/editor-ui/src/views/ForgotMyPasswordView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,18 @@ export default mixins(
},
},
methods: {
async onSubmit(values: {[key: string]: string}) {
async onSubmit(values: { email: string }) {
try {
this.loading = true;
await this.$store.dispatch('users/sendForgotPasswordEmail', values);

this.$showMessage({
type: 'success',
title: this.$locale.baseText('RECOVERY_EMAIL_SENT'),
message: this.$locale.baseText('CHECK_INBOX_AND_SPAM'),
message: this.$locale.baseText(
'FORGOT_PASSWORD_SUCCESS_MESSAGE',
{ interpolate: { email: values.email }},
),
});
} catch (error) {
this.$showError(error, this.$locale.baseText('SENDING_EMAIL_ERROR'));
Expand Down