Learning From Youtube Channel: Net Ninja
Video: MERN Authentication Tutorial
Thank you.
#1 – Intro & Starter Project
Learn how to implement authentication (using JSON web tokens), within the MERN stack, in this tutorial series.
- 下載初始專案、講解專案檔案結構、內容
- 移動到 backend 資料夾 – cd backend
- 安裝 npm 套件 – npm install
- 移動到 frontend 資料夾 – cd frontend
- 安裝 npm 套件 – npm install
- 執行後端 backend – npm run dev
- 執行前端 frontend – npm start
#2 – User Routes, Controller & Model
In this MERN auth tutorial, you’ll make a user controller & model, and set up some routes for authentication.
- 在 routes 資料夾裡面建立 user.js 檔案
- 修改 user.js 檔案
- 在 controllers 資料夾裡面建立 userController.js 檔案
- 修改 userController.js 檔案
- 修改 user.js 檔案,controller functions
- 修改 server.js 檔案
- 在 models 資料夾裡面建立 userModel.js 檔案
- 修改 useModel.js 檔案
- 修改 userController.js 檔案,User
- 使用 API 測試工具 Postman
http://localhost:4000/api/user/login,POST 方法
Body > raw > JSON - 使用 API 測試工具
http://localhost:4000/api/user/signup,POST 方法
Body > raw > JSON
// backend/routes/user.js
const express = require('express')
// controller functions
const { signupUser, loginUser } = require('../controllers/userController')
const router = express.Router()
// login route
router.post('/login', loginUser)
// signup route
router.post('/signup', signupUser)
module.exports = router
// backend/controllers/userController.js
const User = require('../models/userModel')
// login user
const loginUser = async (req, res) => {
res.json({mssg: 'login user'})
}
// signup user
const signupUser = async (req, res) => {
res.json({mssg: 'signup user'})
}
module.exports = { signupUser, loginUser }
// backend/server.js
require('dotenv').config()
const express = require('express')
const mongoose = require('mongoose')
const workoutRoutes = require('./routes/workouts')
const userRoutes = require('./routes/user')
// express app
const app = express()
// middleware
app.use(express.json())
app.use((req, res, next) => {
console.log(req.path, req.method)
next()
})
// routes
app.use('/api/workouts', workoutRoutes)
app.use('/api/user', userRoutes)
// connect to db
mongoose.connect(process.env.MONGO_URI)
.then(() => {
// listen for requests
app.listen(process.env.PORT, () => {
console.log('connected to db & listening on port', process.env.PORT)
})
})
.catch((error) => {
console.log(error)
})
// backend/models/userModel.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const userSchema = new Schema({
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true,
}
})
module.exports = mongoose.model('User', userSchema)
// Body > raw > JSON - login
{
"email": "yoshi@netninja.dev",
"password": "abc123"
}
// Body > raw > JSON - signup
{
"email": "yoshi@netninja.dev",
"password": "abc123"
}
#3 – Signing Up & Hashing Passwords
- 修改 userModel.js 檔案,static signup method
- 安裝 bcrypt 套件 – npm install bcrypt
修改 userModel.js 檔案 - 修改 userController.js 檔案
- 修改 userModels.js 檔案,function
- 重新執行後端 server – nodemon server
- 使用 API 測試工具 Postman
http://localhost:4000/api/user/signup,POST 方法
Body > raw > JSON
// backend/models/userModel.js
const mongoose = require('mongoose')
const bcrypt = require('bcrypt')
const Schema = mongoose.Schema
const userSchema = new Schema({
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true,
}
})
// static signup method
userSchema.statics.signup = async function (email, password) {
const exists = await this.findOne({ email })
if (exists) {
throw Error('Email already in use')
}
const salt = await bcrypt.genSalt(10)
const hash = await bcrypt.hash(password, salt)
const user = await this.create({ email, password: hash })
return user
}
module.exports = mongoose.model('User', userSchema)
// backend/controllers/userController.js
const User = require('../models/userModel')
// login user
const loginUser = async (req, res) => {
res.json({mssg: 'login user'})
}
// signup user
const signupUser = async (req, res) => {
const { email, password } = req.body
try {
const user = await User.signup(email, password)
res.status(200).json({ email, user })
} catch (error) {
res.status(400).json({ error: error.message })
}
}
module.exports = { signupUser, loginUser }
// Body > raw > JSON
{
"email": "yoshi@netninja.dev",
"password": "abc123"
}
#4 – Email & Password Validation
- 使用 API 測試工具 Postman
http://localhost:4000/api/user/signup,POST 方法
使用錯誤的 email 格式測試,沒有完整格式、空值 - 安裝 validator 套件 – npm install validator
- 修改 userModel.js 檔案,validator
- 使用 API 測試工具 Postman
http://localhost:4000/api/user/signup,POST 方法
使用錯誤的 email 格式測試,沒有完整格式、空值
// backend/models/userModel.js
const mongoose = require('mongoose')
const bcrypt = require('bcrypt')
const validator = require('validator')
const Schema = mongoose.Schema
const userSchema = new Schema({
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true,
}
})
// static signup method
userSchema.statics.signup = async function (email, password) {
// validation
if (!email || !password) {
throw Error('All fields must be filled')
}
if (!validator.isEmail(email)) {
throw Error('Email not valid')
}
if (!validator.isStrongPassword(password)) {
throw Error('Password not strong enough')
}
const exists = await this.findOne({ email })
if (exists) {
throw Error('Email already in use')
}
const salt = await bcrypt.genSalt(10)
const hash = await bcrypt.hash(password, salt)
const user = await this.create({ email, password: hash })
return user
}
module.exports = mongoose.model('User', userSchema)
#5 – JSON Web Tokens (theory)
In this MERN Authentication tutorial you’ll learn about JSON Web Tokens (JWT’s) and how they work under the hood when it comes to authentication.
- 講解 JSON Web Tokens
- 介紹 JWT 的 Encoded、Decoded
- 講解 Header、Payload、Signature
JSON Web Tokens
Header
Contains the algorithm used for the JWT
Payload
Contains non-sensitive user data (e.g. a user id)
Signature
Used to verify the token by the server
#6 – Signing Tokens
In this MERN Authentication tutorial, we’ll see how to sign tokens and send them back to the client.
- 安裝 jsonwebtoken 套件 – npm install jsonwebtoken
- 修改 userController.js 檔案
- 修改 .env 檔案,避免敏感、重要的資料上傳到 GitHub
- 使用 API 測試工具 Postman
http://localhost:4000/api/user/signup,POST 方法
Body > raw > JSON
// backend/controllers/userController.js
const User = require('../models/userModel')
const jwt = require('jsonwebtoken')
const createToken = (_id) => {
return jwt.sign({_id}, process.env.SECRET, { expiresIn: '3d' })
}
// login user
const loginUser = async (req, res) => {
res.json({mssg: 'login user'})
}
// signup user
const signupUser = async (req, res) => {
const { email, password } = req.body
try {
const user = await User.signup(email, password)
// create a token
const token = createToken(user._id)
res.status(200).json({ email, token })
} catch (error) {
res.status(400).json({ error: error.message })
}
}
module.exports = { signupUser, loginUser }
// backend/.env
PORT=4000
MONGO_URI=mongodb+srv://mario:test1234@mernapp.bldt4v0.mongodb.net/?retryWrites=true&w=majority&appName=MERNapp
SECRET =learnfromnetninjathankyou
// Body > raw > JSON
{
"email": "luigi@netninja.dev",
"password": "ABCabc123!"
}
#7 – Logging Users In
- 修改 userModel.js 檔案,static login method
- 修改 userController.js 檔案,login user
- 使用 API 測試工具 Postman,測試各種資料情形
http://localhost:4000/api/user/login,POST 方法
Body > raw > JSON
// backend/models/userModel.js
const mongoose = require('mongoose')
const bcrypt = require('bcrypt')
const validator = require('validator')
const Schema = mongoose.Schema
const userSchema = new Schema({
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true,
}
})
// static signup method
userSchema.statics.signup = async function (email, password) {
// validation
if (!email || !password) {
throw Error('All fields must be filled')
}
if (!validator.isEmail(email)) {
throw Error('Email not valid')
}
if (!validator.isStrongPassword(password)) {
throw Error('Password not strong enough')
}
const exists = await this.findOne({ email })
if (exists) {
throw Error('Email already in use')
}
const salt = await bcrypt.genSalt(10)
const hash = await bcrypt.hash(password, salt)
const user = await this.create({ email, password: hash })
return user
}
// static login method
userSchema.statics.login = async function (email, password) {
if (!email || !password) {
throw Error('All fields must be filled')
}
const user = await this.findOne({ email })
if (!user) {
throw Error('Incorrect email')
}
const match = await bcrypt.compare(password, user.password)
if (!match) {
throw Error('Incorrect password')
}
return user
}
module.exports = mongoose.model('User', userSchema)
// backend/controllers/userController.js
const User = require('../models/userModel')
const jwt = require('jsonwebtoken')
const createToken = (_id) => {
return jwt.sign({_id}, process.env.SECRET, { expiresIn: '3d' })
}
// login user
const loginUser = async (req, res) => {
const { email, password } = req.body
try {
const user = await User.login(email, password)
// create a token
const token = createToken(user._id)
res.status(200).json({ email, token })
} catch (error) {
res.status(400).json({ error: error.message })
}
}
// signup user
const signupUser = async (req, res) => {
const { email, password } = req.body
try {
const user = await User.signup(email, password)
// create a token
const token = createToken(user._id)
res.status(200).json({ email, token })
} catch (error) {
res.status(400).json({ error: error.message })
}
}
module.exports = { signupUser, loginUser }
// Body > raw > JSON
{
"email": "luigi@netninja.dev",
"password": "ABCabc123!"
}
#8 – React Auth Context
- 在 context 資料夾裡面建立 AuthContext.js 檔案
- 修改 AuthContext.js 檔案
- 修改 index.js 檔案,匯入 AuthContextProvider
- 在 hook 資料夾裡面建立 useAuthContext.js 檔案
- 修改 useAuthContext.js 檔案
// backend/src/context/AuthContext.js
import { createContext, useReducer } from 'react'
export const AuthContext = createContext()
export const authReducer = (state, action) => {
switch (action.type) {
case 'LOGIN':
return { user: action.payload }
case 'LOGOUT':
return { user: null}
default:
return state
}
}
export const AuthContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(authReducer, {
user: null
})
console.log('AuthContext State: ', state)
return (
<AuthContext.Provider value={{...state, dispatch}}>
{ children }
</AuthContext.Provider>
)
}
// frontend/src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { WorkoutsContextProvider } from './context/WorkoutContext';
import { AuthContextProvider } from './context/AuthContext';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<AuthContextProvider>
<WorkoutsContextProvider>
<App />
</WorkoutsContextProvider>
</AuthContextProvider>
</React.StrictMode>
);
// frontend/src/hooks/useAuthContext.js
import { AuthContext } from "../context/AuthContext";
import { useContext } from 'react'
export const useAuthContext = () => {
const context = useContext(AuthContext)
if (!context) {
throw Error('useAuthContext must be used inside an AuthContextProvider')
}
return context
}
#9 – Login & Signup Forms
In this MERN auth tutorial, we’ll flesh out the login and signup forms.
- 在 pages 資料夾裡面建立 Signup.js 檔案
- 修改 Signup.js 檔案
- 在 pages 資料夾裡面建立 Login.js 檔案
- 修改 Login.js 檔案,從 Signup.js 複製修改
- 修改 App.js 檔案
- 修改 Navbar.js 檔案
- 修改 index.css 檔案
- 測試網頁表單 Sign up 關於 console 查詢顯示
// frontend/src/pages/Signup.js
import { useState } from 'react'
const Signup = () => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = async (e) => {
e.preventDefault()
console.log(email, password)
}
return (
<form className='signup' onSubmit={handleSubmit}>
<h3>Sign up</h3>
<label>Email:</label>
<input
type="email"
onChange={(e) => setEmail(e.target.value)}
value={email}
/>
<label>Password:</label>
<input
type="password"
onChange={(e) => setPassword(e.target.value)}
value={password}
/>
<button>Sign up</button>
</form>
)
}
export default Signup
// frontend/src/pages/Login.js
import { useState } from 'react'
const Login = () => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = async (e) => {
e.preventDefault()
console.log(email, password)
}
return (
<form className='login' onSubmit={handleSubmit}>
<h3>Log in</h3>
<label>Email:</label>
<input
type="email"
onChange={(e) => setEmail(e.target.value)}
value={email}
/>
<label>Password:</label>
<input
type="password"
onChange={(e) => setPassword(e.target.value)}
value={password}
/>
<button>Log in</button>
</form>
)
}
export default Login
// frontend/src/App.js
import { BrowserRouter , Routes, Route } from 'react-router-dom'
// pages & components
import Home from './pages/Home'
import Login from './pages/Login';
import Signup from './pages/Signup';
import Navbar from './components/Navbar'
function App() {
return (
<div className="App">
<BrowserRouter>
<Navbar />
<div className="pages">
<Routes>
<Route
path="/"
element={<Home />}
/>
<Route
path="/login"
element={<Login />}
/>
<Route
path="/signup"
element={<Signup />}
/>
</Routes>
</div>
</BrowserRouter>
</div>
);
}
export default App;
// frontend/src/components/Navbar.js
import { Link } from "react-router-dom"
const Navbar = () => {
return (
<header>
<div className="container">
<Link to="/">
<h1>Workout Buddy</h1>
</Link>
<nav>
<div>
<Link to="/login">Login</Link>
<Link to="/signup">Signup</Link>
</div>
</nav>
</div>
</header>
)
}
export default Navbar
// frontend/src/index.css
/* google font */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;800&family=VT323&display=swap');
/* layout */
:root {
--primary: #1aac83;
--error: #e7195a;
}
body {
background: #f1f1f1;
margin: 0;
font-family: "Poppins";
}
header {
background: #fff;
}
header .container {
max-width: 1400px;
margin: 0 auto;
padding: 10px 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
header a {
color: #333;
text-decoration: none;
}
.pages{
max-width: 1400px;
padding: 20px;
margin: 0 auto;
}
/* homepage */
.home {
display: grid;
grid-template-columns: 3fr 1fr;
gap: 100px;
}
.workout-details {
background: #fff;
border-radius: 4px;
margin: 20px auto;
padding: 20px;
position: relative;
box-shadow: 2px 2px 5px rgba(0,0,0,0.05);
}
.workout-details h4 {
margin: 0 0 10px 0;
font-size: 1.2em;
color: var(--primary);
}
.workout-details p {
margin: 0;
font-size: 0.9em;
color: #555;
}
.workout-details span {
position: absolute;
top: 20px;
right: 20px;
cursor: pointer;
background: #f1f1f1;
padding: 6px;
border-radius: 50%;
color: #333;
}
/* new workout form */
label, input {
display: block;
}
input {
padding: 10px;
margin-top: 10px;
margin-bottom: 20px;
width: 100%;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
form button {
background: var(--primary);
border: 0;
color: #fff;
padding: 10px;
font-family: "Poppins";
border-radius: 4px;
cursor: pointer;
}
div.error {
padding: 10px;
background: #ffefef;
border: 1px solid var(--error);
color: var(--error);
border-radius: 4px;
margin: 20px 0;
}
input.error {
border: 1px solid var(--error);
}
/* navbar */
nav {
display: flex;
align-items: center;
}
nav a {
margin-left: 10px;
}
/* auth forms */
form.signup, form.login {
max-width: 400px;
margin: 40px auto;
padding: 20px;
background: #fff;
border-radius: 4px;
}
#10 – Making a useSignup Hook
In this MERN auth lesson, you’ll create a custom, reusable React hook that we can use to sign new users up to the application.
- 在 hooks 資料夾裡面建立 useSignup.js 檔案
- 修改 useSignup.js 檔案
- 修改 Signup.js 檔案,useSignup
- 測試 Sign up 表單能否正常運作
// frontend/src/hooks/useSignup.js
import { useState } from 'react'
import { useAuthContext } from './useAuthContext'
export const useSignup = () => {
const [error, setError] = useState(null)
const [isLoading, setIsLoading] = useState(null)
const { dispatch } = useAuthContext()
const signup = async (email, password) => {
setIsLoading(true)
setError(null)
const response = await fetch('/api/user/signup', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email, password})
})
const json = await response.json()
if (!response.ok) {
setIsLoading(false)
setError(json.error)
}
if (response.ok) {
// save the user to local storage
localStorage.setItem('user', JSON.stringify(json))
// update the auth context
dispatch({type: 'LOGIN', payload: json})
setIsLoading(false)
}
}
return { signup, isLoading, error}
}
// frontend/src/pages/Signup.js
import { useState } from 'react'
import { useSignup } from '../hooks/useSignup'
const Signup = () => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const {signup, error, isLoading} = useSignup()
const handleSubmit = async (e) => {
e.preventDefault()
await signup(email, password)
}
return (
<form className='signup' onSubmit={handleSubmit}>
<h3>Sign up</h3>
<label>Email:</label>
<input
type="email"
onChange={(e) => setEmail(e.target.value)}
value={email}
/>
<label>Password:</label>
<input
type="password"
onChange={(e) => setPassword(e.target.value)}
value={password}
/>
<button disabled={isLoading}>Sign up</button>
{error && <div className='error'>{error}</div>}
</form>
)
}
export default Signup
#11 – Making a useLogout Hook
In this MERN auth tutorial, we’ll make a custom useLogout hook to log users out of the application.
- 在 hooks 資料夾裡面建立 useLogout.js 檔案
- 修改 useLogout.js 檔案
- 修改 Navbar.js 檔案
- 修改 index.css 檔案
- 測試 Log out 能否正確刪除 Local Storage 資料
console 檢視狀態
// frontend/src/hooks/useLogout.js
import { useAuthContext } from './useAuthContext'
export const useLogout = () => {
const { dispatch } = useAuthContext()
const logout = () => {
// remove user from storage
localStorage.removeItem('user')
// dispatch logout action
dispatch({type: 'LOGOUT'})
}
return {logout}
}
// frontend/src/components/Navbar.js
import { Link } from "react-router-dom"
import { useLogout } from '../hooks/useLogout'
const Navbar = () => {
const { logout } = useLogout()
const handleClick = () => {
logout()
}
return (
<header>
<div className="container">
<Link to="/">
<h1>Workout Buddy</h1>
</Link>
<nav>
<div>
<button onClick={handleClick}>Log out</button>
</div>
<div>
<Link to="/login">Login</Link>
<Link to="/signup">Signup</Link>
</div>
</nav>
</div>
</header>
)
}
export default Navbar
// frontend/src/index.css
/* google font */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;800&family=VT323&display=swap');
/* layout */
:root {
--primary: #1aac83;
--error: #e7195a;
}
body {
background: #f1f1f1;
margin: 0;
font-family: "Poppins";
}
header {
background: #fff;
}
header .container {
max-width: 1400px;
margin: 0 auto;
padding: 10px 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
header a {
color: #333;
text-decoration: none;
}
.pages{
max-width: 1400px;
padding: 20px;
margin: 0 auto;
}
/* homepage */
.home {
display: grid;
grid-template-columns: 3fr 1fr;
gap: 100px;
}
.workout-details {
background: #fff;
border-radius: 4px;
margin: 20px auto;
padding: 20px;
position: relative;
box-shadow: 2px 2px 5px rgba(0,0,0,0.05);
}
.workout-details h4 {
margin: 0 0 10px 0;
font-size: 1.2em;
color: var(--primary);
}
.workout-details p {
margin: 0;
font-size: 0.9em;
color: #555;
}
.workout-details span {
position: absolute;
top: 20px;
right: 20px;
cursor: pointer;
background: #f1f1f1;
padding: 6px;
border-radius: 50%;
color: #333;
}
/* new workout form */
label, input {
display: block;
}
input {
padding: 10px;
margin-top: 10px;
margin-bottom: 20px;
width: 100%;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
form button {
background: var(--primary);
border: 0;
color: #fff;
padding: 10px;
font-family: "Poppins";
border-radius: 4px;
cursor: pointer;
}
div.error {
padding: 10px;
background: #ffefef;
border: 1px solid var(--error);
color: var(--error);
border-radius: 4px;
margin: 20px 0;
}
input.error {
border: 1px solid var(--error);
}
/* navbar */
nav {
display: flex;
align-items: center;
}
nav a {
margin-left: 10px;
}
nav button {
background: #fff;
color: var(--primary);
border: 2px solid var(--primary);
padding: 6px 10px;
border-radius: 4px;
font-family: "Poppins";
cursor: pointer;
font-size: 1em;
}
/* auth forms */
form.signup, form.login {
max-width: 400px;
margin: 40px auto;
padding: 20px;
background: #fff;
border-radius: 4px;
}
#12 – Making a useLogin Hook
In this MERN auth tutorial, we’ll make a custom useLogin hook to log users in to the application.
- 在 hooks 資料夾裡面建立 useLogin.js 檔案
- 修改 useLogin.js 檔案,複製 useSignup.js 程式碼做修改
- 修改 Login.js 檔案
- 註冊好帳號,測試登入帳號
講解 Local Storage、Token、Refresh
// frontend/src/hooks/useLogin.js
import { useState } from 'react'
import { useAuthContext } from './useAuthContext'
export const useLogin = () => {
const [error, setError] = useState(null)
const [isLoading, setIsLoading] = useState(null)
const { dispatch } = useAuthContext()
const login = async (email, password) => {
setIsLoading(true)
setError(null)
const response = await fetch('/api/user/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email, password})
})
const json = await response.json()
if (!response.ok) {
setIsLoading(false)
setError(json.error)
}
if (response.ok) {
// save the user to local storage
localStorage.setItem('user', JSON.stringify(json))
// update the auth context
dispatch({type: 'LOGIN', payload: json})
setIsLoading(false)
}
}
return { login, isLoading, error}
}
// frontend/src/pages/Login.js
import { useState } from 'react'
import { useLogin } from '../hooks/useLogin'
const Login = () => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const {login, error, isLoading} = useLogin()
const handleSubmit = async (e) => {
e.preventDefault()
await login(email, password)
}
return (
<form className='login' onSubmit={handleSubmit}>
<h3>Log in</h3>
<label>Email:</label>
<input
type="email"
onChange={(e) => setEmail(e.target.value)}
value={email}
/>
<label>Password:</label>
<input
type="password"
onChange={(e) => setPassword(e.target.value)}
value={password}
/>
<button disabled={isLoading}>Log in</button>
{error && <div className='error'>{error}</div>}
</form>
)
}
export default Login
#13 – Setting the Initial Auth Status
- 修改 Navbar.js 檔案,匯入 useAuthContext
- 測試登入表單確認功能是否正常
- 重整頁面後會回到登入頁面
- 修改 AuthContext.js 檔案,useEffect
- 重整頁面後仍會保持在登入狀態
- 測試登出、登入功能是否正常
// frontend/src/components/Navbar.js
import { Link } from "react-router-dom"
import { useLogout } from '../hooks/useLogout'
import { useAuthContext } from "../hooks/useAuthContext"
const Navbar = () => {
const { logout } = useLogout()
const { user } = useAuthContext()
const handleClick = () => {
logout()
}
return (
<header>
<div className="container">
<Link to="/">
<h1>Workout Buddy</h1>
</Link>
<nav>
{user && (
<div>
<span>{user.email}</span>
<button onClick={handleClick}>Log out</button>
</div>
)}
{!user && (
<div>
<Link to="/login">Login</Link>
<Link to="/signup">Signup</Link>
</div>
)}
</nav>
</div>
</header>
)
}
export default Navbar
// frontend/src/context/AuthContext.js
import { createContext, useReducer, useEffect } from 'react'
export const AuthContext = createContext()
export const authReducer = (state, action) => {
switch (action.type) {
case 'LOGIN':
return { user: action.payload }
case 'LOGOUT':
return { user: null}
default:
return state
}
}
export const AuthContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(authReducer, {
user: null
})
useEffect(() => {
const user = JSON.parse(localStorage.getItem('user'))
if (user) {
dispatch({ type: 'LOGIN', payload: user })
}
}, [])
console.log('AuthContext State: ', state)
return (
<AuthContext.Provider value={{...state, dispatch}}>
{ children }
</AuthContext.Provider>
)
}
#14 – Protecting API Routes
In this MERN auth tutorial, you’ll learn how to protect certain API routes from unauthenticated users.
- 在 backend 資料夾建立 middleware 資料夾
- 在 middleware 資料夾裡面建立 requireAuth.js 檔案
- 查看 workouts.js 檔案
- 修改 requireAuth.js 檔案
- 修改 workouts.js 檔案,requireAuth
- 使用 API 測試工具 Postman
http://localhost:4000/api/workouts/,GET 方法
Auth > Bearer > Bearer Token
http://localhost:4000/api/user/login,POST 方法
取得 token 資料
// backend/middleware/requireAuth.js
const jwt = require('jsonwebtoken')
const User = require('../models/userModel')
const requireAuth = async (req, res, next) => {
// verify authentication
const { authorization } = req.headers
if (!authorization) {
return res.status(401).json({error: 'Authorization token required'})
}
const token = authorization.split(' ')[1]
try {
const {_id} = jwt.verify(token, process.env.SECRET)
req.user = await User.findOne({ _id }).select('_id')
next()
} catch (error) {
console.log(error);
res.status(401).json({error: 'Request is not authorized'})
}
}
module.exports = requireAuth
// backend/routes/workouts.js
const express = require('express')
const {
createWorkout,
getWorkouts,
getWorkout,
deleteWorkout,
updateWorkout
} = require('../controllers/workoutController')
const requireAuth = require('../middleware/requireAuth')
const router = express.Router()
// require auth for all workout routes
router.use(requireAuth)
// GET all workouts
router.get('/', getWorkouts)
// GET a single workout
router.get('/:id', getWorkout)
// POST a new workout
router.post('/', createWorkout)
// DELETE a workout
router.delete('/:id', deleteWorkout)
// UPDATE a workout
router.patch('/:id', updateWorkout)
module.exports = router
#15 – Making Authorized Requests
- 需要調整的有三個地方,Home.js、WorkoutForm.js、WorkoutDetails.js 檔案
- 修改 Home.js 檔案
- 修改 WorkoutForm.js 檔案
- 修改 WorkoutDetails.js 檔案
- 使用 Add a New Workout 測試功能是否正常
- 登入帳號才能新增、刪除 Workout
// frontend/src/pages/Home.js
import { useEffect } from 'react'
import { useWorkoutsContext } from '../hooks/useWorkoutsContext'
import { useAuthContext } from '../hooks/useAuthContext'
// components
import WorkoutDetails from '../components/WorkoutDetails'
import WorkoutForm from '../components/WorkoutForm'
const Home = () => {
const {workouts, dispatch} = useWorkoutsContext()
const {user} = useAuthContext()
useEffect(() => {
const fetchWorkouts = async () => {
const response = await fetch('/api/workouts', {
headers: {
'Authorization': `Bearer ${user.token}`
}
})
const json = await response.json()
if (response.ok) {
dispatch({type: 'SET_WORKOUTS', payload: json})
}
}
if (user) {
fetchWorkouts()
}
}, [dispatch, user])
return (
<div className="home">
<div className="workouts">
{workouts && workouts.map((workout) => {
return <WorkoutDetails key={workout._id} workout={workout} />
})}
</div>
<WorkoutForm />
</div>
)
}
export default Home
// frontend/src/components/WorkoutForm.js
import { useState } from "react"
import { useWorkoutsContext } from "../hooks/useWorkoutsContext"
import { useAuthContext } from "../hooks/useAuthContext"
const WorkoutForm = () => {
const { dispatch } = useWorkoutsContext()
const { user } = useAuthContext()
const [title, setTitle] = useState('')
const [load, setLoad] = useState('')
const [reps, setReps] = useState('')
const [error, setError] = useState(null)
const [emptyFields, setEmptyFields] = useState([])
const handleSubmit = async (e) => {
e.preventDefault()
if (!user) {
setError('You must be logged in')
return
}
const workout = {title, load, reps}
const response = await fetch('/api/workouts', {
method: 'POST',
body: JSON.stringify(workout),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${user.token}`
}
})
const json = await response.json()
if (!response.ok) {
setError(json.error)
setEmptyFields(json.emptyFields)
}
if (response.ok) {
setTitle('')
setLoad('')
setReps('')
setError(null)
setEmptyFields([])
console.log('new workout added', json);
dispatch({type: 'CREATE_WORKOUT', payload: json})
}
}
return (
<form className="create" onSubmit={handleSubmit}>
<h3>Add a New Workout</h3>
<label>Exercise Title:</label>
<input
type="text"
onChange={(e) => setTitle(e.target.value)}
value={title}
className={emptyFields.includes('title') ? 'error' : ''}
/>
<label>Load (in kg):</label>
<input
type="number"
onChange={(e) => setLoad(e.target.value)}
value={load}
className={emptyFields.includes('load') ? 'error' : ''}
/>
<label>Reps:</label>
<input
type="number"
onChange={(e) => setReps(e.target.value)}
value={reps}
className={emptyFields.includes('reps') ? 'error' : ''}
/>
<button>Add Workout</button>
{error && <div className="error">{error}</div>}
</form>
)
}
export default WorkoutForm
// frontend/src/components/WorkoutDetails.js
import { useWorkoutsContext } from '../hooks/useWorkoutsContext'
import { useAuthContext } from '../hooks/useAuthContext'
// date fns
import formatDistanceToNow from 'date-fns/formatDistanceToNow'
const WorkoutDetails = ({ workout }) => {
const { dispatch } = useWorkoutsContext()
const { user } = useAuthContext()
const handleClick = async () => {
if (!user) {
return
}
const response = await fetch('/api/workouts/' + workout._id, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${user.token}`
}
})
const json = await response.json()
if (response.ok) {
dispatch({type: 'DELETE_WORKOUT', payload: json})
}
}
return (
<div className="workout-details">
<h4>{workout.title}</h4>
<p><strong>Load (kg): </strong>{workout.load}</p>
<p><strong>Reps: </strong>{workout.reps}</p>
<p>{formatDistanceToNow(new Date(workout.createdAt), { addSuffix: true })}</p>
<span className='material-symbols-outlined' onClick={handleClick}>delete</span>
</div>
)
}
export default WorkoutDetails
#16 – Protecting React Routes
In this MERN auth tutorial we’ll protect some of the React routes from users that are not authenticated.
- 修改 App.js 檔案,匯入 Navigate、useAuthContext
- 修改 index.css 檔案
// frontend/src/App.js
import { BrowserRouter , Routes, Route, Navigate } from 'react-router-dom'
import { useAuthContext } from './hooks/useAuthContext'
// pages & components
import Home from './pages/Home'
import Login from './pages/Login';
import Signup from './pages/Signup';
import Navbar from './components/Navbar'
function App() {
const { user } = useAuthContext()
return (
<div className="App">
<BrowserRouter>
<Navbar />
<div className="pages">
<Routes>
<Route
path="/"
element={user ? <Home /> : <Navigate to="/login" />}
/>
<Route
path="/login"
element={!user ? <Login /> : <Navigate to="/" />}
/>
<Route
path="/signup"
element={!user ? <Signup /> : <Navigate to="/" />}
/>
</Routes>
</div>
</BrowserRouter>
</div>
);
}
export default App;
// frontend/src/index.css
/* google font */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;800&family=VT323&display=swap');
/* layout */
:root {
--primary: #1aac83;
--error: #e7195a;
}
body {
background: #f1f1f1;
margin: 0;
font-family: "Poppins";
}
header {
background: #fff;
}
header .container {
max-width: 1400px;
margin: 0 auto;
padding: 10px 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
header a {
color: #333;
text-decoration: none;
}
.pages{
max-width: 1400px;
padding: 20px;
margin: 0 auto;
}
/* homepage */
.home {
display: grid;
grid-template-columns: 3fr 1fr;
gap: 100px;
}
.workout-details {
background: #fff;
border-radius: 4px;
margin: 20px auto;
padding: 20px;
position: relative;
box-shadow: 2px 2px 5px rgba(0,0,0,0.05);
}
.workout-details h4 {
margin: 0 0 10px 0;
font-size: 1.2em;
color: var(--primary);
}
.workout-details p {
margin: 0;
font-size: 0.9em;
color: #555;
}
.workout-details span {
position: absolute;
top: 20px;
right: 20px;
cursor: pointer;
background: #f1f1f1;
padding: 6px;
border-radius: 50%;
color: #333;
}
/* new workout form */
label, input {
display: block;
}
input {
padding: 10px;
margin-top: 10px;
margin-bottom: 20px;
width: 100%;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
form button {
background: var(--primary);
border: 0;
color: #fff;
padding: 10px;
font-family: "Poppins";
border-radius: 4px;
cursor: pointer;
}
div.error {
padding: 10px;
background: #ffefef;
border: 1px solid var(--error);
color: var(--error);
border-radius: 4px;
margin: 20px 0;
}
input.error {
border: 1px solid var(--error);
}
/* navbar */
nav {
display: flex;
align-items: center;
}
nav a {
margin-left: 10px;
}
nav button {
background: #fff;
color: var(--primary);
border: 2px solid var(--primary);
padding: 6px 10px;
border-radius: 4px;
font-family: "Poppins";
cursor: pointer;
font-size: 1em;
margin-left: 10px;
}
/* auth forms */
form.signup, form.login {
max-width: 400px;
margin: 40px auto;
padding: 20px;
background: #fff;
border-radius: 4px;
}
#17 – Assigning Workouts to Users
- 刪除所有的 Workouts
- 修改 workoutModel.js 檔案,user_id
- 修改 workoutController.js 檔案,user_id
- 增加 Workout 內容
目前還是會在不同帳號看到相同內容
分別在不同帳號增加 Workout 內容 - 使用 API 測試工具 Postman,GET 方法
http://localhost:4000/api/workouts/
必須加上 Bearer
可以看到 Response 的 user_id 的差異 - 修改 workoutController.js,get all workouts 加入 user_id
- 登入不同的帳號確認 Workout 內容
問題: 會短暫看到其他人的 Workout 內容 - clear global workouts state
修改 useLogout.js 檔案,匯入 useWorkoutsContext
// backend/models/workoutModel.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const workoutSchema = new Schema({
title: {
type:String,
required: true
},
reps: {
type: Number,
required: true
},
load: {
type: Number,
required: true
},
user_id: {
type: String,
required: true
}
}, { timestamps: true })
module.exports = mongoose.model('Workout', workoutSchema)
// backend/controllers/workoutController.js
const Workout = require('../models/workoutModel')
const mongoose = require('mongoose')
// get all workouts
const getWorkouts = async (req, res) => {
const user_id = req.user._id
const workouts = await Workout.find({ user_id }).sort({createdAt: -1})
res.status(200).json(workouts)
}
// get a single workout
const getWorkout = async (req, res) => {
const { id } = req.params
if (!mongoose.Types.ObjectId.isValid(id)) {
return res.status(404).json({error: 'No such workout'})
}
const workout = await Workout.findById(id)
if (!workout) {
return res.status(404).json({error: 'No such workout'})
}
res.status(200).json(workout)
}
// create new workout
const createWorkout = async (req, res) => {
const { title, load, reps } = req.body
let emptyFields = []
if(!title) {
emptyFields.push('title')
}
if(!load) {
emptyFields.push('load')
}
if(!reps) {
emptyFields.push('reps')
}
if(emptyFields.length > 0) {
return res.status(400).json({ error: 'Please fill in all fields', emptyFields })
}
// add doc to db
try {
const user_id = req.user._id
const workout = await Workout.create({title, load, reps, user_id})
res.status(200).json(workout)
} catch (error) {
res.status(400).json({ error: error.message })
}
}
// delete a workout
const deleteWorkout = async (req, res) => {
const { id } = req.params
if (!mongoose.Types.ObjectId.isValid(id)) {
return res.status(404).json({error: 'No such workout'})
}
const workout = await Workout.findOneAndDelete({_id: id})
if (!workout) {
return res.status(404).json({error: 'No such workout'})
}
res.status(200).json(workout)
}
// update a workout
const updateWorkout = async (req, res) => {
const { id } = req.params
if (!mongoose.Types.ObjectId.isValid(id)) {
return res.status(404).json({error: 'No such workout'})
}
const workout = await Workout.findOneAndUpdate({_id: id}, {
...req.body
})
if (!workout) {
return res.status(404).json({error: 'No such workout'})
}
res.status(200).json(workout)
}
module.exports = {
getWorkouts,
getWorkout,
createWorkout,
deleteWorkout,
updateWorkout
}
// frontend/src/hooks/useLogout.js
import { useAuthContext } from './useAuthContext'
import { useWorkoutsContext } from './useWorkoutsContext'
export const useLogout = () => {
const { dispatch } = useAuthContext()
const { dispatch: workoutsDispatch } = useWorkoutsContext()
const logout = () => {
// remove user from storage
localStorage.removeItem('user')
// dispatch logout action
dispatch({type: 'LOGOUT'})
workoutsDispatch({type: 'SET_WORKOUTS', payload: null})
}
return {logout}
}