Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 28 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,19 @@
# node-mentoring-shop
[https://d17btkcdsmqrmh.cloudfront.net/node-gmp/docs/nosql/homework](https://d17btkcdsmqrmh.cloudfront.net/node-gmp/docs/nosql/homework)
[https://d17btkcdsmqrmh.cloudfront.net/node-gmp/docs/authorization/homework](https://d17btkcdsmqrmh.cloudfront.net/node-gmp/docs/authorization/homework)

We are going to create an Express application for online shop which sells different types of products (like e.g Amazon).

In this task we will modify the application we created in [Express and Layered Architecture](https://github.com/kandalova/node-mentoring-shop/pull/1) module by moving data storage to NoSQL database.

The application has 4 primary entities:

<code>User</code> - can add some products to the cart and then order them ([scheme](https://github.com/kandalova/node-mentoring-shop/blob/task_6_shop_express.js/src/scheme/UserScheme.ts)).

<code>Product</code> - represents product information that user can order ([scheme](https://github.com/kandalova/node-mentoring-shop/blob/task_6_shop_express.js/src/scheme/ProductScheme.ts)).

<code>Cart</code> - contains a list of products and their amount that user wants to order ([scheme](https://github.com/kandalova/node-mentoring-shop/blob/task_6_shop_express.js/src/scheme/CartScheme.ts)).

<code>Order</code> - contains list of products from cart that user has ordered ([scheme](https://github.com/kandalova/node-mentoring-shop/blob/task_6_shop_express.js/src/scheme/OrderScheme.ts)).

**Relations between entities:**

Each <code>User</code> can have only one non-deleted <code>Cart</code> at a time.
Each <code>Cart</code> is attached to a specific <code>User</code>.

One <code>User</code> can have multiple <code>Order</code>. Each <code>Order</code> is attached to a specific <code>User</code>.

<code>Cart</code> contains a list of products that user wants to order with the amount of those products specified.
In this task we will need to modify existing [Express application](https://github.com/kandalova/node-mentoring-shop/pull/3) by extending user model, adding authorization and authentication flows.

**Implementation criteria:**

- TypeScript is used.
- Data is stored in `MongoDB` database.
- `Docker` image is used for local development (check Running [MongoDB as a Docker Container](https://www.baeldung.com/linux/mongodb-as-docker-container#2-building-container-using-a-compose-file) for an example of docker-compose file).
- `Mongoose` is used as ODM for querying.
- `Data Access Layer` is rewritten to query `MongoDB`.
- Models are created based on entity schemas used in Express and Layered Architecture module.
- Models have proper relations between each other based on information specified above.

**API endpoints**

[swagger](https://github.com/kandalova/node-mentoring-shop/blob/task_6_shop_express.js/swagger.md)

<img width="729" alt="image" src="https://github.com/kandalova/node-mentoring-shop/assets/26093763/f1e36899-e171-436e-83c4-72dcb20264e2">
- User entity is added - contains id, email (unique), password, role (admin or simple user). Password is stored as hashed value in the database.
- [Bcrypt](https://www.npmjs.com/package/bcrypt) module is used for hashing passwords.
- New API endpoint is added for user **sign up** by email and password e.g `/register`. It creates user entity in the database.
- New API endpoint is added for user **sign in** by email and password e.g `/login`. It returns JWT token which contains user information. Pay attention that password is not encoded in token payload. JWT token expires in 2 hours.
- JWT token is passed in `Authorization` header for each request (except sign in and sign up) in the following format `Authorization: Bearer <token>`
- Authentication middleware is added to check if token provided is valid and if user encoded in token exists. If no, `401 Unauthorized` status code is returned. Otherwise, user can do action they intended to do.
- Only admin users can delete user cart. Authorization middleware is added for this purpose. If token provided doesn't belong to admin member, `403 Forbidden` status code is returned.

**Setup for startring (once)**

Expand All @@ -60,8 +35,26 @@ In the project root folder

**First time starting**
- Create your first database in MongoDB. Then, create your first collection (Example - User).
- It's possible to use seeder in `index.ts` to mock `Product` model.
- `Register` and `login` user, use returned token in requests's headers.

**API endpoints**

[swagger](https://github.com/kandalova/node-mentoring-shop/blob/task_9_authorization/swagger.md)

![image](https://github.com/kandalova/node-mentoring-shop/assets/26093763/cfdc8b06-f10c-4c05-8d9c-335a30572dde)

**How to convert `yaml` to `md`**

`widdershins swagger.yaml --language_tabs 'http:HTTP' -o swagger.md`

**Results**

![image](https://github.com/kandalova/node-mentoring-shop/assets/26093763/9e525a2f-c86b-4ecd-9f4e-5f8fe95a6904)
<img width="630" alt="image" src="https://github.com/kandalova/node-mentoring-shop/assets/26093763/3eca50a1-c116-4b33-9b9c-20326e6baaca">

<img width="636" alt="image" src="https://github.com/kandalova/node-mentoring-shop/assets/26093763/25ad2ebf-467f-4e4c-8e0f-15862ce99225">

<img width="638" alt="image" src="https://github.com/kandalova/node-mentoring-shop/assets/26093763/d94ccf58-e32b-4548-bc63-84bcbd43110b">

<img width="629" alt="image" src="https://github.com/kandalova/node-mentoring-shop/assets/26093763/3c73b35d-5304-4fbf-9553-2e58b6ca01ed">

129 changes: 125 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@types/bcryptjs": "^2.4.5",
"@types/express": "^4.17.18",
"@types/jsonwebtoken": "^9.0.4",
"@types/lodash.clonedeep": "^4.5.7",
"@types/mongoose": "^5.11.97",
"@types/node": "^20.8.10",
Expand All @@ -25,8 +27,11 @@
"typescript": "^5.2.2"
},
"dependencies": {
"bcryptjs": "^2.4.3",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"joi": "^17.11.0",
"jsonwebtoken": "^9.0.2",
"lodash.clonedeep": "^4.5.0",
"mongodb": "^6.2.0",
"mongoose": "^7.6.4",
Expand Down
29 changes: 29 additions & 0 deletions src/config/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { connect, disconnect } from "mongoose";

export const connectDB = async () => {
const { MONGO_URI } = process.env;
console.log(MONGO_URI)

if (!MONGO_URI) {
console.log("Please provide DataBase URI to connect. exiting now...");
process.exit(1);
}

try {
await connect(MONGO_URI);
console.log("Successfully connected to database");

// listen (ctrl-c)
process.on("SIGINT", async () => {
await disconnect();
console.log("DataBase is disconnected");
process.exit();
});

}
catch (err) {
console.log("DataBase connection failed. exiting now...");
console.error(err);
process.exit(1);
}
}
12 changes: 7 additions & 5 deletions src/controller/cart.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import express, { NextFunction, Request, Response } from "express";
import { createOrGetCart, createOrder, deleteCart, updateCart } from "../service/cart.service";
import Joi from "joi";
import { isAdmin } from "../middleware/isAdmin";
import { userHeaderHandler } from "../middleware/auth";
import { createOrGetCart, createOrder, deleteCart, updateCart } from "../service/cart.service";
import { getPutSchemeError } from "../utils/errors";

const cartRouter = express.Router();
Expand All @@ -23,7 +25,7 @@ const putValidator = async (req: Request, _: Response, next: NextFunction) => {

cartRouter.get("/", async (req: Request, res: Response, next: NextFunction) => {
try {
const userId = req.header('x-user-id')!;
const userId = req.user.user_id;
const cart = await createOrGetCart(userId);
res.send(cart);
}
Expand All @@ -32,7 +34,7 @@ cartRouter.get("/", async (req: Request, res: Response, next: NextFunction) => {
}
});

cartRouter.delete("/", async (req: Request, res: Response, next: NextFunction) => {
cartRouter.delete("/", isAdmin, userHeaderHandler, async (req: Request, res: Response, next: NextFunction) => {
try {
const userId = req.header('x-user-id')!;
const data = await deleteCart(userId);
Expand All @@ -45,7 +47,7 @@ cartRouter.delete("/", async (req: Request, res: Response, next: NextFunction) =

cartRouter.post("/checkout", async (req: Request, res: Response, next: NextFunction) => {
try {
const userId = req.header('x-user-id')!;
const userId = req.user.user_id;
const order = await createOrder(userId, req.body);
res.send(order);
}
Expand All @@ -56,7 +58,7 @@ cartRouter.post("/checkout", async (req: Request, res: Response, next: NextFunct

cartRouter.put("/", putValidator, async (req: Request, res: Response, next: NextFunction) => {
try {
const userId = req.header('x-user-id')!;
const userId = req.user.user_id;
const order = await updateCart(userId, req.body);
res.send(order);
}
Expand Down
Loading