MongoDB Multiple Collections

The full project code for the example code listed on this webpage can be downloaded from this link.

We shall create a second collection, called "users". This collection will hold a user's name, email, password and access level.

The file server/routes/users.js shows how registration and login works on the server-side. Here, we add a property called "user" to the server-side session variable.

The file client/src/components/Login showns that whenever a user logs in, the user's name and access level are sent back to the client. This is then stored in the sessionStorage, so that it is available on the client-side.

Client-Side

client/src/components/Register.js

import React, {Component} from "react"
import {Redirect, Link} from "react-router-dom"
import axios from "axios"

import LinkInClass from "../components/LinkInClass"

import {SERVER_HOST} from "../config/global_constants"


export default class Register extends Component
{
    constructor(props)
    {
        super(props)
        
        this.state = {
            name:"",
            email:"",
            password:"",
            confirmPassword:"",    
            isRegistered:false
        } 
    }
    
    
    handleChange = (e) => 
    {
        this.setState({[e.target.name]: e.target.value})
    }
    
    
    handleSubmit = (e) => 
    {
        e.preventDefault()

        axios.post(`${SERVER_HOST}/register/${this.state.name}/${this.state.email}/${this.state.password}`)
        .then(res => 
        {     
            if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else // user successfully registered
                { 
                    console.log("User registered")                    
                    
                    this.setState({isRegistered:true})
                }        
            }
            else
            {
                console.log("Registration failed")
            }
        })   
    }


    render() 
    {     
        return (
            <form className="form-container" noValidate = {true} id = "loginOrRegistrationForm">
           
                {this.state.isRegistered ? <Redirect to="/DisplayAllCars"/> : null} 
            
                <h2>New User Registration</h2>
           
                <input  
                    name = "name"              
                    type = "text"
                    placeholder = "Name"
                    autoComplete="name"
                    value = {this.state.name}
                    onChange = {this.handleChange}
                    ref = {(input) => { this.inputToFocus = input }} 
                /><br/>           

	        <input  
                    name = "email"              
                    type = "email"
                    placeholder = "Email"
                    autoComplete="email"
                    value = {this.state.email}
                    onChange = {this.handleChange}
                /><br/>              

	        <input  
                    name = "password"           
                    type = "password"
                    placeholder = "Password"
                    autoComplete="password"
                    title = "Password must be at least ten-digits long and contains at least one lowercase letter, one uppercase letter, one digit and one of the following characters (£!#€$%^&*)"
                    value = {this.state.password}
                    onChange = {this.handleChange}
                /><br/>           

                <input          
                    name = "confirmPassword"    
                    type = "password"
                    placeholder = "Confirm password"
                    autoComplete="confirmPassword"
                    value = {this.state.confirmPassword}
                    onChange = {this.handleChange}
                /><br/><br/>
                
                <LinkInClass value="Register New User" className="green-button" onClick={this.handleSubmit} />
                <Link className="red-button" to={"/DisplayAllCars"}>Cancel</Link>   
            </form>
        )
    }
}

When the flag this.state.isRegistered is set to true, we redirect to the DisplayAllCars component.

When the user submits the form, the name, email and password are passed to the server-side router, where they will be added to the database.

    handleSubmit = (e) => 
    {
        e.preventDefault()

        axios.post(`${SERVER_HOST}/register/${this.state.name}/${this.state.email}/${this.state.password}`)
        .then(res => 
        {     
            if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else // user successfully registered
                { 
                    console.log("User registered")                    
                    
                    this.setState({isRegistered:true})
                }        
            }
            else
            {
                console.log("Registration failed")
            }
        })   
    }

client/src/components/ResetDatabase.js

import React, {Component} from "react"
import {Redirect, Link} from "react-router-dom"
import axios from "axios"

import LinkInClass from "../components/LinkInClass"
import {SERVER_HOST} from "../config/global_constants"


export default class ResetDatabase extends Component
{
    constructor(props)
    {
        super(props)
        
        this.state = {   
            isReset:false
        } 
    }
    
    
    handleChange = (e) => 
    {
        this.setState({[e.target.name]: e.target.value})
    }
    

    resetUsersModel = () =>
    {
        axios.post(`${SERVER_HOST}/reset_user_collection`)
        .then(res => 
        {     
            if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else // user successfully reset the User collection
                { 
                    console.log("User collection reset")
                }        
            }
            else
            {
                console.log("Failed to reset User collection")
            }
            
            this.setState({isReset:true})
        })   
    }



    render() 
    { 
        return (
            <form className="form-container" noValidate = {true} id = "loginOrRegistrationForm">

               {this.state.isReset ? <Redirect to="/DisplayAllCars"/> : null} 

                <p>"Reset User Database" is only for testing purposes.<br/>All code on the client-side and server-side relating to resetting the database should be removed from any development release</p>
                <LinkInClass value="Reset User Database" className="red-button" onClick={this.resetUsersModel}/> <br/><br/>
                <p>Reset the database and set up an administrator with:<br/> * email <strong>admin@admin.com</strong><br/> * password <strong>123!"£qweQWE</strong></p>        
            
                <Link className="red-button" to={"/DisplayAllCars"}>Cancel</Link>
            </form>
        )
    }
}

