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>