19.5. MongoDB Dynamic Pages with Node.js

Example 19.7. Dynamic Routes Router, nodeMyGPNode/nativeskeletonMongo/routes/router.js
"use strict";
/*
 * check if routed handler function exists
 * if yes call it, else complain
 */
const handlers = require("../controllers/handlers");  // handlers module
const requestHandlers = {                             // application urls here
    GET: {
        "/": handlers.home,
        "/cities": handlers.cities,
        "/about": handlers.other,
        "/contact": handlers.other,
        "/contacts": handlers.contacts,
        "/login": handlers.login,
        "/logout": handlers.logout,
        "/notfound": handlers.notfound,
        "js": handlers.js,
        "css": handlers.css,
        "png": handlers.png,
        "svg": handlers.svg,
        "ico": handlers.ico
    },
    POST: {
        "/contact": handlers.receiveContacts,
        "/login": handlers.verifyLogin
    }
}

module.exports = {
    route(req, res, bodydata) {
        let urls = req.url.split("?");              // separate query string from url
        req.url = urls[0];                          // clean url
        let arr = req.url.split(".");               // filename with suffix
        let ext = arr[arr.length - 1];              // get suffix
        if (typeof requestHandlers[req.method][req.url] === 'function') {       // look for route
            requestHandlers[req.method][req.url](req, res, bodydata);           // if found, call
        } else if (typeof requestHandlers[req.method][ext] === "function") {    // if css, js, png, or
            requestHandlers[req.method][ext](req, res);                         // get that
        } else {                                                                // else
            requestHandlers.GET["/notfound"](req, res);                         // use notfound
        }
    }
}

Example 19.8. The Handlers, nodeMyGPNode/nativeskeletonMongo/controllers/handlers.js
'use strict';
/*
 * handlers.js
 * Requesthandlers to be called by the router mechanism
 */
const bcrypt = require('bcryptjs');                         // hashing sw
const fs = require("fs");                                   // file system access
const httpStatus = require("http-status-codes");            // http sc

const cookie = require("../controllers/sess");              // session cookies
const lib = require("../controllers/libWebUtil");           // home grown utilities
const nmlPlate = require("../controllers/myTemplater");     // home grown templater
const models = require("../models/inputOutput");            // models are datahandlers

const isLoggedIn = async function (req, res) {
    let name = cookie.get(req, res);                        // read cookie
    if (name) {                                             // logged in?
        cookie.set(req, res, name);                         // if logged in, extend
        return name;
    } else {
        return false;
    }
};

const getAndServe = async function (res, path, contentType) {   // asynchronous
    let obj;
    let args = [...arguments];                              // arguments to array
    let myargs = args.slice(3);                             // dump first three
                                                            // if more they are
                                                            // data for template

    await fs.readFile(path, function(err, data) {           // awaits async read
        if (err) {
            console.log(`Not found file: ${path}`);
        } else {
            res.writeHead(httpStatus.OK, {                  // yes, write header
                "Content-Type": contentType
            });
                                                            // call templater
            while( typeof (obj = myargs.shift()) !== 'undefined' ) {
                if (obj.cities)
                    data = nmlPlate.doMoreMagic(data, obj);  // inject var data to html
                else
                    data = nmlPlate.doTheMagic(data, obj);   // inject var data to html
            }
            res.write(data);
            res.end();
        }
    });
};

