From 952d3e20e9fce86439fc3f97a67b9c2eaf869b03 Mon Sep 17 00:00:00 2001 From: Adam Buczynski Date: Tue, 28 Jun 2016 19:13:43 +1200 Subject: [PATCH] Use sendgrid directly, CORS_ORGINS config, fix auth.ctrl --- .gitattributes | 5 - .gitignore | 5 - .npmignore | 6 +- app/app.js | 23 +-- app/auth/auth.ctrl.js | 11 -- app/error/middleware/issue-on-github.js | 2 +- app/locales/en.json | 30 ++-- app/services/mailer.js | 143 +++++++++++++++--- .../emails/password-has-changed.html | 0 app/user/emails/password-has-changed.js | 6 +- .../emails/password-has-changed.txt | 0 app/{ => user}/emails/reset-password.html | 0 app/user/emails/reset-password.js | 14 +- app/{ => user}/emails/reset-password.txt | 0 .../emails/verify-email-address.html | 0 app/user/emails/verify-email-address.js | 10 +- .../emails/verify-email-address.txt | 0 config/dev.js | 11 +- config/{production.js => prod.js} | 10 +- package.json | 7 +- 20 files changed, 175 insertions(+), 108 deletions(-) rename app/{ => user}/emails/password-has-changed.html (100%) rename app/{ => user}/emails/password-has-changed.txt (100%) rename app/{ => user}/emails/reset-password.html (100%) rename app/{ => user}/emails/reset-password.txt (100%) rename app/{ => user}/emails/verify-email-address.html (100%) rename app/{ => user}/emails/verify-email-address.txt (100%) rename config/{production.js => prod.js} (92%) diff --git a/.gitattributes b/.gitattributes index 83c13d6..4282322 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,2 @@ # Set the default behavior, in case people don't have core.autocrlf set. * text eol=lf - -# Binary files -*.png binary -*.jpg binary -*.woff binary diff --git a/.gitignore b/.gitignore index 020223e..2102ce3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,6 @@ -# Common .DS_Store -Thumbs.db -*.sublime-* *.log node_modules coverage - -# App config/local.* keys diff --git a/.npmignore b/.npmignore index 633241e..2102ce3 100644 --- a/.npmignore +++ b/.npmignore @@ -1,10 +1,6 @@ -# Common .DS_Store -Thumbs.db -*.sublime-* *.log node_modules coverage - -# App config/local.* +keys diff --git a/app/app.js b/app/app.js index a938964..fe6e6c3 100644 --- a/app/app.js +++ b/app/app.js @@ -21,13 +21,12 @@ let config = require('./config'); /** * Configuration */ -const ENV = config.ENV; +const CORS_ORIGINS = config.CORS_ORIGINS; const I18N_LOCALES = config.I18N_LOCALES; const I18N_DEFAULT_LOCALE = config.I18N_DEFAULT_LOCALE; const TOKEN_TYPES = config.TOKEN_TYPES; const TOKEN_DEFAULT_ISSUER = config.TOKEN_DEFAULT_ISSUER; const TOKEN_DEFAULT_AUDIENCE = config.TOKEN_DEFAULT_AUDIENCE; -const APP_DOMAIN = config.APP_DOMAIN; const SERVER_LATENCY = config.SERVER_LATENCY; const SERVER_LATENCY_MIN = config.SERVER_LATENCY_MIN; const SERVER_LATENCY_MAX = config.SERVER_LATENCY_MAX; @@ -62,23 +61,12 @@ module.exports = function() { }); tokens.register(TOKEN_TYPES); - //Trust proxy (for Google Cloud forwarding of requests) + //Trust proxy (for Cloud hosted forwarding of requests) app.set('trust_proxy', 1); - //Determine origin for CORS - let origin = [ - new RegExp('[a-z0-9\-]+\.' + APP_DOMAIN.replace(/\./g, '\\\.')) - ]; - - //Add dev origins - if (ENV === 'dev') { - origin.push(/localhost\:8080/); - origin.push(/192\.168\.1\.[0-9]+/); - } - //CORS app.use(cors({ - origin, + origin: CORS_ORIGINS, credentials: true //NOTE: needed for cross domain cookies to work })); @@ -135,11 +123,6 @@ module.exports = function() { .map(handler => require('./error/middleware/' + handler)) .forEach(handler => app.use(handler)); - //NOTE: Prevent Express from using the default error handler - //See: https://github.com/expressjs/express/issues/3024 - /* jshint -W098 */ - app.use(function(err, req, res, next) {}); - //Return express server instance return app; }; diff --git a/app/auth/auth.ctrl.js b/app/auth/auth.ctrl.js index 886777c..6cb230f 100644 --- a/app/auth/auth.ctrl.js +++ b/app/auth/auth.ctrl.js @@ -8,7 +8,6 @@ let moment = require('moment'); let NotAuthenticatedError = require('../error/type/auth/not-authenticated'); let NotAuthorizedError = require('../error/type/auth/not-authorized'); let UserSuspendedError = require('../error/type/auth/user-suspended'); -let UserPendingError = require('../error/type/auth/user-pending'); let tokens = require('../services/tokens'); let config = require('../config'); @@ -96,11 +95,6 @@ module.exports = { error = new UserSuspendedError(); } - //User pending approval? - else if (!user.isApproved) { - error = new UserPendingError(); - } - //Check error if (error) { return next(error); @@ -184,11 +178,6 @@ module.exports = { error = new UserSuspendedError(); } - //User pending approval? - else if (!user.isApproved) { - error = new UserPendingError(); - } - //Check error if (error) { return next(error); diff --git a/app/error/middleware/issue-on-github.js b/app/error/middleware/issue-on-github.js index 0c7609b..85dd037 100644 --- a/app/error/middleware/issue-on-github.js +++ b/app/error/middleware/issue-on-github.js @@ -40,7 +40,7 @@ module.exports = function(error, req, res, next) { //User data if (user) { - parts.push('Member: `' + user.id + '`'); + parts.push('User: `' + user.id + '`'); } //User agent diff --git a/app/locales/en.json b/app/locales/en.json index 165bea5..7e542e4 100644 --- a/app/locales/en.json +++ b/app/locales/en.json @@ -1,26 +1,20 @@ { - "user": { + "mail": { "verifyEmailAddress": { - "mail": { - "subject": "Verify your email address", - "instructions": "Please verify your email address by clicking on the following link:", - "action": "verify my email address" - } + "subject": "Verify your email address", + "instructions": "Please verify your email address by clicking on the following link:", + "action": "verify my email address" }, "resetPassword": { - "mail": { - "subject": "Reset your password", - "instructions": "Reset your password by clicking on the following link:", - "action": "reset password", - "validityNotice": "This link is only valid for {{numHours}} hours.", - "ignoreNotice": "If you did not request to change your password, you can ignore this email." - } + "subject": "Reset your password", + "instructions": "Reset your password by clicking on the following link:", + "action": "reset password", + "validityNotice": "This link is only valid for {{numHours}} hours.", + "ignoreNotice": "If you did not request to change your password, you can ignore this email." }, "passwordHasChanged": { - "mail": { - "subject": "Your password has been reset", - "confirmation": "Your password has been reset." - } + "subject": "Your password has been reset", + "confirmation": "Your password has been reset." } - } + } } diff --git a/app/services/mailer.js b/app/services/mailer.js index b11d493..15dab63 100644 --- a/app/services/mailer.js +++ b/app/services/mailer.js @@ -6,29 +6,108 @@ let fs = require('fs'); let path = require('path'); let Promise = require('bluebird'); +let sendgrid = require('sendgrid'); let readFile = Promise.promisify(fs.readFile); -let nodemailer = require('nodemailer'); -let sendgrid = require('nodemailer-sendgrid-transport'); let SendMailError = require('../error/type/server/send-mail'); let config = require('../config'); /** - * Constants + * Initialise sendgrid */ -const SENDGRID_API_KEY = config.SENDGRID_API_KEY; +let sg = sendgrid.SendGrid(config.SENDGRID_API_KEY); /** - * Create mailer + * Split name and email address */ -let mailer = Promise.promisifyAll( - nodemailer.createTransport( - sendgrid({ - auth: { - api_key: SENDGRID_API_KEY +function splitNameEmail(str) { + + //If no email bracket indicator present, return as is + if (str.indexOf('<') === -1) { + return ['', str]; + } + + //Split into name and email + let [name, email] = str.split('<'); + + //Fix up + name = name.trim(); + email = email.replace('>', '').trim(); + + //Return as array + return [name, email]; +} + +/** + * Convert plain data to sendgrid mail object + */ +function getMail(data) { + + //Extract email/name + if (data.to && data.to.indexOf('<') !== -1) { + let [name, email] = splitNameEmail(data.to); + data.to = email; + data.toname = name; + } + if (data.from && data.from.indexOf('<') !== -1) { + let [name, email] = splitNameEmail(data.from); + data.from = email; + data.fromname = name; + } + + //Get sendgrid classes + let Mail = sendgrid.mail.Mail; + let Email = sendgrid.mail.Email; + let Content = sendgrid.mail.Content; + let Personalization = sendgrid.mail.Personalization; + + //Create new mail object + let mail = new Mail(); + + //Create recipients + let recipients = new Personalization(); + recipients.addTo(new Email(data.to, data.toname)); + + //Set recipients + mail.addPersonalization(recipients); + + //Set sender + mail.setFrom(new Email(data.from, data.fromname)); + + //Set subject + mail.setSubject(data.subject); + + //Add content + mail.addContent(new Content('text/plain', data.text)); + mail.addContent(new Content('text/html', data.html)); + + //Return it + return mail; +} + +/** + * Send email (wrapped in promise) + */ +function sendMail(mail) { + return new Promise((resolve, reject) => { + + //Build request + let request = sg.emptyRequest(); + request.method = 'POST'; + request.path = '/v3/mail/send'; + request.body = mail.toJSON(); + + //Send request + sg.API(request, response => { + if (response && response.statusCode && + response.statusCode >= 200 && response.statusCode <= 299) { + resolve(response); } - }) - ) -); + reject(new SendMailError( + 'Sendgrid response error ' + response.statusCode + )); + }); + }); +} /** * Export mailer interface (wrapped in promise) @@ -42,14 +121,21 @@ module.exports = { return name + ' <' + email + '>'; }, + /** + * Split name and email address + */ + splitNameEmail(identity) { + return splitNameEmail(identity); + }, + /** * Load an email (both plain text and html) */ load(email, data) { //Get filenames - let text = path.resolve('./app/emails/' + email + '.txt'); - let html = path.resolve('./app/emails/' + email + '.html'); + let text = path.resolve('./app/' + email + '.txt'); + let html = path.resolve('./app/' + email + '.html'); //Return promise return Promise.all([ @@ -61,11 +147,28 @@ module.exports = { /** * Send mail */ - send(email) { - return mailer.sendMailAsync(email) - .catch(error => { - throw new SendMailError(error); - }); + send(data) { + + //Array + if (Array.isArray(data)) { + + //No emails + if (data.length === 0) { + return Promise.resolve(); + } + + //Multiple emails + let promises = data + .map(getMail) + .map(sendMail); + + //Return all promises wrapped + return Promise.all(promises); + } + + //Get and send the mail + let mail = getMail(data); + return sendMail(mail); } }; diff --git a/app/emails/password-has-changed.html b/app/user/emails/password-has-changed.html similarity index 100% rename from app/emails/password-has-changed.html rename to app/user/emails/password-has-changed.html diff --git a/app/user/emails/password-has-changed.js b/app/user/emails/password-has-changed.js index 1f210d6..0239b43 100644 --- a/app/user/emails/password-has-changed.js +++ b/app/user/emails/password-has-changed.js @@ -22,15 +22,15 @@ module.exports = function passwordHasChanged(user) { //Create data for emails let data = { - confirmation: locale.t('user.passwordHasChanged.mail.confirmation') + confirmation: locale.t('mail.passwordHasChanged.confirmation') }; //Load - return mailer.load('password-has-changed', data) + return mailer.load('user/emails/password-has-changed', data) .spread((text, html) => ({ to: user.email, from: EMAIL_IDENTITY_NOREPLY, - subject: locale.t('user.passwordHasChanged.mail.subject'), + subject: locale.t('mail.passwordHasChanged.subject'), text, html })); }; diff --git a/app/emails/password-has-changed.txt b/app/user/emails/password-has-changed.txt similarity index 100% rename from app/emails/password-has-changed.txt rename to app/user/emails/password-has-changed.txt diff --git a/app/emails/reset-password.html b/app/user/emails/reset-password.html similarity index 100% rename from app/emails/reset-password.html rename to app/user/emails/reset-password.html diff --git a/app/user/emails/reset-password.js b/app/user/emails/reset-password.js index 8105d72..6bff56d 100644 --- a/app/user/emails/reset-password.js +++ b/app/user/emails/reset-password.js @@ -3,8 +3,8 @@ /** * Dependencies */ -let Locale = require('../../services/locale'); let tokens = require('../../services/tokens'); +let Locale = require('../../services/locale'); let mailer = require('../../services/mailer'); let config = require('../../config'); @@ -35,20 +35,20 @@ module.exports = function resetPassword(user) { //Create data for emails let data = { link, - instructions: locale.t('user.resetPassword.mail.instructions'), - action: locale.t('user.resetPassword.mail.action'), - validityNotice: locale.t('user.resetPassword.mail.validityNotice', { + instructions: locale.t('mail.resetPassword.instructions'), + action: locale.t('mail.resetPassword.action'), + validityNotice: locale.t('mail.resetPassword.validityNotice', { numHours }), - ignoreNotice: locale.t('user.resetPassword.mail.ignoreNotice') + ignoreNotice: locale.t('mail.resetPassword.ignoreNotice') }; //Load - return mailer.load('reset-password', data) + return mailer.load('user/emails/reset-password', data) .spread((text, html) => ({ to: user.email, from: EMAIL_IDENTITY_NOREPLY, - subject: locale.t('user.resetPassword.mail.subject'), + subject: locale.t('mail.resetPassword.subject'), text, html })); }; diff --git a/app/emails/reset-password.txt b/app/user/emails/reset-password.txt similarity index 100% rename from app/emails/reset-password.txt rename to app/user/emails/reset-password.txt diff --git a/app/emails/verify-email-address.html b/app/user/emails/verify-email-address.html similarity index 100% rename from app/emails/verify-email-address.html rename to app/user/emails/verify-email-address.html diff --git a/app/user/emails/verify-email-address.js b/app/user/emails/verify-email-address.js index bbd89b5..e9a8802 100644 --- a/app/user/emails/verify-email-address.js +++ b/app/user/emails/verify-email-address.js @@ -3,8 +3,8 @@ /** * Dependencies */ -let Locale = require('../../services/locale'); let tokens = require('../../services/tokens'); +let Locale = require('../../services/locale'); let mailer = require('../../services/mailer'); let config = require('../../config'); @@ -30,16 +30,16 @@ module.exports = function verifyEmailAddress(user) { //Create data for emails let data = { link: APP_BASE_URL + '/email/verify/' + token, - instructions: locale.t('user.verifyEmailAddress.mail.instructions'), - action: locale.t('user.verifyEmailAddress.mail.action') + instructions: locale.t('mail.verifyEmailAddress.instructions'), + action: locale.t('mail.verifyEmailAddress.action') }; //Load - return mailer.load('verify-email-address', data) + return mailer.load('user/emails/verify-email-address', data) .spread((text, html) => ({ to: user.email, from: EMAIL_IDENTITY_NOREPLY, - subject: locale.t('user.verifyEmailAddress.mail.subject'), + subject: locale.t('mail.verifyEmailAddress.subject'), text, html })); }; diff --git a/app/emails/verify-email-address.txt b/app/user/emails/verify-email-address.txt similarity index 100% rename from app/emails/verify-email-address.txt rename to app/user/emails/verify-email-address.txt diff --git a/config/dev.js b/config/dev.js index 2e709ea..e90aaa3 100644 --- a/config/dev.js +++ b/config/dev.js @@ -13,13 +13,18 @@ module.exports = { //App APP_NAME: pkg.name, APP_VERSION: pkg.version, - APP_DOMAIN: 'my-application.com', APP_BASE_URL: 'http://localhost:8080', //API API_BASE_URL: 'http://localhost:8081', API_BASE_PATH: '/api/', + //CORS origins + CORS_ORIGINS: [ + /localhost\:8080/, + /192\.168\.1\.[0-9]+/ + ], + //Server SERVER_PORT: process.env.PORT || 8081, SERVER_HTTPS: false, @@ -48,7 +53,9 @@ module.exports = { GITHUB_USER_AGENT: '', //Error handling middleware - ERROR_MIDDLEWARE: ['normalize', 'log-to-console'], + ERROR_MIDDLEWARE: [ + 'normalize', 'log-to-console' + ], //Internationalization I18N_LOCALES: ['en'], diff --git a/config/production.js b/config/prod.js similarity index 92% rename from config/production.js rename to config/prod.js index df09d8f..9de6c2e 100644 --- a/config/production.js +++ b/config/prod.js @@ -13,13 +13,17 @@ module.exports = { //App APP_NAME: pkg.name, APP_VERSION: pkg.version, - APP_DOMAIN: 'my-application.com', APP_BASE_URL: 'http://my-application.com', //API API_BASE_URL: 'http://my-application.com', API_BASE_PATH: '/api/', + //CORS origins + CORS_ORIGINS: [ + /[[a-z0-9\-]+\.]*my\-application\.com/ + ], + //Server SERVER_PORT: process.env.PORT || 80, SERVER_HTTPS: false, @@ -48,7 +52,9 @@ module.exports = { GITHUB_USER_AGENT: '', //Error handling middleware - ERROR_MIDDLEWARE: ['normalize', 'log-to-console', 'log-to-gcloud'], + ERROR_MIDDLEWARE: [ + 'normalize', 'log-to-console', 'log-to-gcloud' + ], //Internationalization I18N_LOCALES: ['en'], diff --git a/package.json b/package.json index 948eee2..36a5736 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,8 @@ }, "keywords": [], "engines": { - "node": ">=6.2.0", - "npm": ">=3.8.9" + "node": ">=6.2.2", + "npm": ">=3.10.2" }, "main": "scripts/server.js", "scripts": { @@ -60,12 +60,11 @@ "mongoose": "^4.4.19", "morgan": "^1.7.0", "multer": "^1.1.0", - "nodemailer": "^2.1.0", - "nodemailer-sendgrid-transport": "^0.2.0", "passport": "^0.3.2", "passport-http-bearer": "^1.0.1", "passport-local": "^1.0.0", "passport-strategy": "^1.0.0", + "sendgrid": "^3.0.5", "serve-static": "^1.10.3", "yargs": "^4.7.1" },