When the this.state.isReset flag is set to true, we rediect to the DisplayAllCars component.

When the user submits the form, the axios() method calls the server-side reset_user_collection router.

    resetUsersModel = () =>
    {
        axios.post(`${SERVER_HOST}/reset_user_collection`)
        .then(res => 
        {     
            if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else // user successfully reset the User collection
                { 
                    console.log("User collection reset")
                }        
            }
            else
            {
                console.log("Failed to reset User collection")
            }
            
            this.setState({isReset:true})
        })   
    }  

client/src/components/DisplayAllCars.js

import React, {Component} from "react"
import {Link} from "react-router-dom"

import axios from "axios"

import CarTable from "./CarTable"

import {SERVER_HOST} from "../config/global_constants"


export default class DisplayAllCars extends Component 
{
    constructor(props) 
    {
        super(props)
        
        this.state = {
            cars:[]
        }
    }
    
    
    componentDidMount() 
    {
        axios.get(`${SERVER_HOST}/cars/`)
        .then(res => 
        {
            if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else
                {           
                    console.log("Records read")   
                    this.setState({cars: res.data}) 
                }   
            }
            else
            {
                console.log("Record not found")
            }
        })
    }

  
    render() 
    {   
        return (           
            <div className="form-container">
                <div>
                    <Link className="blue-button" to={"/Register"}>Register</Link>  
                    <Link className="red-button" to={"/ResetDatabase"}>Reset Database</Link>  <br/><br/><br/>
                </div>
                
                <div className="table-container">
                    <CarTable cars={this.state.cars} /> 

                    <div className="add-new-car">
                        <Link className="blue-button" to={"/AddCar"}>Add New Car</Link>
                    </div>
                </div>
            </div> 
        )
    }
}

The render() method now includes the Register and Reset Database buttons.

    render() 
    {   
        return (           
            <div className="form-container">
                <div>
                    <Link className="blue-button" to={"/Register"}>Register</Link>  
                    <Link className="red-button" to={"/ResetDatabase"}>Reset Database</Link>  <br/><br/><br/>
                </div>
                
                <div className="table-container">
                    <CarTable cars={this.state.cars} /> 

                    <div className="add-new-car">
                        <Link className="blue-button" to={"/AddCar"}>Add New Car</Link>
                    </div>
                </div>
            </div> 
        )
    }

Server-Side

server/config/.env

# This file holds global constants that are visible on the Server-side

# Database
DB_NAME = D01234567


# Access Levels
ACCESS_LEVEL_GUEST = 0
ACCESS_LEVEL_NORMAL_USER = 1
ACCESS_LEVEL_ADMIN = 2


# Salt length of encryption of user passwords
# The salt length should be 16 or higher for commercially released code
# It has been set to 3 here, so that the password will be generated faster
PASSWORD_HASH_SALT = 3


# Port
SERVER_PORT = 4000


# Local Host
LOCAL_HOST = http://localhost:3000

In our "Cars" example, users will be allowed to be a guest, a normal user or an administrator. We need to set up server-side global variables for these.

# Access Levels
ACCESS_LEVEL_GUEST = 0
ACCESS_LEVEL_NORMAL_USER = 1
ACCESS_LEVEL_ADMIN = 2

server/models/users.js

const mongoose = require(`mongoose`)

let usersSchema = new mongoose.Schema(
   {
        name: {type: String, required:true},
        email: {type: String, required:true},
        password: {type: String,required:true},        
        accessLevel: {type: Number, default:parseInt(process.env.ACCESS_LEVEL_NORMAL_USER)}
   },
   {
       collection: `users`
   })

module.exports = mongoose.model(`users`, usersSchema)

The usersSchema is created in the same way that the carsSchema was previously created in the notes.

Note that, when new users are added, if no accessLevel is provided, then the accessLevel will default to process.env.ACCESS_LEVEL_NORMAL_USER

server/server.js

// Server-side global variables
require(`dotenv`).config({path:`./config/.env`})


// Database
require(`./config/db`)


// Express
const express = require(`express`)
const app = express()

app.use(require(`body-parser`).json())
app.use(require(`cors`)({credentials: true, origin: process.env.LOCAL_HOST}))


// Routers
app.use(require(`../server/routes/cars`))
app.use(require(`../server/routes/users`))


// Port
app.listen(process.env.SERVER_PORT, () => 
{
    console.log(`Connected to port ` + process.env.SERVER_PORT)
})


// Error 404
app.use((req, res, next) => {next(createError(404))})