module.exports = {
    async home(req, res) {
        let name = await isLoggedIn(req, res);
        if (!name)
            name = '';
        let path = "views/index.html";
        let content = "text/html; charset=utf-8";
        getAndServe(res, path, content, {welcome: name});
    },
    login(req, res) {
        let path = "views/login.html";
        let content = "text/html; charset=utf-8";
        getAndServe(res, path, content, {msg: 'Login required'});
    },
    other(req, res) {
        let path = "views" + req.url + ".html";
        let content = "text/html; charset=utf-8";
        getAndServe(res, path, content);
    },
    js(req, res) {
        let path = "public/javascripts" + req.url;
        let content = "application/javascript; charset=utf-8";
        getAndServe(res, path, content);
    },
    css(req, res) {
        let path = "public/stylesheets" + req.url;
        let content = "text/css; charset=utf-8";
        getAndServe(res, path, content);
    },
    png(req, res) {
        let path = "public/images" + req.url;
        let content = "image/png";
        getAndServe(res, path, content);
    },
    svg(req, res) {
        let path = "public" + req.url;
        let content = "image/svg+xml";
        getAndServe(res, path, content);
    },
    ico(req, res) {
        let path ="public" + req.url;
        let content = "image/x-icon";
        getAndServe(res, path, content);
    },

    notfound(req, res) {
        console.log(`Handler 'notfound' was called for route ${req.url}`);
        res.end();
    },

    async cities(req, res) {
        let r = await models.showCities(req, res);
        let content = "text/html; charset=utf-8";
        let path = "views/displayCities.html";
        getAndServe(res, path, content, {cities: r, a: 'right aside', b: 'left aside'}); // extra arg for templater
    },

    async contacts(req, res) {
        if (! await isLoggedIn(req, res)) {
            res.writeHead(httpStatus.MOVED_TEMPORARILY, {   // write header
                "Location": '/login'
            });
            res.end();
        }
        let r = await models.showContacts(req, res);
        let content = "text/html; charset=utf-8";
        let path = "views/displayContacts.html";
        getAndServe(res, path, content, {contacts: r, a: 'right aside', b: 'left aside'}); // extra arg for templater
    },

    async receiveContacts(req, res, data) {
        let obj = lib.makeWebArrays(req, data);         // home made GET and POST objects
        await models.updContacts(obj);
        res.writeHead(httpStatus.MOVED_TEMPORARILY, {   // write header
            "Location": '/'
        });
        res.end();
    },

    async verifyLogin (req, res, data) {
        let obj = lib.makeWebArrays(req, data);         // home made GET and POST objects
        let r = await models.verify(obj);
        if (r.length == 1 && await bcrypt.compare(obj.POST.password, ''+r[0].password)) {
            cookie.set(req, res, '' + r[0].name);           // create login cookie
            res.writeHead(httpStatus.MOVED_TEMPORARILY, {   // write header
                "Location": '/'
            });
            res.end();
        } else {
            res.writeHead(httpStatus.MOVED_TEMPORARILY, {   // write header
                "Location": '/logout'
            });
            res.end();
        }
    },

    async logout (req, res) {
        cookie.unset(req, res);                         // unset login cookie
        res.writeHead(httpStatus.MOVED_TEMPORARILY, {   // write header
            "Location": '/'
        });
        res.end();
    }
}

Example 19.9. The Model, nodeMyGPNode/nativeskeletonMongo/models/inputOutput.js
'use strict';
/*
 * models
 * functions for data manipulation
 */
const bcrypt = require('bcryptjs');
const dbmod = require('./dbMod.js');
const dbw = "world";
const dbc = "nodecontacts";

module.exports = {
    async updContacts(obj) {
        const db = await dbmod.connect(dbc);
        let hashed = await bcrypt.hash(obj.POST.password, 10);
        let o = {name: obj.POST.name, email: obj.POST.email, phone: obj.POST.phone, password: hashed};
        let key = {email: obj.POST.email};
        await db.collection('user').updateOne(key, {"$set": o}, {upsert: true});
        dbmod.close();
    },

    async showContacts () {
        const db = await dbmod.connect(dbc);
        const rows = await db.collection("user").find().toArray();
        dbmod.close();
        return rows;
    },

    async showCities () {
        const db = await dbmod.connect(dbw);
        const rows = await db.collection("city").find({countrycode: "DNK"}).toArray();
        dbmod.close();
        return rows;
    },

    async verify(obj) {
        const db = await dbmod.connect(dbc);
        let o = { email: obj.POST.email };
        const rows = await db.collection('user').find(o).toArray();
        return rows;
    }
}

Example 19.10. The Connector, nodeMyGPNode/nativeskeletonMongo/models/dbMod.js
'use strict';

const { MongoClient } = require('mongodb');

const url = 'mongodb://localhost:27017';
const client = new MongoClient(url);

module.exports = {
    connect: async function (dbname) {
        await client.connect();
        const db = client.db(dbname);
        return db;
    },
    close: async function() {
        client.close();
    }
}

Example 19.11. The Class, nodeMyGPNode/nativeskeletonMongo/models/City.js
'use strict';

module.exports = class City {
    constructor(name, countrycode, district, population) {
        this.name = name;
        this.countrycode = countrycode;
        this.population = population;
        this.district = district;
    }

    toString() {
        return `<tr><td>${this.name}</td>
<td>${this.countrycode}</td>
<td>${this.district}</td>
<td>${this.population}</td></tr>`;
    }
};

Example 19.12. Dynamic Content Page Template, nodeMyGPNode/nativeskeletonMongo/views/displayCities.html
<!doctype html>
<html>
    <head>
        <meta charset='utf-8'/>
        <title class='title'></title>
        <link rel='stylesheet' href='index.css'/>
        <link rel='icon' type='image/svg+xml' href='favicon.svg'/>
        <script type='module' src='contacts.js'></script>
    </head>
    <body>
        <header id='menu'></header>
        <main class='otherpage'>
            <aside><42 b 24></aside>
            <section>
                <h2>Cities</h2>
                <div class='nml42'><42 cities 24></div>
            </section>
            <aside><42 a 24></aside>
        </main>
        <footer>
            <article id="cpryear"></article>
        </footer>
    </body>
</html>

On your CLI do npm test to start the server. Then go to your browser and test the menu items. Check the browser screen as well as the console log in each case.