├── 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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------