Node.js App with User Authentication and Docker Deployment

Introduction

Building a web application with Node.js allows for the creation of scalable and efficient platforms. This article will guide you through setting up a simple application with user authentication and a dashboard interface. We'll use MongoDB for the database, Express for the web framework, and EJS for templating. Additionally, we will containerize our application with Docker for easy deployment.

Prerequisites

Before you begin, ensure you have the following installed on your system:

  • Node.js and npm
  • Docker
  • Docker Compose
  • A text editor or IDE of your choice

Step 1. Setting Up Your Project

  1. Initialize a New Node.js Project

    • Open your terminal.
    • Create a new directory for your project and navigate into it:
      mkdir nodejs-app
      cd nodejs-app
      
    • Initialize a new Node.js project:
      npm init -y
      
  2. Install Dependencies

    • Install Express, EJS, Mongoose, and other necessary packages:

      npm install express ejs mongoose bcryptjs express-session passport passport-local
      

Step 2. Create the Application Structure

Create the necessary directories and files for your application:

nodejs-app/
│
├── app.js
├── Dockerfile
├── docker-compose.yml
├── package.json
├── routes/
│   ├── index.js
│   ├── user.js
├── config/
│   ├── passportConfig.js
├── views/
│   ├── login.ejs
│   ├── register.ejs
│   ├── dashboard.ejs
│   ├── layout.ejs
│   └── partials/
│       └── messages.ejs
├── models/
│   ├── User.js
└── public/
    ├── css/
        ├── style.css

Here is a shell script (create-nodejs-app.sh) to automatically create the directory structure for your Node.js application:

#!/bin/bash

# Define the folder structure
mkdir -p nodejs-app/{routes,config,views,models,public/css}

# Create the necessary files
touch nodejs-app/app.js
touch nodejs-app/Dockerfile
touch nodejs-app/docker-compose.yml
touch nodejs-app/package.json
touch nodejs-app/routes/index.js
touch nodejs-app/routes/user.js
touch nodejs-app/config/passportConfig.js
touch nodejs-app/views/login.ejs
touch nodejs-app/views/register.ejs
touch nodejs-app/views/dashboard.ejs
touch nodejs-app/views/layout.ejs
touch nodejs-app/views/partials/messages.ejs
touch nodejs-app/models/User.js
touch nodejs-app/public/css/style.css

# Confirmation message
echo "Node.js app folder structure created successfully!"

