Node.js: account activation system in ExpressJS

Node.js: account activation system in ExpressJS

In this article we will see how to implement an account activation system in ExpressJS.

In this article we will see how to implement an account activation system in ExpressJS.

The user model that we are going to define in MongoDB provides fields to store the activation token, its storage date and a Boolean flag that indicates whether the account has been activated.

'use strict';

const mongoose  = require('mongoose');

const { Schema }  = mongoose;

const UserSchema = new Schema({
    username: String,
    password: String,
    email: String,
    confirmed: {
        type: Boolean,
        default: false
    },
    token: String,
    expires: {
        type: Number,
        default: Date.now()
    }
    

},{collection: 'users'});

module.exports = mongoose.model('user', UserSchema);

The first step to implement is user registration. We will use the express-validator module to validate the server-side data. If the validation is successful, we will save the user's data in the database and send the e-mail with the activation link.

'use strict';

const crypto = require('crypto');
const express = require('express');
const router = express.Router();
const User = require('../models/user')
const Mail = require('../classes/Mail');
const { mail: mailSettings, siteURL } = require('../config');
const auth = require('../middleware/auth');
const { body, validationResult } = require('express-validator');

router.post('/register', [

    body('username').custom(value => {
        if(!/^[a-z0-9]+$/i.test(value)) {
            throw new Error('The username can contain only letters and numbers.');
        }
        return true;
    }),

    body('email').isEmail().withMessage('Invalid e-mail.'),

    body('password').custom(value => {
        const uppercase = /[A-Z]+/;
        const lowercase = /[a-z]+/;
        const digit = /[0-9]+/;
        const special = /[\W]+/;

        if(!uppercase.test(value) && !lowercase.test(value) && !digit.test(value) && !special.test(value) && value.length < 8) {
            throw new Error('The password must be at least 8 characters long and contain uppercase and lowercase letters, digits and special characters.');
        }

        return true;
    })
    
  ], async (req, res, next) => {

    const validationErrors = validationResult(req);
    if (!validationErrors.isEmpty()) {
        return res.status(400).render('index', { errors: validationErrors.array(), success: '' } );
    }

    try {

    const { username, password, email } = req.body;
    const encPassword = crypto.createHash('md5').update(password).digest('hex');
    const token = crypto.createHash('md5').update(Math.random().toString().substring(2)).digest('hex');

    const user = new User({
        username: username,
        email: email,
        password: encPassword,
        token: token
    });

    await user.save();

    res.render('index', {
        errors: [],
        success: 'Check your inbox to activate your account.'
    });

    const mail = new Mail({
        from: mailSettings.from,
        settings: mailSettings.settings
    });

    await mail.send(email, 'Activate Your Account', `<a href="${siteURL}confirm/${token}">Activate</a>`);

    } catch(err) {
        res.sendStatus(500);
    }


});

To prevent unauthorized access to the user dashboard, we have created the auth middleware defined as follows:

'use strict';

module.exports = (req, res, next) => {
    if(!req.session.user || !req.session.user.confirmed) {
        return res.redirect('/');
    }
    next();
};

The activation of the user account requires the verification of the validity of the token, which must be present in the database and must not be longer than one day. If the verification is successful, the confirmed flag is updated in the user profile and the user object is saved in the current session. Then the user is redirected to his dashboard.

router.get('/confirm/:token', async (req, res, next) => {
    const { token } = req.params;

    try {
        const user = await User.findOne({ token: token });
        
        if(!user) {
            return res.redirect('/');
        }

        const expiresIn = 1000 * 60 * 60 * 60 * 24;

        if((Date.now() - user.expires) > expiresIn) {
            await user.remove();
            return res.redirect('/');
        }

        user.confirmed = true;
        await user.save();

        req.session.user = user;

        return res.redirect('/dashboard');

    } catch(err) {
        res.sendStatus(404);
    }
});

As you can see, this is a relatively simple procedure. If you want to refine the implementation, you can use a specific module for generating the random token.