Nodejs RESTful API Production

Date:

5 Oct 2019

Progress

  • Set Email and Password:
    • If Email exists, this is an reset password action, verification email will be sent

    • Otherwise this is and registration action, verification email will be sent

  • Handle request
    • For every request, Email and Password will be verified

    • If failed, the identity is treated as “public”

    • User can create data

    • User can find data by its ID and only modify the part of content that belongs to them

  • Database Model

data = {
    _id: assigned automatically by database,
    creater: creater email (unique),
    contents: [
        {
            owner: owner 1 email (unique),
            content: owner 1 content,
        },
        {
            owner: owner 2 email (unique),
            content: owner 2 content,
        },
        ...
    ],
}

数据 = {
    _id:  database 自动分配,
    创建者: 创建者邮箱(唯一),
    所有内容: [
        {
            所有者: 所有者 1 邮箱(唯一),
            内容: 所有者 1 内容,
        },
        {
            所有者: 所有者 2 邮箱(唯一),
            内容: 所有者 2 内容,
        },
        ...
    ],
}

Database Model: possible improvements

createData(account,req,res){
    var self = this
    var query = req.body.createData
    var data = new self.DataModel()
    data.creater = account
    data.content = query.content
    data.save()
    console.log("\nNew Data:\n", data)
    query.parents.forEach((parentId)=>{
        self.DataModel.findById(parentId,(err,parent)=>{
            if(parent==null){
                console.log("\nparentId not found\n")
            }
            else{
                parent.content.children.push(data._id)
            }
        })
    })
}

updateData(account,req,res){
    var self = this
    var query = req.body.updateData
    self.DataModel.findById(query.id,(err,data)=>{
        if(data == null){
            console.log("\nupdateDataId not found\n")
        }
        else{
            if(data.creater==account) data.content = query.content
        }
    })
}

//============ Model ============
    var DataSchema = new self.mongoose.Schema({
        creater: String,
        content: self.mongoose.Schema.Types.Mixed,
    })
    DataSchema.index({
        content: 'text',
    })
    self.DataModel = self.mongoose.model('DataModel', DataSchema)

To Do

  • Compare the above Access Control model with Role based access control

  • Figure out what are Hash and Salt and whether I need it if every request requires Email and Password

  • Split the my_modules/SimpleAPI.js into more modules, it has got too long !

Backend Main

  • To use:

sudo service mongod start
node index.js
  • index.js

const app = require('express')()
app.get('/',(req, res)=>{
    res.sendFile(__dirname+'/index.html')
})

const SimpleAPI = require("./my_modules/SimpleAPI.js")
const simpleAPI = new SimpleAPI(
    hostIP = 'localhost',
    port = 3000,
    app,
    appName = "SimpleAPI",
    myAddress = "dingruiqi97m@gmail.com",
    myPassword = ""
)

Backend Module

  • my_modules/SimpleAPI.js

  • Dependencies
    • express // Of course

    • ./SimpleGmail.js // To send verification email

    • crypto // To generate secretCode for verification

    • ejs // To use html templates

    • mongoose (MongoDB) // Database

