48.5. Implementation of Local Strategy

The following is an implementation of the local passport strategy, and is available for cloning or download from https://bitbucket.org/phidip/passportonewithavatars/src/master/. Choose to clone with SSH or HTTPS as befits your own setup.

This application connects to the Mongo database in a different way from Traversy.

This application also requires the user to supply an avatar ingafe file. This will be stored in the database. That aspect is not related to Passport, and it will be explained in Section E.1

Example 48.1. Application Config, code/passportOneWithAvatars/app.js
const cookieParser = require('cookie-parser');
const createError = require('http-errors');
const express = require('express');
const helmet = require("helmet");                       // nml added
const logger = require('morgan');
const path = require('path');

/* part I from Traversy video */
const flash = require('connect-flash');
const session = require('express-session');
const passport = require('passport');
/* end Traversy */

const indexRouter = require('./routes/index');
const usersRouter = require('./routes/users');

const app = express();
app.locals.pretty = app.get('env') === 'development';       // pretty print html

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

app.use(helmet());                                      // nml added
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

/* part II from Traversy Video */
app.use(session(                        // setup session
    {
        secret: '998537qporhgpfangæ143+575?)(%lfjgaæ',  // footprints of the keyboard cat
        resave: true,
        saveUninitialized: true
    }));

// Passport middleware
app.use(passport.initialize());         // init passport
app.use(passport.session());            // connect passport and sessions
require('./config/passport')(passport);

// Connect flash
app.use(flash());

// Global variables
app.use(function(req, res, next) {
  res.locals.success_msg = req.flash('success_msg');
  res.locals.error_msg = req.flash('error_msg');
  res.locals.error = req.flash('error');
  next();
});
/* end Traversy */