Step 3. Developing the Backend

  1. Set Up Express and Middleware

    • In app.js, Set up your Express application and middleware to handle requests.
      const express = require('express');
      const mongoose = require('mongoose');
      const session = require('express-session');
      const flash = require('connect-flash');
      const passport = require('passport');
      const app = express();
      const port = 3000;
      
      // Passport Config
      require('./config/passportConfig')(passport);
      
      // DB Config
      mongoose.connect('mongodb://mongo:27017/nodejs-app', { useNewUrlParser: true, useUnifiedTopology: true })
          .then(() => console.log('MongoDB Connected'))
          .catch(err => console.log(err));
      
      // Middleware
      app.use(express.urlencoded({ extended: false }));
      app.use(express.static('public'));
      
      // EJS
      app.set('view engine', 'ejs');
      
      // Express Session
      app.use(session({
          secret: 'secret',
          resave: true,
          saveUninitialized: true
      }));
      
      // Passport middleware
      app.use(passport.initialize());
      app.use(passport.session());
      
      // Connect Flash
      app.use(flash());
      
      // Global Variables
      app.use((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'));
      app.use('/users', require('./routes/user'));
      
      // Start Server
      app.listen(port, () => {
          console.log(`Server started on port ${port}`);
      });
      
    • Configure Passport for user authentication.
  2. Database Models

    • Use Mongoose to define models in models/User.js.
      const mongoose = require('mongoose');
      
      const UserSchema = new mongoose.Schema({
          name: {
              type: String,
              required: 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);
      
      module.exports = User;
      
  3. User Authentication

    • Implement registration and login functionality in routes/user.js.
      const express = require('express');
      const router = express.Router();
      const bcrypt = require('bcryptjs');
      const passport = require('passport');
      const User = require('../models/User');
      
      // Register Page
      router.get('/register', (req, res) => res.render('register'));
      
      // Register Handle
      router.post('/register', (req, res) => {
          const { name, email, password, password2 } = req.body;
          let errors = [];
      
          // Validation
          if (!name || !email || !password || !password2) {
              errors.push({ msg: 'Please fill in all fields' });
          }
      
          if (password !== password2) {
              errors.push({ msg: 'Passwords do not match' });
          }
      
          if (errors.length > 0) {
              res.render('register', { errors, name, email, password, password2 });
          } else {
              User.findOne({ email: email })
                  .then(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.genSalt(10, (err, salt) => {
                              bcrypt.hash(newUser.password, salt, (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));
                              });
                          });
                      }
                  });
          }
      });
      
      // Login Page
      router.get('/login', (req, res) => res.render('login'));
      
      // Login Handle
      router.post('/login', (req, res, next) => {
          passport.authenticate('local', {
              successRedirect: '/dashboard',
              failureRedirect: '/users/login',
              failureFlash: true
          })(req, res, next);
      });
      
      // Logout Handle
      router.get('/logout', (req, res) => {
          req.logout(() => {
              req.flash('success_msg', 'You are logged out');
              res.redirect('/users/login');
          });
      });
      
      module.exports = router;
      
    • Utilize Passport for handling authentication. config/passportConfig.js
      const LocalStrategy = require('passport-local').Strategy;
      const mongoose = require('mongoose');
      const bcrypt = require('bcryptjs');
      
      // Load User model
      const User = require('../models/User');
      
      module.exports = function (passport) {
          passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, done) => {
              User.findOne({ email: email })
                  .then(user => {
                      if (!user) {
                          return done(null, false, { message: 'That email is not registered' });
                      }
      
                      bcrypt.compare(password, user.password, (err, isMatch) => {
                          if (err) throw err;
                          if (isMatch) {
                              return done(null, user);
                          } else {
                              return done(null, false, { message: 'Password incorrect' });
                          }
                      });
                  })
                  .catch(err => console.log(err));
          }));
      
          passport.serializeUser((user, done) => {
              done(null, user.id);
          });
      
          passport.deserializeUser((id, done) => {
              User.findById(id, (err, user) => {
                  done(err, user);
              });
          });
      };
      
    • routes/user.js.
      const express = require('express');
      const router = express.Router();
      const { ensureAuthenticated } = require('../config/auth');
      
      // Welcome Page
      router.get('/', (req, res) => res.render('welcome'));
      
      // Dashboard Page
      router.get('/dashboard', ensureAuthenticated, (req, res) =>
          res.render('dashboard', {
              user: req.user
          })
      );
      
      module.exports = router;
      
    • config/auth.js
      module.exports = {
          ensureAuthenticated: function (req, res, next) {
              if (req.isAuthenticated()) {
                  return next();
              }
              req.flash('error_msg', 'Please log in to view this resource');
              res.redirect('/users/login');
          }
      };
      

