Migraine Tracker – Create Backend

I have long suffered with migraines.  Currently I track my daily migraines on an Excel spreadsheet, but I am going to create my own app to track them using the MERN stack Node, Express, MongoDB and Mongoose on the server.  I will develop the client in React.

  1. npm init -y to create my package.json file
  2. create index.js – I import Express to create my server.  I also bring in CORS middlesware, which I will need later to handle requests from the client.  I also have Morgan for loggin HTTP requests, and body-parser for parsing JSON data.  The dotenv module is for reading the .env file.

 

require("dotenv").config(); // process.env._____
const express = require("express");
const app = express();
const cors = require("cors");
const morgan = require("morgan");
const bodyParser = require("body-parser");

const PORT = process.env.PORT || 3025;

app.get("/", function(req, res) {
  res.send("Home route");
});

app.listen(PORT, function() {
  console.log("connected on port", PORT);
});

AUTHENTICATIONS

Next I’ll handle authentications.  First I’ll create my mongoose model.  I’ll use bcrypt for hashing the password.

  1. I created my User model for mongoose
  2. auth handler
  3. auth middleware
  4. auth route
// MOGOOSE USER MODEL
const mongoose = require("mongoose");
const bycrypt = require("bcrypt");

const userSchema = new mongoose.Schema({
  email: {
    type: String,
    required: true,
    unique: true
  },
  username: {
    type: String,
    required: true,
    unique: true
  },
  password: {
    type: String,
    require: true
  },
  myHeadaches: [
    {
      type: mongoose.Schema.Types.ObjectId,
      ref: "MyHeadaches"
    }
  ]
});

userSchema.pre("save", async function(next) {
  try {
    if (!this.isModified("password")) {
      return next();
    }
    let hashedPassword = await bycrypt.hash(this.password, 10);
    this.password = hashedPassword;
    return next();
  } catch (err) {
    return next(err);
  }
});

userSchema.methods.comparePassword = async function(candidatePassword, next) {
  try {
    let isMatch = await bycrypt.compare(candidatePassword, this.password);
    return isMatch;
  } catch (err) {
    return next(err);
  }
};

const User = mongoose.model("User", userSchema);

module.exports = User;

For the Auth handler, I store the mongoose models in db, and load jsonwebtoken into jwt to hash a password for a new user, or to compare a password to the stored hash for an existing user.

// AUTH HANDLER
const db = require("../models");
const jwt = require("jsonwebtoken");

exports.signin = async function(req, res, next) {
  //find a user
  try {
    let user = await db.User.findOne({
      email: req.body.email
    });
    let { id, username } = user;
    let isMatch = await user.comparePassword(req.body.passowrd);
    if (isMatch) {
      let token = jwt.sign(
        {
          id,
          username
        },
        process.env.SECRET_KEY
      );
      return res.status(200).json({
        id,
        username,
        token
      });
    } else {
      return next({
        status: 400,
        message: "Invalid Email/Password."
      });
    }
  } catch (err) {
    return next({ status: 400, message: "Invalid Email/Password." });
  }
};

exports.signup = async function(req, res, next) {
  try {
    let user = await db.User.create(req.body);
    let { id, username } = user;
    let token = jwt.sign(
      {
        id,
        username
      },
      process.env.SECRET_KEY
    );
    return res.status(200).json({
      id,
      username,
      token
    });
  } catch (err) {
    if (err.code === 11000) {
      err.message = "Sorry, that username and/or email is taken.";
    }
    return next({
      status: 400,
      message: err.message
    });
  }
};

The Auth middleware loginRequired makes sure the user is logged in before they can input new data or access existing data.  I use “dotenv” to access the .env file for the SECRET_KEY.  I use jsonwebtoken to verify the user.

// AUTH MIDDLEWARE

require("dotenv").load();
const jwt = require("jsonwebtoken");