// Other errors
app.use(function (err, req, res, next)
{
    console.error(err.message)
    if (!err.statusCode) 
    {
        err.statusCode = 500
    }
    res.status(err.statusCode).send(err.message)
})

The users route needs to be included along with the previously developed cars route in the server.js file.

server/routes/cars.js

const router = require(`express`).Router()

const usersModel = require(`../models/users`)

const bcrypt = require('bcrypt');  // needed for password encryption


// IMPORTANT
// Obviously, in a production release, you should never have the code below, as it allows a user to delete a database collection
// The code below is for development testing purposes only 
router.post(`/reset_user_collection`, (req,res) => 
{
    usersModel.deleteMany({}, (error, data) => 
    {
        if(data)
        {
            const adminPassword = `123!"£qweQWE`
            bcrypt.hash(adminPassword, parseInt(process.env.PASSWORD_HASH_SALT), (err, hash) =>  
            {
                usersModel.create({name:"Administrator",email:"admin@admin.com",password:hash,accessLevel:parseInt(process.env.ACCESS_LEVEL_ADMIN)}, (createError, createData) => 
                {
                    if(createData)
                    {
                        res.json(createData)
                    }
                    else
                    {
                        res.json({errorMessage:`Failed to create Admin user for testing purposes`})
                    }
                })
            })
        }
        else
        {
            res.json({errorMessage:`User is not logged in`})
        }
    })                
})


router.post(`/register/:name/:email/:password`, (req,res) => 
{
    // If a user with this email does not already exist, then create new user
    usersModel.findOne({email:req.params.email}, (uniqueError, uniqueData) => 
    {
        if(uniqueData)
        {
            res.json({errorMessage:`User already exists`})
        }
        else
        {
            bcrypt.hash(req.params.password, parseInt(process.env.PASSWORD_HASH_SALT), (err, hash) =>  
            {
                usersModel.create({name:req.params.name,email:req.params.email,password:hash}, (error, data) => 
                {
                    if(data)
                    {
                        res.json({name: data.name, accessLevel:data.accessLevel})                            
                    }
                    else
                    {
                        res.json({errorMessage:`User was not registered`})
                    }
                }) 
            })
        }
    })         
})


module.exports = router

We should encrypt the passwords that we are storing in the users collection. We use the bcrypt package to do this.

const bcrypt = require('bcrypt');  // needed for password encryption

Use the bcrypt.hash() method to encrypt a string.

bcrypt.hash(stringToEncrypt, HASH_SALT, (err, hash) => 
{
    // do something with the hash
})

When we reset the users collection, we set up an admin user who has the password `123!"£qweQWE`, and an process.env.ACCESS_LEVEL_ADMIN accessLevel.

router.post(`/reset_user_collection`, (req,res) => 
{
    usersModel.deleteMany({}, (error, data) => 
    {
        if(data)
        {
            const adminPassword = `123!"£qweQWE`
            bcrypt.hash(adminPassword, parseInt(process.env.PASSWORD_HASH_SALT), (err, hash) => 
            {
                usersModel.create({name:"Administrator",email:"admin@admin.com",password:hash,accessLevel:parseInt(process.env.ACCESS_LEVEL_ADMIN)}, (createError, createData) => 
                {
                    if(createData)
                    {
                        res.json(createData)
                    }
                    else
                    {
                        res.json({errorMessage:`Failed to create Admin user for testing purposes`})
                    }
                })
            })
        }
        else
        {
            res.json({errorMessage:`User is not logged in`})
        }
    })                
})

When we register a new user, we use the findOne() method to ensure that no user with the same email already exits in the collection.
If the email does not already exist, we add the new user to the collection. We do not need to provide an accessLevel for the new user, as it is automatically set to process.env.ACCESS_LEVEL_GUEST.

router.post(`/register/:name/:email/:password`, (req,res) => 
{
    // If a user with this email does not already exist, then create new user
    usersModel.findOne({email:req.params.email}, (uniqueError, uniqueData) => 
    {
        if(uniqueData)
        {
            res.json({errorMessage:`User already exists`})
        }
        else
        {
            bcrypt.hash(req.params.password, parseInt(process.env.PASSWORD_HASH_SALT), (err, hash) =>  
            {
                usersModel.create({name:req.params.name,email:req.params.email,password:hash}, (error, data) => 
                {
                    if(data)
                    {
                        res.json({name: data.name, accessLevel:data.accessLevel})                            
                    }
                    else
                    {
                        res.json({errorMessage:`User was not registered`})
                    }
                }) 
            })
        }
    })         
})

Install the bcrypt package.

 
<div align="center"><a href="../versionC/index.html" title="DKIT Lecture notes homepage for Derek O&#39; Reilly, Dundalk Institute of Technology (DKIT), Dundalk, County Louth, Ireland. Copyright Derek O&#39; Reilly, DKIT." target="_parent" style='font-size:0;color:white;background-color:white'>&nbsp;</a></div>