Before You Begin

Expected Functionality (MVP)

URL HTTP Verb Action Notes
/fruits/ GET index INDEX when a user types localhost:3000/fruits in browser this route shows a list or index of all fruits
/fruits/new GET new NEW when a user types localhost:3000/fruits/new in browser this route shows the user a form to create a NEW fruit
/fruits/:id DELETE destroy DELETE initiates a delete request through a form submission with action = http://localhost:3000/fruits/:idOfFruit and allows the application the ability to delete a fruit
/fruits/:id PUT update UPDATE initiates a put request through a form submission with action = http://localhost:3000/fruits/:idOfFruit and allows the application the ability to Update data about a fruit
/fruits POST create CREATE initiates a post request through a form submission with action = http://localhost:3000/fruits/ and allows the application the ability to Create a fruit
/fruits/:id/edit GET edit EDIT when a user types localhost:3000/fruits/:idOfFruit/edit in browser shows the user a form to edit a fruit
/fruits/:id GET show SHOW when a user types localhost:3000/fruits/:idOfFruit shows the user an Individual fruit in the browser

Advanced Application Refactors

Refactor #1 - Using a Seed File

Seed routes are great convenient but you do run the risk of forgetting to remove the route before releasing the app creating a way a user can just reset your app, even if by accident. May be safer to seed your database in a non-route so only you can seed the database. This is usually dont by creating a seperate file that can be run as a script where you can put any database code you like.

  • create a models/seed.js file
/// ////////////////////////////////////
// Import Dependencies
/// ////////////////////////////////////
const db = require('./db')
const Fruit = require('./fruit')

/// ////////////////////////////////////////
// Seed Code
/// /////////////////////////////////////////

// Make sure code is not run till connected
db.on('open', () => {
  /// ////////////////////////////////////////////
  // Write your Seed Code Below
  /// ///////////////////////////////////////////

  // Run any database queries in this function
  const startFruits = [
    { name: 'Orange', color: 'orange', readyToEat: false },
    { name: 'Grape', color: 'purple', readyToEat: false },
    { name: 'Banana', color: 'orange', readyToEat: false },
    { name: 'Strawberry', color: 'red', readyToEat: false },
    { name: 'Coconut', color: 'brown', readyToEat: false }
  ]

  // Delete all fruits
  Fruit.deleteMany({})
    .then((deletedFruits) => {
      // add the starter fruits
      Fruit.create(startFruits)
        .then((NewFruits) => {
          // log the New fruits to confirm their creation
          console.log(NewFruits)
          db.close()
        })
        .catch((error) => {
          console.log(error)
          db.close()
        })
    })
    .catch((error) => {
      console.log(error)
      db.close()
    })

  /// ////////////////////////////////////////////
  // Write your Seed Code Above
  /// ///////////////////////////////////////////
})

Let's write a script in package.json that will run this file for us

  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js",
    "seed": "node models/seed.js"
  },

Now we can run our seed with npm run seed

JWT is the industry standard which we will use in Unit 3 but Sessions are just a good nice to know and the way that you set it up is close enough that it still helps you understand what we are doing.

Refactor #2 - Adding Session Based Authentication

Let's define some terms

  • Authentication: Proving the user exists usually with a password (logging in)
  • Authorization: Checking if the logged in user is allowed to use the specified resource

Authentication

This is typically done by comparing a password a user has entered with one they provided at signup. The only wrinkle is for security purposes we will want to encrypt the password using something called bCrypt.

Authorization

There are two main ways to achieve this:

  • Session Based Auth: This is a better choice if all resources the logged in user needs to access is on the same backend server. In this scenario a user logs in and a cookie is created for the browser session (until they close the browser or logout). This cookie will identify a tiny packet of data on our server we can then access with anything we'd like to store in it like the users login status or user information.

The data is on the server, the cookie allows us to know which data belongs to which logged in user.

  • JWT Token based Auth: This is a better choice for API and applications split accross multiple servers. Instead of saving the data on the server, the data is encoded into a token that each server knows how to decode. When accessing protected resources on the server the token must be presented. If the server can successfully decode the token then the user must be logged in, and the users info will be payload inside the decoded token.

The data is on the token, the token proves we are logged in and contains our info in the browser not the server

For our particular app, session based auth will be the better pattern. In units 3 & 4 where we will build APIs, JWT tokens will be the better way to do it.

Authorization

  • Let's install some dependencies npm i bcryptjs express-session connect-mongo

    • bcryptjs: a pre-compiled version of bcrypt which we will use to encrypt passwords
    • express-session: middleware for creating session cookies
    • connect-mongo: plugin that will allow express session to save session data in our mongo database

