From 1e76679cf6da43f5943eb610c8201d55ad88473b Mon Sep 17 00:00:00 2001 From: Leyla Kandalova Date: Fri, 3 Nov 2023 15:32:46 +0400 Subject: [PATCH 01/13] Update README.md --- README.md | 61 ++++++++----------------------------------------------- 1 file changed, 9 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 947daac..54c0db7 100644 --- a/README.md +++ b/README.md @@ -1,38 +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: - -User - 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)). - -Product - 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)). - -Cart - 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)). - -Order - 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 User can have only one non-deleted Cart at a time. -Each Cart is attached to a specific User. - -One User can have multiple Order. Each Order is attached to a specific User. - -Cart 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. +- User entity is added - contains id ([uuid](https://www.npmjs.com/package/uuid)), 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 ` +- 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. **API endpoints** @@ -40,28 +21,4 @@ One User can have multiple Order. Each Order -**Setup for startring (once)** - -In the project root folder - -- Install [podman](https://podman.io/docs/installation) -- Install [podman-compose](https://github.com/containers/podman-compose#installation) -- Init podman machine `podman machine init` -- Install [MongoDB server](https://www.mongodb.com/try/download/community) -- Install MongoDB tools to connect to MongoDB: [Shell](https://www.mongodb.com/try/download/shell) or [Compass Gui](https://www.mongodb.com/try/download/compass) - -**To start(every time)** -- `podman machine start` - start virtual machine -- `podman-compose up -d` - starts container from `docker-compose.yaml` -- `podman ps` - check running containers -- Connect to Mongo via Compass or shell `mongosh --port 27017` - -[Running MongoDB as a Docker Container](https://www.baeldung.com/linux/mongodb-as-docker-container#2-building-container-using-a-compose-file) - -**First time starting** -- Create your first database in MongoDB. Then, create your first collection (Example - User). - **Results** - -![image](https://github.com/kandalova/node-mentoring-shop/assets/26093763/9e525a2f-c86b-4ecd-9f4e-5f8fe95a6904) - From 687f2984a43ee090098911d483c85ff8244d18e9 Mon Sep 17 00:00:00 2001 From: Leyla Kandalova Date: Fri, 3 Nov 2023 21:19:26 +0600 Subject: [PATCH 02/13] Separate db config --- package-lock.json | 129 +++++++++++++++++++++++++++++++++++++++-- package.json | 5 ++ src/config/database.ts | 30 ++++++++++ src/index.ts | 27 +++++---- 4 files changed, 173 insertions(+), 18 deletions(-) create mode 100644 src/config/database.ts diff --git a/package-lock.json b/package-lock.json index abf2a2e..d863849 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,15 +9,20 @@ "version": "1.0.0", "license": "ISC", "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", "uuid": "^9.0.1" }, "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", @@ -309,6 +314,12 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "node_modules/@types/bcryptjs": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.5.tgz", + "integrity": "sha512-tOF6TivOIvq+TWQm78335CMdyVJhpBG3NUdWQDAp95ax4E2rSKbws/ELHLk5EBoucwx/tHt3/hhLOHwWJgVrSw==", + "dev": true + }, "node_modules/@types/body-parser": { "version": "1.19.3", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz", @@ -364,6 +375,15 @@ "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.4.tgz", + "integrity": "sha512-8UYapdmR0QlxgvJmyE8lP7guxD0UGVMfknsdtCFZh4ovShdBl3iOI4zdvqBHrB/IS+xUj3PSx73Qkey1fhWz+g==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/lodash": { "version": "4.14.199", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.199.tgz", @@ -876,6 +896,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -951,6 +976,11 @@ "node": ">=16.20.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1185,6 +1215,25 @@ "node": ">=6.0.0" } }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1946,6 +1995,46 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kareem": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", @@ -1996,17 +2085,51 @@ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -2680,7 +2803,6 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -3139,8 +3261,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yn": { "version": "3.1.1", diff --git a/package.json b/package.json index c1c88b9..b03d3b4 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/src/config/database.ts b/src/config/database.ts new file mode 100644 index 0000000..6e5c6bd --- /dev/null +++ b/src/config/database.ts @@ -0,0 +1,30 @@ +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); + } +} diff --git a/src/index.ts b/src/index.ts index 06157ba..b7bccf0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,8 +3,9 @@ import cartRouter from "./controller/cart.controller.ts"; import productRouter from "./controller/product.controller.ts"; import { ResponseError, getForbidenError, getUnauthorizedError } from "./utils/errors.ts"; import { getResponseError } from "./utils/utils.ts"; -import { connect, disconnect } from "mongoose"; import { UserModel } from "./scheme/UserScheme.ts"; +import { config } from "dotenv"; +import { connectDB } from "./config/database.ts"; //for seeders // import { runUserSeeder } from "./seeder/userSeeder.ts"; @@ -13,8 +14,6 @@ import { UserModel } from "./scheme/UserScheme.ts"; //for cart check // import { CartModel } from "./scheme/CartScheme.ts"; -export const app = express(); -const PORT = 3000; const headerHandler = async (req: Request, _: Response, next: NextFunction) => { try { @@ -43,15 +42,22 @@ const errorHandler = async (err: Error | ResponseError, _req: Request, res: Resp res.send(await getResponseError(err.message)); } -app.use('/api/profile/cart', headerHandler, cartRouter, errorHandler); -app.use('/api/products', headerHandler, productRouter, errorHandler); - export const init = (async () => { try { - await connect("mongodb://127.0.0.1:27017/NM_SHOP"); + config(); + const { API_PORT } = process.env; + const PORT = API_PORT || 3000; + + await connectDB(); + const app = express(); + + app.use('/api/profile/cart', headerHandler, cartRouter, errorHandler); + app.use('/api/products', headerHandler, productRouter, errorHandler); + app.listen(PORT, () => { console.log(`Server is Fire at http://localhost:${PORT}`); }); + //mock data for first run // const user = await UserModel.findOne(); // if (!user) { @@ -70,10 +76,3 @@ export const init = (async () => { console.log(err); } })(); - -// listen (ctrl-c) -process.on("SIGINT", async () => { - await disconnect(); - console.log("Server is disconnected"); - process.exit(); -}); From 5612db266b5c763c9165b7cbae9c1149af1da3ee Mon Sep 17 00:00:00 2001 From: Leyla Kandalova Date: Fri, 3 Nov 2023 23:45:34 +0600 Subject: [PATCH 03/13] Add register user --- src/controller/user.controller.ts | 38 +++++++++++++++++++++++++++++++ src/index.ts | 2 ++ src/scheme/UserScheme.ts | 7 ++++++ src/service/user.service.ts | 18 +++++++++++++++ src/utils/cartUtils.ts | 2 +- src/utils/errors.ts | 4 ++++ 6 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 src/controller/user.controller.ts create mode 100644 src/service/user.service.ts diff --git a/src/controller/user.controller.ts b/src/controller/user.controller.ts new file mode 100644 index 0000000..e03d3b2 --- /dev/null +++ b/src/controller/user.controller.ts @@ -0,0 +1,38 @@ +import express, { NextFunction, Request, Response } from "express"; +import Joi from "joi"; +import { createUser } from "../service/user.service"; +import { getPutSchemeError } from "../utils/errors"; + +const registerSchema = Joi.object({ + name: Joi.string().required(), + isAuthor: Joi.boolean().required(), + email: Joi.string().email().required(), + password: Joi.string().length(10).required(), +}) + +const registerValidator = async (req: Request, _: Response, next: NextFunction) => { + try { + await registerSchema.validateAsync(req.body); + next(); + } + catch (err) { + next(getPutSchemeError()); + } +} + +const userRouter = express.Router(); +userRouter.use(express.json()); + +userRouter.post("/register", registerValidator, async (req: Request, res: Response, next: NextFunction) => { + try { + const { name, isAuthor, email, password } = req.body; + await createUser(name, isAuthor, email, password); + res.status(201).send('User successfully registered'); + } + catch (err) { + next(err); + } +}); + + +export default userRouter; diff --git a/src/index.ts b/src/index.ts index b7bccf0..48228f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import { getResponseError } from "./utils/utils.ts"; import { UserModel } from "./scheme/UserScheme.ts"; import { config } from "dotenv"; import { connectDB } from "./config/database.ts"; +import userRouter from "./controller/user.controller.ts"; //for seeders // import { runUserSeeder } from "./seeder/userSeeder.ts"; @@ -53,6 +54,7 @@ export const init = (async () => { app.use('/api/profile/cart', headerHandler, cartRouter, errorHandler); app.use('/api/products', headerHandler, productRouter, errorHandler); + app.use('/api/user', userRouter, errorHandler); app.listen(PORT, () => { console.log(`Server is Fire at http://localhost:${PORT}`); diff --git a/src/scheme/UserScheme.ts b/src/scheme/UserScheme.ts index d3b8c38..3f39e21 100644 --- a/src/scheme/UserScheme.ts +++ b/src/scheme/UserScheme.ts @@ -2,10 +2,17 @@ import { Schema, model } from "mongoose"; export interface IUser { name: string, + email: string, + password: string, + role: string + } const userSchema = new Schema({ name: { type: String, required: true }, + email: { type: String, unique: true, required: true }, + password: { type: String, required: true }, + role: { type: String, required: true }, }, { versionKey: false }) export const UserModel = model("User", userSchema); diff --git a/src/service/user.service.ts b/src/service/user.service.ts new file mode 100644 index 0000000..5d58965 --- /dev/null +++ b/src/service/user.service.ts @@ -0,0 +1,18 @@ +import bcrypt from "bcryptjs"; + +import { UserModel } from "../scheme/UserScheme"; +import { throwRegisterError } from "../utils/errors"; + +export const createUser = async (name: string, isAuthor: boolean, email: string, password: string) => { + const oldUser = await UserModel.findOne({ email }); + if (oldUser) { + throwRegisterError(); + } + const encryptedPassword = await bcrypt.hash(password, 10); + await UserModel.create({ + name, + email: email.toLowerCase(), + password: encryptedPassword, + role: isAuthor ? "author" : "reader" + }); +} diff --git a/src/utils/cartUtils.ts b/src/utils/cartUtils.ts index d66e008..e8c456f 100644 --- a/src/utils/cartUtils.ts +++ b/src/utils/cartUtils.ts @@ -1,4 +1,4 @@ -import { IPopulatedCartItem, ICartResponse, ICart, IDeleteCartResponse, OmitCart, PopulatedCart } from "../scheme/CartScheme"; +import { IPopulatedCartItem, ICartResponse, ICart, IDeleteCartResponse, OmitCart, PopulatedCart } from "../scheme/CartScheme"; import { IOrderInfo } from "../scheme/OrderScheme"; export const getOmitCart = ({ _id, items }: PopulatedCart | ICart): OmitCart => { diff --git a/src/utils/errors.ts b/src/utils/errors.ts index 9832f11..73ce42a 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -58,3 +58,7 @@ export const getForbidenError = (status = 403) => { export const getPutSchemeError = (status = 400) => { return getResposeError(`Bad request body`, status) } + +export const throwRegisterError = (status = 409) => { + throw getResposeError(`User Already Exist. Please Login`, status) +} From 97d26b2e36d9dce519caa46e3bda9fb495fb45dc Mon Sep 17 00:00:00 2001 From: Leyla Kandalova Date: Sat, 4 Nov 2023 00:11:25 +0600 Subject: [PATCH 04/13] Add login user --- src/controller/user.controller.ts | 28 +++++++++++++++++++++++++++- src/service/user.service.ts | 24 +++++++++++++++++++++++- src/utils/errors.ts | 4 ++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/controller/user.controller.ts b/src/controller/user.controller.ts index e03d3b2..5e709af 100644 --- a/src/controller/user.controller.ts +++ b/src/controller/user.controller.ts @@ -1,6 +1,6 @@ import express, { NextFunction, Request, Response } from "express"; import Joi from "joi"; -import { createUser } from "../service/user.service"; +import { createUser, loginUser } from "../service/user.service"; import { getPutSchemeError } from "../utils/errors"; const registerSchema = Joi.object({ @@ -10,6 +10,11 @@ const registerSchema = Joi.object({ password: Joi.string().length(10).required(), }) +const loginSchema = Joi.object({ + email: Joi.string().email().required(), + password: Joi.string().length(10).required(), +}) + const registerValidator = async (req: Request, _: Response, next: NextFunction) => { try { await registerSchema.validateAsync(req.body); @@ -20,6 +25,16 @@ const registerValidator = async (req: Request, _: Response, next: NextFunction) } } +const loginValidator = async (req: Request, _: Response, next: NextFunction) => { + try { + await loginSchema.validateAsync(req.body); + next(); + } + catch (err) { + next(getPutSchemeError()); + } +} + const userRouter = express.Router(); userRouter.use(express.json()); @@ -34,5 +49,16 @@ userRouter.post("/register", registerValidator, async (req: Request, res: Respon } }); +userRouter.post("/login", loginValidator, async (req: Request, res: Response, next: NextFunction) => { + try { + const { email, password } = req.body; + const token = await loginUser(email, password); + res.send(token) + } + catch (err) { + next(err); + } +}); + export default userRouter; diff --git a/src/service/user.service.ts b/src/service/user.service.ts index 5d58965..1a34f69 100644 --- a/src/service/user.service.ts +++ b/src/service/user.service.ts @@ -1,7 +1,12 @@ import bcrypt from "bcryptjs"; +import * as jwt from "jsonwebtoken"; import { UserModel } from "../scheme/UserScheme"; -import { throwRegisterError } from "../utils/errors"; +import { throwLoginUserError, throwRegisterError } from "../utils/errors"; + +const findUserOrFail = async (email: string) => { + return UserModel.findOne({ email }).orFail(() => throwLoginUserError()); +} export const createUser = async (name: string, isAuthor: boolean, email: string, password: string) => { const oldUser = await UserModel.findOne({ email }); @@ -16,3 +21,20 @@ export const createUser = async (name: string, isAuthor: boolean, email: string, role: isAuthor ? "author" : "reader" }); } + +export const loginUser = async (email: string, password: string) => { + const user = await findUserOrFail(email); + if (!isTheSameCrypt(password, user.password)) { + throwLoginUserError() + } + const token = jwt.sign( + { user_id: user._id, email, role: user.role }, + process.env.TOKEN_KEY!, + { expiresIn: '2h' } + ); + return { token }; +} + +const isTheSameCrypt = async (str1: string, str2: string) => { + await bcrypt.compare(str1, str2) +} diff --git a/src/utils/errors.ts b/src/utils/errors.ts index 73ce42a..59a5e53 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -62,3 +62,7 @@ export const getPutSchemeError = (status = 400) => { export const throwRegisterError = (status = 409) => { throw getResposeError(`User Already Exist. Please Login`, status) } + +export const throwLoginUserError = (status = 400) => { + throw getResposeError(`Invalid Credentials`, status) +} From 00586aff926fd9c4844ab39f4a2fdce501850840 Mon Sep 17 00:00:00 2001 From: Leyla Kandalova Date: Sat, 4 Nov 2023 21:00:37 +0600 Subject: [PATCH 05/13] Add auth and separate error middlewares --- src/index.ts | 45 +++++++++++++--------------------------- src/middleware/auth.ts | 46 +++++++++++++++++++++++++++++++++++++++++ src/middleware/error.ts | 13 ++++++++++++ src/utils/errors.ts | 8 +++++++ 4 files changed, 81 insertions(+), 31 deletions(-) create mode 100644 src/middleware/auth.ts create mode 100644 src/middleware/error.ts diff --git a/src/index.ts b/src/index.ts index 48228f2..eda0f6b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,11 @@ -import express, { NextFunction, Request, Response } from "express"; +import express from "express"; import cartRouter from "./controller/cart.controller.ts"; import productRouter from "./controller/product.controller.ts"; -import { ResponseError, getForbidenError, getUnauthorizedError } from "./utils/errors.ts"; -import { getResponseError } from "./utils/utils.ts"; -import { UserModel } from "./scheme/UserScheme.ts"; import { config } from "dotenv"; import { connectDB } from "./config/database.ts"; import userRouter from "./controller/user.controller.ts"; +import { CurrentUser, verifyToken } from "./middleware/auth.ts"; +import { errorHandler } from "./middleware/error.ts"; //for seeders // import { runUserSeeder } from "./seeder/userSeeder.ts"; @@ -15,34 +14,15 @@ import userRouter from "./controller/user.controller.ts"; //for cart check // import { CartModel } from "./scheme/CartScheme.ts"; - -const headerHandler = async (req: Request, _: Response, next: NextFunction) => { - try { - const userId = req.header('x-user-id') ?? ''; - if (!userId) { - next(getForbidenError()); - } - const user = await UserModel.findById(userId); - if (!user) { - next(getUnauthorizedError()); +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Express { + interface Request { + user: CurrentUser } - next(); - } - catch (err) { - next(err); } } -const errorHandler = async (err: Error | ResponseError, _req: Request, res: Response, _next: NextFunction) => { - if (err instanceof ResponseError) { - res.status(err.status); - } - else { - res.status(500); - } - res.send(await getResponseError(err.message)); -} - export const init = (async () => { try { config(); @@ -52,9 +32,12 @@ export const init = (async () => { await connectDB(); const app = express(); - app.use('/api/profile/cart', headerHandler, cartRouter, errorHandler); - app.use('/api/products', headerHandler, productRouter, errorHandler); - app.use('/api/user', userRouter, errorHandler); + app.use('/api', verifyToken); + app.use('/api/profile/cart', cartRouter); + app.use('/api/products', productRouter); + app.use('/user', userRouter); + app.use(errorHandler); + app.listen(PORT, () => { console.log(`Server is Fire at http://localhost:${PORT}`); diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts new file mode 100644 index 0000000..fafcdfa --- /dev/null +++ b/src/middleware/auth.ts @@ -0,0 +1,46 @@ +import { NextFunction, Request, Response } from "express"; +import * as jwt from "jsonwebtoken"; +import { getAuthError, getTokenError } from "../utils/errors"; + +export interface CurrentUser { + user_id: string + email: string, + role: string +} + +export const verifyToken = async (req: Request, res: Response, next: NextFunction) => { + try { + const authHeader = req.headers.authorization; + if (!authHeader) { + next(getAuthError()); + } + const [tokenType, token] = authHeader!.split(" "); + if (tokenType !== "Bearer") { + next(getTokenError(403)); + } + const user = jwt.verify(token, process.env.TOKEN_KEY!) as CurrentUser; + req.user = user; + next(); + } + catch (err) { + next(getTokenError()); + } +} + +//obsolete +// const headerHandler = async (req: Request, _: Response, next: NextFunction) => { +// try { +// const userId = req.header('x-user-id') ?? ''; +// if (!userId) { +// next(getForbidenError()); +// } +// const user = await UserModel.findById(userId); +// if (!user) { +// next(getUnauthorizedError()); +// } +// next(); +// } +// catch (err) { +// next(err); +// } +// } diff --git a/src/middleware/error.ts b/src/middleware/error.ts new file mode 100644 index 0000000..380d76e --- /dev/null +++ b/src/middleware/error.ts @@ -0,0 +1,13 @@ +import { NextFunction, Request, Response } from "express"; +import { ResponseError } from "../utils/errors"; +import { getResponseError } from "../utils/utils"; + +export const errorHandler = async (err: Error | ResponseError, _req: Request, res: Response, _next: NextFunction) => { + if (err instanceof ResponseError) { + res.status(err.status); + } + else { + res.status(500); + } + res.send(await getResponseError(err.message)); +} diff --git a/src/utils/errors.ts b/src/utils/errors.ts index 59a5e53..b6f16da 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -66,3 +66,11 @@ export const throwRegisterError = (status = 409) => { export const throwLoginUserError = (status = 400) => { throw getResposeError(`Invalid Credentials`, status) } + +export const getAuthError = (status = 401) => { + return getResposeError(`Token is required`, status); +} + +export const getTokenError = (status = 401) => { + return getResposeError(`Invalid Token`, status); +} From 36bde3c05e7cd4227e8902367c53384b2f683593 Mon Sep 17 00:00:00 2001 From: Leyla Kandalova Date: Sat, 4 Nov 2023 21:30:43 +0600 Subject: [PATCH 06/13] Add Authorization middleware for cart deleting --- src/controller/cart.controller.ts | 10 ++++--- src/controller/user.controller.ts | 6 ++--- src/middleware/auth.ts | 44 ++++++++++++++++--------------- src/middleware/isAdmin.ts | 12 +++++++++ src/service/user.service.ts | 4 +-- src/utils/errors.ts | 4 +++ 6 files changed, 50 insertions(+), 30 deletions(-) create mode 100644 src/middleware/isAdmin.ts diff --git a/src/controller/cart.controller.ts b/src/controller/cart.controller.ts index a6bd7b7..ee39d9d 100644 --- a/src/controller/cart.controller.ts +++ b/src/controller/cart.controller.ts @@ -2,6 +2,8 @@ import express, { NextFunction, Request, Response } from "express"; import { createOrGetCart, createOrder, deleteCart, updateCart } from "../service/cart.service"; import Joi from "joi"; import { getPutSchemeError } from "../utils/errors"; +import { isAdmin } from "../middleware/isAdmin"; +import { userHeaderHandler } from "../middleware/auth"; const cartRouter = express.Router(); cartRouter.use(express.json()); @@ -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); } @@ -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); @@ -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); } @@ -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); } diff --git a/src/controller/user.controller.ts b/src/controller/user.controller.ts index 5e709af..911f344 100644 --- a/src/controller/user.controller.ts +++ b/src/controller/user.controller.ts @@ -5,7 +5,7 @@ import { getPutSchemeError } from "../utils/errors"; const registerSchema = Joi.object({ name: Joi.string().required(), - isAuthor: Joi.boolean().required(), + isAdmin: Joi.boolean().required(), email: Joi.string().email().required(), password: Joi.string().length(10).required(), }) @@ -40,8 +40,8 @@ userRouter.use(express.json()); userRouter.post("/register", registerValidator, async (req: Request, res: Response, next: NextFunction) => { try { - const { name, isAuthor, email, password } = req.body; - await createUser(name, isAuthor, email, password); + const { name, isAdmin, email, password } = req.body; + await createUser(name, isAdmin, email, password); res.status(201).send('User successfully registered'); } catch (err) { diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index fafcdfa..a402637 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -1,6 +1,7 @@ import { NextFunction, Request, Response } from "express"; import * as jwt from "jsonwebtoken"; -import { getAuthError, getTokenError } from "../utils/errors"; +import { getAuthError, getTokenError, getUnauthorizedError } from "../utils/errors"; +import { UserModel } from "../scheme/UserScheme"; export interface CurrentUser { user_id: string @@ -18,29 +19,30 @@ export const verifyToken = async (req: Request, res: Response, next: NextFunctio if (tokenType !== "Bearer") { next(getTokenError(403)); } - const user = jwt.verify(token, process.env.TOKEN_KEY!) as CurrentUser; - req.user = user; - next(); + else { + const user = jwt.verify(token, process.env.TOKEN_KEY!) as CurrentUser; + req.user = user; + next(); + } } catch (err) { next(getTokenError()); } } -//obsolete -// const headerHandler = async (req: Request, _: Response, next: NextFunction) => { -// try { -// const userId = req.header('x-user-id') ?? ''; -// if (!userId) { -// next(getForbidenError()); -// } -// const user = await UserModel.findById(userId); -// if (!user) { -// next(getUnauthorizedError()); -// } -// next(); -// } -// catch (err) { -// next(err); -// } -// } +export const userHeaderHandler = async (req: Request, _: Response, next: NextFunction) => { + try { + const userId = req.header('x-user-id') ?? ''; + if (!userId) { + next(getUnauthorizedError()); + } + const user = await UserModel.findById(userId); + if (!user) { + next(getUnauthorizedError()); + } + next(); + } + catch (err) { + next(err); + } +} diff --git a/src/middleware/isAdmin.ts b/src/middleware/isAdmin.ts new file mode 100644 index 0000000..b0d317a --- /dev/null +++ b/src/middleware/isAdmin.ts @@ -0,0 +1,12 @@ +import { NextFunction, Request, Response } from "express"; +import { getAdminError } from "../utils/errors"; + +export const isAdmin = async (req: Request, res: Response, next: NextFunction) => { + const currentUser = req.user; + if (currentUser.role !== 'admin') { + next(getAdminError()); + } + else { + next(); + } +} diff --git a/src/service/user.service.ts b/src/service/user.service.ts index 1a34f69..f26c29f 100644 --- a/src/service/user.service.ts +++ b/src/service/user.service.ts @@ -8,7 +8,7 @@ const findUserOrFail = async (email: string) => { return UserModel.findOne({ email }).orFail(() => throwLoginUserError()); } -export const createUser = async (name: string, isAuthor: boolean, email: string, password: string) => { +export const createUser = async (name: string, isAdmin: boolean, email: string, password: string) => { const oldUser = await UserModel.findOne({ email }); if (oldUser) { throwRegisterError(); @@ -18,7 +18,7 @@ export const createUser = async (name: string, isAuthor: boolean, email: string, name, email: email.toLowerCase(), password: encryptedPassword, - role: isAuthor ? "author" : "reader" + role: isAdmin ? "admin" : "customer" }); } diff --git a/src/utils/errors.ts b/src/utils/errors.ts index b6f16da..4f55c38 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -74,3 +74,7 @@ export const getAuthError = (status = 401) => { export const getTokenError = (status = 401) => { return getResposeError(`Invalid Token`, status); } + +export const getAdminError = (status = 403) => { + return getResposeError(`Forbidden`, status); +} From 85bb1fd7d941a9e28bb8aad217aa0de515c54d5c Mon Sep 17 00:00:00 2001 From: Leyla Kandalova Date: Sat, 4 Nov 2023 21:37:43 +0600 Subject: [PATCH 07/13] Fix cast error --- src/service/user.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service/user.service.ts b/src/service/user.service.ts index f26c29f..6c80b51 100644 --- a/src/service/user.service.ts +++ b/src/service/user.service.ts @@ -28,7 +28,7 @@ export const loginUser = async (email: string, password: string) => { throwLoginUserError() } const token = jwt.sign( - { user_id: user._id, email, role: user.role }, + { user_id: user._id.toString(), email, role: user.role }, process.env.TOKEN_KEY!, { expiresIn: '2h' } ); From 2eeb8b15108ae46bf03c588ba0cb770c3a05a520 Mon Sep 17 00:00:00 2001 From: Leyla Kandalova Date: Sat, 4 Nov 2023 21:49:32 +0600 Subject: [PATCH 08/13] Remove unused and old --- src/config/database.ts | 1 - src/controller/cart.controller.ts | 4 +- src/db/cart.db.js | 1 - src/db/db.js | 7 --- src/db/order.db.js | 1 - src/db/user.db.js | 6 --- src/index.ts | 17 ++----- src/middleware/auth.ts | 2 +- src/repository/cart.repository.ts | 70 ---------------------------- src/repository/order.repository.ts | 32 ------------- src/repository/product.repository.ts | 11 ----- src/repository/user.repository.ts | 5 -- src/scheme/UserScheme.ts | 1 - src/seeder/userSeeder.ts | 9 ---- src/service/user.service.ts | 1 - src/utils/errors.ts | 24 ---------- src/utils/utils.ts | 19 -------- 17 files changed, 6 insertions(+), 205 deletions(-) delete mode 100644 src/db/cart.db.js delete mode 100644 src/db/order.db.js delete mode 100644 src/db/user.db.js delete mode 100644 src/repository/cart.repository.ts delete mode 100644 src/repository/order.repository.ts delete mode 100644 src/repository/product.repository.ts delete mode 100644 src/repository/user.repository.ts delete mode 100644 src/seeder/userSeeder.ts diff --git a/src/config/database.ts b/src/config/database.ts index 6e5c6bd..da5998a 100644 --- a/src/config/database.ts +++ b/src/config/database.ts @@ -1,6 +1,5 @@ import { connect, disconnect } from "mongoose"; - export const connectDB = async () => { const { MONGO_URI } = process.env; console.log(MONGO_URI) diff --git a/src/controller/cart.controller.ts b/src/controller/cart.controller.ts index ee39d9d..0a0df00 100644 --- a/src/controller/cart.controller.ts +++ b/src/controller/cart.controller.ts @@ -1,9 +1,9 @@ import express, { NextFunction, Request, Response } from "express"; -import { createOrGetCart, createOrder, deleteCart, updateCart } from "../service/cart.service"; import Joi from "joi"; -import { getPutSchemeError } from "../utils/errors"; 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(); cartRouter.use(express.json()); diff --git a/src/db/cart.db.js b/src/db/cart.db.js deleted file mode 100644 index 263d7f4..0000000 --- a/src/db/cart.db.js +++ /dev/null @@ -1 +0,0 @@ -export const carts = []; diff --git a/src/db/db.js b/src/db/db.js index 9f039a3..17ef717 100644 --- a/src/db/db.js +++ b/src/db/db.js @@ -1,9 +1,2 @@ import { products } from "./product.db.js" -import { carts } from "./cart.db.js" -import { orders } from "./order.db.js" -import { users } from "./user.db.js"; - -export const userDB = users; export const productDB = products; -export const cartDB = carts; -export const orderDB = orders; diff --git a/src/db/order.db.js b/src/db/order.db.js deleted file mode 100644 index abdacfb..0000000 --- a/src/db/order.db.js +++ /dev/null @@ -1 +0,0 @@ -export const orders=[]; diff --git a/src/db/user.db.js b/src/db/user.db.js deleted file mode 100644 index 558bc33..0000000 --- a/src/db/user.db.js +++ /dev/null @@ -1,6 +0,0 @@ -export const users = [ - '99c27b1a-0ae2-486d-9d72-9c82579cf361', - 'ed3d57d6-c699-4909-910a-364745332f88', - 'e73b79aa-9cb0-4406-97fd-df231243beec', - 'cf35167a-574d-49e3-a0ed-affd8d746d6c', -] diff --git a/src/index.ts b/src/index.ts index eda0f6b..52eead3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,15 @@ import express from "express"; -import cartRouter from "./controller/cart.controller.ts"; -import productRouter from "./controller/product.controller.ts"; import { config } from "dotenv"; import { connectDB } from "./config/database.ts"; +import cartRouter from "./controller/cart.controller.ts"; +import productRouter from "./controller/product.controller.ts"; import userRouter from "./controller/user.controller.ts"; import { CurrentUser, verifyToken } from "./middleware/auth.ts"; import { errorHandler } from "./middleware/error.ts"; //for seeders -// import { runUserSeeder } from "./seeder/userSeeder.ts"; // import { ProductModel } from "./scheme/ProductScheme.ts"; // import { runProductSeeder } from "./seeder/productSeeder.ts"; -//for cart check -// import { CartModel } from "./scheme/CartScheme.ts"; declare global { // eslint-disable-next-line @typescript-eslint/no-namespace @@ -43,19 +40,11 @@ export const init = (async () => { console.log(`Server is Fire at http://localhost:${PORT}`); }); - //mock data for first run - // const user = await UserModel.findOne(); - // if (!user) { - // await runUserSeeder(); - // } + //mock product data for first run // const product = await ProductModel.findOne(); // if (!product) { // await runProductSeeder(); // } - //check cart - // const doc = new CartModel({items: [{ count: 5 }]}); - // console.log('doc', doc); - // console.log(doc.items[0].ownerDocument()===doc); } catch (err) { console.log(err); diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index a402637..0880cbe 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -1,7 +1,7 @@ import { NextFunction, Request, Response } from "express"; import * as jwt from "jsonwebtoken"; -import { getAuthError, getTokenError, getUnauthorizedError } from "../utils/errors"; import { UserModel } from "../scheme/UserScheme"; +import { getAuthError, getTokenError, getUnauthorizedError } from "../utils/errors"; export interface CurrentUser { user_id: string diff --git a/src/repository/cart.repository.ts b/src/repository/cart.repository.ts deleted file mode 100644 index 4dafc3c..0000000 --- a/src/repository/cart.repository.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { cartDB } from "../db/db"; -import { IPopulatedCartItem } from "../scheme/CartScheme"; -import { getUUID } from "../utils/utils"; - -interface ICart { - id: string; // uuid - userId: string; - isDeleted: boolean; - items: IPopulatedCartItem[]; -} - -enum CartEditableProperties { - Items = 'items', - IsDeleted = "isDeleted" -} - -export const generateCart = (userId: string): ICart => { - const newCart: ICart = { - id: getUUID(), - userId: userId, - isDeleted: false, - items: [], - } - return newCart; -} - -export const pushCart = async (cart: ICart) => { - await cartDB.push(cart); -} - -export const findUserCart = async (userId: string): Promise => { - const cart = await cartDB.find((cart) => cart.userId === userId && !cart.isDeleted); - return cart; -} - -export const findUserCartIdx = async (userId: string) => { - const index = await cartDB.findIndex((cart) => cart.userId === userId && !cart.isDeleted); - return index; -} - -export const findUserCartIdxByID = async (userId: string, cartId: string) => { - const index = await cartDB.findIndex((cart) => cart.userId === userId && cart.id === cartId && !cart.isDeleted); - return index; -} - -export const updateCartProperty = async (cartIdx: number, property: CartEditableProperties, value: boolean | IPopulatedCartItem[]) => { - cartDB[cartIdx][property] = value; -} - -export const getCartByIdx = async (cartIdx: number): Promise => { - return await cartDB[cartIdx]; -} -export const getCartProductIdx = async (cartIdx: number, productId: string) => { - const index = await cartDB[cartIdx].items.findIndex((item: IPopulatedCartItem) => item.product.product === productId); - return index; -} - -export const pushCartItem = async (cartIdx: number, product: IPopulatedCartItem) => { - await cartDB[cartIdx].items.push(product) -} - -export const updateCartItemCount = async (cartIdx: number, productIdx: number, count: number) => { - const cartItem = cartDB[cartIdx].items[productIdx]; - cartItem.count = count; -} - -export const deleteCartItem = async (cartIdx: number, productIdx: number) => { - const cartItems = cartDB[cartIdx].items; - await cartItems.splice(productIdx, 1); -} diff --git a/src/repository/order.repository.ts b/src/repository/order.repository.ts deleted file mode 100644 index 950dc29..0000000 --- a/src/repository/order.repository.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { orderDB } from "../db/db"; -import { IPopulatedCartItem } from "../scheme/CartScheme"; - -type ORDER_STATUS = 'created' | 'completed'; - -interface IPopulatedOrder { - id: string, - userId: string; - cartId: string; - items: IPopulatedCartItem[] - payment: { - type: string, - address?: unknown, - creditCard?: unknown, - }, - delivery: { - type: string, - address: unknown, - }, - comments: string, - status: ORDER_STATUS; - total: number; -} - -export const pushOrder = async (order: IPopulatedOrder): Promise => { - await orderDB.push(order); - return orderDB.length - 1; -} - -export const getOrderByIdx = async (idx: number): Promise => { - return await orderDB[idx]; -} diff --git a/src/repository/product.repository.ts b/src/repository/product.repository.ts deleted file mode 100644 index 32ff596..0000000 --- a/src/repository/product.repository.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { productDB } from "../db/db"; -import { IProduct } from "../scheme/ProductScheme"; - -export const findProduct = async (id: string): Promise => { - const product = await productDB.find((product) => product.id === id); - return product; -} - -export const findProducts = async (): Promise => { - return productDB; -} diff --git a/src/repository/user.repository.ts b/src/repository/user.repository.ts deleted file mode 100644 index 6651535..0000000 --- a/src/repository/user.repository.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { userDB } from "../db/db"; - -export const findUser = async (userId: string) => { - return await userDB.find((id) => id === userId); -} diff --git a/src/scheme/UserScheme.ts b/src/scheme/UserScheme.ts index 3f39e21..34bdb33 100644 --- a/src/scheme/UserScheme.ts +++ b/src/scheme/UserScheme.ts @@ -5,7 +5,6 @@ export interface IUser { email: string, password: string, role: string - } const userSchema = new Schema({ diff --git a/src/seeder/userSeeder.ts b/src/seeder/userSeeder.ts deleted file mode 100644 index a8c3548..0000000 --- a/src/seeder/userSeeder.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { userDB } from "../db/db" -import { UserModel } from "../scheme/UserScheme"; - -export const runUserSeeder = async () => { - userDB.forEach(async (value, i) => { - const newUser = new UserModel({ name: `User-${i}` }); - await newUser.save(); - }); -} diff --git a/src/service/user.service.ts b/src/service/user.service.ts index 6c80b51..c61728d 100644 --- a/src/service/user.service.ts +++ b/src/service/user.service.ts @@ -1,6 +1,5 @@ import bcrypt from "bcryptjs"; import * as jwt from "jsonwebtoken"; - import { UserModel } from "../scheme/UserScheme"; import { throwLoginUserError, throwRegisterError } from "../utils/errors"; diff --git a/src/utils/errors.ts b/src/utils/errors.ts index 4f55c38..207a01c 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -15,26 +15,10 @@ const getResposeError = (message: string, status: number): ResponseError => { return err; } -export const throwNoUser = (id: string) => { - throw new Error(`No user ${id}`); -} - -export const throwCartExistsError = (id: string): ResponseError => { - throw new ResponseError(`Cart for user ${id} already exists`, 400); -} - -export const throwNoCartExistsById = (id: string, cartId: string) => { - throw new Error(`No cart ${cartId} for user ${id}`); -} - export const throwNoCartExists = (id: string) => { throw new ResponseError(`No cart for user ${id}`, 400); } -export const getNoCartExists = (id: string): ResponseError => { - return new ResponseError(`No cart for user ${id}`, 400); -} - export const throwNoProductExists = () => { throw new ResponseError(`No product for user`, 400); } @@ -43,18 +27,10 @@ export const throwEmptyCart = (id: string) => { throw new Error(`Cart for user ${id} is empty`); } -export const throwUnauthorized = (status = 401) => { - throw getResposeError(`Header x-user-id is missing or no user with such id`, status) -} - export const getUnauthorizedError = (status = 401) => { return getResposeError(`Header x-user-id is missing or no user with such id`, status) } -export const getForbidenError = (status = 403) => { - return getResposeError(`You must be authorized user`, status) -} - export const getPutSchemeError = (status = 400) => { return getResposeError(`Bad request body`, status) } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 5662690..0c76711 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,24 +1,5 @@ -import { v4 as uuid } from 'uuid'; -import cloneDeep from 'lodash.clonedeep'; -import { NextFunction } from 'express'; import { IResponse, IResponseError } from '../scheme/ServiceUtils'; -export const getUUID = () => { - return uuid(); -}; - -export const createDeepCopy = (arr: T[]): T[] => { - return cloneDeep(arr); -} - -export const promiseHandler = async (prom: Promise, next: NextFunction) => { - try { - return await prom; - } catch (error) { - next(error); - } -} - export const getResponseError = async (message: string): Promise => { return { data: null, From 631241b4d042e31a33a10ac0bf2777306d789988 Mon Sep 17 00:00:00 2001 From: Leyla Kandalova Date: Sat, 4 Nov 2023 19:54:36 +0400 Subject: [PATCH 09/13] Update README.md --- README.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 54c0db7..6c000b2 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ In this task we will need to modify existing [Express application](https://githu **Implementation criteria:** -- User entity is added - contains id ([uuid](https://www.npmjs.com/package/uuid)), email (unique), password, role (admin or simple user). Password is stored as hashed value in the database. +- 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. @@ -15,6 +15,30 @@ In this task we will need to modify existing [Express application](https://githu - 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)** + +In the project root folder + +- Install [podman](https://podman.io/docs/installation) +- Install [podman-compose](https://github.com/containers/podman-compose#installation) +- Init podman machine `podman machine init` +- Install [MongoDB server](https://www.mongodb.com/try/download/community) +- Install MongoDB tools to connect to MongoDB: [Shell](https://www.mongodb.com/try/download/shell) or [Compass Gui](https://www.mongodb.com/try/download/compass) + +**To start(every time)** +- `podman machine start` - start virtual machine +- `podman-compose up -d` - starts container from `docker-compose.yaml` +- `podman ps` - check running containers +- Connect to Mongo via Compass or shell `mongosh --port 27017` + +[Running MongoDB as a Docker Container](https://www.baeldung.com/linux/mongodb-as-docker-container#2-building-container-using-a-compose-file) + +**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_6_shop_express.js/swagger.md) From 8f08edcec121fbc09d8f5c6c31cece53a0980a76 Mon Sep 17 00:00:00 2001 From: Leyla Kandalova Date: Sat, 4 Nov 2023 23:46:04 +0600 Subject: [PATCH 10/13] Update swagger --- swagger.md | 1696 ++++++++++++++++++++++++++++++++++++++++++++------ swagger.yaml | 295 ++++++--- 2 files changed, 1697 insertions(+), 294 deletions(-) diff --git a/swagger.md b/swagger.md index 6e1231d..36e6d26 100644 --- a/swagger.md +++ b/swagger.md @@ -1,14 +1,17 @@ --- -title: API v1.0.0 +title: shop api v1.0.0 language_tabs: - - http: http - - HTTP: HTTP -language_clients: - - http: "" - - HTTP: "" + - shell: Shell + - http: HTTP + - javascript: JavaScript + - ruby: Ruby + - python: Python + - php: PHP + - java: Java + - go: Go toc_footers: [] includes: [] -search: false +search: true highlight_theme: darkula headingLevel: 2 @@ -16,41 +19,164 @@ headingLevel: 2 -

null v1.0.0

+

shop api v1.0.0

> Scroll down for code samples, example requests and responses. Select a language for code samples from the tabs above or the mobile navigation menu. -

cart

+# Authentication + +- HTTP Authentication, scheme: bearer + +

cart

## get__api_profile_cart > Code samples +```shell +# You can also use wget +curl -X GET /api/profile/cart \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer {access-token}' + +``` + ```http GET /api/profile/cart HTTP/1.1 Accept: application/json -x-user-id: eb5a26af-6e4c-4f31-a9b1-3450d42ac66c ``` -```http -GET /api/profile/cart HTTP/1.1 +```javascript -Accept: application/json -x-user-id: eb5a26af-6e4c-4f31-a9b1-3450d42ac66c +const headers = { + 'Accept':'application/json', + 'Authorization':'Bearer {access-token}' +}; + +fetch('/api/profile/cart', +{ + method: 'GET', + + headers: headers +}) +.then(function(res) { + return res.json(); +}).then(function(body) { + console.log(body); +}); ``` -`GET /api/profile/cart` +```ruby +require 'rest-client' +require 'json' -*Get user cart or create if it is missing* +headers = { + 'Accept' => 'application/json', + 'Authorization' => 'Bearer {access-token}' +} -

Parameters

+result = RestClient.get '/api/profile/cart', + params: { + }, headers: headers -|Name|In|Type|Required|Description| -|---|---|---|---|---| -|x-user-id|header|string(uuid)|true|User id (uuid)| +p JSON.parse(result) + +``` + +```python +import requests +headers = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +r = requests.get('/api/profile/cart', headers = headers) + +print(r.json()) + +``` + +```php + 'application/json', + 'Authorization' => 'Bearer {access-token}', +); + +$client = new \GuzzleHttp\Client(); + +// Define array of request body. +$request_body = array(); + +try { + $response = $client->request('GET','/api/profile/cart', array( + 'headers' => $headers, + 'json' => $request_body, + ) + ); + print_r($response->getBody()->getContents()); + } + catch (\GuzzleHttp\Exception\BadResponseException $e) { + // handle exception or api errors. + print_r($e->getMessage()); + } + + // ... + +``` + +```java +URL obj = new URL("/api/profile/cart"); +HttpURLConnection con = (HttpURLConnection) obj.openConnection(); +con.setRequestMethod("GET"); +int responseCode = con.getResponseCode(); +BufferedReader in = new BufferedReader( + new InputStreamReader(con.getInputStream())); +String inputLine; +StringBuffer response = new StringBuffer(); +while ((inputLine = in.readLine()) != null) { + response.append(inputLine); +} +in.close(); +System.out.println(response.toString()); + +``` + +```go +package main + +import ( + "bytes" + "net/http" +) + +func main() { + + headers := map[string][]string{ + "Accept": []string{"application/json"}, + "Authorization": []string{"Bearer {access-token}"}, + } + + data := bytes.NewBuffer([]byte{jsonReq}) + req, err := http.NewRequest("GET", "/api/profile/cart", data) + req.Header = headers + + client := &http.Client{} + resp, err := client.Do(req) + // ... +} + +``` + +`GET /api/profile/cart` + +*Get user cart or create if it is missing* > Example responses @@ -60,11 +186,11 @@ x-user-id: eb5a26af-6e4c-4f31-a9b1-3450d42ac66c { "data": { "cart": { - "id": "eb5a26af-6e4c-4f31-a9b1-3450d42ac66c", + "id": "6546650c159ab6e8bb59f925", "items": [ { "product": { - "id": "891389f0-4312-42d6-a650-6fda0959c734", + "id": "65426846103aa5e276433519", "title": "Book", "description": "Interesting book", "price": 200 @@ -85,7 +211,7 @@ x-user-id: eb5a26af-6e4c-4f31-a9b1-3450d42ac66c { "data": null, "error": { - "message": "User is not authorized" + "message": "Token is required" } } ``` @@ -96,7 +222,7 @@ x-user-id: eb5a26af-6e4c-4f31-a9b1-3450d42ac66c { "data": null, "error": { - "message": "You must be authorized user" + "message": "Invalid Token" } } ``` @@ -117,33 +243,167 @@ x-user-id: eb5a26af-6e4c-4f31-a9b1-3450d42ac66c |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Returns user cart|[CartResponse](#schemacartresponse)| -|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized (when no user matching authorization header is found)|[CartResponse](#schemacartresponse)| -|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden (when authorization header is missing)|[CartResponse](#schemacartresponse)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized (when no token is found)|[CartResponse](#schemacartresponse)| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden (when token is missed or invalid)|[CartResponse](#schemacartresponse)| |500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error|[CartResponse](#schemacartresponse)| -