// make sure user is logged in
exports.loginRequired = function(req, res, next) {
  try {
    const token = req.headers.authorization.split(" ")[1];
    jwt.verify(token, process.env.SECRET_KEY, function(err, decoded) {
      if (decoded) {
        return next();
      } else {
        return next({
          status: 401,
          message: "Please login first."
        });
      }
    });
  } catch (err) {
    return next({
      status: 401,
      message: "Please login first"
    });
  }
};

// make sure we get the correct user - Authorization
exports.ensureCorrectUser = function(req, res, next) {
  try {
    const token = req.headers.authorization.split(" ")[1];
    jwt.verify(token, process.env.SECRET_KEY, function(err, decoded) {
      if (decoded && decoded.id === req.params.id) {
        return next();
      } else {
        return next({
          status: 401,
          message: "Unauthorized"
        });
      }
    });
  } catch (err) {
    return next({
      status: 401,
      message: "Unauthorized"
    });
  }
};

Below I create the Auth routes for signup and signin.

// AUTH ROUTE

const express = require("express");
const router = express.Router();
const { signup, signin } = require("../handlers/auth");

router.post("/signup", signup);
router.post("/signin", signin);

module.exports = router;

Next, I test that the signup route is working, which it seems to be.  If I login, it returns with a JSON web token, which is the behavior I’m expecting.

shimo@mvly-BHafner MINGW64 ~/OneDrive - brianhafner.com/Documents/Development/MigraineTracker
$ HTTP POST localhost:3025/api/auth/signup username=test password=test [email protected]
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Connection: keep-alive
Content-Length: 234
Content-Type: application/json; charset=utf-8
Date: Sun, 02 Sep 2018 19:37:18 GMT
ETag: W/"ea-3wrVTn6tT4WS7gQ5iwRAi2MkWjs"
X-Powered-By: Express

{
    "id": "5b8c3bee634c83252484666d",
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjViOGMzYmVlNjM0YzgzMjUyNDg0NjY2ZCIsInVzZXJuYW1lIjoidGVzdCIsImlhdCI6MTUzNTkxNzAzOH0.BhHCx9xURnczelA1IYyWxPkpwL9Zkwh3V5fFYLV1kHQ",
    "username": "test"
}

ADDING NEW HEADACHE RECORDS

  1. I created my myHeadaches model for mongoose
  2. myHeadaches handler
  3. headaches route
// MYHEADACHES MODEL
const mongoose = require("mongoose");
const User = require("./user");

const myHeadacheSchema = new mongoose.Schema(
  {
    date: {
      type: String,
      unique: true
    },
    painLevel: Number,
    comment: String,
    user: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "User"
    }
  },
  {
    timestamps: true
  }
);

myHeadacheSchema.pre("remove", async function(next) {
  try {
    let user = await User.findById(this.user);
    // remove the id of the headache entry from the list
    user.myHeadacheSchema.remove(this.id);
    //save the user
    await user.save();
    return next();
  } catch (err) {
    return next(err);
  }
});

const MyHeadaches = mongoose.model("MyHeadaches", myHeadacheSchema);

module.exports = MyHeadaches;

Handlers – In the myHeadaches handler, I create functions createHeadache, getHeadache, deleteHeadache, and getAllHeadaches to create, delete, and get a list of all headaches.

const db = require("../models");

exports.createHeadache = async function(req, res, next) {
  console.log("createHeadache called");
  console.log(
    "handlers/myCoins.js - createCoin req, res, next -- REQ -",
    req.body,
    "REQ.PARAMS",
    req.params,
    "RES",
    res.body
  );
  try {
    let headache = await db.MyHeadaches.create({
      date: req.body.date,
      painLevel: req.body.painLevel,
      comment: req.body.comment
    });
    console.log("myHeadaches/handlers - req.body/headache", headache);
    console.log("req.params.id", req.params.id);

    let foundUser = await db.User.findById(req.params.id);
    console.log("foundUser", foundUser);
    foundUser.myHeadaches.push(headache._id);
    await foundUser.save();
    let foundHeadache = await db.MyHeadaches.findById(headache._id).populate(
      "user",
      {
        username: true
      }
    );
    return res.status(200).json(foundHeadache);
  } catch (err) {
    return next(err);
  }
};