CREATE USER MODEL

create model/user.js

/// ///////////////////////////////////////////
// Import Dependencies
/// ///////////////////////////////////////////
const mongoose = require('mongoose')

/// /////////////////////////////////////////////
// Define Model
/// /////////////////////////////////////////////
// pull schema and model from mongoose
const { Schema, model } = mongoose

// make fruits schema
const userSchema = new Schema({
  username: { type: String, required: true, unique: true },
  password: { type: String, required: true }
})

// make fruit model
const User = model('User', userSchema)

/// ////////////////////////////////////////////////
// Export Model
/// ////////////////////////////////////////////////
module.exports = User

Create a User Controller

  • create controllers/authController.js
////////////////////////////////////////
// Import Dependencies
////////////////////////////////////////
/// /////////////////////////////////////
// Import Dependencies
/// /////////////////////////////////////
const express = require('express')
const User = require('../models/user')
const bcrypt = require('bcryptjs')

/// //////////////////////////////////////
// Create Route
/// //////////////////////////////////////
const router = express.Router()

/// //////////////////////////////////////
// Routes
/// //////////////////////////////////////

// The SignUp Routes (Get => form, post => submit form)
router.get('/signup', (req, res) => {
  res.render('user/SignUp.jsx')
})

router.post('/signup', (req, res) => {
  res.send('signup')
})

// The login Routes (Get => form, post => submit form)
router.get('/login', (req, res) => {
  res.render('user/Login.jsx')
})

router.post('/login', (req, res) => {
  res.send('login')
})

/// ///////////////////////////////////////
// Export the Router
/// ///////////////////////////////////////
module.exports = router
  • connect the router to server.js
require('dotenv').config()
// Require modules
const express = require('express')
const methodOverride = require('method-override')
const cors = require('cors')
const db = require('./models/db')
const app = express()

// Configure the app (app.set)
/* Start Config */
app.use(express.urlencoded({ extended: true })) // This code makes us have req.body <=============
app.use(express.json())
app.use(cors())
app.use((req, res, next) => {
  res.locals.data = {}
  next()
})
app.engine('jsx', require('jsx-view-engine').createEngine())
app.set('view engine', 'jsx') // register the jsx view engine
db.once('open', () => {
  console.log('connected to MongoDB Atlas')
})
/* Start Middleware */
app.use(methodOverride('_method'))
app.use(express.static('public'))
app.use('/fruits', require('./controllers/routeController'))
// send all "/fruits" routes to main router (in a more advanced app we could have multiple routers for multiple paths)
app.use('/user', require('./controllers/authController'))
// send all "/user" routes to auth router
/* END Middleware */

// Tell the app to listen on a port
app.listen(3000, () => {
  console.log('Listening on Port 3000')
})

CREATE USER VIEWS

  • create views/user/SignUp.jsx and views/user/Login.jsx

SignUp.jsx

const React = require('react')
const Default = require('../layouts/Default')


class SignUp extends React.Component {
  render () {
    return (
      <Default title="Sign Up For Fruits">
        <form action='/user/signup' method='POST'>
          <fieldset>
            <legend>New User</legend>
            <label>USERNAME: <input type='text' name='username' required /> </label>
            <label>PASSWORD: <input type='password' name='password' required />
            </label>
            <input type='submit' value='Create Account' />
          </fieldset>
        </form>
      </Default>
    )
  }
}

module.exports = SignUp

Login.jsx

const React = require('react')
const Default = require('../layouts/Default')


class Login extends React.Component {
  render () {
    return (
      <Default title="Login to Fruits Account">
        <form action='/user/login' method='POST'>
          <fieldset>
            <legend>New User</legend>
            <label>USERNAME: <input type='text' name='username' required /> </label>
            <label>PASSWORD: <input type='password' name='password' required />
            </label>
            <input type='submit' value='Login Account' />
          </fieldset>
        </form>
      </Default>
    )
  }
}

module.exports = Login

Make SignUp Post Response

controllers/authController.js

router.post('/signup', async (req, res) => {
  // encrypt password
  req.body.password = await bcrypt.hash(
    req.body.password,
    await bcrypt.genSalt(10)
  )
  // create the New user
  User.create(req.body)
    .then((user) => {
      // redirect to login page
      res.redirect('/user/login')
    })
    .catch((error) => {
      // send error as json
      console.log(error)
      res.json({ error })
    })
})

Make Login Post Response

