We create a remote repo by the name node_passport_login
and then do the following locally. Notice the
use of the Express CLI inreface to create the
application. This ensures uniformity between projects.
The structure will be familiar from one project to the next.
Our lives will be easier.
express --view=pug node_passport_login cd node_passport_login npm install git init git add . git commit -m 'first commit' git remote add origin <name from remote git> git push -u origin master npm start
This should of course result in the normal opening Welcome to Express. If it does we are ready to roll. Now we do
npm i mongoose npm i passport passport-local npm i express-session npm i bcryptjs connect-flash
Now edit yout app.js so that it holds
node_passport_login/app.js
const createError = require('http-errors');
const path = require('path');
const logger = require('morgan');
const express = require('express');
const mongoose = require('mongoose');
const passport = require('passport');
const flash = require('connect-flash');
const session = require('express-session');
// Passport Config
require('./config/passport')(passport);
// DB Config and server connect
const db = require('./config/keys').mongoURI;
mongoose.connect('mongodb://localhost/node-auth', {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true
})
.then( function() { console.log('mongoose connection open'); })
.catch( function(err) { console.error(err); });
const app = express();
app.locals.pretty = app.get('env') === 'development'; // pretty print html
// view engine setup pug and static
app.use(express.static(path.join(__dirname, 'public')));
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
// Express body parser
app.use(express.urlencoded({ extended: true }));
app.use(logger('dev'));
// Express session
app.use(require('express-session')({ // passport initialize
secret: 'ioeruir!rznbzvmn8768576hdsw&%', // do the keyboard cat
resave: true, // to create entropy
saveUninitialized: false
}));
// Passport middleware
app.use(passport.initialize());
app.use(passport.session());
// 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();
});
// Routes
app.use('/', require('./routes/index.js'));
app.use('/users', require('./routes/users.js'));
// 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;
The codelines 15 through 22 sets up access to the MongoDB software, and opens a connection to the database server. This means that the connection will remain open throughout the duration of the lifespan of the web server.
The codelines 43-45 gets access to the Passport software, and establishes the authentication of this web application.
Now create the directory models, and
in it the file
node_passport_login/models/User.js
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true,
unique: true
},
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
date: {
type: Date,
default: Date.now
}
});
const User = mongoose.model('User', UserSchema, 'user');
module.exports = User;
In this tutorial we see a slightly different architecture than we have used previously. Until now we have mixed routing and controller code somewhat. This may be done when applications are relatively small. An application of scale may be better served with a separate appplication logic layer, the controller layer. Routing as we shall see now, shrinks to a terse "with this route the controller is <some function name>"
node_passport_login/routes/index.js
const express = require('express');
const router = express.Router();
const { ensureAuthenticated, forwardAuthenticated } = require('../config/auth');
const idx = require("../controllers/indexController");
router.get('/', forwardAuthenticated, idx.frontpage);
router.get('/dashboard', ensureAuthenticated, idx.dashboard);
module.exports = router;node_passport_login/routes/users.js
const express = require('express');
const router = express.Router();
const auth = require("../controllers/authController.js");
const { forwardAuthenticated } = require('../config/auth');
router.get('/register', forwardAuthenticated, auth.register);
router.post('/register', auth.postRegister);
router.get('/login', forwardAuthenticated, auth.login);
router.post('/login', auth.postLogin);
router.get('/logout', auth.logout);
module.exports = router;
We notice that some of the routers have more than one
callback functions. This is documented in the Express
documentation. Whatever callback that is not last in
the chain must end with a return next();
to pass control to the next callback function in the
chain. This is shown further down in Example 48.8.
On larger sites we use a controller per subject matter area. This is exemplified here by a controller for regular functionalities, and one for user matters i.e. resgistration and login, also known as authentication matters. Perhaps at this point it is convenient to show the directory structure for this "kind of best practice" project:
.
├── README.md
├── app.js
├── bin
│ └── www
├── config
│ ├── auth.js
│ ├── keys.js
│ └── passport.js
├── controllers
│ ├── authController.js
│ └── indexController.js
├── models
│ └── User.js
├── node_modules contents of node_modules
... not shown
├── package-lock.json
├── package.json
├── public
│ ├── favicon.svg
│ ├── images
│ ├── javascripts
│ └── stylesheets
├── routes
│ ├── index.js
│ └── users.js
└── views
├── dashboard.pug
├── error.pug
├── index.pug
├── layout.pug
├── login.pug
├── register.pug
└── regsave.pugnode_passport_login/controllers/indexController.js
exports.frontpage = function (req, res) {
res.render('index', {
title: 'Demoing PassportJS',
subtitle: 'Inspired by Traversy'
});
};
exports.dashboard = function (req,res) {
res.render('dashboard', {
title: 'Demoing PassportJS',
subtitle: 'Here\'s What We Do:',
user: req.user
});
};node_passport_login/controllers/authController.js
const bcrypt = require('bcryptjs');
const passport = require('passport');
const mongoose = require('mongoose');
const User = require('../models/User');
const saltRounds = 10;
exports.register = function (req, res) {
res.render('register', {
title: 'Demoing PassportJS',
subtitle: 'Inspired by Traversy'
});
};
exports.postRegister = function (req, res) {
const { name, email, password, password2 } = req.body;
let errors = [];
if (!name || !email || !password || !password2) {
errors.push({ msg: 'Please enter all fields' });
}
if (password != password2) {
errors.push({ msg: 'Passwords do not match' });
}
if (password.length < 12) {
errors.push({ msg: 'Password must be at least 12 characters' });
}
if (errors.length > 0) {
res.render('register', {
errors,
name,
email,
password,
password2
});
} else {
User.findOne({ email: email }).then( function (user) {
if (user) {
errors.push({ msg: 'Email already exists' });
res.render('register', {
errors,
name,
email,
password,
password2
});
} else {
const newUser = new User({
name,
email,
password
});
bcrypt.hash(newUser.password, saltRounds, function (err, hash) {
if (err) throw err;
newUser.password = hash;
newUser
.save()
.then(user => {
req.flash(
'success_msg',
'You are now registered and can log in'
);
res.redirect('/users/login');
})
.catch(err => console.log(err));
});
}
});
}
};
exports.login = function (req, res) {
res.render('login', {
title: 'Demoing PassportJS',
subtitle: 'Inspired by Traversy'
});
};
exports.postLogin = function (req, res, next) {
passport.authenticate('local', {
successRedirect: '/dashboard',
failureRedirect: '/users/login',
failureFlash: true
})(req, res, next);
};
exports.logout = function (req, res) {
req.logout();
req.flash('success_msg', 'You are logged out');
res.redirect('/users/login');
};node_passport_login/config/auth.js
Referred to from the routers as guards of the pages only authenticated users have access to.
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('/dashboard');
}
};
node_passport_login/config/passport.js
This file is required for Passport use.
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcryptjs');
// Load User model
const User = require('../models/User');
module.exports = function(passport) {
passport.use(
new LocalStrategy({ usernameField: 'email' }, function (email, password, done) {
// Match user
User.findOne({
email: email
}).then(function (user) {
if (!user) {
return done(null, false, { message: 'That email is not registered' });
}
// Match password
bcrypt.compare(password, user.password, function (err, isMatch) {
if (err) throw err;
if (isMatch) {
return done(null, user);
} else {
return done(null, false, { message: 'Password incorrect' });
}
});
});
})
);
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
};
node_passport_login/config/keys.js
This file is required somewhere, but none of its content is used in the current project.
dbPassword = 'mongodb+srv://YOUR_USERNAME_HERE:'+ encodeURIComponent('YOUR_PASSWORD_HERE') + '@CLUSTER_NAME_HERE.mongodb.net/test?retryWrites=true';
module.exports = {
mongoURI: dbPassword
};
node_passport_login/views/layout.pug
doctype
html
head
title= title
meta(charset='utf-8')
meta(name="viewport" content="width=device-width, initial-scale=1.0")
link(rel="icon", type="image/svg+xml", href="/favicon.svg")
link(rel='stylesheet', href='/stylesheets/style.css')
script(type="module", src='/javascripts/nquery.js')
body
header
h2= title
nav
ul
li
a(href="/") Home
if user
li
a(href="/dashboard") Insiders
li
a(href="/users/logout") Logout
if !user
li
a(href="/users/register") Register
li
a(href="/users/login") Login
main
block content
footer
div(id="cpy")
div
a(href="#") twitternode_passport_login/views/index.pug
extends layout
block content
h1= subtitle
p Welcome to #{title}
node_passport_login/views/register.pug
extends layout
block content
h1 Register
if errors != undefined && errors.length != 0
each error in errors
div(role='alert' id='a1') #{error.msg}
if success_msg != ''
div(role='alert' id='a2') #{success_msg}
if error_msg != ''
div(role='alert' id='a3') #{error_msg}
if error != ''
div(role='alert' id='a4') #{error}
form(id='myformr' action='/users/register' method='post')
table
tr
td Name:
td
input(type='name' name='name' placeholder='firstname' required)
tr
td Email:
td
input(type='email' name='email' placeholder='n@mm.tld' required)
tr
td Password:
td
input(type='password' name='password' placeholder='password, minimum 12 mixed characters' required)
tr
td Password confirm:
td
input(type='password' name='password2' placeholder='repeat password' required)
tr
td
td
input(type='submit' value='Go')
p Have an account?
a(href='/users/login') Loginnode_passport_login/views/login.pug
extends layout
block content
h1 Login
if errors != undefined && errors.length != 0
each error in errors
div(role='alert' id='a1') #{error.msg}
if success_msg != ''
div(role='alert' id='a2') #{success_msg}
if error_msg != ''
div(role='alert' id='a3') #{error_msg}
if error != ''
div(role='alert' id='a4') #{error}
form(id='myforml' action='/users/login' method='post')
table
tr
td Email:
td
input(type='text' name='email' required)
tr
td Password:
td
input(type='password' name='password' required)
tr
td
td
input(type='submit' value='Go')
p No account?
a(href='/users/register') Registernode_passport_login/views/dashboard.pug
extends layout
block content
h1= subtitle
p Welcome to #{user.name}