├── Develop ├── .npmrc ├── client │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.jsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── components │ │ │ ├── LoginForm.jsx │ │ │ ├── Navbar.jsx │ │ │ └── SignupForm.jsx │ │ ├── main.jsx │ │ ├── pages │ │ │ ├── SavedBooks.jsx │ │ │ └── SearchBooks.jsx │ │ └── utils │ │ │ ├── API.js │ │ │ ├── auth.js │ │ │ └── localStorage.js │ └── vite.config.js ├── package.json └── server │ ├── config │ └── connection.js │ ├── controllers │ └── user-controller.js │ ├── models │ ├── Book.js │ ├── User.js │ └── index.js │ ├── package.json │ ├── routes │ ├── api │ │ ├── index.js │ │ └── user-routes.js │ └── index.js │ ├── server.js │ └── utils │ └── auth.js └── README.md /Develop/.npmrc: -------------------------------------------------------------------------------- 1 | production=false 2 | 3 | progress=false 4 | -------------------------------------------------------------------------------- /Develop/client/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:react/recommended', 6 | 'plugin:react/jsx-runtime', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 10 | settings: { react: { version: '18.2' } }, 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': 'warn', 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /Develop/client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /Develop/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Develop/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "start": "vite", 9 | "build": "vite build", 10 | "lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0", 11 | "preview": "vite preview" 12 | }, 13 | "dependencies": { 14 | "bootstrap": "^5.2.3", 15 | "jwt-decode": "^3.1.2", 16 | "react": "^18.2.0", 17 | "react-bootstrap": "^2.7.4", 18 | "react-dom": "^18.2.0", 19 | "react-router-dom": "^6.11.2" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "^18.0.28", 23 | "@types/react-dom": "^18.0.11", 24 | "@vitejs/plugin-react": "^4.2.1", 25 | "eslint": "^8.38.0", 26 | "eslint-plugin-react": "^7.32.2", 27 | "eslint-plugin-react-hooks": "^4.6.0", 28 | "eslint-plugin-react-refresh": "^0.3.4", 29 | "vite": "^5.1.6" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Develop/client/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Develop/client/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Rubik', sans-serif; 3 | } 4 | 5 | h1, 6 | h2, 7 | h3, 8 | h4, 9 | h5, 10 | h6 { 11 | font-family: 'Rubik Mono One', sans-serif; 12 | } 13 | 14 | .small { 15 | font-weight: 300; 16 | font-style: italic; 17 | } 18 | -------------------------------------------------------------------------------- /Develop/client/src/App.jsx: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | import { Outlet } from 'react-router-dom'; 3 | 4 | import Navbar from './components/Navbar'; 5 | 6 | function App() { 7 | return ( 8 | <> 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /Develop/client/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Develop/client/src/components/LoginForm.jsx: -------------------------------------------------------------------------------- 1 | // see SignupForm.js for comments 2 | import { useState } from 'react'; 3 | import { Form, Button, Alert } from 'react-bootstrap'; 4 | 5 | import { loginUser } from '../utils/API'; 6 | import Auth from '../utils/auth'; 7 | 8 | const LoginForm = () => { 9 | const [userFormData, setUserFormData] = useState({ email: '', password: '' }); 10 | const [validated] = useState(false); 11 | const [showAlert, setShowAlert] = useState(false); 12 | 13 | const handleInputChange = (event) => { 14 | const { name, value } = event.target; 15 | setUserFormData({ ...userFormData, [name]: value }); 16 | }; 17 | 18 | const handleFormSubmit = async (event) => { 19 | event.preventDefault(); 20 | 21 | // check if form has everything (as per react-bootstrap docs) 22 | const form = event.currentTarget; 23 | if (form.checkValidity() === false) { 24 | event.preventDefault(); 25 | event.stopPropagation(); 26 | } 27 | 28 | try { 29 | const response = await loginUser(userFormData); 30 | 31 | if (!response.ok) { 32 | throw new Error('something went wrong!'); 33 | } 34 | 35 | const { token, user } = await response.json(); 36 | console.log(user); 37 | Auth.login(token); 38 | } catch (err) { 39 | console.error(err); 40 | setShowAlert(true); 41 | } 42 | 43 | setUserFormData({ 44 | username: '', 45 | email: '', 46 | password: '', 47 | }); 48 | }; 49 | 50 | return ( 51 | <> 52 |
53 | setShowAlert(false)} show={showAlert} variant='danger'> 54 | Something went wrong with your login credentials! 55 | 56 | 57 | Email 58 | 66 | Email is required! 67 | 68 | 69 | 70 | Password 71 | 79 | Password is required! 80 | 81 | 87 |
88 | 89 | ); 90 | }; 91 | 92 | export default LoginForm; 93 | -------------------------------------------------------------------------------- /Develop/client/src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Navbar, Nav, Container, Modal, Tab } from 'react-bootstrap'; 4 | import SignUpForm from './SignupForm'; 5 | import LoginForm from './LoginForm'; 6 | 7 | import Auth from '../utils/auth'; 8 | 9 | const AppNavbar = () => { 10 | // set modal display state 11 | const [showModal, setShowModal] = useState(false); 12 | 13 | return ( 14 | <> 15 | 16 | 17 | 18 | Google Books Search 19 | 20 | 21 | 22 | 38 | 39 | 40 | 41 | {/* set modal data up */} 42 | setShowModal(false)} 46 | aria-labelledby='signup-modal'> 47 | {/* tab container to do either signup or login component */} 48 | 49 | 50 | 51 | 59 | 60 | 61 | 62 | 63 | 64 | setShowModal(false)} /> 65 | 66 | 67 | setShowModal(false)} /> 68 | 69 | 70 | 71 | 72 | 73 | 74 | ); 75 | }; 76 | 77 | export default AppNavbar; 78 | -------------------------------------------------------------------------------- /Develop/client/src/components/SignupForm.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Form, Button, Alert } from 'react-bootstrap'; 3 | 4 | import { createUser } from '../utils/API'; 5 | import Auth from '../utils/auth'; 6 | 7 | const SignupForm = () => { 8 | // set initial form state 9 | const [userFormData, setUserFormData] = useState({ username: '', email: '', password: '' }); 10 | // set state for form validation 11 | const [validated] = useState(false); 12 | // set state for alert 13 | const [showAlert, setShowAlert] = useState(false); 14 | 15 | const handleInputChange = (event) => { 16 | const { name, value } = event.target; 17 | setUserFormData({ ...userFormData, [name]: value }); 18 | }; 19 | 20 | const handleFormSubmit = async (event) => { 21 | event.preventDefault(); 22 | 23 | // check if form has everything (as per react-bootstrap docs) 24 | const form = event.currentTarget; 25 | if (form.checkValidity() === false) { 26 | event.preventDefault(); 27 | event.stopPropagation(); 28 | } 29 | 30 | try { 31 | const response = await createUser(userFormData); 32 | 33 | if (!response.ok) { 34 | throw new Error('something went wrong!'); 35 | } 36 | 37 | const { token, user } = await response.json(); 38 | console.log(user); 39 | Auth.login(token); 40 | } catch (err) { 41 | console.error(err); 42 | setShowAlert(true); 43 | } 44 | 45 | setUserFormData({ 46 | username: '', 47 | email: '', 48 | password: '', 49 | }); 50 | }; 51 | 52 | return ( 53 | <> 54 | {/* This is needed for the validation functionality above */} 55 |
56 | {/* show alert if server response is bad */} 57 | setShowAlert(false)} show={showAlert} variant='danger'> 58 | Something went wrong with your signup! 59 | 60 | 61 | 62 | Username 63 | 71 | Username is required! 72 | 73 | 74 | 75 | Email 76 | 84 | Email is required! 85 | 86 | 87 | 88 | Password 89 | 97 | Password is required! 98 | 99 | 105 |
106 | 107 | ); 108 | }; 109 | 110 | export default SignupForm; 111 | -------------------------------------------------------------------------------- /Develop/client/src/main.jsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client' 2 | import { createBrowserRouter, RouterProvider } from 'react-router-dom' 3 | import 'bootstrap/dist/css/bootstrap.min.css' 4 | 5 | import App from './App.jsx' 6 | import SearchBooks from './pages/SearchBooks' 7 | import SavedBooks from './pages/SavedBooks' 8 | 9 | const router = createBrowserRouter([ 10 | { 11 | path: '/', 12 | element: , 13 | errorElement:

