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.jsfile
/// ////////////////////////////////////
// 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 = UserCreate 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.jsxandviews/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 = SignUpLogin.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 = LoginMake 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 = HomeAuthorization
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=thisCanBeAnythingYouWantLet'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 = routerAdd 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 = DefaultUser 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?
- 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- 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()
}
})
}- 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 = middlewareNow 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}`))