app/models/contactVerificationRequest.js

/**
 * ContactVerificationRequest model
 * @module
 */

var async = require('async');
var ContactVerificationRequest;
var crypto = require('crypto');
var logger = require('app/lib/logger');
var mailer = require('app/lib/mailer');
var modelFactory = require('app/factories/model');
var ObjectId = require('mongoose').Types.ObjectId;

/**
 * Represents request to verify contact information
 * @class ContactVerificationRequest
 * @property {boolean} [authenticateSession=false] - Whether to authenticate the client's session upon verification
 * @property {string} clientOrigin - Origin URL of client that requested contact verification (e.g. "https://example.com")
 * @property {string} [code] - Secure code generated to fulfill request
 * @property {string} contact - Contact identifier used to send request using method (e.g. "user@example.com")
 * @property {Object[]} [createNotificationRequests] - Array of properties for notification requests to create related to user upon verification
 * @property {event} createNotificationRequests[].event - Event for new notification request
 * @property {boolean} [createUser=false] - Whether to create a user upon verification if one doesn't already exist with matching contact info
 * @property {string} method - Method used to send request to contact (e.g. "email")
 * @property {boolean} [verified=false] - Whether document has been verified 
 */
module.exports = ContactVerificationRequest = modelFactory.new('ContactVerificationRequest', {
  authenticateSession: { type: Boolean, default: false },
  clientOrigin: { type: String, required: true },
  code: String,
  contact: {
    type: String,
    required: true,
    validate: {
      validator: function(value) {
        return mailer.isValidEmail(value);
      },
      message: '"{VALUE}" is not a supported contact value'
    }
  },
  createNotificationRequests: {
    type: Array,
    validate: {
      validator: function(value) {
        try {
          if (['undefined', 'object'].indexOf(typeof value) === -1) {
            throw new Error('Invalid value found');
          } else if (typeof value === 'object') {
            value.forEach(function(notificationRequest) {
              if (typeof notificationRequest.event === 'undefined') {
                throw new Error('Invalid notificationRequest found');
              }
            });
          }

          return true;
        } catch (error) {
          return false;
        }
      },
      message: '"{VALUE}" is not a supported createNotificationRequests value'
    }
  },
  createUser: { type: Boolean, default: false },
  method: {
    type: String,
    required: true,
    validate: {
      validator: function(value) {
        return (['email'].indexOf(value) > -1);
      },
      message: '"{VALUE}" is not a supported method value'
    }
  },
  verified: { type: Boolean, default: false }
}, {
  jsonapi: {
    get: 'admin',
    patch: {
      allowed: 'public',
      queryConditions: function(req, done) {
        if (!req.body.data.attributes.code) {
          return done(new Error('Code value of attributes value not provided within data value of request'));
        }

        ContactVerificationRequest.findOne({
          _id: ObjectId(req.body.data.id),
          code: req.body.data.attributes.code
        }, (error, contactVerificationRequest) => {

          if (!error && !contactVerificationRequest) {
            error = new Error('No contactVerificationRequest found with id and code');
          }

          if (error) {
            done(error);
          } else {
            done(undefined, {
              code: contactVerificationRequest.code
            });
          }
        });
      },
    },
    post: {
      allowed: 'public',
      queryConditions: {
        code: undefined
      },

      /**
       * Generate and deliver verification code after POST
       * @param {Object} req - Express request object
       * @param {Object} res - Express response object
       * @param {Object} contactVerificationRequest - Mongoose contactVerificationRequest document created by POST request
       * @param {function} done - Callback function expecting error, res, req and contactVerificationRequest params
       */
      post: function(req, res, contactVerificationRequest, done) {
        var generateCodeAndSave = (done) => {
          crypto.randomBytes(32, (error, buffer) => {
            if (error) {
              return done(new Error(`Failed to generate code: ${error.message}`));
            }

            contactVerificationRequest.code = buffer.toString('hex');
            contactVerificationRequest.save(done);
          });
        };

        var deliverCode = (done) => {
          if (contactVerificationRequest.method === 'email') {
            mailer.sendMail({
              to: contactVerificationRequest.contact,
              subject: 'Please verify your email address to get notified about ' + process.env.SYNC_SERVER_NAME,
              text: 'Thanks for submitting your email address to ' + process.env.SYNC_SERVER_NAME + ' so we can notify you when it becomes available!\n\nHowever, before we can do so, we need you to confirm your address by visiting:\n\n' + contactVerificationRequest.clientOrigin + '/contact-verification-requests/' + contactVerificationRequest._id + '?code=' + contactVerificationRequest.code + ' \n\n\nPlease take a second to do so!\n\nNote: If you weren\'t the one to submit your email address to us, don\'t worry; we won\'t contact you further about this matter. You can simply ignore this message.'
            }, function(error) {
              if (error) {
                logger.error('ContactVerificationRequest model failed to send email for new contactVerificationRequest', {
                  contactVerificationRequestId: contactVerificationRequest.id,
                  error: error.message 
                });
              } else {
                logger.milestone('ContactVerificationRequest model sent email for new contactVerificationRequest', {
                  contactVerificationRequestId: contactVerificationRequest.id
                });
              }

              done();
            });
          }
        };

        async.series([
          generateCodeAndSave,
          deliverCode
        ], function(error) {
          if (error) {
            logger.error('ContactVerificationRequest model post-POST procedure failed', { error: error.message });
          }

          done(error, req, res, contactVerificationRequest);
        });
      }
    }
  }
});