The HTML5 form must be slightly augmented to allow for sendeing an image from the user's local computer to the server for storing.
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
In code line 16, an enctype
, enclosure type,
is added. This is necessary to signify that the amount of
data to transmit when an image is included cannot be
transmitted in one package. Several, if not many, are
required.
In line 34 you see the input type file
.
This provides a browse button in the form through which
the user can navigate her file system for the wanted
image file.
To contain an image the User model must contain a field
with the proper type. Lines 26 through 29 in the following
schema caters for that. The Buffer
type
will hold the image itself while the field named
contentType
will hold the mimetype of the
image, eg image/png
or image/jpeg
.
The mimetype is necessary for the browser to be able to
render the image. All regular image files contain a
mimetype for that purpose.
models/User.js
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
uid: {
type: String,
required: true,
unique: true
},
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
date: {
type: Date,
created: Date.now
},
avatar: { // for image
data: Buffer, // the image itself
contentType: String // the mimetype
}
});
const User = mongoose.model('User', UserSchema, 'user');
module.exports = User;
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;
The index router above contains the route /dashboard
with the middleware ensureauthenticated
which in this case will allow the dashboard view to be
displayed only users that are logged in. This bit has
nothing to do with the technology required to show the
image. It is purely for the example application.
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;
The users router hold two aspects essential for handling
images in MongoDB. Line 9 routes to the controller
function for storing the user with the image in the database.
The route in line 25 ins an endpoint for displaying the image.
This materializes a REST API that serves a
secondary request induced by the browser meeting an
img
tag requiring the image.
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);
};
The file above requires a module formidable
in line 2.
formidable
has a more nuanced way of
interpreting the form
data than we would have without it. It is able to
distinguish between regular form fields and those induce
by the file
input type. This is used in
lines 18, and 19.
The fs
module is included for reading the
uploaded image file which is temporarily stored by the
server. This is used in line 63. The user object created
in line 62 is then augmented with the image and its mimetype
in lines 63 and 64, and
being saved as usually. The result is, that apart from
the regular text data, the database document for the user
now also contains the user's avatar, an image. This may be
verified by running mongo
from the command
line, and displaying the user.
When we want to display data about the user, we need to
read the data from the database and pass the to the view
rendering mechanism. We normally display image data
by writing an img
element sourcing an
image file. We have a problem with the user image. There
is no image file because the image is in the document in the
database.
Since the src
of the img
element
is a url we make a trick. Instead of
the image file we request the image froma REST endpoint.
On line 12 below you will see the
img
element. It results in a request
being handled by the REST endpoint in
users.js
router. That route reads the
image data with the lookupImage
function
in lines 102-109 in authController.js
shown above.
views/dashboard.pug
extends layout
block content
main.otherpage
aside
section
h2 #{title}
h3 Demo Display of Images from Database
div.uid
p= user
p
img(src='/users/getimage/' + user)
aside
The code for this example is available for cloning from https://bitbucket.org/phidip/passportonewithavatars/src/master/.