Wrong page!

, 14 | children: [ 15 | { 16 | index: true, 17 | element: 18 | }, { 19 | path: '/saved', 20 | element: 21 | } 22 | ] 23 | } 24 | ]) 25 | 26 | ReactDOM.createRoot(document.getElementById('root')).render( 27 | 28 | ) 29 | -------------------------------------------------------------------------------- /Develop/client/src/pages/SavedBooks.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { 3 | Container, 4 | Card, 5 | Button, 6 | Row, 7 | Col 8 | } from 'react-bootstrap'; 9 | 10 | import { getMe, deleteBook } from '../utils/API'; 11 | import Auth from '../utils/auth'; 12 | import { removeBookId } from '../utils/localStorage'; 13 | 14 | const SavedBooks = () => { 15 | const [userData, setUserData] = useState({}); 16 | 17 | // use this to determine if `useEffect()` hook needs to run again 18 | const userDataLength = Object.keys(userData).length; 19 | 20 | useEffect(() => { 21 | const getUserData = async () => { 22 | try { 23 | const token = Auth.loggedIn() ? Auth.getToken() : null; 24 | 25 | if (!token) { 26 | return false; 27 | } 28 | 29 | const response = await getMe(token); 30 | 31 | if (!response.ok) { 32 | throw new Error('something went wrong!'); 33 | } 34 | 35 | const user = await response.json(); 36 | setUserData(user); 37 | } catch (err) { 38 | console.error(err); 39 | } 40 | }; 41 | 42 | getUserData(); 43 | }, [userDataLength]); 44 | 45 | // create function that accepts the book's mongo _id value as param and deletes the book from the database 46 | const handleDeleteBook = async (bookId) => { 47 | const token = Auth.loggedIn() ? Auth.getToken() : null; 48 | 49 | if (!token) { 50 | return false; 51 | } 52 | 53 | try { 54 | const response = await deleteBook(bookId, token); 55 | 56 | if (!response.ok) { 57 | throw new Error('something went wrong!'); 58 | } 59 | 60 | const updatedUser = await response.json(); 61 | setUserData(updatedUser); 62 | // upon success, remove book's id from localStorage 63 | removeBookId(bookId); 64 | } catch (err) { 65 | console.error(err); 66 | } 67 | }; 68 | 69 | // if data isn't here yet, say so 70 | if (!userDataLength) { 71 | return

LOADING...

; 72 | } 73 | 74 | return ( 75 | <> 76 |
77 | 78 |

Viewing saved books!

79 |
80 |
81 | 82 |

83 | {userData.savedBooks.length 84 | ? `Viewing ${userData.savedBooks.length} saved ${userData.savedBooks.length === 1 ? 'book' : 'books'}:` 85 | : 'You have no saved books!'} 86 |

87 | 88 | {userData.savedBooks.map((book) => { 89 | return ( 90 | 91 | 92 | {book.image ? : null} 93 | 94 | {book.title} 95 |

Authors: {book.authors}

96 | {book.description} 97 | 100 |
101 |
102 | 103 | ); 104 | })} 105 |
106 |
107 | 108 | ); 109 | }; 110 | 111 | export default SavedBooks; 112 | -------------------------------------------------------------------------------- /Develop/client/src/pages/SearchBooks.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { 3 | Container, 4 | Col, 5 | Form, 6 | Button, 7 | Card, 8 | Row 9 | } from 'react-bootstrap'; 10 | 11 | import Auth from '../utils/auth'; 12 | import { saveBook, searchGoogleBooks } from '../utils/API'; 13 | import { saveBookIds, getSavedBookIds } from '../utils/localStorage'; 14 | 15 | const SearchBooks = () => { 16 | // create state for holding returned google api data 17 | const [searchedBooks, setSearchedBooks] = useState([]); 18 | // create state for holding our search field data 19 | const [searchInput, setSearchInput] = useState(''); 20 | 21 | // create state to hold saved bookId values 22 | const [savedBookIds, setSavedBookIds] = useState(getSavedBookIds()); 23 | 24 | // set up useEffect hook to save `savedBookIds` list to localStorage on component unmount 25 | // learn more here: https://reactjs.org/docs/hooks-effect.html#effects-with-cleanup 26 | useEffect(() => { 27 | return () => saveBookIds(savedBookIds); 28 | }); 29 | 30 | // create method to search for books and set state on form submit 31 | const handleFormSubmit = async (event) => { 32 | event.preventDefault(); 33 | 34 | if (!searchInput) { 35 | return false; 36 | } 37 | 38 | try { 39 | const response = await searchGoogleBooks(searchInput); 40 | 41 | if (!response.ok) { 42 | throw new Error('something went wrong!'); 43 | } 44 | 45 | const { items } = await response.json(); 46 | 47 | const bookData = items.map((book) => ({ 48 | bookId: book.id, 49 | authors: book.volumeInfo.authors || ['No author to display'], 50 | title: book.volumeInfo.title, 51 | description: book.volumeInfo.description, 52 | image: book.volumeInfo.imageLinks?.thumbnail || '', 53 | })); 54 | 55 | setSearchedBooks(bookData); 56 | setSearchInput(''); 57 | } catch (err) { 58 | console.error(err); 59 | } 60 | }; 61 | 62 | // create function to handle saving a book to our database 63 | const handleSaveBook = async (bookId) => { 64 | // find the book in `searchedBooks` state by the matching id 65 | const bookToSave = searchedBooks.find((book) => book.bookId === bookId); 66 | 67 | // get token 68 | const token = Auth.loggedIn() ? Auth.getToken() : null; 69 | 70 | if (!token) { 71 | return false; 72 | } 73 | 74 | try { 75 | const response = await saveBook(bookToSave, token); 76 | 77 | if (!response.ok) { 78 | throw new Error('something went wrong!'); 79 | } 80 | 81 | // if book successfully saves to user's account, save book id to state 82 | setSavedBookIds([...savedBookIds, bookToSave.bookId]); 83 | } catch (err) { 84 | console.error(err); 85 | } 86 | }; 87 | 88 | return ( 89 | <> 90 |
91 | 92 |

Search for Books!

93 |
94 | 95 | 96 | setSearchInput(e.target.value)} 100 | type='text' 101 | size='lg' 102 | placeholder='Search for a book' 103 | /> 104 | 105 | 106 | 109 | 110 | 111 |
112 |
113 |
114 | 115 | 116 |