router.post('/login', async (req, res) => {
  // get the data from the request body
  const { username, password } = req.body
  // search for the user
  User.findOne({ username })
    .then(async (user) => {
      // check if user exists
      if (user) {
        // compare password
        const result = await bcrypt.compare(password, user.password)
        if (result) {
          // redirect to fruits page if successful
          res.redirect('/fruits')
        } else {
          // error if password doesn't match
          res.json({ error: "password doesn't match" })
        }
      } else {
        // send error if user doesn't exist
        res.json({ error: "user doesn't exist" })
      }
    })
    .catch((error) => {
      // send error as json
      console.log(error)
      res.json({ error })
    })
})

Delete the public/index.html file and add a Home.jsx file in views.

Users need a way to get to the login and sign up pages so let's create a home route in server.js.

app.get("/", (req, res) => {
  res.render("Home.jsx");
});

let's create a views/Home.jsx

const React = require('react')
const Default = require('./layouts/Default')

class Home extends React.Component {
  render () {
    return (
      <Default title='Home Page'>
        <a href='/user/signup'><button>SignUp</button></a>
        <a href='/user/login'><button>Login</button></a>
      </Default>
    )
  }
}

module.exports = Home

Authorization

So now a user can signup and login but it doesn't really do much for us. A user can still access all pages whether they are logged in or not and the app doesn't remember if they are logged in after they switch pages. So while the user is authenticated we need to setup the ability to track whether they have authority to access things, authorization.

Configuring Sessions Middleware

Add a SECRET to your environment variables

SECRET=thisCanBeAnythingYouWant

Let's adjust our middleware in server.js

require('dotenv').config()
// Require modules
const express = require('express')
const methodOverride = require('method-override')
const cors = require('cors')
const session = require("express-session");
const MongoStore = require("connect-mongo");
const db = require('./models/db')
const app = express()

// Configure the app (app.set)
/* Start Config */
app.use(express.urlencoded({ extended: true })) // This code makes us have req.body <=============
app.use(express.json())
app.use(cors())
app.use((req, res, next) => {
  res.locals.data = {}
  next()
})
app.engine('jsx', require('jsx-view-engine').createEngine())
app.set('view engine', 'jsx') // register the jsx view engine
db.once('open', () => {
  console.log('connected to MongoDB Atlas')
})

/* Start Middleware */
app.use(methodOverride('_method'))
app.use(express.static('public'))

app.use(
  session({
    secret: process.env.SECRET,
    store: MongoStore.create({ mongoUrl: process.env.MONGO_URI }),
    saveUninitialized: true,
    resave: false,
  })
)

app.use('/fruits', require('./controllers/routeController'))
// send all "/fruits" routes to main router (in a more advanced app we could have multiple routers for multiple paths)
app.use('/user', require('./controllers/authController'))
// send all "/user" routes to auth router
app.get('/', (req, res) => {
  res.render('Home.jsx')
})
/* END Middleware */

// Tell the app to listen on a port
app.listen(3000, () => {
  console.log('Listening on Port 3000')
})

This now adds a property to the request object (req.session), we can use this object to store data between requests. Perfect for storing whether the user is logged in or not!

Saving Login info in Session

Refactor your login post route in controllers/authController.js

router.post('/login', async (req, res) => {
  // get the data from the request body
  const { username, password } = req.body
  // search for the user
  User.findOne({ username })
    .then(async (user) => {
      // check if user exists
      if (user) {
        // compare password
        const result = await bcrypt.compare(password, user.password)
        if (result) {
          // store some properties in the session object
          req.session.username = username
          req.session.loggedIn = true
          // redirect to fruits page if successful
          // redirect to fruits page if successful
          res.redirect('/fruits')
        } else {
          // error if password doesn't match
          res.json({ error: "password doesn't match" })
        }
      } else {
        // send error if user doesn't exist
        res.json({ error: "user doesn't exist" })
      }
    })
    .catch((error) => {
      // send error as json
      console.log(error)
      res.json({ error })
    })
})

add a logout route to destroy the session

router.get('/logout', (req, res) => {
  // destroy session and redirect to main page
  req.session.destroy((err) => {
    if (err) {
      console.error(err)
      res.status(500).json(err)
    } else {
      res.redirect('/')
    }    
  })
})

Authorization Middleware

So now the user data is in sessions, we just need middleware to check it and bounce users to login if they try to access pages that require login.

We will only add this protection to our /fruits routes, so we'll add it as middleware in our fruits router.

controllers/routeController.js