exports.getHeadache = async function(req, res, next) {
  try {
    let headache = await db.MyHeadaches.find(req.params.coin_id);
    return res.status(200).json(headache);
  } catch (err) {
    return next(err);
  }
};

exports.deleteHeadache = async function(req, res, next) {
  try {
    let foundMyHeadache = await db.MyHeadaches.findById(req.params.headache_id);
    await foundMyHeadache.remove();
    return res.status(200).json(foundMyHeadache);
  } catch (err) {
    return next(err);
  }
};

exports.getAllHeadaches = async function(req, res, next) {
  try {
    let allHeadaches = await db.MyHeadaches.find({});
    return res.status(200).json(allHeadaches);
  } catch (err) {
    next(err);
  }
};

Routes – Here are my routes for creating, deleting and retrieving all records.

// HEADACHE ROUTES

const express = require("express");
const router = express.Router({ mergeParams: true });

const {
  createHeadache,
  getHeadache,
  deleteHeadache,
  getAllHeadaches
} = require("../handlers/myHeadaches");

router.route("/").post(createHeadache);

// Delete route
// prefix - /api/users/:id/headache
router
  .route("/:headache_id")
  .get(getHeadache)
  .delete(deleteHeadache);

router.route("/allHeadaches").get(getAllHeadaches);

module.exports = router;

I tested posting a new entry, and it works.

shimo@mvly-BHafner MINGW64 ~/OneDrive - brianhafner.com/Documents/Development/MigraineTracker
$ http POST localhost:3025/api/users/5b8c3bee634c83252484666d/headaches "Authorization:Bearer eyJhbGciOiJI
UzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjViOGMzYmVlNjM0YzgzMjUyNDg0NjY2ZCIsInVzZXJuYW1lIjoidGVzdCIsImlhdCI6MTUzN
TkxNzAzOH0.BhHCx9xURnczelA1IYyWxPkpwL9Zkwh3V5fFYLV1kHQ" date="09/02/2018" painLevel=6 comment="a bit bette
r today"
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Connection: keep-alive
Content-Length: 185
Content-Type: application/json; charset=utf-8
Date: Sun, 02 Sep 2018 22:07:16 GMT
ETag: W/"b9-Tq4q2/EueNlC7JVjM+i90eYwgPU"
X-Powered-By: Express

{
    "__v": 0,
    "_id": "5b8c5f14a204f02794eb098f",
    "comment": "a bit better today",
    "createdAt": "2018-09-02T22:07:16.829Z",
    "date": "09/02/2018",
    "painLevel": 6,
    "updatedAt": "2018-09-02T22:07:16.829Z"
}

And deleting a post also works.

// DELETE A POST

shimo@mvly-BHafner MINGW64 ~/OneDrive - brianhafner.com/Documents/Development/MigraineTracker
$ http DELETE localhost:3025/api/users/5b8c3bee634c83252484666d/headaches/5b8c77d6ab694d4e343fbee4 "Author
ization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjViOGMzYmVlNjM0YzgzMjUyNDg0NjY2ZCIsInVzZXJuYW
1lIjoidGVzdCIsImlhdCI6MTUzNTkxNzAzOH0.BhHCx9xURnczelA1IYyWxPkpwL9Zkwh3V5fFYLV1kHQ"
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Connection: keep-alive
Content-Length: 220
Content-Type: application/json; charset=utf-8
Date: Sun, 02 Sep 2018 23:54:19 GMT
ETag: W/"dc-UXAfqMSV2z4Mv2dP5sxlrnaYAF0"
X-Powered-By: Express

{
    "__v": 0,
    "_id": "5b8c77d6ab694d4e343fbee4",
    "comment": "yet another comment",
    "createdAt": "2018-09-02T23:52:54.946Z",
    "date": "08/31/2018",
    "painLevel": 9,
    "updatedAt": "2018-09-02T23:52:54.946Z",
    "user": "5b8c3bee634c83252484666d"
}

Now I’m ready to move onto the frontend.

Leave a Reply

Your email address will not be published. Required fields are marked *