Chat App - basic login and registration

You can look at here to check your code against mine.

Note: Above GitHub URL has final code and might not look like exactly as described below. We will add more code as series progresses.

For brevity, I have removed CSS and error handling.

In this we should be able to do basic registration and login without maintaining any session.

Creating HTTP server

const express = require('express');
const app = express();
app.get('/', (req, res) => res.send('Server is working'));
app.listen(3000);

Go ahead and type node index.js in your command line or use nodeman. You can configure package.json to use npm start command. Go in your browser and check localhost:3000 to see if everything is working.

Express automatically create the HTTP server for you and start listening on the given port.

Setting up MongoDB

We need database to store credential and later on verify those credentials. You can install MongoDB or you can use docker. I used docker because I don't wanted to install software for one time purpose. You may want to install docker-compose to make your life easier.

Side Note: docker is basically like a lightweight virtual machine (not really). For our purpose this is good enough.

Once installed, create a docker-compose.yml file in your chat application root directory like here. Copy the following

version: '3.7'
services:
  mongodb_container:
    image: mongo:latest
    ports:
      - 27017:27017
    volumes:
      - mongodb_data_container:/data/db

volumes:
  mongodb_data_container:

Notice: we created mongodb_data_container volume to persist data. By default docker does not persist data.

In your command line fire docker-compose up -d. It may take some time to download and running the MongoDB. It will only happen initially and after that docker is going to use local cache and it will be a breeze.

Connecting Node.js to MongoDB

For this I am using mongoose framework to make our life simpler.

const moongose = require('mongoose');
// if app db will not exist in our MongoDB, it will automatically create it when we write it first time to the db.
moongose.connect("mongodb://localhost:27017/app", {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    useCreateIndex: true
  })
  .then(res => console.log('connected to db'))
  .catch(error => console.error(error));

exports.db = moongose.connection

Whenever we write require('mongoose') in different file, it is going to use same connection.

Side Note: You may not want to catch error here. If connection fail, we may want to crash our app here.

I skipped password protection for MongoDB here. You may want to set that up.

Setting up User collection schema

In User collection, we are going to store username and password documents for each user.

// file: models/user.js
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
    username: {
        type: String,
        unique: true,
        require: true,
        trim: true
    },
    password: {
        type: String,
        required: true
    }
});

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

Creating login/registration page

To save time, I used same page for registration and login. Create a login.html file.

<html>
    <head>
        <script async src="/static/js/login.js"></script>
    </head>
    <body>
        <form>
            <input type="text" name="username" id="username"/>
            <input type="password" name="password" id="password"/>
            <input id="login" type="button" value="Login"/>
            <input id="register" type="button" value="Register"/>
        </form>
    </body>
</html>
//login.js
const register = document.getElementById('register');
const login = document.getElementById('login');

function sendRequest(url) {
    const form = document.querySelector('form');
    const formData = new FormData(form);
    return fetch(url, {
        method: 'POST',
        headers: {
        'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            username: formData.get('username'),
            password: formData.get('password')
        })
    })
    .then(res => res.json())
    .catch((error) => {
        console.error('Error:', error);
    });
}

register.addEventListener('click', () => {
    sendRequest('/register').then(({ registered }) => {
        if (registered) window.location.href = '/chat';
    });;
});

login.addEventListener('click', () => {
    sendRequest('/login').then(({ authenticated }) => {
        if (authenticated) window.location.href = '/chat';
    });
});

Reason for sending the data using fetch API instead of submission because this way I can have separate endpoints for login and registration.

On server-side create two endpoints:

// For brevity, I removed validation middleware which basically checks if name or password field are not set.
app.post('/login', authenticateUser);
app.post('/register', registerUser);
// In original code, I used then instead of async and await.
exports.registerUser = async (req, res, next) => {
    const userData = {
        username: req.body.username,
        password: req.body.password
    }

    try {
        await User.create(userData);
        res.json({
            registered: true
        });
    } catch(err => {
        // error handling
    });
};

To serve static file such as /static/js/login.js we are going to use express.static controller. This way we do not have to write app.get for each file.

app.use('/static', express.static(path.join(__dirname, 'public')));

Basically we are saying, for /static URL use express.static and in express.static we are passing the directory to handle static content. So actual location of login.js is public/js/login.js.

At this point you should be able to register user. You can check the data in database.

Encrypting password

This is good but we are storing password as text. This is problematic if some get access to our data dump. To encrypt password we are going to use bcrypt library. We are going to add following code in the User schema file.

// file: models/user.js
const bcrypt = require('bcrypt');

UserSchema.pre('save', function(next) {
  const user = this;
  bcrypt.hash(user.password, 10)
    .then(hash => {
      user.password = hash;
      next();
    })
    .catch(e => next(e));
});

pre is a mongoose middleware defined on the collection schema. Here, we are saying before saving run bcrypt hash method and replace user.password with its hash value. So in User collection, hash value is getting stored.

10 in bcrypt.hash is a saltsRound. Higher the number, more time will it take to brute force the data. All the relevant information is attached to the encrypted hash.

Try to register the user again and check password field in the database. You will now see an encrypted hash.

Login User

For login, we need a mechanism to compare plain text password against encrypted password. For this we will create a static method on the User model and use bcrypt.compare for comparison.

// using promise here. In the code I used callback approach.
UserSchema.statics.authenticate = async function(username, password) {
    const user = await User.findOne({ username }).exec();
    if (!user) return false;
    return await bcrypt.compare(password, user.password);
}

Remember we used authenticateUser for post login request.

//modified code from the original because now I am using promises.
exports.authenticateUser = async (req, res) => {
    const result = await User.authenticate(req.body.username, req.body.password);
    const status = result ? 200 : 401;
    res.status(status).json({
        authenticated: result
    });
};

You can check the status in browser network tool.

Issue: For each privileged request client has to send the password because we are not maintaining any session info.