const express = require('express')
const router = express.Router()
const viewController = require('./viewController.js')
const dataController = require('./dataController.js')
const apiController = require('./apiController.js')

////////////////////////////////////////
// Router Middleware
////////////////////////////////////////
// Authorization Middleware
router.use((req, res, next) => {
    if (req.session.loggedIn) {
      next()
    } else {
      res.redirect("/user/login")
    }
  })
// add routes
// Index
router.get('/api', dataController.index, apiController.index)
// Delete
router.delete('/api/:id', dataController.destroy, apiController.show)
// Update
router.put('/api/:id', dataController.update, apiController.show)
// Create
router.post('/api', dataController.create, apiController.show)
// Show
router.get('/api/:id', dataController.show, apiController.show)

// Index
router.get('/', dataController.index, viewController.index)
// New
router.get('/new', viewController.newView)
// Delete
router.delete('/:id', dataController.destroy, viewController.redirectHome)
// Update
router.put('/:id', dataController.update, viewController.redirectShow)
// Create
router.post('/', dataController.create, viewController.redirectHome)
// Edit
router.get('/:id/edit', dataController.show, viewController.edit)
// Show
router.get('/:id', dataController.show, viewController.show)
// export router
module.exports = router

Add Logout Button

Add the following button to views/layouts/Default

<a href="/user/logout"><button>Logout</button></a>

Display the Navbar as flex in app.css

@import url('https://fonts.googleapis.com/css?family=Comfortaa|Righteous');