117 | {searchedBooks.length 118 | ? `Viewing ${searchedBooks.length} results:` 119 | : 'Search for a book to begin'} 120 |

121 | 122 | {searchedBooks.map((book) => { 123 | return ( 124 | 125 | 126 | {book.image ? ( 127 | 128 | ) : null} 129 | 130 | {book.title} 131 |

Authors: {book.authors}

132 | {book.description} 133 | {Auth.loggedIn() && ( 134 | 142 | )} 143 |
144 |
145 | 146 | ); 147 | })} 148 |
149 |
150 | 151 | ); 152 | }; 153 | 154 | export default SearchBooks; 155 | -------------------------------------------------------------------------------- /Develop/client/src/utils/API.js: -------------------------------------------------------------------------------- 1 | // route to get logged in user's info (needs the token) 2 | export const getMe = (token) => { 3 | return fetch('/api/users/me', { 4 | headers: { 5 | 'Content-Type': 'application/json', 6 | authorization: `Bearer ${token}`, 7 | }, 8 | }); 9 | }; 10 | 11 | export const createUser = (userData) => { 12 | return fetch('/api/users', { 13 | method: 'POST', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | }, 17 | body: JSON.stringify(userData), 18 | }); 19 | }; 20 | 21 | export const loginUser = (userData) => { 22 | return fetch('/api/users/login', { 23 | method: 'POST', 24 | headers: { 25 | 'Content-Type': 'application/json', 26 | }, 27 | body: JSON.stringify(userData), 28 | }); 29 | }; 30 | 31 | // save book data for a logged in user 32 | export const saveBook = (bookData, token) => { 33 | return fetch('/api/users', { 34 | method: 'PUT', 35 | headers: { 36 | 'Content-Type': 'application/json', 37 | authorization: `Bearer ${token}`, 38 | }, 39 | body: JSON.stringify(bookData), 40 | }); 41 | }; 42 | 43 | // remove saved book data for a logged in user 44 | export const deleteBook = (bookId, token) => { 45 | return fetch(`/api/users/books/${bookId}`, { 46 | method: 'DELETE', 47 | headers: { 48 | authorization: `Bearer ${token}`, 49 | }, 50 | }); 51 | }; 52 | 53 | // make a search to google books api 54 | // https://www.googleapis.com/books/v1/volumes?q=harry+potter 55 | export const searchGoogleBooks = (query) => { 56 | return fetch(`https://www.googleapis.com/books/v1/volumes?q=${query}`); 57 | }; 58 | -------------------------------------------------------------------------------- /Develop/client/src/utils/auth.js: -------------------------------------------------------------------------------- 1 | // use this to decode a token and get the user's information out of it 2 | import decode from 'jwt-decode'; 3 | 4 | // create a new class to instantiate for a user 5 | class AuthService { 6 | // get user data 7 | getProfile() { 8 | return decode(this.getToken()); 9 | } 10 | 11 | // check if user's logged in 12 | loggedIn() { 13 | // Checks if there is a saved token and it's still valid 14 | const token = this.getToken(); 15 | return !!token && !this.isTokenExpired(token); // handwaiving here 16 | } 17 | 18 | // check if token is expired 19 | isTokenExpired(token) { 20 | try { 21 | const decoded = decode(token); 22 | if (decoded.exp < Date.now() / 1000) { 23 | return true; 24 | } else return false; 25 | } catch (err) { 26 | return false; 27 | } 28 | } 29 | 30 | getToken() { 31 | // Retrieves the user token from localStorage 32 | return localStorage.getItem('id_token'); 33 | } 34 | 35 | login(idToken) { 36 | // Saves user token to localStorage 37 | localStorage.setItem('id_token', idToken); 38 | window.location.assign('/'); 39 | } 40 | 41 | logout() { 42 | // Clear user token and profile data from localStorage 43 | localStorage.removeItem('id_token'); 44 | // this will reload the page and reset the state of the application 45 | window.location.assign('/'); 46 | } 47 | } 48 | 49 | export default new AuthService(); 50 | -------------------------------------------------------------------------------- /Develop/client/src/utils/localStorage.js: -------------------------------------------------------------------------------- 1 | export const getSavedBookIds = () => { 2 | const savedBookIds = localStorage.getItem('saved_books') 3 | ? JSON.parse(localStorage.getItem('saved_books')) 4 | : []; 5 | 6 | return savedBookIds; 7 | }; 8 | 9 | export const saveBookIds = (bookIdArr) => { 10 | if (bookIdArr.length) { 11 | localStorage.setItem('saved_books', JSON.stringify(bookIdArr)); 12 | } else { 13 | localStorage.removeItem('saved_books'); 14 | } 15 | }; 16 | 17 | export const removeBookId = (bookId) => { 18 | const savedBookIds = localStorage.getItem('saved_books') 19 | ? JSON.parse(localStorage.getItem('saved_books')) 20 | : null; 21 | 22 | if (!savedBookIds) { 23 | return false; 24 | } 25 | 26 | const updatedSavedBookIds = savedBookIds?.filter((savedBookId) => savedBookId !== bookId); 27 | localStorage.setItem('saved_books', JSON.stringify(updatedSavedBookIds)); 28 | 29 | return true; 30 | }; 31 | -------------------------------------------------------------------------------- /Develop/client/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { 8 | port: 3000, 9 | open: true, 10 | proxy: { 11 | '/api': { 12 | target: 'http://localhost:3001', 13 | secure: false, 14 | changeOrigin: true 15 | } 16 | } 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /Develop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "googlebooks-app", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server/server.js", 6 | "scripts": { 7 | "start": "node server/server.js", 8 | "develop": "concurrently \"cd server && npm run watch\" \"cd client && npm start\"", 9 | "install": "cd server && npm i && cd ../client && npm i", 10 | "build": "cd client && npm run build", 11 | "render-build":"npm install && npm run build" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "concurrently": "^5.1.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Develop/server/config/connection.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | mongoose.connect(process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/googlebooks'); 4 | 5 | module.exports = mongoose.connection; 6 | -------------------------------------------------------------------------------- /Develop/server/controllers/user-controller.js: -------------------------------------------------------------------------------- 1 | // import user model 2 | const { User } = require('../models'); 3 | // import sign token function from auth 4 | const { signToken } = require('../utils/auth'); 5 | 6 | module.exports = { 7 | // get a single user by either their id or their username 8 | async getSingleUser({ user = null, params }, res) { 9 | const foundUser = await User.findOne({ 10 | $or: [{ _id: user ? user._id : params.id }, { username: params.username }], 11 | }); 12 | 13 | if (!foundUser) { 14 | return res.status(400).json({ message: 'Cannot find a user with this id!' }); 15 | } 16 | 17 | res.json(foundUser); 18 | }, 19 | // create a user, sign a token, and send it back (to client/src/components/SignUpForm.js) 20 | async createUser({ body }, res) { 21 | const user = await User.create(body); 22 | 23 | if (!user) { 24 | return res.status(400).json({ message: 'Something is wrong!' }); 25 | } 26 | const token = signToken(user); 27 | res.json({ token, user }); 28 | }, 29 | // login a user, sign a token, and send it back (to client/src/components/LoginForm.js) 30 | // {body} is destructured req.body 31 | async login({ body }, res) { 32 | const user = await User.findOne({ $or: [{ username: body.username }, { email: body.email }] }); 33 | if (!user) { 34 | return res.status(400).json({ message: "Can't find this user" }); 35 | } 36 | 37 | const correctPw = await user.isCorrectPassword(body.password); 38 | 39 | if (!correctPw) { 40 | return res.status(400).json({ message: 'Wrong password!' }); 41 | } 42 | const token = signToken(user); 43 | res.json({ token, user }); 44 | }, 45 | // save a book to a user's `savedBooks` field by adding it to the set (to prevent duplicates) 46 | // user comes from `req.user` created in the auth middleware function 47 | async saveBook({ user, body }, res) { 48 | console.log(user); 49 | try { 50 | const updatedUser = await User.findOneAndUpdate( 51 | { _id: user._id }, 52 | { $addToSet: { savedBooks: body } }, 53 | { new: true, runValidators: true } 54 | ); 55 | return res.json(updatedUser); 56 | } catch (err) { 57 | console.log(err); 58 | return res.status(400).json(err); 59 | } 60 | }, 61 | // remove a book from `savedBooks` 62 | async deleteBook({ user, params }, res) { 63 | const updatedUser = await User.findOneAndUpdate( 64 | { _id: user._id }, 65 | { $pull: { savedBooks: { bookId: params.bookId } } }, 66 | { new: true } 67 | ); 68 | if (!updatedUser) { 69 | return res.status(404).json({ message: "Couldn't find user with this id!" }); 70 | } 71 | return res.json(updatedUser); 72 | }, 73 | }; 74 | -------------------------------------------------------------------------------- /Develop/server/models/Book.js: -------------------------------------------------------------------------------- 1 | const { Schema } = require('mongoose'); 2 | 3 | // This is a subdocument schema, it won't become its own model but we'll use it as the schema for the User's `savedBooks` array in User.js 4 | const bookSchema = new Schema({ 5 | authors: [ 6 | { 7 | type: String, 8 | }, 9 | ], 10 | description: { 11 | type: String, 12 | required: true, 13 | }, 14 | // saved book id from GoogleBooks 15 | bookId: { 16 | type: String, 17 | required: true, 18 | }, 19 | image: { 20 | type: String, 21 | }, 22 | link: { 23 | type: String, 24 | }, 25 | title: { 26 | type: String, 27 | required: true, 28 | }, 29 | }); 30 | 31 | module.exports = bookSchema; 32 | -------------------------------------------------------------------------------- /Develop/server/models/User.js: -------------------------------------------------------------------------------- 1 | const { Schema, model } = require('mongoose'); 2 | const bcrypt = require('bcrypt'); 3 | 4 | // import schema from Book.js 5 | const bookSchema = require('./Book'); 6 | 7 | const userSchema = new Schema( 8 | { 9 | username: { 10 | type: String, 11 | required: true, 12 | unique: true, 13 | }, 14 | email: { 15 | type: String, 16 | required: true, 17 | unique: true, 18 | match: [/.+@.+\..+/, 'Must use a valid email address'], 19 | }, 20 | password: { 21 | type: String, 22 | required: true, 23 | }, 24 | // set savedBooks to be an array of data that adheres to the bookSchema 25 | savedBooks: [bookSchema], 26 | }, 27 | // set this to use virtual below 28 | { 29 | toJSON: { 30 | virtuals: true, 31 | }, 32 | } 33 | ); 34 | 35 | // hash user password 36 | userSchema.pre('save', async function (next) { 37 | if (this.isNew || this.isModified('password')) { 38 | const saltRounds = 10; 39 | this.password = await bcrypt.hash(this.password, saltRounds); 40 | } 41 | 42 | next(); 43 | }); 44 | 45 | // custom method to compare and validate password for logging in 46 | userSchema.methods.isCorrectPassword = async function (password) { 47 | return bcrypt.compare(password, this.password); 48 | }; 49 | 50 | // when we query a user, we'll also get another field called `bookCount` with the number of saved books we have 51 | userSchema.virtual('bookCount').get(function () { 52 | return this.savedBooks.length; 53 | }); 54 | 55 | const User = model('User', userSchema); 56 | 57 | module.exports = User; 58 | -------------------------------------------------------------------------------- /Develop/server/models/index.js: -------------------------------------------------------------------------------- 1 | const User = require('./User'); 2 | 3 | module.exports = { User }; 4 | -------------------------------------------------------------------------------- /Develop/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "watch": "nodemon server.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "bcrypt": "^4.0.1", 15 | "express": "^4.17.1", 16 | "jsonwebtoken": "^8.5.1", 17 | "mongoose": "^8.0.0" 18 | }, 19 | "devDependencies": { 20 | "nodemon": "^2.0.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Develop/server/routes/api/index.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const userRoutes = require('./user-routes'); 3 | 4 | router.use('/users', userRoutes); 5 | 6 | module.exports = router; 7 | -------------------------------------------------------------------------------- /Develop/server/routes/api/user-routes.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { 3 | createUser, 4 | getSingleUser, 5 | saveBook, 6 | deleteBook, 7 | login, 8 | } = require('../../controllers/user-controller'); 9 | 10 | // import middleware 11 | const { authMiddleware } = require('../../utils/auth'); 12 | 13 | // put authMiddleware anywhere we need to send a token for verification of user 14 | router.route('/').post(createUser).put(authMiddleware, saveBook); 15 | 16 | router.route('/login').post(login); 17 | 18 | router.route('/me').get(authMiddleware, getSingleUser); 19 | 20 | router.route('/books/:bookId').delete(authMiddleware, deleteBook); 21 | 22 | module.exports = router; 23 | -------------------------------------------------------------------------------- /Develop/server/routes/index.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const path = require('path'); 3 | const apiRoutes = require('./api'); 4 | 5 | router.use('/api', apiRoutes); 6 | 7 | // serve up react front-end in production 8 | router.use((req, res) => { 9 | res.sendFile(path.join(__dirname, '../../client/build/index.html')); 10 | }); 11 | 12 | module.exports = router; 13 | -------------------------------------------------------------------------------- /Develop/server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const db = require('./config/connection'); 4 | const routes = require('./routes'); 5 | 6 | const app = express(); 7 | const PORT = process.env.PORT || 3001; 8 | 9 | app.use(express.urlencoded({ extended: true })); 10 | app.use(express.json()); 11 | 12 | // if we're in production, serve client/build as static assets 13 | if (process.env.NODE_ENV === 'production') { 14 | app.use(express.static(path.join(__dirname, '../client/build'))); 15 | } 16 | 17 | app.use(routes); 18 | 19 | db.once('open', () => { 20 | app.listen(PORT, () => console.log(`🌍 Now listening on localhost:${PORT}`)); 21 | }); 22 | -------------------------------------------------------------------------------- /Develop/server/utils/auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | 3 | // set token secret and expiration date 4 | const secret = 'mysecretsshhhhh'; 5 | const expiration = '2h'; 6 | 7 | module.exports = { 8 | // function for our authenticated routes 9 | authMiddleware: function (req, res, next) { 10 | // allows token to be sent via req.query or headers 11 | let token = req.query.token || req.headers.authorization; 12 | 13 | // ["Bearer", ""] 14 | if (req.headers.authorization) { 15 | token = token.split(' ').pop().trim(); 16 | } 17 | 18 | if (!token) { 19 | return res.status(400).json({ message: 'You have no token!' }); 20 | } 21 | 22 | // verify token and get user data out of it 23 | try { 24 | const { data } = jwt.verify(token, secret, { maxAge: expiration }); 25 | req.user = data; 26 | } catch { 27 | console.log('Invalid token'); 28 | return res.status(400).json({ message: 'invalid token!' }); 29 | } 30 | 31 | // send to next endpoint 32 | next(); 33 | }, 34 | signToken: function ({ username, email, _id }) { 35 | const payload = { username, email, _id }; 36 | 37 | return jwt.sign({ data: payload }, secret, { expiresIn: expiration }); 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Book Search Engine Starter Code 2 | --------------------------------------------------------------------------------