

1 Set Up Folder Structure
💪 Practice Exercise Breakout Rooms - Delete Files We don't nee App.jsx (30 minutes)
- Make sure that you delete any files that you see that we don't need in the SRC Folder
- Make a
styles.cssfile and remove the file that is no longer needed (i expect you to figure out which file you don't need, you know how we set up our react apps) - Import the
styles.csswhere you think it makes most logical sense the root of your app that imports everything else (what is the root of everything?) - Move the
App.jsto thepagesfolder and make aApp folderinpagesand place theApp.jsin that folder - Update the imports in your
App.jsandindex.js. The current imports will not work because you have now moved folders and changed file names. - We will be learning how to make SCSS Modules which makes special scoped css that only applies to the component that you attach the SCSS Module to.
- run
npm i sassto install sass, so that we can use SCSSSassy Cascading Style SheetsyesS A S Sis the correct spelling for thenpm module - Create the following folder structure
ROOT
-
config/
- database.js
-
controllers/
-
api/
- fruits.js
- users.js
-
-
models/
- fruit.js
- user.js
-
public/ (don't change anything it's all there already)
- favicon.ico
- index.html
- logo*.png
- manifest.json
- robots.txt
-
routes/
-
api/
- fruits.js
- users.js
-
-
src/
-
components/
-
NavBar/
- NavBar.js
- NavBar.module.scss
-
-
pages/
-
App/
- App.js
- App.module.css
-
FruitsPage/
- FruitsPage.js
- FruitsPage.module.scss
-
AuthPage/
- AuthPage.js
- AuthPage.module.scss
-
NewOrderPage/
- NewOrderPage.js
- NewOrderPage.module.scss
-
OrderHistoryPage/
- OrderHistoryPage.js
- OrderHistoryPage.module.scss
-
-
utilities/
- .gitkeep
- styles.css
- index.js
-
- .gitignore
- README.md
- crud-helper.js
- seed.js
- server.js
- .env
- .env-example
The CSS for the SEI CAFE app is not trivial, so instead of adding CSS to styles.css in bits and pieces, let's go ahead and add all the general purpose CSS up front:
/* CSS Custom Properties */
:root {
--white: #FFFFFF;
--tan-1: #FBF9F6;
--tan-2: #E7E2DD;
--tan-3: #E2D9D1;
--tan-4: #D3C1AE;
--orange: #F67F00;
--text-light: #968c84;
--text-dark: #615954;
}
*, *:before, *:after {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: var(--tan-4);
padding: 2vmin;
height: 100vh;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
#root {
height: 100%;
}
.align-ctr {
text-align: center;
}
.align-rt {
text-align: right;
}
.smaller {
font-size: smaller;
}
.flex-ctr-ctr {
display: flex;
justify-content: center;
align-items: center;
}
.flex-col {
flex-direction: column;
}
.flex-j-end {
justify-content: flex-end;
}
.scroll-y {
overflow-y: scroll;
}
.section-heading {
display: flex;
justify-content: space-around;
align-items: center;
background-color: var(--tan-1);
color: var(--text-dark);
border: .1vmin solid var(--tan-3);
border-radius: 1vmin;
padding: .6vmin;
text-align: center;
font-size: 2vmin;
}
.form-container {
padding: 3vmin;
background-color: var(--tan-1);
border: .1vmin solid var(--tan-3);
border-radius: 1vmin;
}
p.error-message {
color: var(--orange);
text-align: center;
}
form {
display: grid;
grid-template-columns: 1fr 3fr;
gap: 1.25vmin;
color: var(--text-light);
}
label {
font-size: 2vmin;
display: flex;
align-items: center;
}
input {
padding: 1vmin;
font-size: 2vmin;
border: .1vmin solid var(--tan-3);
border-radius: .5vmin;
color: var(--text-dark);
background-image: none !important; /* prevent lastpass */
outline: none;
}
input:focus {
border-color: var(--orange);
}
button, a.button {
margin: 1vmin;
padding: 1vmin;
color: var(--white);
background-color: var(--orange);
font-size: 2vmin;
font-weight: bold;
text-decoration: none;
text-align: center;
border: .1vmin solid var(--tan-2);
border-radius: .5vmin;
outline: none;
cursor: pointer;
}
button.btn-sm {
font-size: 1.5vmin;
padding: .6vmin .8vmin;
}
button.btn-xs {
font-size: 1vmin;
padding: .4vmin .5vmin;
}
button:disabled, form:invalid button[type="submit"] {
cursor: not-allowed;
background-color: var(--tan-4);
}
button[type="submit"] {
grid-column: span 2;
margin: 1vmin 0 0;
}That's better! But rest assured we'll continue to improve the layout and styling as we continue coding out mern-infrastructure/SEI CAFE.
2 Create Sign-Up Form
2. Code the <SignUpForm> Component as a Class Component
We'll be adding a <SignUpForm> component that will be rendered by <AuthPage>.
In the new <SignUpForm> component, as you saw in the React Fundamentals - Handling Input and Events lesson, we'll need to manage state for the controlled inputs used to gather information from the user that wants to sign up.
However, instead of using a function component, we'll define <SignUpForm> as a class component which used to be the go to for defining components that needed to manage state prior to hooks being added to the React library.
💪 Practice Exercise - Set Up the Module for the <SignUpForm> Component (1 minute)
The naming conventions of the folders and files for class components is no different than that of function components.
- Create the folder and module for
<SignUpForm>following the same conventions and best practices as always.
Hint: Be careful of typos (
SignUpFormvs.SignupForm).
Set Up <SignUpForm> as a Class Component
Class components in React typically inherit from the Component class defined in the React library, let's import it:
// SignUpForm.jsx
import { Component } from 'react'Check out the Further Study section to learn more about the
PureComponentclass available to inherit from as well.
This is how we Set up a class component:
export default class SignUpForm extends Component {
render() {
return (
<div>
SignUpForm
</div>
);
}
}A class component must implement a render method responsible for returning the component's JSX - making the render method itself very much like a function component.
Importing and Rendering a Class Component
There's no difference in how we import and render function and class components.
Let's import and render <SignUpForm> within AuthPage.jsx and do a minor refactor while we're at it:
// AuthPage.jsx
import SignUpForm from '../../components/SignUpForm/SignUpForm';
export default function AuthPage() {
return (
<main>
<h1>AuthPage</h1>
<SignUpForm />
</main>
)
}So, as you can see, we can't even tell if a component is defined as a function or class when we import and render it:
Initializing a Class Component's State
Our <SignUpForm> is going to need the following state:
name: Name of the useremail: The email address of the userpassword: The user's passwordconfirm: Used to confirm the password is entered correctlyerror: Used to display an error message if the sign up fails
Unlike with a function component that can define multiple pieces of state by using the useState hook multiple times, a class component's state is always a single object assigned to a state property on the instance of the component.
There are two ways to initialize the state property:
- Using the
constructormethod (the original approach). - Using the newer class fields approach.
Of course, we'll use the class fields approach that all the cool kids are using, but here's how it would look if we initialized the state using the constructor method:
export default class SignUpForm extends Component {
// state is always an object with a property for each "piece" of state
constructor() {
this.state = {
name: '',
email: '',
password: '',
confirm: '',
error: ''
};
}The following is the class field approach we're going with:
export default class SignUpForm extends Component {
state = {
name: '',
email: '',
password: '',
confirm: '',
error: ''
};FYI: Internally, the class field syntax is converted into the constructor method approach.
this Keyword in a Class Component
When we use class components, it's important to realize that the components themselves are instances of the class instantiated by React the first time they are rendered.
❓ An instance of a class is an _______.
object
❓ An object's methods accesses other properties/methods of the object via the _______ keyword.
this
Unlike with function components, a class component accesses its props and methods using this, for example:
this.props: Accesses the class component'spropsobject, e.g.,this.props.someProp.this.state: Accesses the class component'sstateobject, e.g.,this.state.email.this.someMethod(): Invokes a method defined in the class component or inherited by the superclass (Component) such asthis.setState()used to update state.
The Complete render Method of <SignUpForm>
Here's the completed code of the render() method that we'll copy/paste and discuss:
render() {
const disable = this.state.password !== this.state.confirm;
return (
<div>
<div className="form-container">
<form autoComplete="off" onSubmit={this.handleSubmit}>
<label>Name</label>
<input type="text" name="name" value={this.state.name} onChange={this.handleChange} required />
<label>Email</label>
<input type="email" name="email" value={this.state.email} onChange={this.handleChange} required />
<label>Password</label>
<input type="password" name="password" value={this.state.password} onChange={this.handleChange} required />
<label>Confirm</label>
<input type="password" name="confirm" value={this.state.confirm} onChange={this.handleChange} required />
<button type="submit" disabled={disable}>SIGN UP</button>
</form>
</div>
<p className="error-message"> {this.state.error}</p>
</div>
);
}The form is rendering but it's ugly and we can't type in the inputs yet because the this.handleChange method assigned to the onChange prop is not defined yet...
Defining Event Handler Methods in a Class Component
The handleChange() method can't be defined using the usual syntax for defining an instance method of a class like that of the render() method.
The reason the usual syntax won't work is because the method will be invoked as a callback and thus will not have this bound to the component instance as necessary if we want to be able to access this.props, this.setState(), etc.
There are two solutions to ensure that a method has this correctly set to the component instance:
- Define the method as usual and use JavaScript's
bindmethod in theconstructormethod to explicitly set the value ofthis. - Use the class field syntax along with an arrow function when defining the method which by its very nature fixes the issue due to the way class fields are actually initialized in the
constructormethod.
FYI: The trouble with the binding of
thisin class components is definitely one of the main inspirations for React hooks!
So, here's how we use class field syntax to properly define methods used to handle events in class components:
// The object passed to setState is merged with the current state object
handleChange = (evt) => {
this.setState({
[evt.target.name]: evt.target.value,
error: ''
});
};
render() {
...
}The controlled inputs should now update their appropriate state!
Even the cool little line of codeconst disable = this.state.password !== this.state.confirm;
combined with the disabled prop on the submit button should be working!
That code is pretty much what we learned in the React Fundamentals - Handling Input and Events lesson except...
Updating State in a Class Component
There's two key differences in how state is updated in class vs. function components:
- Class components always update state by invoking the inherited
setState()method vs. invoking the setter function(s) returned by theuseStatehook in function components. -
setState()accepts an object as an arg and this object is merged into the existing state object. This differs with how a function component's setter function replaces that state with the value provided.Note:
setStatealso has a couple of other signatures - refer to the docs for more info.
Handling the onSubmit Event
The <form> React Element in <SignUpForm> already has an event handler method assigned to its onSubmit prop.
💪 Practice Exercise - Set Up the handleSubmit() Method (2 minutes)
- Using the same class field syntax used when defining
handleChange(), define a method namedhandleSubmit()above therender()method. - As we learned during the React Fundamentals - Handling Input and Events lesson, we need to prevent the form from being submitted to the server by including
evt.preventDefault();as the first line of code. - Baby step by adding this additional line of code
alert(JSON.stringify(this.state));.
Check it out by typing in some info and submitting the form:
Final Sign Up Code
import { Component } from 'react';
export default class SignUpForm extends Component {
state = {
name: '',
email: '',
password: '',
confirm: '',
error: ''
}
handleChange = (evt) => {
this.setState({...this.state, [evt.target.name]: evt.target.value, error: '' })
}
handleSubmit = (evt) => {
evt.preventDefault();
alert(JSON.stringify(this.state))
}
render() {
const disable = this.state.password !== this.state.confirm;
return (
<div>
<div className="form-container">
<form autoComplete="off" onSubmit={this.handleSubmit}>
<label>Name</label>
<input type="text" name="name" value={this.state.name} onChange={this.handleChange} required />
<label>Email</label>
<input type="email" name="email" value={this.state.email} onChange={this.handleChange} required />
<label>Password</label>
<input type="password" name="password" value={this.state.password} onChange={this.handleChange} required />
<label>Confirm</label>
<input type="password" name="confirm" value={this.state.confirm} onChange={this.handleChange} required />
<button type="submit" disabled={disable}>SIGN UP</button>
</form>
</div>
<p className="error-message"> {this.state.error}</p>
</div>
)
}
}3. Set up Backend For JWT & User Sign Up
- Make the Model
- Make the Controller
- Add Config Files
- Make the Router
- Add the Router to to the Server
- Add the JWT & BCRYPT packages
- Set up login & create functions to use JWT
- Create the configuration files
- Make sure everything works
User Model setup
- install bcrypt
npm i bcrypt
const { Schema, model } = require('mongoose')
const bcrypt = require('bcrypt')
const SALT_ROUNDS = 6
const userSchema = new Schema({
name: {type: String, required: true},
email: {
type: String,
unique: true,
trim: true,
lowercase: true,
required: true
},
password: {
type: String,
trim: true,
minLength: 3,
required: true
}
}, {
timestamps: true,
toJSON: {
transform (doc, ret) {
delete ret.password
return ret
}
}
})
userSchema.pre('save', async function (next) {
// 'this' is the user doc
if (!this.isModified('password')) return next()
// update the password with the computed hash
this.password = await bcrypt.hash(this.password, SALT_ROUNDS)
return next()
})
module.exports = mongoose.model('User', userSchema)Add Controllers
- install jsonwebtoken
npm i jsonwebtoken
// /controllers/api/users.js
const User = require('../../models/user')
const jwt = require('jsonwebtoken')
const bcrypt = require('bcrypt')
const checkToken = (req, res) => {
console.log('req.user', req.user)
res.json(req.exp)
}
const dataController = {
async create (req, res, next) {
try {
const user = await User.create(req.body)
// token will be a string
const token = createJWT(user)
// send back the token as a string
// which we need to account for
// in the client
res.locals.data.user = user
res.locals.data.token = token
next()
} catch (e) {
res.status(400).json(e)
}
},
async login (req, res, next) {
try {
const user = await User.findOne({ email: req.body.email })
if (!user) throw new Error()
const match = await bcrypt.compare(req.body.password, user.password)
if (!match) throw new Error()
res.locals.data.user = user
res.locals.data.token = createJWT(user)
next()
} catch {
res.status(400).json('Bad Credentials')
}
}
}
const apiController = {
auth (req, res) {
res.json(res.locals.data.token)
}
}
module.exports = {
checkToken,
dataController,
apiController
}
/* -- Helper Functions -- */
function createJWT (user) {
return jwt.sign(
// data payload
{ user },
process.env.SECRET,
{ expiresIn: '24h' }
)
}Add Config Folder Code
You should already have database.js
// /config/database.js
const mongoose = require('mongoose')
mongoose.connect(process.env.MONGO_URI)
const db = mongoose.connection
db.on('connected', function () {
console.log(`Connected to ${db.name} at ${db.host}:${db.port}`)
})
module.exports = mongooseNow lets create the checkToken Middleware and add it to server.js as well as make a route the frontend can call to check the token
As we learned many moons ago, middleware is used to process requests in an Express app.
Yay! Another opportunity to write a custom middleware function that:
- Checks if there's a token sent in an
Authorizationheader of the HTTP request. For additional flexibility, we'll also check for a token being sent as a query string parameter. - Verifies the token is valid and hasn't expired.
- Decodes the token to obtain the user data from its payload.
- Then finally, adds the user payload to the Express request object.
// /config/checkToken.js
const jwt = require('jsonwebtoken')
module.exports = (req, res, next) => {
// Check for the token being sent in a header or a query parameter
let token = req.get('Authorization') || req.query.token
if (token) {
token = token.replace('Bearer ', '')
// Check if token is valid and not expired
jwt.verify(token, process.env.SECRET, function (err, decoded) {
req.user = err ? null : decoded.user
// Can remove this...
// If your app doesn't care
req.exp = err ? null : new Date(decoded.exp * 1000)
})
return next()
} else {
// No token was sent
req.user = null
return next()
}
}// /config/ensureLoggedIn.js
module.exports = (req, res, next) => {
if (!req.user) return res.status(401).json('Unauthorized')
next()
};Add Router
Routes API
// /routes/api/users.js
const express = require('express')
const router = express.Router()
const { checkToken, dataController, apiController } = require('../../controllers/api/users')
const ensureLoggedIn = require('../../config/ensureLoggedIn')
// POST /api/users
router.post('/', dataController.create, apiController.auth)
// POST /api/users/login
router.post('/login', dataController.login, apiController.auth)
// GET /api/users/check-token
router.get('/check-token', ensureLoggedIn, checkToken)
module.exports = routerUpdate Server.js
// ./server.js
require('dotenv').config()
require('./config/database')
const express = require('express')
const path = require('path')
const favicon = require('serve-favicon')
const logger = require('morgan')
const PORT = process.env.PORT || 3001
const app = express()
app.use(logger('dev'))
// there's no need to mount express.urlencoded middleware
// why is that?
app.use(express.json())
// Configure both serve-favicon & static middleware
// to serve from the production 'build' folder
app.use(favicon(path.join(__dirname, 'build', 'favicon.ico')))
app.use(express.static(path.join(__dirname, 'build')))
// Check if token and create req.user
app.use(require('./config/checkToken'))
// Put API routes here, before the "catch all" route
app.use('/api/users', require('./routes/api/users'))
app.use('/api/fruits', require('./routes/api/fruits'))
// The following "catch all" route (note the *) is necessary
// to return the index.html on all non-AJAX requests
app.get('*', function (req, res) {
res.sendFile(path.join(__dirname, 'build', 'index.html'))
})
// Configure to use port 3001 instead of 3000 during
// development to avoid collision with React's dev server
app.listen(PORT, function () {
console.log(`Express app running on port ${PORT}`)
})4 Setup Frontend API Services and Utilities
- Make functions to extract the user from the JWT
- Make functions to handle login and signup
- Connect the signup form
- Setup the login form
- Swap between Login & Signup
- Make the NavBar React to logged in state of the user
JWT Breakdown
- The token can contain whatever custom data (called claims) we want to put in it.
- The token is cryptographically signed by the server when it is created so that if the token is changed in any way, it is considered invalid.
- The token is encoded, but not encrypted. It is encoded (converted) using a standard known as base64url encoding so that it can be serialized across the internet or even be included in a URL's querystring. It may seem that encoded data is "secret" - it's not as you'll soon see!
> const jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ';
> const payload = jwt.split('.')[1] // only interested in the payload (claims)
> atob(payload)
< "{"sub":"1234567890","name":"John Doe","admin":true}"The
atob()method decodes a base-64 encoded string andbtoa()base-64 encodes data.
Because the data in a JWT can be easily read, it's important not to include sensitive/secret data such as Social Security Numbers, etc.
Okay, JWT-based auth is cool, let's see how we use them in a SPA...
Typical Token-Based Flow in a SPA
The following depicts the typical flow of JWT-based auth in a SPA:
Additional clarification on the above steps:
- STEP 1: Applies to logging in and signing up.
- STEP 2: The JWT is created only after the login credentials have been validated, or the visitor signing up has been saved to the database.
- STEP 3: After the JWT has been received by the client, it needs to be persisted, usually in local storage, so that it can be sent in future requests as needed (STEP 4).
- STEP 4: We will be including the JWT with any request that needs to be authenticated on the server.
- STEP 5: We will write a tidy middleware function used to validate the token and add the user data to Express's
reqobject - cool beans for sure! - STEP 6: Send a response positive or negative back to the client
Final Front End Code
send-request
Note: The
sendRequestfunction always returns a promise and we are passing that promise to the caller of checkToken.
Each of these utilities work together to make our life easier and reference eachother
import { getToken } from './users-service'
export default async function sendRequest (url, method = 'GET', payload = null) {
// Fetch takes an optional options object as the 2nd argument
// used to include a data payload, set headers, etc.
const options = { method }
if (payload) {
options.headers = { 'Content-Type': 'application/json' }
options.body = JSON.stringify(payload)
}
const token = getToken()
if (token) {
// Ensure headers object exists
options.headers = options.headers || {}
// Add token to an Authorization header
// Prefacing with 'Bearer' is recommended in the HTTP specification
options.headers.Authorization = `Bearer ${token}`
}
const res = await fetch(url, options)
// res.ok will be false if the status code set to 4xx in the controller action
if (res.ok) return res.json()
throw new Error('Bad Request')
}users-service.js
import * as usersAPI from './users-api'
export async function signUp (userData) {
// Delete the network request code to the
// users-api.js module which will ultimately
// return the JWT
const token = await usersAPI.signUp(userData)
// Persist the token to localStorage
window.localStorage.setItem('token', token)
return getUser()
}
export async function login (credentials) {
const token = await usersAPI.login(credentials)
// Persist the token to window.localStorage
window.localStorage.setItem('token', token)
return getUser()
}
export function getToken () {
const token = window.localStorage.getItem('token')
// getItem will return null if no key
if (!token) return null
const payload = JSON.parse(atob(token.split('.')[1]))
// A JWT's expiration is expressed in seconds, not miliseconds
if (payload.exp < Date.now() / 1000) {
// Token has expired
window.localStorage.removeItem('token')
return null
}
return token
}
export function getUser () {
const token = getToken()
return token ? JSON.parse(atob(token.split('.')[1])).user : null
}
export function logOut () {
window.localStorage.removeItem('token')
}users-api.js
import sendRequest from './send-request'
const BASE_URL = '/api/users'
export function signUp(userData) {
return sendRequest(BASE_URL, 'POST', userData)
}
export function login(credentials) {
return sendRequest(`${BASE_URL}/login`, 'POST', credentials)
}Sign Up
import { Component } from 'react'
import { signUp } from '../../utilities/users-service'
export default class SignUpForm extends Component {
state = {
name: '',
email: '',
password: '',
confirm: '',
error: ''
}
handleChange = (evt) => {
this.setState({ ...this.state, [evt.target.name]: evt.target.value, error: '' })
}
handleSubmit = async (evt) => {
evt.preventDefault()
try {
const formData = { ...this.state }
delete formData.error
delete formData.confirm
const user = await signUp(formData)
this.props.setUser(user)
} catch (error) {
this.setState({ error: 'Sign Up Failed' })
}
}
render () {
const disable = this.state.password !== this.state.confirm
return (
<div>
<div className='form-container'>
<form autoComplete='off' onSubmit={this.handleSubmit}>
<label>Name</label>
<input type='text' name='name' value={this.state.name} onChange={this.handleChange} required />
<label>Email</label>
<input type='email' name='email' value={this.state.email} onChange={this.handleChange} required />
<label>Password</label>
<input type='password' name='password' value={this.state.password} onChange={this.handleChange} required />
<label>Confirm</label>
<input type='password' name='confirm' value={this.state.confirm} onChange={this.handleChange} required />
<button type='submit' disabled={disable}>SIGN UP</button>
</form>
</div>
<h1 className='error-message'> {this.state.error}</h1>
</div>
)
}
}Login
import { useState } from 'react'
import * as userService from '../../utilities/users-service'
export default function LoginForm ({ setUser }) {
const [credentials, setCredentials] = useState({
email: '',
password: ''
})
const [error, setError] = useState('')
const handleChange = (evt) => {
setCredentials({ ...credentials, [evt.target.name]: evt.target.value })
setError('')
}
const handleSubmit = async (evt) => {
evt.preventDefault()
try {
const user = await userService.login(credentials)
setUser(user)
} catch (error) {
setError(error.message)
}
}
return (
<div>
<div className='form-container'>
<form autoComplete='off' onSubmit={handleSubmit}>
<label>Email</label>
<input type='email' name='email' value={credentials.email} onChange={handleChange} required />
<label>Password</label>
<input type='password' name='password' value={credentials.password} onChange={handleChange} required />
<button type='submit'>LOG IN</button>
</form>
</div>
<h1 className='error-message'> {error}</h1>
</div>
)
}