module.exports = class SimpleAPI{
    constructor(hostIP, port, app, appName, myAddress, myPassword){
        var self = this
        self.appName = appName
        // express server
        const jsonParser = require('body-parser').json()

        app.get('/SimpleAPI/setAccountVerify',(req, res)=>{
            self.verifyEmail(req, res)
        })
        app.post('/SimpleAPI',jsonParser,(req,res)=>{

            if(req.body.type == "setAccount"){
                self.setAccount(req,res)
            }
            else if(req.body.type == "listThenDeleteAll"){
                self.listThenDeleteAll()
            }
            else self.verifyAccount(req,res,(account)=>{
                if(req.body.type=="saveData"){
                    self.saveData(account,req,res)
                }
                else if(req.body.type=="updateData"){
                    self.updateData(account,req,res)
                }
            })
        })
        self.host = "http://"+hostIP+":"+port
        app.listen(port, hostIP, () => console.log(self.host))

        // mongodb mongoose
        self.mongoose = require('mongoose')
        self.mongoose.connect('mongodb://localhost/SimpleAPI',{
            useNewUrlParser: true,
            useFindAndModify: false,
            useCreateIndex: true,
            useUnifiedTopology: true
        })  //Fix Deprecation Warnings: https://mongoosejs.com/docs/deprecations.html
        self.db = self.mongoose.connection
        self.db.on('error',console.error.bind(console,'db err:'))
        self.defineSchemasAndModels()

        // SimpleGmail
        const SimpleGmail = require("./SimpleGmail.js")
        self.simpleGmail = new SimpleGmail(myAddress, myPassword)
    }

    setAccount(req,res){
        var self = this
        var secretCode = require('crypto').randomBytes(16).toString('hex')

        var query = req.body.setAccount
        self.AccountModel.findOneAndUpdate(
            {type: query.type, email: query.email},
            {state: "ToBeVerified"+secretCode},
            {new: true},
            (err, modifiedAccount)=>{
                if(modifiedAccount==null){
                    var account = new self.AccountModel({
                        type: query.type, email: query.email,
                        state: "ToBeVerified"+secretCode
                    })
                    account.save()
                    console.log("\nNew Account:\n", account)
                }
                else console.log("\nModified Account:\n", modifiedAccount)
            }
        )

        // send user email for verification
        var email = query.email
        var subject = self.appName+": Set Account Verification"

        var htmlPromise = require('ejs').renderFile(__dirname+'/SetAccountVerification.ejs',{
            subject: subject,
            host: self.host,
            setAccount: query,
            state: "ToBeVerified"+secretCode,
        })
        htmlPromise.then( (html)=>{
            self.simpleGmail.send(email, subject, html)
            res.send(html) // For Testing
        })
    }

    verifyEmail(req, res){
        var self = this
        var query = req.query
        self.AccountModel.findOneAndUpdate({
                type: query.type, email: query.email,
                state: query.state
            },
            {password: query.password, state:"Verified"},
            (err,doc)=>{
                if(doc==null) res.send("Error: This is not the latest link!")
                else res.json(doc)
            }
        )
    }

    verifyAccount(req,res, callback){
        var self = this
        var query = req.body.verifyAccount
        self.AccountModel.findOne(
            {   type: query.type, email: query.email,
                password : query.password},
            (err, account)=>{
                if(account==null){
                    console.log("public")
                    callback("public")
                }
                else{
                    console.log(query.email)
                    callback(query.email)
                }
            }
        )
    }

    saveData(account, req, res){
        var self = this
        var query = req.body.saveData
        var data = new self.DataModel()
        data.creater = account
        data.contents.push({owner:account, content:query.content})
        data.save()
        console.log("\nNew Data:\n", data)
    }

    updateData(account, req, res){
        var self = this
        var query = req.body.updateData
        self.DataModel.findById(query.id,(err, data)=>{
            if(data == null){
                console.log("\nId not found\n")
            }
            else{
                var index = data.contents.findIndex((content)=>{
                    return content.owner==account
                })
                if(index>=0){
                    data.contents[index].content = query.content
                    console.log("\nUpdate Data:\n", data)
                }
                else{
                    data.contents.push({owner:account, content:query.content})
                    console.log("\nUpdate New Data:\n", data)
                }
            }
        })
    }

    defineSchemasAndModels(){
        // https://stackoverflow.com/questions/28775051/best-way-to-perform-a-full-text-search-in-mongodb-and-mongoose
        var self = this
        var AccountSchema = new self.mongoose.Schema({
            // for security
            type: String,
            email: String,
            password: String,
            state: String,
            content: String,
        })
        AccountSchema.index({
            type: 'text',
            content: 'text',
        })
        self.AccountModel = self.mongoose.model('AccountModel', AccountSchema)

        var DataSchema = new self.mongoose.Schema({
            creater: String,
            contents: [self.mongoose.Schema.Types.Mixed],
        })
        DataSchema.index({
            contents: 'text',
        })
        self.DataModel = self.mongoose.model('DataModel', DataSchema)
    }

    listThenDeleteAll(){
        var self = this
        self.AccountModel.find((err,accounts)=>{
            console.log("\nCheck Accounts:\n", accounts)
            self.AccountModel.deleteMany((err,report)=>{
                console.log("\nDelete Accounts:\n", report)
            })
        })
        self.DataModel.find((err,data)=>{
            console.log("\nCheck Data:\n", data)
            self.DataModel.deleteMany((err,report)=>{
                console.log("\nDelete Data:\n", report)
            })
        })
    }
}

Backend Email Verification Template

  • my_modules/SetAccountVerification.ejs

<h><%= subject %></h>

<p>setAccount: <%= JSON.stringify(setAccount) %></p>

<button>
    <a href="
        <%= host+'/SimpleAPI/setAccountVerify?'
            +'type='        + setAccount.type
            +'&email='      + setAccount.email
            +'&password='   + setAccount.password
            +'&state='      + state
        %>"
    >Confirm</a>
</button>

Frontend For Testing

  • index.html

  • The following code is ugly since it is only for testing!

<h3>setAccount</h3>
<h4>click again to reset password</h4>
<button onclick="setAccount()">setAccount</button>
<h4>email will look like this</h4>
<div id="setAccount"></div>

<h3>To verify your account</h3>
<button onclick="verifyAccount()">verifyAccount</button>
<button onclick="verifyAccountFail()">verifyAccountFail</button>

<h3>Create a new data</h3>
<button onclick="saveData()">saveData</button>

<h3>To update a data, type its id here</h3>
<input type="text" id="dataId"></input>
<button onclick="updateData()">updateData</button>

<h3>To list Then clear database for testing</h3>
<button onclick="listThenDeleteAll()">listThenDeleteAll</button>

<script>
    var xhttp = new XMLHttpRequest();
      xhttp.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
              document.getElementById("setAccount").innerHTML = this.responseText;
        }
      };

      function send(reqbody){
          xhttp.open("POST", "http://localhost:3000/SimpleAPI", true);
          xhttp.setRequestHeader("Content-type", "application/json");
        xhttp.send(JSON.stringify(reqbody));
      }

      const account = {
        type: "buyer",
        email: "416640656@qq.com",
        password: "qwerty",
    }

      function setAccount(){
          send({
            type:"setAccount",
            setAccount: account
         })
      }

      function verifyAccount(){
          send({
            type:"verifyAccount",
            verifyAccount: account
          })
      }

    function verifyAccountFail(){
        send({
            type:"verifyAccount",
            verifyAccount: {
                type: "buyer",
                email: "416640656@qq.com",
                password: "wrong!",
            }
        })
    }

      function saveData(){
          send({
            type:"saveData",
            verifyAccount: account,
            saveData:{
                content:"This is test content"
            }
          })
      }

      function updateData(){
          var id = document.getElementById("dataId").value
          send({
            type:"updateData",
            verifyAccount: account,
            updateData: {
                id: id,
                content:"This is updated test content"
            }
          })
      }

      function listThenDeleteAll(){
        send({
            type:"listThenDeleteAll"
          })
      }
</script>