├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── src ├── index.js ├── setupTests.js ├── server │ ├── constants.js │ ├── database │ │ ├── users.json │ │ └── books.json │ ├── shared.js │ └── server.js ├── index.css ├── styles.css ├── util.js ├── App.js ├── components │ ├── MyFavorite.js │ ├── AppHeader.js │ ├── Books.js │ ├── Login.js │ ├── Users.js │ └── AddBook.js └── serviceWorker.js ├── .gitignore ├── package.json └── README.md /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deekshasharma/secure-js-api-jwt/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deekshasharma/secure-js-api-jwt/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deekshasharma/secure-js-api-jwt/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById("root") 11 | ); 12 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/server/constants.js: -------------------------------------------------------------------------------- 1 | module.exports.JWT_OPTIONS = { 2 | MEMBER_AUDIENCE: ["SHOW_FAVORITE", "LOGIN", "SHOW_BOOKS"], 3 | ADMIN_AUDIENCE: [ 4 | "SHOW_FAVORITE", 5 | "LOGIN", 6 | "SHOW_BOOKS", 7 | "ADD_BOOK", 8 | "SHOW_USERS", 9 | ], 10 | }; 11 | 12 | module.exports.ADD_BOOK = "ADD_BOOK"; 13 | module.exports.SHOW_USERS = "SHOW_USERS"; 14 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | *.idea/ 25 | *.env -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .Content { 6 | display: flex; 7 | align-items: center; 8 | justify-content: space-between; 9 | min-height: 20vh; 10 | margin-top: 12vh; 11 | } 12 | 13 | .Book { 14 | padding: 2em; 15 | margin: 2em; 16 | border: 2px solid gray; 17 | } 18 | 19 | .User { 20 | border: 2px solid gray; 21 | padding: 1em; 22 | margin-bottom: 2em; 23 | border-radius: 10px; 24 | } 25 | 26 | .AddBook { 27 | display: flex; 28 | align-items: center; 29 | min-height: 20vh; 30 | padding: 7em; 31 | } 32 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/server/database/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "f2775f38-92fc-42e5-98a5-b137a0887a40", 4 | "username": "deeksha30", 5 | "key": "$2b$10$ph9/OK1lN/.9KzkeGKGPK.bxOkqJ2b9A2AqH/5iPkS7dmqAnUn.vi", 6 | "firstName": "Deeksha", 7 | "lastName": "Sharma", 8 | "favorite": [ 9 | "6cc12b5e-cb5e-11ea-87d0-0242ac130003", 10 | "765384e6-cb5e-11ea-87d0-0242ac130003" 11 | ], 12 | "role": "member" 13 | }, 14 | { 15 | "id": "677c96e2-cb5e-11ea-87d0-0242ac130003", 16 | "username": "zenmade23", 17 | "key": "$2b$10$ruGV.xw6P0zuPUa0vt694eLO5LwckcxFZ1NfzdzDQKF12E2240vZy", 18 | "firstName": "Amy", 19 | "lastName": "Robinson", 20 | "favorite": ["722f584a-cb5e-11ea-87d0-0242ac130003"], 21 | "role": "admin" 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | let jwtDecode = require("jwt-decode"); 2 | 3 | export const updateAppSettings = (token) => { 4 | localStorage.clear(); 5 | if (token) { 6 | localStorage.setItem("displayName", jwtDecode(token)["sub"]); 7 | localStorage.setItem("token", token); 8 | } 9 | }; 10 | 11 | export const isMember = () => { 12 | const token = localStorage.getItem("token"); 13 | if (token) { 14 | const audience = jwtDecode(token)["aud"]; 15 | return !audience.includes("SHOW_USERS") && !audience.includes("ADD_BOOK"); 16 | } 17 | }; 18 | 19 | export const constructHeader = (contentType) => { 20 | const auth = "Bearer " + localStorage.getItem("token") || ""; 21 | return contentType 22 | ? { "Content-type": contentType, Authorization: auth } 23 | : { Authorization: auth }; 24 | }; 25 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./styles.css"; 3 | import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; 4 | import { Login } from "./components/Login"; 5 | import { Books } from "./components/Books"; 6 | import { Users } from "./components/Users"; 7 | import { AddBook } from "./components/AddBook"; 8 | import { MyFavorite } from "./components/MyFavorite"; 9 | 10 | export default function App() { 11 | return ( 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secure-js-api-jwt", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.11.0", 7 | "@material-ui/icons": "^4.9.1", 8 | "@testing-library/jest-dom": "^4.2.4", 9 | "@testing-library/react": "^9.3.2", 10 | "@testing-library/user-event": "^7.1.2", 11 | "base-64": "^0.1.0", 12 | "bcrypt": "^5.0.0", 13 | "cookie-parser": "^1.4.5", 14 | "cors": "^2.8.5", 15 | "dotenv": "^8.2.0", 16 | "express": "^4.17.1", 17 | "jsonfile": "^6.0.1", 18 | "jsonwebtoken": "^8.5.1", 19 | "jwt-decode": "^2.2.0", 20 | "prettier": "^2.0.5", 21 | "react": "^16.13.1", 22 | "react-dom": "^16.13.1", 23 | "react-router-dom": "^5.2.0", 24 | "react-scripts": "3.4.1", 25 | "uuidv4": "^6.2.0" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts start", 29 | "build": "react-scripts build", 30 | "test": "react-scripts test", 31 | "eject": "react-scripts eject" 32 | }, 33 | "eslintConfig": { 34 | "extends": "react-app" 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/server/database/books.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "6cc12b5e-cb5e-11ea-87d0-0242ac130003", 4 | "name": "Surrounded by idiots", 5 | "author": "Thomas Erikson" 6 | }, 7 | { 8 | "id": "722f584a-cb5e-11ea-87d0-0242ac130003", 9 | "name": "Stillness is the key", 10 | "author": "Ryan Holiday" 11 | }, 12 | { 13 | "id": "765384e6-cb5e-11ea-87d0-0242ac130003", 14 | "name": "The Tipping Point", 15 | "author": "Malcolm Gladwell" 16 | }, 17 | { 18 | "id": "7a4fe9ea-cb5e-11ea-87d0-0242ac130003", 19 | "name": "Principles for Success", 20 | "author": "Ray Dalio" 21 | }, 22 | { 23 | "id": "89a4ke9ea-cb5e-11ea-87d0-0242ac189908", 24 | "name": "Being Mortal", 25 | "author": "Atul Gawande" 26 | }, 27 | { 28 | "id": "228877ef-e264-46a2-948f-5a28dd592322", 29 | "name": "Digital Minimalisml", 30 | "author": "Cal Newport" 31 | }, 32 | { 33 | "id": "19a36fd2-9823-4498-9ea2-261c549d806d", 34 | "name": "The 1% Rule", 35 | "author": "Tommy Baker" 36 | }, 37 | { 38 | "id": "79ebfeb6-eff0-4ea8-b4fd-92ca7dcebac4", 39 | "name": "Into Thin Air", 40 | "author": "Jon Krakauer" 41 | }, 42 | { 43 | "id": "6e19e4e7-b86b-4255-8b7e-d84d12d49f16", 44 | "name": "Thinking Fast and Slow", 45 | "author": "Daniel Kahneman" 46 | }, 47 | { 48 | "id": "90c9a136-0b40-40bc-9c93-ab44befe21ad", 49 | "name": "Company of One", 50 | "author": "Paul Jarvis" 51 | }, 52 | { 53 | "id": "1e3ba4f8-6ef7-4065-9240-abb67fe616c6", 54 | "name": "Motivation Manifesto", 55 | "author": "Brendon Burchard" 56 | }, 57 | { 58 | "id": "445c3cbc-8cf9-41e1-be55-6508a6a5c374", 59 | "name": "Disrupt You!", 60 | "author": "Jay Samit" 61 | } 62 | ] 63 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/MyFavorite.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Grid, Paper, Typography } from "@material-ui/core"; 3 | import "../styles.css"; 4 | import { AppHeader } from "./AppHeader"; 5 | import { useHistory } from "react-router-dom"; 6 | import { constructHeader, updateAppSettings } from "../util"; 7 | const url = "http://localhost:5000/favorite"; 8 | 9 | export const MyFavorite = () => { 10 | const [favBooks, setFavBooks] = useState([]); 11 | const history = useHistory(); 12 | 13 | const redirect = () => { 14 | localStorage.clear(); 15 | history.push("/login"); 16 | }; 17 | 18 | useEffect(() => { 19 | fetch(url, { headers: constructHeader() }) 20 | .then((res) => (res.status === 401 ? redirect() : res.json())) 21 | .then((json) => { 22 | if (json) { 23 | updateAppSettings(json.token); 24 | setFavBooks([...json.favorites]); 25 | } 26 | }) 27 | .catch((err) => 28 | console.log("Error getting favorite books ", err.message) 29 | ); 30 | // eslint-disable-next-line react-hooks/exhaustive-deps 31 | }, []); 32 | 33 | return ( 34 |
35 | 36 | 37 | 38 | 39 | Your Favorite Books! 40 | 41 | 👍 42 | 43 | 44 | 45 | 46 | {favBooks.map((book, key) => { 47 | return ( 48 | 49 | 50 | 51 | {book.name} 52 | 53 | 54 | {book.author} 55 | 56 | 57 | 58 | ); 59 | })} 60 | 61 | 62 |
63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /src/components/AppHeader.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "../styles.css"; 3 | import { 4 | AppBar, 5 | Tab, 6 | Tabs, 7 | MenuItem, 8 | IconButton, 9 | Menu, 10 | Toolbar, 11 | } from "@material-ui/core"; 12 | import { useHistory } from "react-router-dom"; 13 | import AccountCircle from "@material-ui/icons/AccountCircle"; 14 | import { constructHeader, isMember } from "../util"; 15 | const url = "http://localhost:5000/logout"; 16 | 17 | export const AppHeader = ({ tabValue }) => { 18 | const tabs = ["/books", "/favorite", "/book", "/users"]; 19 | const [anchorEl, setAnchorEl] = React.useState(null); 20 | const open = Boolean(anchorEl); 21 | const history = useHistory(); 22 | const shouldDisable = isMember(); 23 | 24 | const handleClick = (event, newValue) => history.push(tabs[newValue]); 25 | 26 | const handleMenu = (event) => setAnchorEl(event.currentTarget); 27 | 28 | const handleClose = () => { 29 | setAnchorEl(null); 30 | }; 31 | 32 | const onClickLogout = () => { 33 | fetch(url, { headers: constructHeader() }).then((res) => { 34 | localStorage.clear(); 35 | history.push("/login"); 36 | }); 37 | }; 38 | 39 | return ( 40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
50 | 57 | 58 | 59 | 65 | {localStorage.getItem("displayName")} 66 | Logout 67 | 68 | 69 | 70 |
71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /src/components/Books.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Button, Grid, Paper, Typography } from "@material-ui/core"; 3 | import "../styles.css"; 4 | import { AppHeader } from "./AppHeader"; 5 | import { constructHeader, updateAppSettings } from "../util"; 6 | import { useHistory } from "react-router-dom"; 7 | const url = "http://localhost:5000/books"; 8 | 9 | export const Books = () => { 10 | const [books, setBooks] = useState([]); 11 | const history = useHistory(); 12 | 13 | const redirect = () => { 14 | localStorage.clear(); 15 | history.push("/login"); 16 | }; 17 | 18 | useEffect(() => { 19 | fetch(url, { headers: constructHeader() }) 20 | .then((res) => (res.status === 401 ? redirect() : res.json())) 21 | .then((json) => { 22 | if (json) { 23 | updateAppSettings(json.token); 24 | setBooks([...json.books]); 25 | } 26 | }) 27 | .catch((err) => console.log("Error fetching books ", err.message)); 28 | // eslint-disable-next-line react-hooks/exhaustive-deps 29 | }, []); 30 | 31 | return ( 32 |
33 | 34 | 35 | 36 | 37 | Curated Books! 38 | 39 | 📚 40 | 41 | 42 | 43 | 44 | {books.map((book, key) => { 45 | return ( 46 | console.log("My Favorite")} 53 | /> 54 | ); 55 | })} 56 | 57 | 58 |
59 | ); 60 | }; 61 | 62 | const Book = ({ name, id, author, onClick }) => { 63 | return ( 64 | 65 | 66 | 67 | {name} 68 | 69 | 70 | {author} 71 | 72 | 80 | 81 | 82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /src/components/Login.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Grid, Typography, TextField, Button } from "@material-ui/core"; 3 | import { useHistory } from "react-router-dom"; 4 | import { updateAppSettings } from "../util"; 5 | let base64 = require("base-64"); 6 | let headers = new Headers(); 7 | const url = "http://localhost:5000/login"; 8 | 9 | export const Login = () => { 10 | const [userName, setUserName] = useState(""); 11 | const [password, setPassword] = useState(""); 12 | const [loginError, setLoginError] = useState(""); 13 | const history = useHistory(); 14 | 15 | const onChangeUsername = (username) => setUserName(username); 16 | const onChangePassword = (password) => setPassword(password); 17 | 18 | const onClickLogin = () => { 19 | headers.set( 20 | "Authorization", 21 | "Basic " + base64.encode(userName + ":" + password) 22 | ); 23 | fetch(url, { headers: headers, method: "POST" }) 24 | .then((res) => res.json()) 25 | .then((json) => { 26 | if (json.message) setLoginError(json.message); 27 | else { 28 | updateAppSettings(json.token); 29 | history.push("/books"); 30 | } 31 | }) 32 | .catch((err) => console.log("Error logging into app ", err.message)); 33 | }; 34 | 35 | return ( 36 | 42 | 43 | 44 | Welcome to Bookie! 45 | 46 | 📚 47 | 48 | 49 | 50 | 51 | onChangeUsername(e.target.value)} 56 | /> 57 | 58 | 59 | onChangePassword(e.target.value)} 65 | /> 66 | 67 | 68 | 77 | 78 | 79 | 80 | {loginError} 81 | 82 | 83 | 84 | ); 85 | }; 86 | -------------------------------------------------------------------------------- /src/components/Users.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Avatar, Grid, Typography } from "@material-ui/core"; 3 | import "../styles.css"; 4 | import { AppHeader } from "./AppHeader"; 5 | import { constructHeader, isMember, updateAppSettings } from "../util"; 6 | import { useHistory } from "react-router-dom"; 7 | const url = "http://localhost:5000/users"; 8 | 9 | export const Users = () => { 10 | const [users, setUsers] = useState([]); 11 | const history = useHistory(); 12 | const showPage = !isMember(); 13 | 14 | const redirect = () => { 15 | localStorage.clear(); 16 | history.push("/login"); 17 | }; 18 | 19 | useEffect(() => { 20 | fetch(url, { headers: constructHeader() }) 21 | .then((res) => (res.status === 401 ? redirect() : res.json())) 22 | .then((json) => { 23 | if (json) { 24 | updateAppSettings(json.token); 25 | setUsers([...json.users]); 26 | } 27 | }) 28 | .catch((err) => console.log("Error fetching users ", err.message)); 29 | // eslint-disable-next-line react-hooks/exhaustive-deps 30 | }, []); 31 | 32 | return ( 33 |
34 | 35 | {!showPage &&
} 36 | {showPage && ( 37 | 38 | 39 | 40 | Bookie Users! 41 | 42 | 🤓🤠 43 | 44 | 45 | 46 | 47 | {users.map((user, key) => { 48 | return ( 49 | 56 | ); 57 | })} 58 | 59 | 60 | )} 61 |
62 | ); 63 | }; 64 | 65 | const User = ({ firstName, lastName, userName, role }) => { 66 | return ( 67 | 68 | 69 | 75 | 76 | {firstName.charAt(0)} 77 | 78 | 79 | {userName + " (" + role + ") "} 80 | 81 | 82 | 83 | 84 | 85 | {firstName + " " + lastName} 86 | 87 | 88 | 89 | ); 90 | }; 91 | -------------------------------------------------------------------------------- /src/server/shared.js: -------------------------------------------------------------------------------- 1 | const jsonfile = require("jsonfile"); 2 | const users = "./database/users.json"; 3 | const inventory = "./database/books.json"; 4 | const bcrypt = require("bcrypt"); 5 | const jwt = require("jsonwebtoken"); 6 | const Constants = require("./constants"); 7 | 8 | var getUserByUsername = (exports.getUserByUsername = async function (username) { 9 | try { 10 | const allUsers = await jsonfile.readFile(users); 11 | const filteredUserArray = allUsers.filter( 12 | (user) => user.username === username 13 | ); 14 | return filteredUserArray.length === 0 ? {} : filteredUserArray[0]; 15 | } catch (err) { 16 | console.log("Error reading users: ", err.message); 17 | } 18 | }); 19 | 20 | exports.isEmptyObject = (object) => Object.entries(object).length === 0; 21 | 22 | exports.isPasswordCorrect = async function (key, password) { 23 | return bcrypt.compare(password, key).then((result) => result); 24 | }; 25 | 26 | exports.getAllBooks = async function () { 27 | try { 28 | return await jsonfile.readFile(inventory); 29 | } catch (err) { 30 | console.log("Error reading books: ", err); 31 | } 32 | }; 33 | 34 | exports.getAllUsers = async function () { 35 | try { 36 | const allUsers = await jsonfile.readFile(users); 37 | let updatedUsers = []; 38 | allUsers.forEach((user) => { 39 | updatedUsers.push({ 40 | username: user.username, 41 | firstName: user.firstName, 42 | lastName: user.lastName, 43 | role: user.role, 44 | }); 45 | }); 46 | return updatedUsers; 47 | } catch (err) { 48 | console.log("Error reading users from datastore ", err.message); 49 | } 50 | }; 51 | 52 | exports.addBook = async function (book) { 53 | try { 54 | const allBooks = await jsonfile.readFile(inventory); 55 | allBooks.push(book); 56 | return await jsonfile.writeFile(inventory, allBooks); 57 | } catch (err) { 58 | return err; 59 | } 60 | }; 61 | 62 | const getUsernameFromToken = (token) => jwt.decode(token)["sub"]; 63 | 64 | exports.getAudienceFromToken = (token) => jwt.decode(token)["aud"]; 65 | 66 | exports.generateToken = async function (prevToken, userName) { 67 | const name = userName || getUsernameFromToken(prevToken); 68 | const user = await getUserByUsername(name); 69 | const options = { 70 | algorithm: process.env.ALGORITHM, 71 | expiresIn: process.env.EXPIRY, 72 | issuer: process.env.ISSUER, 73 | subject: userName || user.username, 74 | audience: 75 | user.role === "admin" 76 | ? Constants.JWT_OPTIONS.ADMIN_AUDIENCE 77 | : Constants.JWT_OPTIONS.MEMBER_AUDIENCE, 78 | }; 79 | return jwt.sign({}, process.env.SECRET, options); 80 | }; 81 | 82 | exports.verifyToken = (req, res, next) => { 83 | if (!req.headers.authorization) 84 | res.status(401).send({ message: "Not authorized to access data" }); 85 | else { 86 | const token = req.headers.authorization.split(" ")[1]; 87 | if (!token) 88 | res.status(401).send({ message: "Not Authorized to access data" }); 89 | else { 90 | jwt.verify(token, process.env.SECRET, function (err) { 91 | if (err) { 92 | res.status(401).send({ message: "Please login again" }); 93 | } else next(); 94 | }); 95 | } 96 | } 97 | }; 98 | 99 | exports.getFavoriteBooksForUser = async function (token) { 100 | const username = getUsernameFromToken(token); 101 | const user = await getUserByUsername(username); 102 | const favoriteBookIds = user["favorite"]; 103 | const favoriteBooks = []; 104 | if (favoriteBookIds.length === 0) return favoriteBooks; 105 | const allBooks = await jsonfile.readFile(inventory); 106 | favoriteBookIds.forEach((id) => 107 | favoriteBooks.push(allBooks.filter((book) => id === book.id)[0]) 108 | ); 109 | return favoriteBooks; 110 | }; 111 | -------------------------------------------------------------------------------- /src/components/AddBook.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { 3 | Grid, 4 | Typography, 5 | Button, 6 | TextField, 7 | Snackbar, 8 | } from "@material-ui/core"; 9 | import "../styles.css"; 10 | import { AppHeader } from "./AppHeader"; 11 | import { useHistory } from "react-router-dom"; 12 | import { constructHeader, isMember, updateAppSettings } from "../util"; 13 | 14 | const url = "http://localhost:5000/book"; 15 | 16 | export const AddBook = () => { 17 | const [book, setBookName] = useState(""); 18 | const [author, setAuthorName] = useState(""); 19 | const [open, setOpen] = useState(false); 20 | const [message, setMessage] = useState(""); 21 | const history = useHistory(); 22 | const showPage = !isMember(); 23 | 24 | useEffect(() => { 25 | if (!localStorage.getItem("token")) history.push("/login"); 26 | // eslint-disable-next-line react-hooks/exhaustive-deps 27 | }, []); 28 | 29 | const onChangeBookName = (book) => setBookName(book); 30 | 31 | const onChangeAuthorName = (author) => setAuthorName(author); 32 | 33 | const redirect = () => { 34 | localStorage.clear(); 35 | history.push("/login"); 36 | }; 37 | 38 | const clearTextFields = () => { 39 | setBookName(""); 40 | setAuthorName(""); 41 | }; 42 | 43 | const onClick = () => { 44 | const bookData = { name: book, author: author }; 45 | fetch(url, { 46 | headers: constructHeader("application/json"), 47 | method: "POST", 48 | body: JSON.stringify(bookData), 49 | }) 50 | .then((res) => { 51 | if (res.status === 401) redirect(); 52 | else { 53 | setOpen(true); 54 | if (res.status === 200) clearTextFields(); 55 | } 56 | return res.json(); 57 | }) 58 | .then((json) => { 59 | if (json) { 60 | updateAppSettings(json.token || ""); 61 | setMessage(json.message || ""); 62 | } 63 | }) 64 | .catch((err) => console.log("Error adding book ", err.message)); 65 | }; 66 | 67 | const handleClose = () => setOpen(false); 68 | 69 | return ( 70 |
71 | 72 | {!showPage &&
} 73 | {showPage && ( 74 | 75 | 76 | 77 | Add New Book! 78 | 79 | 📘 80 | 81 | 82 | 83 | 84 | onChangeBookName(e.target.value)} 90 | /> 91 | 92 | 93 | onChangeAuthorName(e.target.value)} 99 | /> 100 | 101 | 102 | 111 | 112 | 113 | 119 | 120 | 121 | )} 122 |
123 | ); 124 | }; 125 | -------------------------------------------------------------------------------- /src/server/server.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config({ path: "./variables.env" }); 2 | const express = require("express"); 3 | const cors = require("cors"); 4 | const { uuid } = require("uuidv4"); 5 | const { 6 | getUserByUsername, 7 | isEmptyObject, 8 | isPasswordCorrect, 9 | getAllBooks, 10 | getAllUsers, 11 | addBook, 12 | verifyToken, 13 | getFavoriteBooksForUser, 14 | getAudienceFromToken, 15 | generateToken, 16 | } = require("./shared"); 17 | const Constants = require("./constants"); 18 | const app = express(); 19 | const port = process.env.PORT || 5000; 20 | app.listen(port, () => console.log(`Listening on port ${port}`)); 21 | app.use(express.json()); 22 | app.use(cors()); 23 | 24 | app.get("/users", verifyToken, (req, res) => { 25 | const token = req.headers.authorization.split(" ")[1]; 26 | if (getAudienceFromToken(token).includes(Constants.SHOW_USERS)) { 27 | getAllUsers().then((users) => { 28 | if (users && users.length > 0) { 29 | generateToken(token, null).then((token) => { 30 | res.status(200).send({ users: users, token: token }); 31 | }); 32 | } else res.status(500).send({ users: [], token: token }); 33 | }); 34 | } else 35 | res 36 | .status(403) 37 | .send({ message: "Not authorized to view users", token: token }); 38 | }); 39 | 40 | app.get("/books", verifyToken, (req, res) => { 41 | const token = req.headers.authorization.split(" ")[1]; 42 | getAllBooks().then((books) => { 43 | if (books && books.length > 0) { 44 | generateToken(token, null).then((token) => { 45 | res.status(200).send({ books: books, token: token }); 46 | }); 47 | } else res.status(500).send({ books: [], token: token }); 48 | }); 49 | }); 50 | 51 | app.post("/login", (req, res) => { 52 | let base64Encoding = req.headers.authorization.split(" ")[1]; 53 | let credentials = Buffer.from(base64Encoding, "base64").toString().split(":"); 54 | const username = credentials[0]; 55 | const password = credentials[1]; 56 | getUserByUsername(username).then((user) => { 57 | if (user && !isEmptyObject(user)) { 58 | isPasswordCorrect(user.key, password).then((result) => { 59 | if (!result) 60 | res 61 | .status(401) 62 | .send({ message: "username or password is incorrect" }); 63 | else { 64 | generateToken(null, username).then((token) => { 65 | res 66 | .status(200) 67 | .send({ username: user.username, role: user.role, token: token }); 68 | }); 69 | } 70 | }); 71 | } else 72 | res.status(401).send({ message: "username or password is incorrect" }); 73 | }); 74 | }); 75 | 76 | app.get("/logout", verifyToken, (req, res) => { 77 | res.status(200).send({ message: "Signed out" }); 78 | }); 79 | 80 | app.get("/favorite", verifyToken, (req, res) => { 81 | const token = req.headers.authorization.split(" ")[1]; 82 | getFavoriteBooksForUser(token).then((books) => { 83 | generateToken(token, null).then((token) => { 84 | res.status(200).send({ favorites: books, token: token }); 85 | }); 86 | }); 87 | }); 88 | 89 | app.post("/book", verifyToken, (req, res) => { 90 | const token = req.headers.authorization.split(" ")[1]; 91 | if (getAudienceFromToken(token).includes(Constants.ADD_BOOK)) { 92 | addBook({ name: req.body.name, author: req.body.author, id: uuid() }).then( 93 | (err) => { 94 | if (err) res.status(500).send({ message: "Cannot add this book" }); 95 | else { 96 | generateToken(token, null).then((token) => { 97 | res 98 | .status(200) 99 | .send({ message: "Book added successfully", token: token }); 100 | }); 101 | } 102 | } 103 | ); 104 | } else 105 | res 106 | .status(403) 107 | .send({ message: "Not authorized to add a book", token: token }); 108 | }); 109 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | # System Requirements for Course 4 | Before running the project, please make sure you have the following: 5 | 6 | - Node.js LTS version which can be found [here](https://nodejs.org/en/download/). The course is upto date using this version at all times. 7 | - Please refer to the help section below to resolve most common questions. 8 | 9 | # Help 10 | 11 | ### - Can I use my own IDE to develop the project during the course ? 12 | Yes, feel free to use your own IDE for the course. 13 | 14 | ### - How do I check my Node version ? 15 | To check your current Node.js version, open your terminal and type the command below to see your current Node.js version. 16 | ``` 17 | node -v 18 | ``` 19 | 20 | ### - How do I install Node.js LTS version on my machine ? 21 | If you do not have the Node.js LTS version on your machine, you can download using either of the following: 22 | 1. Please go [here](https://nodejs.org/en/download/) and download the LTS version of Node.js installable file for your operating system. 23 | 24 | 2. Alternatively, you can use Node Version Manager (`nvm`) to install LTS version of Node.js in case 25 | you do not want to delete the existing Node version on your machine.
26 | `NVM` allows you to use multiple Node versions on your machine and prevent disrupting other 27 | projects you may be running with different `Node` versions.
28 | 29 | ### - How do I use nvm to install Node.js ? 30 | Click on [this link](https://github.com/nvm-sh/nvm) and follow the instructions provided in their README.md file 31 | to install nvm on your machine depending on your platform. 32 | 33 | ### - Should I install npm separately ? 34 | No, `npm` comes with `Node.js` 35 | No matter what approach you use to install Node.js, npm will always come with it. 36 | 37 | ### - How do I check my npm version ? 38 | Open your terminal and type the command below to get your npm version. 39 | ``` 40 | npm -v 41 | ``` 42 | 43 | ### - What version of npm comes with LTS version of Node.js ? 44 | Click on [this click](https://nodejs.org/en/download/) and the `npm` version should be mentioned under the title _**Downloads**_. 45 | You must ensure that the npm version and node version should match with what is mentioned on this official page. 46 | 47 | ### - What is the version of Material-UI used for this course ? 48 | This course uses v4.0.0 of Material-UI library 49 | 50 | ### - What is the React version need for this course ? 51 | ******************** 52 | We are using `react` >=16.8.0 and `react-dom` >= 16.8.0 at all times. All the dependecies needed to run this project will be available in package.json 53 | file. You do not have to worry about finding the peer dependencies to run the project. 54 | All you need are the 2 following commands to get started as long as you have the right version of Node. 55 | 56 | `npm install` 57 | 58 | `npm start` 59 | 60 | Alternatively, you can also use `yarn` command. 61 | 62 | `yarn install` 63 | 64 | `yarn start` 65 | 66 | 67 | ### - Do I need Webpack or Babel to run this project ? 68 | No, You don’t need to install or configure tools. You just need the LTS version of Node.js and the npm version that comes with it. 69 | They are preconfigured and hidden so that you can focus on the code. 70 | 71 | ### - Which browser are we using for this course ? 72 | We shall be using the latest version of Chrome as of today. Be sure to install/update Chrome on your computer. 73 | 74 | ### - How do I open Chrome Browser in Mobile View ? 75 | - To open Chrome in Mobile view mode using Mac, press ```Command+Option+i``` 76 | 77 | ### - How do I run the Client application in browser? 78 | To run the app in the development mode, 79 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 80 | We are using Chrome Developer console in this course. 81 | 82 | ### Where is the Node server running? 83 | Your server is will run at port 5000 and the URL for server APIs is [http://localhost:5000](http://localhost:5000). 84 | 85 | ### Is it mandatory to use Material-UI library for Styling? 86 | No, feel free to use simple CSS for styling or any other styling library you like. The focus of this course is to 87 | understand the use of JSON Web Token to secure the backend APIs and the front end styling is just an 88 | extra beautification layer. 89 | 90 | ### How to test the backend APIs using CLI and Postman? 91 | Install [Postman](https://www.postman.com/) on your machine and start creating the collections where you 92 | can keep track of the API end points currently under testing. They have extensive documentation on how to use the tool. 93 | in case your are interested. 94 | 95 | ### I am running into issues when installing `bcrypt` library. How do I resolve them? 96 | Install `bcrypt` from `npm` using the commands below. 97 | Note that `node-gyp` should be installed globally for the most recent Operating System on Mac which is Catalina. 98 | ```bash 99 | npm install -g node-gyp 100 | npm install --save bcrypt 101 | ``` 102 | 103 | ### What user credentials are used in the Bookie App? 104 | Below are the credentials you may want to use when logging to the app as member or admin. 105 | 106 | **Member** 107 | ```bash 108 | deeksha30 109 | kdje89#$% 110 | ``` 111 | 112 | **Admin** 113 | ```bash 114 | zenmade23 115 | 728193kfej**( 116 | ``` 117 | 118 | ### How do I start the server? 119 | Go inside the `server/` directory and run teh command below. 120 | ```bash 121 | node server.js 122 | ``` 123 | **Note:** Make sure you restart your server each time you checkout a new branch for every module and for 124 | every code change in the server side code. 125 | 126 | 127 | # Git Branches 128 | Checkout the branches listed below as you progress through different modules. 129 | 130 | ### MODULE 02 131 | `module02_jwt_security` 132 | 133 | ### MODULE 03 134 | `module03_jwt_security` 135 | 136 | ### MODULE 04 137 | There are 2 git branches used in this module. 138 | 139 | To send JWT in a Cookie, checkout `module04_jwt_security_cookies` 140 | 141 | To send JWT in Auth Header Bearer Token, checkout `module04_jwt_security_bearer_token` 142 | 143 | Below are the contents of `variables.env` file. 144 | ``` 145 | SECRET=")x2f-l-opsnd)w!!z2m7ykvony99pt@6@6m+=q2uk3%w8*7$ow" 146 | ALGORITHM="HS256" 147 | ISSUER="BOOKIE_ORG" 148 | EXPIRY="1h" 149 | ``` 150 | 151 | 152 | ### MODULE 05 153 | `module05_jwt_security_bearer_token_client` 154 | 155 | # Resources 156 | 157 | - [Proxying API Requests in React Development](https://create-react-app.dev/docs/proxying-api-requests-in-development/) 158 | - [JWT Debugger](https://jwt.io/) 159 | - [RFC 7519 - JSON Web Token (JWT) - IETF Tools](https://tools.ietf.org/html/rfc7519) 160 | - [Using HTTP cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) 161 | - [Decode JWT on the client using with jwt-decode](https://github.com/auth0/jwt-decode) 162 | - [GitHub code samples for usage of JWT by Auth0](https://github.com/auth0/jwt-handbook-samples/blob/master/stateless-sessions/app.js) 163 | 164 | 165 | Below are some good questions and answers by the community on StackOverflow. 166 | - [Authentication: JWT usage vs session](https://stackoverflow.com/questions/43452896/authentication-jwt-usage-vs-session) 167 | - [Where to save a JWT in a browser-based application and how to use it](https://stackoverflow.com/questions/26340275/where-to-save-a-jwt-in-a-browser-based-application-and-how-to-use-it) 168 | - [JavaScript and third party cookies](https://stackoverflow.com/questions/3363495/javascript-and-third-party-cookies) 169 | - [Which way to create cookie, by frontend or backend?](https://stackoverflow.com/questions/26082511/which-way-to-create-cookie-by-frontend-or-backend) 170 | - [How does server return JWT token to the client?](https://stackoverflow.com/questions/51503024/how-does-server-return-jwt-token-to-the-client) 171 | 172 | 173 | 174 | 175 | 176 | --------------------------------------------------------------------------------