Step 4. Creating the Frontend

  1. EJS Templates

    • Create views for registration, login, and the dashboard in the views/ directory.
    • views/register.ejs 
      <%- include('layout') %>
      <form action="/users/register" method="POST">
          <div class="form-group">
              <input type="text" name="name" class="form-control" placeholder="Name">
          </div>
          <div class="form-group">
              <input type="email" name="email" class="form-control" placeholder="Email">
          </div>
          <div class="form-group">
              <input type="password" name="password" class="form-control" placeholder="Password">
          </div>
          <div class="form-group">
              <input type="password" name="password2" class="form-control" placeholder="Confirm Password">
          </div>
          <button type="submit" class="btn btn-primary">Register</button>
      </form>
      
    • views/login.ejs.
      <%- include('layout') %>
      <form action="/users/login" method="POST">
          <div class="form-group">
              <input type="email" name="email" class="form-control" placeholder="Email">
          </div>
          <div class="form-group">
              <input type="password" name="password" class="form-control" placeholder="Password">
          </div>
          <button type="submit" class="btn btn-primary">Login</button>
      </form>
      
    • views/dashboard.ejs
      <%- include('layout') %>
      <h1>Welcome, <%= user.name %>!</h1>
      <a href="/users/logout" class="btn btn-danger">Logout</a>
      
    • views/layout.ejs
      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>Node.js App</title>
          <link rel="stylesheet" href="/css/style.css">
      </head>
      <body>
          <nav>
              <a href="/">Home</a>
              <a href="/users/login">Login</a>
              <a href="/users/register">Register</a>
          </nav>
          <div class="container">
              <%- include('partials/messages') %>
          </div>
      </body>
      </html>
      
  2. Create the partials Directory and messages.ejs

    • Inside your views directory, create a new folder called partials.
    • Inside this partials folder, create a file called messages.ejs.
    • Here is an example of the content for messages.ejs:

      <% if (typeof success_msg != 'undefined') { %>
          <div class="alert alert-success">
              <%= success_msg %>
          </div>
      <% } %>
      
      <% if (typeof error_msg != 'undefined') { %>
          <div class="alert alert-danger">
              <%= error_msg %>
          </div>
      <% } %>
      
      <% if (typeof errors != 'undefined' && errors.length > 0) { %>
          <div class="alert alert-danger">
              <ul>
              <% errors.forEach(function(error) { %>
                  <li><%= error.msg %></li>
              <% }) %>
              </ul>
          </div>
      <% } %>
      
  3. Static Files

    • Style your application using CSS in the public/css/style.css file.
      /* Reset some default browser styles */
      * {
          margin: 0;
          padding: 0;
          box-sizing: border-box;
      }
      
      body {
          font-family: Arial, sans-serif;
          background-color: #f4f4f4;
          color: #333;
          line-height: 1.6;
          margin: 0;
          padding: 0;
      }
      
      /* Container */
      .container {
          max-width: 1170px;
          margin: 0 auto;
          padding: 20px;
      }
      
      /* Navigation */
      nav {
          background: #333;
          color: #fff;
          padding: 15px;
          text-align: center;
      }
      
      nav a {
          color: #fff;
          text-decoration: none;
          margin: 0 10px;
      }
      
      nav a:hover {
          text-decoration: underline;
      }
      
      /* Form Styling */
      form {
          background: #fff;
          padding: 20px;
          margin-top: 20px;
          border-radius: 5px;
          box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
      }
      
      .form-group {
          margin-bottom: 15px;
      }
      
      .form-group input {
          width: 100%;
          padding: 10px;
          border: 1px solid #ccc;
          border-radius: 5px;
      }
      
      button {
          background: #333;
          color: #fff;
          border: 0;
          padding: 10px 15px;
          cursor: pointer;
          border-radius: 5px;
      }
      
      button:hover {
          background: #555;
      }
      
      /* Messages */
      .alert {
          padding: 10px;
          background-color: #f4f4f4;
          color: #333;
          margin-bottom: 20px;
          border: 1px solid #ccc;
      }
      
      .alert-success {
          background-color: #dff0d8;
          color: #3c763d;
      }
      
      .alert-error {
          background-color: #f2dede;
          color: #a94442;
      }
      
      /* Dashboard */
      h1 {
          font-size: 24px;
          margin-bottom: 20px;
      }
      
      .btn-danger {
          background-color: #e74c3c;
      }
      
      .btn-danger:hover {
          background-color: #c0392b;
      }
      

Step 5. Dockerizing the Application

  1. Dockerfile

    • Create a Dockerfile to specify the build process for your application.
      FROM node:16
      
      WORKDIR /app
      
      COPY package*.json ./
      
      RUN npm install
      
      COPY . .
      
      EXPOSE 3000
      
      CMD ["node", "app.js"]
      
  2. Docker Compose

    • Define services, including your Node.js app and MongoDB, in docker-compose.yml.
      version: '3'
      services:
        app:
          build: .
          ports:
            - "3000:3000"
          depends_on:
            - mongo
        mongo:
          image: mongo
          ports:
            - "27017:27017"
      

Step 6. Running Your Application

  • Build and Run with Docker Compose

    docker-compose up --build
    
  • Access the App: Go to http://localhost:3000 in your browser.


Similar Articles
Ezmata Technologies Pvt Ltd
You manage your core business, while we manage your Infrastructure through ITaaS.