app.use('/', indexRouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

Example 48.2. Users Routing, code/passportOneWithAvatars/routes/users.js
const express = require('express');
const router = express.Router();
const auth = require("../controllers/authController.js");
const { ensureAuthenticated, forwardAuthenticated } = require('../config/auth');

/* registration form  */
router.get('/register', forwardAuthenticated, auth.register);
/* receive registration data  */
router.post('/register', auth.postRegister);

/* login form  */
router.get('/login', forwardAuthenticated, auth.login);
/* handle login */
router.post('/login', auth.postLogin)

/* logout, kills session and redirects to frontpage  */
router.get('/logout', auth.logout);

/*
* This is a REST endpoint that GETs an image from the database
* it is requested from an HTML img tag
* re the pug file dashboard.pug, and the endpoint handler 
* in auth.lookupImage
*/
router.get('/getimage/:userid', ensureAuthenticated, auth.lookupImage);

module.exports = router;

Example 48.3. Regular Routes, code/passportOneWithAvatars/routes/index.js
const express = require('express');
const index = require("../controllers/indexController.js");
const router = express.Router();
const { ensureAuthenticated, forwardAuthenticated } = require('../config/auth');

/* GET home page. */
router.get('/', forwardAuthenticated, index.frontpage);
/* dashnord for insiders  */
router.get('/dashboard', ensureAuthenticated, index.dashboard);

module.exports = router;

Example 48.4. Controller, code/passportOneWithAvatars/controllers/authController.js
const bcrypt = require('bcryptjs');
const formidable = require('formidable');                       // required for image upload
const fs = require('fs');                                       // required for reading temp image file
const mongoose = require('mongoose');
const mongoUtil = require("../models/MongoHandler");
const passport = require('passport');
const User = require('../models/User');

const saltRounds = 10;

exports.register = function (req, res) {
    res.render('register', {
            title: 'Register'
    });
};

exports.postRegister = async function (req, res) {
    let form = new formidable.IncomingForm();
    form.parse(req, async function(err, fields, files) {
      if (err) { console.error(err); }

      let { name, uid, email, password, passwordr } = fields;
      let errors = [];

      if (!name || !uid || !email || !password || !passwordr) {
          errors.push({ msg: 'Please enter all fields' });
      }
      if (password != passwordr) {
          errors.push({ msg: 'Passwords do not match' });
      }
      if (password.length < 32) {
          errors.push({ msg: 'Password must be at least 32 characters' });
      }
      if (errors.length > 0) {            // respond if errors
          res.render('register', {
              errors,
              name,
              uid,
              email,
              password,
              passwordr
          });
      }

      let db = await mongoUtil.mongoConnect();                            // connect
      let user = await User.findOne({ uid: uid });
      if (user) {
          errors.push({ msg: 'users already exists' });
          res.render('register', {                                        // respond if already exists
              errors,
              name,
              uid,
              email,
              password,
              passwordr
          });
      }

      try {
          db = await mongoUtil.mongoConnect();                            // connect
          let hash = await bcrypt.hash(password, saltRounds);             // hash password and create obj
          let newUser = new User({name: name, uid: uid, email: email, password: hash});
          newUser.avatar.data = await fs.readFileSync(files.avatar.path); // add to obj with read uploaded image
          newUser.avatar.contentType = files.avatar.type;                 // get its mimetype
          await newUser.save();
          req.flash('success_msg', 'You are now registered and can log in');
          res.render('login', {title: 'Login'});
      } catch (err) {
          errors.push({ msg: 'an error occurred' });
          res.render('register', {                                        // respond if already exists
              errors,
              name,
              uid,
              email,
              password,
              passwordr
          });
      }
    }) 
};

exports.login = function (req, res) {
    res.render('login', {
        title: 'Login'
    });
};

exports.postLogin = async function (req, res, next) {
    await passport.authenticate('local', {
        successRedirect: '/dashboard',
        failureRedirect: '/users/login',
        failureFlash: true
    })(req, res, next);
};

exports.logout = function (req, res) {
    req.logout();
    res.redirect('/');
};

/*
 *  REST endpoint for serving image
 */
exports.lookupImage = async function (req, res) {
    let query = {uid: req.params.userid};
    const db = await mongoUtil.mongoConnect();
    let user = await User.findOne(query);

    res.contentType(user.avatar.contentType);
    res.send(user.avatar.data);
};

Example 48.5. Passport Configuration, code/passportOneWithAvatars/config/passport.js
const bcrypt = require('bcryptjs');
const LocalStrategy = require('passport-local').Strategy;
const User = require('../models/User');

module.exports = function(passport) {
    passport.use(
        new LocalStrategy({
            usernameField: 'username'
        },
        async function (username, password, done) {
            try {
                let user = await User.findOne({ username: username });
                if (!user) {
                    return done(null, false, { error_msg: 'Unknown user' });
                }
                let isMatch = await bcrypt.compare(password, user.password);
                if (isMatch) {
                    return done(null, user);
                } else {
                    return done(null, false, { error_msg: 'Password incorrect' });
                }
            } catch (err) {
                return done(null, false, {error_msg: 'unspecified error'});
            }

        })
    );

    passport.serializeUser(function(user, done) {           // invoked on login
        done(null, user);
    });

    passport.deserializeUser(async function(user, done) {   // invoked when using session
        done(null, user);
    });
};

Example 48.6. Passport Configuration, code/passportOneWithAvatars/config/auth.js
module.exports = {
    ensureAuthenticated: function(req, res, next) {
        if (req.isAuthenticated()) {
            return next();
        }
        req.flash('error_msg', 'Please log in to view that resource');
        res.redirect('/users/login');
    },

    forwardAuthenticated: function(req, res, next) {
        if (!req.isAuthenticated()) {
            return next();
        }
        res.redirect('/');
    }
};

Example 48.7. Mongo Configuration Params, code/passportOneWithAvatars/config/Params.js
<xi:include></xi:include>

Example 48.8. Mongo Singleton Connector, code/passportOneWithAvatars/models/MongoHandler.js
/*
 * MongoConnect as a Singleton
 */
const mongoose = require('mongoose');
const MongoParams = require('../config/MongoParams').MongoParams;
const PARAMS =  {
    useNewUrlParser: true,
    useUnifiedTopology: true
};

class MongoConnection {
    static conn = false;

    static async mongoConnect() {
        if (!MongoConnection.conn) {
            try {
                await mongoose.connect(MongoParams.CONSTR, PARAMS);
                MongoConnection.conn = mongoose.connection;
                console.log(`Connected to mongo server`);
            } catch (err) {
                console.log('something mongo???' + err);
            }
        }
        return MongoConnection.conn
    }
}
module.exports = MongoConnection;

Example 48.9. Register View, code/passportOneWithAvatars/views/register.pug
extends layout

block content
    main.otherpage
        aside
        section
            h2 Please Enter Your Details to #{title}
            include messages.pug
            form(method='post' action='/users/register' enctype='multipart/form-data')
                p
                    label Name
                    br
                    input(type='text' minlength='2' name='name' required)
                p
                    label LoginID
                    br
                    input(type='text' minlength='2' name='uid' required)
                p
                    label Email
                    br
                    input(type='email' minlength='6' name='email' required)
                p
                    label Password
                    br
                    input(type='password' minlength='32' name='password' required)
                p
                    label Repeat Password
                    br
                    input(type='password' minlength='32' name='passwordr' required)
                p
                    label Avatar
                    br
                    input(type='file' name='avatar' placeholder='optional')
                p
                    label
                    input(type='submit', value='Send')
        aside

Example 48.10. Login View, code/passportOneWithAvatars/views/login.pug
extends layout

block content
    main.otherpage
        aside
        section
            h2 Please #{title}
            include messages.pug
            form(method='post' action='/users/login')
                p
                    label LoginID
                    br
                    input(type='text' minlength='2' name='uid')
                p
                    label Password
                    br
                    input(type='password' minlength='32' name='password')
                p
                    label
                    input(type='submit', value='Login')
        aside