etnbrd/flx-compiler

View on GitHub
test-set/express-couchUser-master/index.js

Summary

Maintainability
F
1 wk
Test Coverage
// # expressUserCouchDb
//
// This module is an express plugin module, which means you can
// require the module in your express app, and add it to your express
// application by using `app.use(user(config));`
//
var express = require('express');
var nano = require('nano');
var uuid = require('uuid');
var emailTemplates = require('email-templates');
var nodemailer = require('nodemailer');
var _ = require('underscore');
var only = require('only');

module.exports = function(config) {
  var app = express(),
    safeUserFields = config.safeUserFields ? config.safeUserFields : "name email roles",
    db;

  function configureNano(cookie) {
    return nano({
      url: config.users,
      request_defaults: config.request_defaults,
      cookie: cookie
    });
  }

  db = configureNano();

  var transport;
  try {
    transport = nodemailer.createTransport(
      config.email.service,
      config.email[config.email.service]
    );
  } catch (err) {
    console.log('*** Email Service is not configured ***');
  }

  if (!config.validateUser) {
    config.validateUser = function(input, cb) {
      cb();
    };
  }

  // required properties on req.body
  // * name
  // * password
  // * email
  //
  // ### note: you can add more properties to
  // your user registration object
  app.post('/api/user/signup', function(req, res) {
    if (!req.body || !req.body.name || !req.body.password || !req.body.email) {
      return res.send(400, JSON.stringify({ok: false, message: 'A name, password, and email address are required.'}));
    }

    if (req.body.confirm_password) delete req.body.confirm_password;

    req.body.type = 'user';

    // Check to see whether a user with the same email address already exists.  Throw an error if it does.
    db.view('user', 'all', { key: req.body.email }, function(err, body) {
      if (err) { return res.send(err.status_code ? err.status_code : 500, err); }
      if (body.rows && body.rows.length > 0) { return res.send(400, {ok: false, message: "A user with this email address already exists.  Try resetting your password instead."})};

      // We can now safely create the user.
      db.insert(req.body, 'org.couchdb.user:' + req.body.name, done);
    });

    function done(err, body) {
      if (err) { return res.send(err.status_code, err); }

      if (config.verify) {
        try {
          validateUserByEmail(req.body.email);
          db.get(body._id, function(err,user) {
            if (err) { return res.send(err.status_code, err); }
            res.send(200, JSON.stringify({ok: true, user: strip(user)}));
          });
        }
        catch (email_err) {
          res.send(err.status_code, email_err);
        }
      } else {
        res.send(200, JSON.stringify( _.extend(req.body, {_rev: body.rev, ok: true} ) ));
      }
    }
  });

  // login user
  // required properties on req.body
  // * name
  // * password
  app.post('/api/user/signin', function(req, res) {

    if (!req.body || !req.body.name || !req.body.password) {
      return res.send(400, JSON.stringify({ok: false, message: 'A name, and password are required.'}));
    }

    db.auth(req.body.name, req.body.password, populateSessionWithUser(function(err, user) {
      if (err) {
        return res.send(err.statusCode ? err.statusCode : 500, {ok: false, message: err.message, error: err.error});
      }

      res.end(JSON.stringify({ok:true, user: strip(user)}));
    }));

    function populateSessionWithUser(cb) {
      return function(err, body, headers) {
        if (err) { return cb(err); }

        getUserName(body.name, headers['set-cookie'], function(err, name) {
          if (err) { return cb(err); }

          lookupUser(name, function(err, user) {
            if(err) { return cb(err); }

            if (config.verify && !user.verified) {
              return cb({statusCode: 401, ok: false, message: 'You must verify your account before you can log in.  Please check your email (including spam folder) for more details.'});
            }

            if(user.enabled === false) {
              return cb({statusCode: 403, ok: false, message: 'Your account is no longer enabled.  Please contact an Administrator to enable your account.'});
            }

            config.validateUser({req: req, user: user, headers: headers}, function(err, data) {
              if(err) {
                err.statusCode = err.statusCode || 401;
                err.message = err.message || 'Invalid User Login';
                err.error = err.error || 'unauthorized';
                return cb(err);
              }

              createSession(user, data, function(){
                cb(null, user);
              });

            });

          });
        });

      }
    }

    function getUserName(name, authCookie, cb) {
      if (name) {
        cb(null, name);
      } else {
        /**
         * Work around for issue:  https://issues.apache.org/jira/browse/COUCHDB-1356
         * Must fetch the session after authentication in order to find username of server admin that logged in
         */
        configureNano(authCookie).session(function(err, session) {
          cb(err, session.userCtx.name);
        });
      }
    }

    function lookupUser(name, cb) {
      db.get('org.couchdb.user:' + name, cb);
    }

    function createSession(user, data, cb) {
      req.session.regenerate(function() {
        req.session.user = user;
        if(data) {
          _.each(data, function(val, key) {
            req.session[key] = val;
          });
        }
        cb();
      });
    }

});

  // logout user
  // required properties on req.body
  // * name
  app.post('/api/user/signout', function(req, res) {
    req.session.destroy(function (err) {
      if (err) {
        console.log('Error destroying session during logout' + err);
      }
      res.send(200, JSON.stringify({ok: true, message: "You have successfully logged out."}));
    });
  });


  // forgot user password
  // required properties on req.body
  // * email
  app.post('/api/user/forgot', function(req,res) {
    if (!req.body || !req.body.email) {
      return res.send(400, JSON.stringify({ok: false, message: 'An email address is required.'}));
    }

    var user;
    // use email address to find user
    db.view('user', 'all', { key: req.body.email }, saveUser);

    // generate uuid code
    // and save user record
    function saveUser(err, body) {
      if (err) { return res.send(err.status_code ? err.status_code : 500, err); }

      if (body.rows && body.rows.length === 0) {
        return res.send(500, JSON.stringify({ ok: false, message: 'No user found with that email.' }));
      }

      user = body.rows[0].value;

      if(user.enabled === false) {
        return res.send(403, JSON.stringify({ok: false, message: 'Your account is no longer enabled.  Please contact an Administrator to enable your account.'}));
      }

      // generate uuid save to document
      user.code = uuid.v1();
      db.insert(user, user._id, createEmail);
    }

    // initialize the emailTemplate engine
    function createEmail(err, body) {
      if (err) { return res.send(err.status_code ? err.status_code : 500, err); }
      emailTemplates(config.email.templateDir, renderForgotTemplate);
    }

    // render forgot.ejs
    function renderForgotTemplate(err, template) {
      if (err) { return res.send(err.status_code ? err.status_code : 500, err); }
      // use header host for reset url
      config.app.url = 'http://' + req.headers.host;
      template('forgot', { user: user, app: config.app }, sendEmail);
    }

    // send rendered template to user
    function sendEmail(err, html, text) {
      if (err) { return res.send(err.status_code ? err.status_code : 500, err); }
      if (!transport) { return res.send(500, { error: 'transport is not configured!'}); }
      transport.sendMail({
        from: config.email.from,
        to: user.email,
        subject: config.app.name + ': Reset Password Request',
        html: html,
        text: text }, done);
    }

    // complete action
    function done(err, status) {
      if (err) { return res.send(err.status_code ? err.status_code : 500, err); }
      res.send(200, JSON.stringify({ ok: true, message: "forgot password link sent..." }));
      //app.emit('user: forgot password', user);
    }
  });


app.get('/api/user/code/:code', function(req, res) {
  if (!req.params.code) {
    return res.send(500, JSON.stringify({ok: false, message: 'You must provide a code parameter.'}));
  }

  db.view('user', 'code', {key: req.params.code}, function(err, body) {
    if (err) { return res.send(err.status_code ? err.status_code : 500, err); }
    if (body.rows.length > 1) {
      return res.send(500, JSON.stringify({ ok: false, message: 'More than one user found.'}));
    } else if (body.rows.length === 0) {
      return res.send(500, JSON.stringify({ok: false, message: 'Reset code is not valid.'}));
    } else {
      var user = body.rows[0].value;
      var name = user.name;
      if (user.fname && user.lname) {
        name = user.fname + ' ' + user.lname;
      }
      return res.send(200, JSON.stringify({ok: true, user: strip(user) }));
    }
  });
});

    // reset user password
    // required properties on req.body
    // * code (generated by /api/user/forgot)
    // * password
    app.post('/api/user/reset', function(req, res) {
      if (!req.body || !req.body.code || !req.body.password) {
        return res.send(400, JSON.stringify({ok: false, message: 'A password and valid password reset code are required.'}));
      }

      // get user by code
      db.view('user', 'code', { key: req.body.code }, checkCode);
      function checkCode(err, body) {
        if (err) { return res.send(err.status_code ? err.status_code : 500, err); }
        if (body.rows && body.rows.length === 0) {
          return res.send(500, JSON.stringify({ok: false, message: 'Not Found'}));
        }
        var user = body.rows[0].value;
        user.password = req.body.password;
      // clear code
      delete user.code;
      db.insert(user, user._id, function(err,user) {
        if (err) { return res.send(err.status_code ? err.status_code : 500, err); }
        return res.send(200, JSON.stringify({ok: true, user: strip(user) }));
      });
    }
  });

    // Send (or resend) verification code to a user's email address
    // required properties on req.body
    // * email
    app.post('/api/user/verify', function(req, res) {
      if (!req.body || !req.body.email) {
        return res.send(400, JSON.stringify({ok: false, message: 'An email address must be passed as part of the query string before a verification code can be sent.'}));
      }

      try {
        validateUserByEmail(req.body.email);
        res.send(200,JSON.stringify({ok:true, message: "Verification code sent..."}));
      }
      catch (validate_err) {
        res.send(validate_err.status_code, validate_err);
      }
    });


    // Accept a verification code and flag the user as verified.
    // required properties on req.params
    // * code
    app.get('/api/user/verify/:code', function(req,res) {
      if (!req.params.code) {
        return res.send(400, JSON.stringify({ok: false, message: 'A verification code is required.'}));
      }

      var user;
        // use verification code
        db.view('user', 'verification_code', { key: req.params.code }, saveUser);

        function saveUser(err, body) {
          if (err) { return res.send(err.status_code ? err.status_code : 500, err); }

          if (body.rows && body.rows.length === 0) {
            return res.send(400, JSON.stringify({ ok: false, message: 'Invalid verification code.' }));
          }

            // TODO:  Add an expiration date for the verification code and check it.

            user = body.rows[0].value;
            if (!user.verification_code || user.verification_code !== req.params.code) {
              return res.send(400, JSON.stringify({ ok: false, message: 'The verification code you attempted to use does not match our records.' }));
            }

            delete user.verification_code;
            user.verified = new Date();
            db.insert(user, user._id, function(err, body) {
              if (err) { return res.send(err.status_code ? err.status_code : 500, err); }
              return res.send(200,JSON.stringify({ok:true, message: "Account verified."}));
            });
          }
        });

    // Return the name of the currently logged in user.
    app.get('/api/user/current', function(req, res) {
      if (!req.session || !req.session.user) {
        return res.send(401,JSON.stringify({ok:false, message: "Not currently logged in."}));
      }

      res.send(200, JSON.stringify({ok: true, user: strip(req.session.user)}));
    });

  // Look up another user's information
  app.get('/api/user/:name', function(req, res) {
    if (!req.session || !req.session.user) {
      return res.send(401,JSON.stringify({ok:false, message: "You must be logged in to use this function."}));
    }

    db.get('org.couchdb.user:' + req.params.name, function(err,user) {
      if (err) { return res.send(err.status_code ? err.status_code : 500, err); }
      return res.send(200, JSON.stringify({ok: true, user: strip(user) }));
    });
  });

  // Create a new user or update an existing user
  app.put('/api/user/:name', function(req, res) {
    if (!req.session || !req.session.user) {
      return res.send(401,JSON.stringify({ ok:false, message: "You must be logged in to use this function"}));
    }
    else if (config.adminRoles && !hasAdminPermission(req.session.user) && req.session.user.name !== req.params.name) {
      return res.send(403,JSON.stringify({ok:false, message: "You do not have permission to use this function."}));
    }

    db.get('org.couchdb.user:' + req.params.name, function(err, user) {
      if (err) { return res.send( err.status_code ? err.status_code : 500, err); }
      var updates = strip(req.body);

      var keys = Object.keys(updates);
      for (var i in keys) {
        var key = keys[i];
        if (key === "roles" && !hasAdminPermission(req.session.user)) {
          console.log("Stripped updated role information, non-admin users are not allowed to change roles.");
        } else {
          user[key] = updates[key];
        }
      }

      db.insert(user, 'org.couchdb.user:' + req.params.name, function(err, data) {
        if (err) { return res.send(err.status_code ? err.status_code : 500, err); }

        user._rev = data.rev;

        // If a user updates their record, we need to update the session data
        if (req.session.user.name === req.params.name) {
          req.session.user = strip(user);
        }

        return res.send(200, JSON.stringify({ok: true, user: strip(user) }));
      });
    });
  });

  // Delete a user
  app.del('/api/user/:name', function(req,res) {
    if (!req.session || !req.session.user) {
      return res.send(401, JSON.stringify({ok: false, message: "You must be logged in to use this function"}));
    }
    else if (config.adminRoles && !hasAdminPermission(req.session.user)) {
      return res.send(403,JSON.stringify({ok:false, message: "You do not have permission to use this function."}));
    }

    db.get('org.couchdb.user:' + req.params.name, function(err,user) {
      if (err) { return res.send(err.status_code ? err.status_code : 500, err); }

      db.destroy(user._id, user._rev, function(err,body) {
        if (err) { return res.send(err.status_code ? err.status_code : 500, err); }

        function respondUserDeleted() {
          res.send(200, JSON.stringify({ok: true, message: "User " + req.params.name + " deleted."}));
        }
        // Admins can delete their own accounts, but this will log them out.
        if (req.session.user.name === req.params.name) {
          req.session.destroy(function(err) {
            if (err) {
              console.log('Error destroying session for ' + req.params.name + ' ' + err);
            }
            respondUserDeleted();
          });
        } else {
          respondUserDeleted();
        }

      });
    });
  });

  // Create a user
  app.post('/api/user', function(req, res) {
    if (!req.session || !req.session.user) {
      return res.send(401, JSON.stringify({ok:false, message: "You must be logged in to use this function"}));
    }
    else if (config.adminRoles && !hasAdminPermission(req.session.user)) {
      return res.send(403,JSON.stringify({ok:false, message: "You do not have permission to use this function."}));
    }
    req.body.type = 'user';
    db.insert(req.body, 'org.couchdb.user:' + req.body.name, function(err, data) {
      if (err) { return res.send(err.status_code ? err.status_code : 500, err); }
      res.send(200, JSON.stringify({ok: true, data: data}));
    });
  });

  // Return a list of users matching one or more roles
  app.get('/api/user', function(req, res) {
    if (!req.session || !req.session.user) {
      return res.send(401,JSON.stringify({ok:false, message: "You must be logged in to use this function"}));
    }
    if (!req.query.roles) { return res.send(400, JSON.stringify({ok:false, message: 'Roles are required!'})); }
    var keys = req.query.roles.split(',');
    db.view('user', 'role', {keys: keys}, function(err, body) {
      if (err) { return res.send(err.status_code ? err.status_code : 500, err); }
      var users = _(body.rows).pluck('value');
      res.send(200, JSON.stringify({ok: true, users: stripArray(users)}));
    });
  });

  function strip(value) {
    return only(value, safeUserFields);
  }

  function stripArray(array) {
    var returnArray = [];
    array.forEach(function(value) { returnArray.push(only(value, safeUserFields)); });
    return returnArray;
  }

  function hasAdminPermission(user) {
        // If admin roles are disabled, then everyone has admin permissions
        if (!config.adminRoles) { return true; }

        if (user.roles) {
          for (var i in user.roles) {
            var role = user.roles[i];
            if (config.adminRoles instanceof String) {
              if (config.adminRoles === role) { return true; }
            }
            else if (config.adminRoles instanceof Array) {
              if (config.adminRoles.indexOf(role) >= 0) { return true; }
            }
            else {
              console.log("config.adminRoles must be a String or Array.  Admin checks are disabled until this is fixed.");
              return true;
            }
          }
        }

        return false;
      }

      function validateUserByEmail(email) {
        var user;
        // use email address to find user
        db.view('user', 'all', { key: email }, saveUserVerificationDetails);

        function saveUserVerificationDetails(err, body) {
          if (err) { throw(err); }

          if (body.rows && body.rows.length === 0) {
            var error = new Error('No user found with the specified email address.');
            error.status_code = 404;
            throw(error);
          }

          user = body.rows[0].value;
            // TODO:  Add an expiration date for the verification code and check it.
            user.verification_code = uuid.v1();
            db.insert(user, user._id, verificationEmail);
          }

        // initialize the emailTemplate engine
        function verificationEmail(err, body) {
          if (err) { throw(err); }
          emailTemplates(config.email.templateDir, renderVerificationTemplate);
        }

        // render verify template
        function renderVerificationTemplate(err, template) {
          if (err) { throw(err); }
          template('verify', { user: user, app: config.app }, sendVerificationEmail);
        }

        // send rendered template to user
        function sendVerificationEmail(err, html, text) {
          if (err) { throw(err); }
          if (!transport) {
            var error = new Error('Mail transport is not configured!');
            error.status_code = 500;
            throw(error);
          }
          transport.sendMail({
            from: config.email.from,
            to: user.email,
            subject: config.app.name + ': Please Verify Your Account',
            html: html,
            text: text }, done);
        }

        // complete action
        function done(err, status) {
          if (err) { throw(err); }
            //app.emit('user: verify account', user);
          }
        }

        return app;
      };