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