You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by an...@apache.org on 2019/03/07 08:37:58 UTC

[ignite] branch master updated: IGNITE-11500 Web Console: Create template for email. (#6244)

This is an automated email from the ASF dual-hosted git repository.

anovikov pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/master by this push:
     new 3068fdc  IGNITE-11500 Web Console: Create template for email. (#6244)
3068fdc is described below

commit 3068fdc14ebad77c15b0a050f5660534b7296622
Author: Andrey Novikov <an...@apache.org>
AuthorDate: Thu Mar 7 15:37:51 2019 +0700

    IGNITE-11500 Web Console: Create template for email. (#6244)
---
 modules/web-console/backend/package.json        |   3 +-
 modules/web-console/backend/routes/admin.js     |   5 +-
 modules/web-console/backend/services/auth.js    |   6 +-
 modules/web-console/backend/services/mails.js   | 158 +++++++++++++++---------
 modules/web-console/backend/services/users.js   |  22 ++--
 modules/web-console/backend/templates/base.html |  21 ++++
 6 files changed, 141 insertions(+), 74 deletions(-)

diff --git a/modules/web-console/backend/package.json b/modules/web-console/backend/package.json
index 9d1918e..45a538d 100644
--- a/modules/web-console/backend/package.json
+++ b/modules/web-console/backend/package.json
@@ -34,6 +34,7 @@
       "migrations/*",
       "routes/*",
       "services/*",
+      "templates/*",
       "node_modules/getos/logic/*",
       "node_modules/mongodb-download/node_modules/getos/logic/*"
     ],
@@ -63,7 +64,7 @@
     "mongoose": "4.11.4",
     "morgan": "1.8.2",
     "nconf": "0.8.4",
-    "nodemailer": "4.0.1",
+    "nodemailer": "^5.1.1",
     "passport": "0.3.2",
     "passport-local": "1.0.0",
     "passport-local-mongoose": "4.0.0",
diff --git a/modules/web-console/backend/routes/admin.js b/modules/web-console/backend/routes/admin.js
index 4a38723..70f07b0 100644
--- a/modules/web-console/backend/routes/admin.js
+++ b/modules/web-console/backend/routes/admin.js
@@ -23,20 +23,19 @@ const express = require('express');
 
 module.exports = {
     implements: 'routes/admin',
-    inject: ['settings', 'mongo', 'services/spaces', 'services/mails', 'services/sessions', 'services/users', 'services/notifications']
+    inject: ['settings', 'mongo', 'services/spaces', 'services/sessions', 'services/users', 'services/notifications']
 };
 
 /**
  * @param settings
  * @param mongo
  * @param spacesService
- * @param {MailsService} mailsService
  * @param {SessionsService} sessionsService
  * @param {UsersService} usersService
  * @param {NotificationsService} notificationsService
  * @returns {Promise}
  */
-module.exports.factory = function(settings, mongo, spacesService, mailsService, sessionsService, usersService, notificationsService) {
+module.exports.factory = function(settings, mongo, spacesService, sessionsService, usersService, notificationsService) {
     return new Promise((factoryResolve) => {
         const router = new express.Router();
 
diff --git a/modules/web-console/backend/services/auth.js b/modules/web-console/backend/services/auth.js
index c6da86e..95fe006 100644
--- a/modules/web-console/backend/services/auth.js
+++ b/modules/web-console/backend/services/auth.js
@@ -57,7 +57,7 @@ module.exports.factory = (mongo, settings, errors, utilsService, mailsService) =
 
                     return user.save();
                 })
-                .then((user) => mailsService.emailUserResetLink(host, user));
+                .then((user) => mailsService.sendResetLink(host, user));
         }
 
         /**
@@ -88,7 +88,7 @@ module.exports.factory = (mongo, settings, errors, utilsService, mailsService) =
                         });
                     });
                 })
-                .then((user) => mailsService.emailPasswordChanged(host, user));
+                .then((user) => mailsService.sendPasswordChanged(host, user));
         }
 
         /**
@@ -168,7 +168,7 @@ module.exports.factory = (mongo, settings, errors, utilsService, mailsService) =
 
                     return user.save();
                 })
-                .then((user) =>  mailsService.emailUserActivation(host, user));
+                .then((user) =>  mailsService.sendActivationLink(host, user));
         }
     }
 
diff --git a/modules/web-console/backend/services/mails.js b/modules/web-console/backend/services/mails.js
index 183fbe1..50d61e8 100644
--- a/modules/web-console/backend/services/mails.js
+++ b/modules/web-console/backend/services/mails.js
@@ -17,6 +17,7 @@
 
 'use strict';
 
+const fs = require('fs');
 const _ = require('lodash');
 const nodemailer = require('nodemailer');
 
@@ -34,16 +35,67 @@ module.exports = {
 module.exports.factory = (settings) => {
     class MailsService {
         /**
+         * Read template file.
+         * @param {String} template Path to template file.
+         * @param template
+         */
+        readTemplate(template) {
+            try {
+                return fs.readFileSync(template, 'utf8');
+            }
+            catch (ignored) {
+                throw new Error('Failed to find email template: ' + template);
+            }
+        }
+
+        /**
+         * Get message with resolved variables.
+         *
+         * @param {string} template Message template.
+         * @param {object} ctx Context.
+         * @return Prepared template.
+         * @throws IOException If failed to prepare template.
+         */
+        getMessage(template, ctx) {
+            _.forIn(ctx, (value, key) => template = template.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), value || 'n/a'));
+
+            return template;
+        }
+
+        /**
+         * @param {string} host Web Console host.
+         * @param {Account} user User that signed up.
+         * @param {string} message Message.
+         * @param {object} customCtx Custom context parameters.
+         */
+        buildContext(host, user, message, customCtx) {
+            return {
+                message,
+                ...customCtx,
+                greeting: settings.mail.greeting,
+                sign: settings.mail.sign,
+                firstName: user.firstName,
+                lastName: user.lastName,
+                email: user.res,
+                host,
+                activationLink: `${host}/signin?activationToken=${user.activationToken}`,
+                resetLink: `${host}/password/reset?token=${user.resetPasswordToken}`
+            };
+        }
+
+        /**
          * Send mail to user.
          *
-         * @param {Account} user
-         * @param {String} subject
-         * @param {String} html
-         * @param {String} sendErr
+         * @param {string} template Path to template file.
+         * @param {string} host Web Console host.
+         * @param {Account} user User that signed up.
+         * @param {string} subject Email subject.
+         * @param {string} message Email message.
+         * @param {object} customCtx Custom context parameters.
          * @throws {Error}
          * @return {Promise}
          */
-        send(user, subject, html, sendErr) {
+        send(template, host, user, subject, message, customCtx = {}) {
             const options = settings.mail;
 
             return new Promise((resolve, reject) => {
@@ -61,22 +113,21 @@ module.exports.factory = (settings) => {
                     return transporter.verify().then(() => transporter);
                 })
                 .then((transporter) => {
-                    const sign = options.sign ? `<br><br>--------------<br>${options.sign}<br>` : '';
-                    const to = `"${user.firstName} ${user.lastName}" <${user.email}>`;
+                    const context = this.buildContext(host, user, message, customCtx);
+                    
+                    context.subject = this.getMessage(subject, context);
 
-                    const mail = {
+                    return transporter.sendMail({
                         from: options.from,
-                        to,
-                        subject,
-                        html: html + sign
-                    };
-
-                    return transporter.sendMail(mail);
+                        to: `"${user.firstName} ${user.lastName}" <${user.email}>`,
+                        subject: context.subject,
+                        html: this.getMessage(this.readTemplate(template), context)
+                    });
                 })
                 .catch((err) => {
                     console.log('Failed to send email.', err);
 
-                    return Promise.reject(sendErr ? new Error(sendErr) : err);
+                    return Promise.reject(err);
                 });
         }
 
@@ -87,23 +138,22 @@ module.exports.factory = (settings) => {
          * @param user User that signed up.
          * @param createdByAdmin Whether user was created by admin.
          */
-        emailUserSignUp(host, user, createdByAdmin) {
-            const resetLink = `${host}/password/reset?token=${user.resetPasswordToken}`;
-
-            const sbj = createdByAdmin
-                ? 'Account was created for'
-                : 'Thanks for signing up for';
-
-            const reason = createdByAdmin
-                ? 'administrator created account for you'
-                : 'you have signed up';
-
-            return this.send(user, `${sbj} ${settings.mail.greeting}.`,
-                `Hello ${user.firstName} ${user.lastName}!<br><br>` +
-                `You are receiving this email because ${reason} to use <a href="${host}">${settings.mail.greeting}</a>.<br><br>` +
+        sendWelcomeLetter(host, user, createdByAdmin) {
+            if (createdByAdmin) {
+                return this.send('templates/base.html', host, user, 'Account was created for ${greeting}.',
+                    'You are receiving this email because administrator created account for you to use <a href="${host}">${greeting}</a>.<br><br>' +
+                    'If you do not know what this email is about, please ignore it.<br>' +
+                    'You may reset the password by clicking on the following link, or paste this into your browser:<br><br>' +
+                    '<a href="${resetLink}">${resetLink}</a>'
+                );
+            }
+
+            return this.send('templates/base.html', host, user, 'Thanks for signing up for ${greeting}.',
+                'You are receiving this email because you have signed up to use <a href="${host}">${greeting}</a>.<br><br>' +
                 'If you do not know what this email is about, please ignore it.<br>' +
                 'You may reset the password by clicking on the following link, or paste this into your browser:<br><br>' +
-                `<a href="${resetLink}">${resetLink}</a>`);
+                '<a href="${resetLink}">${resetLink}</a>'
+            );
         }
 
         /**
@@ -112,15 +162,13 @@ module.exports.factory = (settings) => {
          * @param host
          * @param user
          */
-        emailUserActivation(host, user) {
-            const activationLink = `${host}/signin?activationToken=${user.activationToken}`;
-
-            return this.send(user, `Confirm your account on ${settings.mail.greeting}`,
-                `Hello ${user.firstName} ${user.lastName}!<br><br>` +
-                `You are receiving this email because you have signed up to use <a href="${host}">${settings.mail.greeting}</a>.<br><br>` +
+        sendActivationLink(host, user) {
+            return this.send('templates/base.html', host, user, 'Confirm your account on ${greeting}',
+                'You are receiving this email because you have signed up to use <a href="${host}">${greeting}</a>.<br><br>' +
                 'Please click on the following link, or paste this into your browser to activate your account:<br><br>' +
-                `<a href="${activationLink}">${activationLink}</a>`,
-                'Failed to send email with confirm account link!');
+                '<a href="${activationLink}">${activationLink}</a>'
+            )
+                .catch(() => Promise.reject(new Error('Failed to send email with confirm account link!')));
         }
 
         /**
@@ -129,16 +177,14 @@ module.exports.factory = (settings) => {
          * @param host
          * @param user
          */
-        emailUserResetLink(host, user) {
-            const resetLink = `${host}/password/reset?token=${user.resetPasswordToken}`;
-
-            return this.send(user, 'Password Reset',
-                `Hello ${user.firstName} ${user.lastName}!<br><br>` +
+        sendResetLink(host, user) {
+            return this.send('templates/base.html', host, user, 'Password Reset',
                 'You are receiving this because you (or someone else) have requested the reset of the password for your account.<br><br>' +
                 'Please click on the following link, or paste this into your browser to complete the process:<br><br>' +
-                `<a href="${resetLink}">${resetLink}</a><br><br>` +
-                'If you did not request this, please ignore this email and your password will remain unchanged.',
-                'Failed to send email with reset link!');
+                '<a href="${resetLink}">${resetLink}</a><br><br>' +
+                'If you did not request this, please ignore this email and your password will remain unchanged.'
+            )
+                .catch(() => Promise.reject(new Error('Failed to send email with reset link!')));
         }
 
         /**
@@ -146,11 +192,11 @@ module.exports.factory = (settings) => {
          * @param host
          * @param user
          */
-        emailPasswordChanged(host, user) {
-            return this.send(user, 'Your password has been changed',
-                `Hello ${user.firstName} ${user.lastName}!<br><br>` +
-                `This is a confirmation that the password for your account on <a href="${host}">${settings.mail.greeting}</a> has just been changed.<br><br>`,
-                'Password was changed, but failed to send confirmation email!');
+        sendPasswordChanged(host, user) {
+            return this.send('templates/base.html', host, user, 'Your password has been changed',
+                'This is a confirmation that the password for your account on <a href="${host}">${greeting}</a> has just been changed.'
+            )
+                .catch(() => Promise.reject(new Error('Password was changed, but failed to send confirmation email!')));
         }
 
         /**
@@ -158,11 +204,11 @@ module.exports.factory = (settings) => {
          * @param host
          * @param user
          */
-        emailUserDeletion(host, user) {
-            return this.send(user, 'Your account was removed',
-                `Hello ${user.firstName} ${user.lastName}!<br><br>` +
-                `You are receiving this email because your account for <a href="${host}">${settings.mail.greeting}</a> was removed.`,
-                'Account was removed, but failed to send email notification to user!');
+        sendAccountDeleted(host, user) {
+            return this.send('templates/base.html', host, user, 'Your account was removed',
+                'You are receiving this email because your account for <a href="${host}">${greeting}</a> was removed.',
+                'Account was removed, but failed to send email notification to user!')
+                .catch(() => Promise.reject(new Error('Password was changed, but failed to send confirmation email!')));
         }
     }
 
diff --git a/modules/web-console/backend/services/users.js b/modules/web-console/backend/services/users.js
index ecfdc0b..377efed 100644
--- a/modules/web-console/backend/services/users.js
+++ b/modules/web-console/backend/services/users.js
@@ -56,6 +56,11 @@ module.exports.factory = (errors, settings, mongo, spacesService, mailsService,
                     user.resetPasswordToken = utilsService.randomString(settings.tokenLength);
                     user.activated = false;
 
+                    if (settings.activation.enabled) {
+                        user.activationToken = utilsService.randomString(settings.tokenLength);
+                        user.activationSentAt = new Date();
+                    }
+
                     if (settings.server.disableSignup && !user.admin && !createdByAdmin)
                         throw new errors.ServerErrorException('Sign-up is not allowed. Ask your Web Console administrator to create account for you.');
 
@@ -80,20 +85,15 @@ module.exports.factory = (errors, settings, mongo, spacesService, mailsService,
                 })
                 .then((registered) => {
                     if (settings.activation.enabled) {
-                        registered.activationToken = utilsService.randomString(settings.tokenLength);
-                        registered.activationSentAt = new Date();
+                        mailsService.sendActivationLink(host, registered);
 
-                        if (!createdByAdmin) {
-                            return registered.save()
-                                .then(() => {
-                                    mailsService.emailUserActivation(host, registered);
+                        if (createdByAdmin)
+                            return registered;
 
-                                    throw new errors.MissingConfirmRegistrationException(registered.email);
-                                });
-                        }
+                        throw new errors.MissingConfirmRegistrationException(registered.email);
                     }
 
-                    mailsService.emailUserSignUp(host, registered, createdByAdmin);
+                    mailsService.sendWelcomeLetter(host, registered, createdByAdmin);
 
                     return registered;
                 });
@@ -244,7 +244,7 @@ module.exports.factory = (errors, settings, mongo, spacesService, mailsService,
                         .catch((err) => console.error(`Failed to cleanup spaces [user=${user.username}, err=${err}`))
                         .then(() => user);
                 })
-                .then((user) => mailsService.emailUserDeletion(host, user));
+                .then((user) => mailsService.sendAccountDeleted(host, user));
         }
 
         /**
diff --git a/modules/web-console/backend/templates/base.html b/modules/web-console/backend/templates/base.html
new file mode 100644
index 0000000..4b741b1
--- /dev/null
+++ b/modules/web-console/backend/templates/base.html
@@ -0,0 +1,21 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+Hello ${firstName} ${lastName}!<br><br>
+${message}<br><br>
+--------------<br>
+${sign}<br>
\ No newline at end of file