├── .gitignore ├── .env ├── middleware ├── not-found.js └── authenticate.js ├── routes ├── auth.js ├── book.js └── review.js ├── utils ├── tokens.js └── hashing.js ├── models ├── user.js ├── book.js └── review.js ├── package.json ├── config └── connection.js ├── app.js ├── controllers ├── auth.js ├── review.js └── book.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PORT= 2 | DATABASE_NAME= 3 | DATABASE_USERNAME= 4 | DATABASE_PASSWORD= 5 | DATABASE_HOST= 6 | SALT_ROUNDS= 7 | TOKEN_SECRET_KEY= -------------------------------------------------------------------------------- /middleware/not-found.js: -------------------------------------------------------------------------------- 1 | function notFoundHandler(req, res) { 2 | return res.status(404).send(`

Page Not Found!

`); 3 | } 4 | 5 | export default notFoundHandler; -------------------------------------------------------------------------------- /routes/auth.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import * as authControllers from "../controllers/auth.js"; 3 | 4 | const router = Router(); 5 | 6 | router.post("/auth/register", authControllers.register); 7 | router.post("/auth/login", authControllers.login); 8 | 9 | export default router; -------------------------------------------------------------------------------- /utils/tokens.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import dotenv from "dotenv"; 3 | 4 | dotenv.config(); 5 | 6 | export function decodeToken(token) { 7 | return jwt.verify(token, process.env.TOKEN_SECRET_KEY); 8 | } 9 | 10 | 11 | export function createToken(user_id, username) { 12 | return jwt.sign({ user_id, username }, process.env.TOKEN_SECRET_KEY); 13 | } -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | import { DataTypes } from "sequelize"; 2 | import { sequelize } from "../config/connection.js" 3 | 4 | const user = sequelize.define('User', { 5 | username: { 6 | type: DataTypes.STRING, 7 | allowNull: false 8 | }, 9 | password: { 10 | type: DataTypes.STRING, 11 | allowNull: false 12 | } 13 | }); 14 | 15 | export default user; -------------------------------------------------------------------------------- /models/book.js: -------------------------------------------------------------------------------- 1 | import { DataTypes } from "sequelize"; 2 | import { sequelize } from "../config/connection.js" 3 | 4 | const book = sequelize.define('Book', { 5 | ISBN: { 6 | type: DataTypes.STRING, 7 | allowNull: false 8 | }, 9 | title: { 10 | type: DataTypes.STRING, 11 | allowNull: false 12 | }, 13 | author: { 14 | type: DataTypes.STRING, 15 | allowNull: false 16 | } 17 | }); 18 | 19 | export default book; -------------------------------------------------------------------------------- /models/review.js: -------------------------------------------------------------------------------- 1 | import { DataTypes } from "sequelize"; 2 | import { sequelize } from "../config/connection.js" 3 | 4 | // models 5 | import user from "./user.js" 6 | import book from "./book.js" 7 | 8 | const review = sequelize.define('Review', { 9 | review_text: { 10 | type: DataTypes.STRING, 11 | allowNull: false 12 | } 13 | }); 14 | 15 | user.belongsToMany(book, { through: review }); 16 | book.belongsToMany(user, { through: review }); 17 | 18 | export default review; -------------------------------------------------------------------------------- /routes/book.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import * as bookControllers from "../controllers/book.js"; 3 | 4 | const router = Router(); 5 | 6 | router.get("/books", bookControllers.getAllBooks); 7 | router.post("/books/byISBN", bookControllers.getBooksByISBN); 8 | router.post("/books/byTitle", bookControllers.getBooksByTitle); 9 | router.post("/books/byAuthor", bookControllers.getBooksByAuthor); 10 | router.post("/books", bookControllers.addBook); 11 | 12 | 13 | 14 | export default router; -------------------------------------------------------------------------------- /routes/review.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import authenticate from "../middleware/authenticate.js"; 3 | import * as reviewControllers from "../controllers/review.js"; 4 | 5 | const router = Router(); 6 | 7 | // registered users 8 | router.put("/books/:id/reviews", authenticate, reviewControllers.addReview); 9 | router.delete("/books/:id/reviews", authenticate, reviewControllers.deleteReview); 10 | 11 | // general users 12 | router.get("/books/:id/reviews", reviewControllers.getReview); 13 | 14 | 15 | export default router; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ibm_course", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "nodemon app.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "axios": "^1.5.0", 15 | "bcrypt": "^5.1.1", 16 | "cors": "^2.8.5", 17 | "dotenv": "^16.3.1", 18 | "express": "^4.18.2", 19 | "jsonwebtoken": "^9.0.2", 20 | "mysql2": "^3.6.0", 21 | "sequelize": "^6.32.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /utils/hashing.js: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt'; 2 | import dotenv from "dotenv"; 3 | 4 | dotenv.config(); 5 | 6 | export async function hashPassword(password) { 7 | try { 8 | const hashedPassword = await bcrypt.hash(password, parseInt(process.env.SALT_ROUNDS)); 9 | return hashedPassword; 10 | } catch (error) { 11 | console.log(error); 12 | } 13 | } 14 | 15 | export async function compare_hashed_passwords(passwordInput, storedHashedPassword) { 16 | try { 17 | return await bcrypt.compare(passwordInput, storedHashedPassword); 18 | } catch (error) { 19 | console.log(error); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /config/connection.js: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import { Sequelize } from "sequelize"; 3 | 4 | dotenv.config(); 5 | 6 | export const sequelize = new Sequelize( 7 | process.env.DATABASE_NAME, 8 | process.env.DATABASE_USERNAME, 9 | process.env.DATABASE_PASSWORD, 10 | { 11 | host: process.env.DATABASE_HOST, 12 | dialect: 'mysql' 13 | }); 14 | 15 | export async function connectDB() { 16 | try { 17 | // const connectedDataBase = await sequelize.sync(); 18 | console.log("Connected Database!"); 19 | } catch (error) { 20 | console.log("Failed connecting to database", { message: error.message }); 21 | } 22 | } -------------------------------------------------------------------------------- /middleware/authenticate.js: -------------------------------------------------------------------------------- 1 | import { decodeToken } from "../utils/tokens.js" 2 | 3 | function authenticate(req, res, next) { 4 | try { 5 | let tokenHeader = req.headers.authorization; 6 | 7 | // check token integrity 8 | if (!tokenHeader || !tokenHeader.startsWith("Bearer")) { 9 | return res.status(401).json({ message: "You're not authorized to do this action!" }); 10 | } 11 | 12 | tokenHeader = tokenHeader.split(' ')[1]; 13 | // console.log(tokenHeader); 14 | 15 | // verify token & store user_id in request to use it in the next controller 16 | const { user_id } = decodeToken(tokenHeader); 17 | req.user = { user_id }; 18 | 19 | next(); 20 | } catch (error) { 21 | return res.status(401).json({ message: "You're not authorized to do this action!" }); 22 | } 23 | } 24 | 25 | export default authenticate; -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import Express from "express"; 2 | import dotenv from "dotenv"; 3 | import { connectDB } from "./config/connection.js"; 4 | import cors from "cors" 5 | 6 | // models 7 | import user from "./models/user.js"; 8 | import book from "./models/book.js"; 9 | import review from "./models/review.js"; 10 | 11 | // routes 12 | import bookRoutes from "./routes/book.js"; 13 | import authRoutes from "./routes/auth.js"; 14 | import reviewRoutes from "./routes/review.js"; 15 | import notFoundHandler from "./middleware/not-found.js"; 16 | 17 | dotenv.config(); 18 | 19 | 20 | const app = Express(); 21 | app.use(cors()); 22 | // middleware 23 | app.use(Express.json()); 24 | 25 | // routes 26 | const baseURL = "/api/v1"; 27 | app.use(baseURL, authRoutes); 28 | app.use(baseURL, bookRoutes); 29 | app.use(baseURL, reviewRoutes); 30 | 31 | // error handlers 32 | app.use(notFoundHandler) 33 | 34 | connectDB(); 35 | 36 | try { 37 | 38 | const port = process.env.PORT; 39 | app.listen(port, console.log(`Server running on port ${port}!`)); 40 | 41 | } catch (error) { 42 | 43 | console.log(error); 44 | 45 | } -------------------------------------------------------------------------------- /controllers/auth.js: -------------------------------------------------------------------------------- 1 | 2 | import user from "../models/user.js"; 3 | import { hashPassword, compare_hashed_passwords } from "../utils/hashing.js"; 4 | import { createToken } from "../utils/tokens.js" 5 | 6 | export async function register(req, res) { 7 | try { 8 | const { username, password } = req.body; 9 | 10 | // check if the user name found 11 | const foundUser = await user.findOne({ where: { username } }); 12 | if (foundUser) { 13 | return res.json({ message: "This user is already registered!" }); 14 | } 15 | 16 | // hashing the password 17 | const hashedPassword = await hashPassword(password); 18 | 19 | // execute the registration 20 | const newUser = await user.create({ username: username, password: hashedPassword }); 21 | 22 | res.json({ message: "User registered successfully!" }); 23 | } catch (error) { 24 | 25 | res.status(500).json({ message: "Internal Server Error!!" }); 26 | 27 | } 28 | } 29 | 30 | export async function login(req, res) { 31 | try { 32 | 33 | const { username, password } = req.body; 34 | 35 | // check if user registered or not 36 | const registeredUser = await user.findOne({ where: { username } }); 37 | if (!registeredUser) { 38 | return res.json({ message: "Invalid Credentials!" }); 39 | } 40 | 41 | // check password matching 42 | const is_matched = await compare_hashed_passwords(password, registeredUser.password); 43 | if (!is_matched) { 44 | return res.json({ message: "Invalid Credentials!" }); 45 | } 46 | 47 | // create token 48 | const token = createToken(registeredUser.id, username); 49 | 50 | return res.json({ message: "User logged in successfully!", token }); 51 | 52 | } catch (error) { 53 | 54 | res.status(500).json({ message: "Internal Server Error!!" }); 55 | 56 | } 57 | } -------------------------------------------------------------------------------- /controllers/review.js: -------------------------------------------------------------------------------- 1 | import review from "../models/review.js"; 2 | 3 | export async function addReview(req, res) { 4 | try { 5 | // data to be stored 6 | const { user_id } = req.user; 7 | const book_id = req.params.id; 8 | const { review_text } = req.body; 9 | // console.log(user_id, book_id, review_text); 10 | 11 | // check if the review is found or not to decide to create a new one or update the existing one 12 | const foundReview = await review.findOne({ where: { UserId: user_id, BookId: book_id } }) 13 | 14 | if (foundReview) { 15 | const newReview = await review.update({ review_text }, { where: { UserId: user_id, BookId: book_id } }); 16 | return res.json({ message: `Review added/updated successfully!` }); 17 | } 18 | 19 | // execute adding review operation 20 | const newReview = await review.create({ UserId: user_id, BookId: book_id, review_text }); 21 | res.json({ message: `Review added/updated successfully!` }); 22 | } catch (error) { 23 | 24 | res.status(500).json({ message: "Internal Server Error!!" }); 25 | 26 | } 27 | } 28 | 29 | export async function getReview(req, res) { 30 | try { 31 | const { id } = req.params; 32 | const bookReview = await review.findAll({ attributes: ["review_text"], where: { BookId: id } }) 33 | 34 | if (!bookReview.length) { 35 | return res.json({ message: "No review found for this book!" }); 36 | } 37 | 38 | res.json({ message: "review found for this book", bookReview }); 39 | 40 | } catch (error) { 41 | 42 | res.status(500).json({ message: "Internal Server Error!!" }); 43 | 44 | } 45 | } 46 | 47 | export async function deleteReview(req, res) { 48 | try { 49 | 50 | const { user_id } = req.user; 51 | const { id } = req.params; 52 | 53 | const deletedReview = await review.destroy({ where: { UserId: user_id, BookId: id } }); 54 | // console.log(deletedReview); 55 | 56 | if (!deletedReview) { 57 | return res.json({ message: "No review found for that user to delete!" }); 58 | } 59 | 60 | res.json({ message: "review deleted for that user successfully!" }); 61 | 62 | } catch (error) { 63 | 64 | res.status(500).json({ message: "Internal Server Error!!" }); 65 | 66 | } 67 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Online Book Review Server-Side Application 2 | 3 | Welcome to the Online Book Review Server-Side Application, the final project for the IBM Course "Developing back-end apps with Node.js and Express." This application allows users to manage books, write reviews, and interact with book-related data. It utilizes MySQL as the database and Sequelize.js as the ORM (Object-Relational Mapping) tool. 4 | 5 | ## Quick Brief 6 | 7 | The Online Book Review Server-Side Application provides a RESTful API for managing user accounts, books, and reviews. Users can register, log in, and create reviews for books. Books can be added, updated, and deleted. Reviews can be read, edited, and deleted. The application aims to provide a seamless experience for book enthusiasts to share their thoughts on books. 8 | 9 | ## Getting Started 10 | 11 | Follow these instructions to set up and run the application: 12 | 13 | ### Prerequisites 14 | 15 | 1. **Node.js**: Ensure you have Node.js installed on your system. You can download it from [nodejs.org](https://nodejs.org/). 16 | 17 | 2. **MySQL**: You will need a MySQL database server installed and running. You can download MySQL from [mysql.com](https://www.mysql.com/). 18 | 19 | ### Installation 20 | 21 | 1. **Clone the repository**: 22 | 23 | ```bash 24 | git clone 25 | ``` 26 | 27 | 2. **Navigate to the project folder**: 28 | 29 | ```bash 30 | cd 31 | ``` 32 | 33 | 3. **Install dependencies**: 34 | 35 | ```bash 36 | npm install 37 | ``` 38 | 39 | 4. **Configure the database connection**: 40 | 41 | - Create a MySQL database for the application. 42 | - Set your MySQL database credentials and other environment variables in the `.env` file. 43 | 44 | 5. **Start the application**: 45 | 46 | ```bash 47 | npm start 48 | ``` 49 | 50 | ## API Documentation 51 | 52 | For detailed API documentation and examples of how to use the endpoints, please take a look at the [API Documentation](https://documenter.getpostman.com/view/28416524/2s9YBxacHG). 53 | 54 | ## Features 55 | 56 | - **User Management**: Register, log in, and manage user accounts. 57 | - **Book Management**: Add, update, delete, and list books. 58 | - **Review Management**: Write, edit, delete, and read reviews for books. 59 | 60 | ## Technologies Used 61 | 62 | - **Node.js**: JavaScript runtime environment. 63 | - **Express.js**: Web application framework for Node.js. 64 | - **MySQL**: Relational database management system. 65 | - **Sequelize.js**: Promise-based Node.js ORM for MySQL. 66 | -------------------------------------------------------------------------------- /controllers/book.js: -------------------------------------------------------------------------------- 1 | import book from "../models/book.js" 2 | 3 | export async function getAllBooks(req, res) { 4 | try { 5 | const books = await book.findAll(); 6 | res.json(books); 7 | } catch (error) { 8 | res.status(500).json({ message: "Internal Server Error!!" }); 9 | } 10 | } 11 | 12 | export async function addBook(req, res) { 13 | try { 14 | 15 | const foundBook = await book.findOne({ where: req.body }); 16 | if (foundBook) { 17 | return res.json({ message: "Book Already Found!!" }); 18 | } 19 | 20 | const newBook = await book.create(req.body); 21 | res.json({ message: "Book Added Successfully!" }); 22 | 23 | } catch (error) { 24 | res.status(500).json({ message: "Internal Server Error!!" }); 25 | } 26 | } 27 | 28 | export async function getBooksByISBN(req, res) { 29 | try { 30 | 31 | const { ISBN } = req.body; 32 | 33 | if (!ISBN) { 34 | return res.json({ message: "Please, provide a valid ISBN Code!" }); 35 | } 36 | 37 | const foundBooks = await book.findAll({ where: { ISBN } }); 38 | if (foundBooks.length) { 39 | return res.json({ message: "Books are Found!!", foundBooks }); 40 | } 41 | 42 | res.json({ message: "No book found with this ISBN Code!" }); 43 | } catch (error) { 44 | 45 | res.status(500).json({ message: "Internal Server Error!!" }); 46 | 47 | } 48 | } 49 | 50 | export async function getBooksByTitle(req, res) { 51 | try { 52 | 53 | const { title } = req.body; 54 | 55 | if (!title) { 56 | return res.json({ message: "Please, provide a valid title!" }); 57 | } 58 | 59 | const foundBooks = await book.findAll({ where: { title } }); 60 | if (foundBooks.length) { 61 | return res.json({ message: "Books are Found!!", foundBooks }); 62 | } 63 | 64 | res.json({ message: "No book found with this title!" }); 65 | 66 | } catch (error) { 67 | res.status(500).json({ message: "Internal Server Error!!" }); 68 | } 69 | } 70 | 71 | export async function getBooksByAuthor(req, res) { 72 | try { 73 | 74 | const { author } = req.body; 75 | 76 | if (!author) { 77 | return res.json({ message: "Please, provide a valid author name!" }); 78 | } 79 | 80 | const foundBooks = await book.findAll({ where: { author } }); 81 | if (foundBooks.length) { 82 | return res.json({ message: "Books are Found!!", foundBooks }); 83 | } 84 | 85 | res.json({ message: "No book found with this author name!" }); 86 | 87 | } catch (error) { 88 | res.status(500).json({ message: "Internal Server Error!!" }); 89 | } 90 | } 91 | 92 | --------------------------------------------------------------------------------