body {
  background: url(https://images.clipartlogo.com/files/istock/previews/8741/87414357-apple-seamless-pastel-colors-pattern-fruits-texture-background.jpg);
  margin: 0;
  font-family: 'Comfortaa', cursive;
}

h1 {
  font-family: 'Righteous', cursive;
  background: antiquewhite;
  margin:0;
  margin-bottom: .5em;
  padding: 1em;
  text-align: center;
}

a, a > button {
  color: orange;
  text-decoration: none;
  text-shadow: 1px 1px 1px black;
  font-size: 1.5em;
  background: rgba(193, 235, 187, .9);
}

a:hover {
  color: ghostwhite;
}

li {
  list-style: none;
}

li a {
  color: mediumseagreen;
}

input[type=text] {
  padding: .3em;
}

input[type=submit] {
  padding: .3em;
  color: orange;
  background: mediumseagreen;
  font-size: 1em;
  border-radius: 10%;
}

nav {
  display: flex;
  justify-content: space-around;
}

Default Page Updates

const React = require('react')

class Default extends React.Component {
  render () {
    const { fruit, title } = this.props
    return (
      <html>
        <head>
          <link rel='stylesheet' href='/css/app.css' />
          <title>{title}</title>
        </head>
        <body>
          <nav>
            <a href='/fruits'><button>Go to Fruits</button></a>
            <a href='/fruits/new'><button>Create a New Fruit</button></a>
            {title === 'Login to Fruits Account' || title === 'Sign Up For Fruits' ? '' : <a href="/user/logout"><button>Logout</button></a>}
            {fruit ? <a href={`/fruits/${fruit._id}/edit`}> {fruit.name} Edit Page </a> : ''}
            {fruit ? <a href={`/fruits/${fruit._id}`}>{fruit.name} Show Page</a> : ''}
          </nav>
          <h1>
            {title}
          </h1>
          {this.props.children}
        </body>
      </html>
    )
  }
}

module.exports = Default

User Specific Fruits

Wouldn't it be nice if each fruit belonged to a user, and the user can only see their fruits when they login?

  1. first we need to update the model so we can track the username of the creater in models/fruit.js
const mongoose = require('mongoose')

// Make A Schema
const fruitSchema = new mongoose.Schema({
  name: { type: String, required: true },
  color: { type: String, required: true },
  readyToEat: Boolean,
  username: String
})

// Make A Model From The Schema

const Fruit = mongoose.model('Fruit', fruitSchema)

// Export The Model For Use  In The App

module.exports = Fruit
  1. Then we need to refactor the create route to add the username before creating the fruit, in controllers/dataController.js
create (req, res, next) {
    req.body.readyToEat = req.body.readyToEat === 'on'
    req.body.username = req.session.username
    Fruit.create(req.body, (err, createdFruit) => {
      if (err) {
        res.status(400).send({
          msg: err.message
        })
      } else {
        res.locals.data.fruit = createdFruit
        next()
      }
    })
  }
  1. Update Index route to only Show the logged in users fruits, by querying only fruits whose username matches the username stored in session
index (req, res, next) {
  Fruit.find({ username: req.session.username }, (err, foundFruits) => {
    if (err) {
      res.status(400).send({
        msg: err.message
      })
    } else {
      res.locals.data.fruits = foundFruits
      next()
    }
  })
}

There you go, users can login and out and only see fruits associated with their account!

Bonus Refactor #3 - Isolating the Middleware

Let's move the middleware into it's own file like we did the models and controllers

utils/middleware.js

/// //////////////////////////////////////
// Dependencies
/// //////////////////////////////////////
require('dotenv').config() // Load ENV Variables
const express = require('express') // import express
const methodOverride = require('method-override')
const session = require('express-session')
const MongoStore = require('connect-mongo')

/// //////////////////////////////////
// MiddleWare Function
/// ///////////////////////////////////

const middleware = (app) => {
  app.use(express.urlencoded({ extended: true })) // parse urlencoded request bodies
  app.use(express.json())
  app.use((req, res, next) => {
    res.locals.data = {}
    next()
  })
  app.use(methodOverride('_method')) // override for put and delete requests from forms
  app.use(express.static('public')) // serve files from public statically
  app.use(
    session({
      secret: process.env.SECRET,
      store: MongoStore.create({ mongoUrl: process.env.DATABASE_URL }),
      saveUninitialized: true,
      resave: false
    })
  )
  app.use(cors())
  app.engine('jsx', require('jsx-view-engine').createEngine())
  app.set('view engine', 'jsx') // register the jsx view engine
}

/// ////////////////////////////////////////
// Export Middleware Function
/// ///////////////////////////////////////
module.exports = middleware

Now we can really strip down our server.js

/// //////////////////////////////////////////
// Import Our Dependencies
/// //////////////////////////////////////////
const express = require('express')
const middleware = require('./utils/middleware')
const db = require('./models/db')

/// //////////////////////////////////////////////
// Create our Express Application Object
/// //////////////////////////////////////////////
const app = express()
db.on('open', () => {
  console.log('Connected to Mongo')
})
/// //////////////////////////////////////////////////
// Middleware
/// //////////////////////////////////////////////////
middleware(app)

/// /////////////////////////////////////////
// Routes
/// /////////////////////////////////////////
app.use('/fruits', require('./controllers/routeController'))
// send all "/fruits" routes to main router (in a more advanced app we could have multiple routers for multiple paths)
app.use('/user', require('./controllers/authController'))
// send all "/user" routes to auth router
app.get('/', (req, res) => {
  res.render('Home.jsx')
})

/// ///////////////////////////////////////////
// Server Listener
/// ///////////////////////////////////////////

// Tell the app to listen on a port
const PORT = process.env.PORT || 3000
app.listen(PORT, () => console.log(`Now Listening on port ${PORT}`))

Bonus Refactor #4 - the home router for any non-rest or authentication routes

Let's get all routes outside of server.js by making a homerouter for "/" routes

  • create controllers/homeController.js
////////////////////////////////////////
// Import Dependencies
////////////////////////////////////////
const express = require("express");

/////////////////////////////////////////
// Create Route
/////////////////////////////////////////
const router = express.Router();

/////////////////////////////////////////
// Routes
/////////////////////////////////////////

router.get("/", (req, res) => {
  res.render("Index.jsx");
});

//////////////////////////////////////////
// Export the Router
//////////////////////////////////////////
module.exports = router;
  • let's connect the router in our server.js
/// //////////////////////////////////////////
// Import Our Dependencies
/// //////////////////////////////////////////
const express = require('express')
const middleware = require('./utils/middleware')
const db = require('./models/db')

/// //////////////////////////////////////////////
// Create our Express Application Object
/// //////////////////////////////////////////////
const app = express()
db.on('open', () => {
  console.log('Connected to Mongo')
})
/// //////////////////////////////////////////////////
// Middleware
/// //////////////////////////////////////////////////
middleware(app)

/// /////////////////////////////////////////
// Routes
/// /////////////////////////////////////////
app.use('/fruits', require('./controllers/routeController'))
// send all "/fruits" routes to main router (in a more advanced app we could have multiple routers for multiple paths)
app.use('/user', require('./controllers/authController'))
// send all "/user" routes to auth router
app.use('/', require('./controllers/homeController'))
/// ///////////////////////////////////////////
// Server Listener
/// ///////////////////////////////////////////

// Tell the app to listen on a port
const PORT = process.env.PORT || 3000
app.listen(PORT, () => console.log(`Now Listening on port ${PORT}`))