diff --git a/.env-example b/.env-example index f61150e6..2db76d37 100644 --- a/.env-example +++ b/.env-example @@ -1,9 +1,13 @@ +PORT = + +SERVER_URL=http://localhost: +DEPLOYED_URL=https://hackers-ec-be.onrender.com + DB_DEV_URL=postgres://:@localhost:5432/ DB_TEST_URL= DB_PROD_URL= DEV_MODE=development DB_HOSTED_MODE=local -PORT = SESSION_SECRET= @@ -27,3 +31,6 @@ SERVICE = < your SERVICE > EMAIL= PASSWORD= +CLOUDINARY_CLOUD_NAME= +CLOUDINARY_API_KEY= +CLOUDINARY_API_SECRET= diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 8bc642d8..65619d38 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -31,12 +31,12 @@ jobs: DB_PROD_URL: ${{ secrets.DB_PROD_URL }} DB_DEV_URL: ${{ secrets.DB_DEV_URL }} GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} - - DEPLOYED_URL: ${{ secrets.DEPLOYED_URL }} CLOUDINARY_CLOUD_NAME: ${{ secrets.CLOUDINARY_CLOUD_NAME }} CLOUDINARY_API_KEY: ${{ secrets.CLOUDINARY_API_KEY }} CLOUDINARY_API_SECRET: ${{ secrets.CLOUDINARY_API_SECRET }} CLOUDINARY_FOLDER_NAME: ${{ secrets.CLOUDINARY_FOLDER_NAME }} + SERVER_URL: ${{ secrets.SERVER_URL }} + DEPLOYED_URL: ${{ secrets.DEPLOYED_URL }} JWT_SECRET: ${{ secrets.JWT_SECRET }} strategy: matrix: @@ -52,12 +52,14 @@ jobs: run: npm install - name: Run tests run: npm run test + - name: Setup Code Climate test-reporter run: | # Download test reporter as a static binary curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter chmod +x ./cc-test-reporter ./cc-test-reporter before-build + - name: Store coverage report if: always() run: mkdir -p coverage diff --git a/package-lock.json b/package-lock.json index 483ff0f4..ff6410ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@types/jsonwebtoken": "^9.0.6", "@types/nodemailer": "^6.4.14", "bcrypt": "^5.1.1", + "cloudinary": "^2.2.0", "cors": "^2.8.5", "cross-env": "^7.0.3", "dotenv": "^16.4.5", @@ -20,6 +21,7 @@ "express-session": "^1.18.0", "joi": "^17.12.3", "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", "nodemailer": "^6.9.13", "npm-run-all": "^4.1.5", "passport": "^0.7.0", @@ -41,6 +43,7 @@ "@types/express-session": "^1.18.0", "@types/jest": "^29.5.12", "@types/jsonwebtoken": "^9.0.6", + "@types/multer": "^1.4.11", "@types/node": "^20.12.7", "@types/nodemailer": "^6.4.14", "@types/passport": "^1.0.16", @@ -1806,6 +1809,15 @@ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, + "node_modules/@types/multer": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz", + "integrity": "sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "20.12.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", @@ -2354,6 +2366,11 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -2834,8 +2851,18 @@ "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } }, "node_modules/bytes": { "version": "3.1.2", @@ -3104,6 +3131,18 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/cloudinary": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.2.0.tgz", + "integrity": "sha512-akbLTZcNegGSkl07Frnt9fyiK9KZ2zPS+a+j7uLrjNYxVhDpDdIBz9G6snPCYqgk+WLVMRPfXTObalLr5L6g0Q==", + "dependencies": { + "lodash": "^4.17.21", + "q": "^1.5.1" + }, + "engines": { + "node": ">=9" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -3186,6 +3225,52 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", @@ -3251,6 +3336,11 @@ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -7157,7 +7247,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7233,6 +7322,34 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/multer/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -8345,6 +8462,11 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -8406,6 +8528,15 @@ } ] }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -9267,6 +9398,14 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -10134,6 +10273,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "node_modules/typescript": { "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", diff --git a/package.json b/package.json index 206a5218..2e78846a 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "dev": "ts-node-dev --require dotenv/config src/index.ts", "build": "tsc", "start": "node --require dotenv/config dist/index.js", + "drop-all-tables": "psql -U your_username -d your_database_name -c \"SELECT 'DROP TABLE IF EXISTS \"' || tablename || '\" CASCADE;' FROM pg_tables WHERE schemaname = 'public'\" | psql -U your_username -d your_database_name", "migrate": "npx sequelize-cli db:migrate", "migrate:reset": "npx sequelize-cli db:migrate:undo:all && npm run migrate && npm run seed", "migrate:down": "sequelize cli db:migrate:undo:all", @@ -31,11 +32,13 @@ "bcrypt": "^5.1.1", "cross-env": "^7.0.3", "cors": "^2.8.5", + "cloudinary": "^2.2.0", "dotenv": "^16.4.5", "express": "^4.19.2", "express-session": "^1.18.0", "joi": "^17.12.3", "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", "nodemailer": "^6.9.13", "npm-run-all": "^4.1.5", "passport": "^0.7.0", @@ -66,7 +69,8 @@ "/src/database/config", "src/helpers", "src/documention/index.ts", - "/src/utils" + "/src/utils", + "src/middlewares/multer.ts" ] }, "devDependencies": { @@ -77,6 +81,7 @@ "@types/express-session": "^1.18.0", "@types/jest": "^29.5.12", "@types/jsonwebtoken": "^9.0.6", + "@types/multer": "^1.4.11", "@types/node": "^20.12.7", "@types/nodemailer": "^6.4.14", "@types/passport": "^1.0.16", diff --git a/src/__test__/category.test.ts b/src/__test__/category.test.ts new file mode 100644 index 00000000..ab7f5969 --- /dev/null +++ b/src/__test__/category.test.ts @@ -0,0 +1,227 @@ +import app from "../app"; +import request from "supertest"; +import database_models, { + connectionToDatabase, +} from "../database/config/db.config"; +import { deleteTableData } from "../utils/database.utils"; +import { + new_buyer_user, + new_category, + new_seller_user, + new_updated_category, + two_factor_authentication_data, +} from "../mock/static"; +import { generateAccessToken } from "../helpers/security.helpers"; + +jest.setTimeout(30000); + +function logErrors( + err: { stack: any }, + _req: any, + _res: any, + next: (arg0: any) => void, +) { + console.log(err.stack); + next(err); +} + +const Jest_request = request(app.use(logErrors)); +let seller_token: string; +let category_id: string; +let second_seller_token: string; + +describe("CATEGORY ENDPOINTS", () => { + beforeAll(async () => { + await connectionToDatabase(); + }); + + afterAll(async () => { + await deleteTableData(database_models.Product, "products"); + await deleteTableData(database_models.Category, "categories"); + await deleteTableData(database_models.User, "users"); + }); + + it("it should register a user and return 201", async () => { + const { body } = await Jest_request.post("/api/v1/users/register") + .send(new_buyer_user) + .expect(201); + expect(body.status).toStrictEqual("SUCCESS"); + expect(body.message).toStrictEqual( + "Account Created successfully, Please Verify your Account", + ); + const tokenRecord = await database_models.Token.findOne(); + second_seller_token = tokenRecord?.dataValues.token ?? ""; + }); + + it("should return 403 when a buyer try to add a category", async () => { + const { body } = await Jest_request.post("/api/v1/categories") + .set("Authorization", `Bearer ${second_seller_token}`) + .send(new_category) + .expect(403); + + expect(body.status).toStrictEqual("FORBIDDEN"); + expect(body.message).toStrictEqual(" Only seller can perform this action!"); + }); + + it("it should register a user and return 201", async () => { + const { body } = await Jest_request.post("/api/v1/users/register") + .send(new_seller_user) + .expect(201); + expect(body.status).toStrictEqual("SUCCESS"); + expect(body.message).toStrictEqual( + "Account Created successfully, Please Verify your Account", + ); + }); + + it("should authenticate the user and return SUCCESS", async () => { + const user = await database_models.User.findOne(); + await database_models.User.update( + { role: "13afd4f1-0bed-4a3b-8ad5-0978dabf8fcd" }, + { + where: { id: user?.dataValues.id }, + }, + ); + const authenticatetoken = generateAccessToken({ + id: user?.dataValues.id as string, + role: "SELLER", + otp: two_factor_authentication_data.otp, + }); + const { body } = await Jest_request.post( + `/api/v1/users/2fa/${authenticatetoken}`, + ) + .send(two_factor_authentication_data) + .expect(200); + + expect(body.message).toStrictEqual("Account authentication successfully!"); + expect(body.data).toBeDefined(); + seller_token = body.data; + }); + + it("should create a new category and return 201", async () => { + const { body } = await Jest_request.post("/api/v1/categories") + .set("Authorization", `Bearer ${seller_token}`) + .send(new_category) + .expect(201); + + expect(body.status).toStrictEqual("SUCCESS"); + expect(body.message).toStrictEqual("Category added successfully!"); + expect(body.data).toBeDefined(); + + category_id = body.data.id; + }); + + it("should create a new category and return 201", async () => { + const { body } = await Jest_request.post("/api/v1/categories") + .set("Authorization", `Bearer ${seller_token}`) + .send(new_category) + .expect(409); + + expect(body.status).toStrictEqual("CONFLICT"); + expect(body.message).toStrictEqual( + `Category named ${new_category.name} already exist!`, + ); + }); + + it("should return 409 when category already exist", async () => { + const { body } = await Jest_request.post("/api/v1/categories") + .set("Authorization", `Bearer ${seller_token}`) + .send(new_category) + .expect(409); + + expect(body.status).toStrictEqual("CONFLICT"); + expect(body.message).toStrictEqual( + `Category named ${new_category.name} already exist!`, + ); + }); + + it("should fetch all catgories and return 200", async () => { + const { body } = await Jest_request.get("/api/v1/categories") + .set("Authorization", `Bearer ${seller_token}`) + .expect(200); + + expect(body.status).toStrictEqual("SUCCESS"); + expect(body.message).toStrictEqual("Categories fetched successfully!"); + expect(body.data).toBeDefined(); + }); + + it("should return when a user provide an invalid id", async () => { + const { body } = await Jest_request.get( + `/api/v1/categories/96ff9146-ad09-4dbc-b100-94d3b0c33`, + ) + .set("Authorization", `Bearer ${seller_token}`) + .expect(400); + + expect(body.status).toStrictEqual("BAD REQUEST"); + expect(body.message).toStrictEqual("You provided Invalid ID!"); + }); + + it("should fetch single category and return 200", async () => { + const { body } = await Jest_request.get(`/api/v1/categories/${category_id}`) + .set("Authorization", `Bearer ${seller_token}`) + .expect(200); + + expect(body.status).toStrictEqual("SUCCESS"); + expect(body.message).toStrictEqual("Category fetched successfully!"); + expect(body.data).toBeDefined(); + }); + + it("should return 200 when the category is not found", async () => { + const { body } = await Jest_request.get( + `/api/v1/categories/f826ac00-915b-44f1-ba4e-952e50952df4`, + ) + .set("Authorization", `Bearer ${seller_token}`) + .expect(404); + + expect(body.status).toStrictEqual("NOT FOUND"); + expect(body.message).toStrictEqual("Category not found!"); + }); + + it("should update a category and return 400", async () => { + const { body } = await Jest_request.patch( + `/api/v1/categories/${category_id}`, + ) + .set("Authorization", `Bearer ${seller_token}`) + .send({}) + .expect(400); + + expect(body.status).toStrictEqual("BAD REQUEST"); + expect(body.message).toStrictEqual("No field provided to update!"); + }); + + it("should return 400 when you try to update unexisting category", async () => { + const { body } = await Jest_request.patch( + `/api/v1/categories/f826ac00-915b-44f1-ba4e-952e50952df4`, + ) + .set("Authorization", `Bearer ${seller_token}`) + .send(new_updated_category) + .expect(404); + + expect(body.status).toStrictEqual("NOT FOUND"); + expect(body.message).toStrictEqual("Category not found!"); + }); + + it("should update a category and return 200", async () => { + const { body } = await Jest_request.patch( + `/api/v1/categories/${category_id}`, + ) + .set("Authorization", `Bearer ${seller_token}`) + .send(new_updated_category) + .expect(200); + + expect(body.status).toStrictEqual("SUCCESS"); + expect(body.message).toStrictEqual("Category updated successfully!"); + expect(body.data).toBeDefined(); + }); + + it("should return 400 when a user provided an invalid id", async () => { + const { body } = await Jest_request.patch( + `/api/v1/categories/96ff9146-ad09-4dbc-b100-94d3b0c33`, + ) + .set("Authorization", `Bearer ${seller_token}`) + .send(new_updated_category) + .expect(400); + + expect(body.status).toStrictEqual("BAD REQUEST"); + expect(body.message).toStrictEqual("You provided Invalid ID!"); + }); +}); diff --git a/src/__test__/product.test.ts b/src/__test__/product.test.ts new file mode 100644 index 00000000..1cb607c9 --- /dev/null +++ b/src/__test__/product.test.ts @@ -0,0 +1,312 @@ +import app from "../app"; +import request from "supertest"; +import database_models, { + connectionToDatabase, +} from "../database/config/db.config"; +import { deleteTableData } from "../utils/database.utils"; +import { + new_category, + new_product, + new_seller_user, + new_update_product, + two_factor_authentication_data, +} from "../mock/static"; +import { generateAccessToken } from "../helpers/security.helpers"; + +jest.setTimeout(100000); + +function logErrors( + err: { stack: any }, + _req: any, + _res: any, + next: (arg0: any) => void, +) { + console.log(err.stack); + next(err); +} + +const Jest_request = request(app.use(logErrors)); +let seller_token: string; +let category_id: string; +let product_id: any; + +describe("PRODUCT API TEST", () => { + beforeAll(async () => { + await connectionToDatabase(); + }); + + afterAll(async () => { + await deleteTableData(database_models.Product, "products"); + await deleteTableData(database_models.Category, "categories"); + await deleteTableData(database_models.User, "users"); + }); + + it("it should register a user and return 201", async () => { + const { body } = await Jest_request.post("/api/v1/users/register") + .send(new_seller_user) + .expect(201); + expect(body.status).toStrictEqual("SUCCESS"); + expect(body.message).toStrictEqual( + "Account Created successfully, Please Verify your Account", + ); + }); + + it("should authenticate the user and return SUCCESS", async () => { + const user = await database_models.User.findOne(); + await database_models.User.update( + { role: "13afd4f1-0bed-4a3b-8ad5-0978dabf8fcd" }, + { + where: { id: user?.dataValues.id }, + }, + ); + const authenticatetoken = generateAccessToken({ + id: user?.dataValues.id as string, + role: "SELLER", + otp: two_factor_authentication_data.otp, + }); + const { body } = await Jest_request.post( + `/api/v1/users/2fa/${authenticatetoken}`, + ) + .send(two_factor_authentication_data) + .expect(200); + + expect(body.message).toStrictEqual("Account authentication successfully!"); + expect(body.data).toBeDefined(); + seller_token = body.data; + }); + + it("should create a new category and return 201", async () => { + const { body } = await Jest_request.post("/api/v1/categories") + .set("Authorization", `Bearer ${seller_token}`) + .send(new_category) + .expect(201); + + expect(body.status).toStrictEqual("SUCCESS"); + expect(body.data).toBeDefined(); + + category_id = body.data?.id; + }); + + it("Seller should create a product", async () => { + const all_images = new_product.images; + + let request = Jest_request.post("/api/v1/products") + .set("Authorization", `Bearer ${seller_token}`) + .field("name", new_product.name) + .field("price", new_product.price) + .field("discount", new_product.discount) + .field("quantity", new_product.quantity) + .field("categoryId", category_id) + .field("expiryDate", new_product.expiryDate); + + for (const image of all_images) { + request = request.attach("images", image); + } + + await request.expect(201); + expect((await request).body.status).toStrictEqual("SUCCESS"); + expect((await request).body.message).toStrictEqual( + "Product added successfully!", + ); + expect((await request).body.data).toBeDefined(); + + product_id = (await request).body.data?.id; + }); + + it("should return 409 when the seller already have the product", async () => { + const all_images = new_product.images; + + let request = Jest_request.post("/api/v1/products") + .set("Authorization", `Bearer ${seller_token}`) + .field("name", new_product.name) + .field("price", new_product.price) + .field("discount", new_product.discount) + .field("quantity", new_product.quantity) + .field("categoryId", category_id) + .field("expiryDate", new_product.expiryDate); + + for (const image of all_images) { + request = request.attach("images", image); + } + + await request.expect(409); + expect((await request).body.status).toStrictEqual("CONFLICT"); + expect((await request).body.message).toStrictEqual( + "Product already exist, You can update it instead!", + ); + }); + + it("should return 400 if the category doesn't exist", async () => { + const all_images = new_product.images; + + let request = Jest_request.post("/api/v1/products") + .set("Authorization", `Bearer ${seller_token}`) + .field("name", new_update_product.name) + .field("price", new_product.price) + .field("discount", new_product.discount) + .field("quantity", new_product.quantity) + .field("categoryId", "0c2b96e7-59da-40e2-92fe-943dc7130762") + .field("expiryDate", new_product.expiryDate); + + for (const image of all_images) { + request = request.attach("images", image); + } + + await request.expect(400); + expect((await request).body.status).toStrictEqual("BAD REQUEST"); + expect((await request).body.message).toStrictEqual( + "Category doesn't exist, create one and try again!", + ); + }); + + it("should fetch all products and return 200", async () => { + const { body } = await Jest_request.get("/api/v1/products") + .set("Authorization", `Bearer ${seller_token}`) + .expect(200); + + expect(body.status).toStrictEqual("SUCCESS"); + expect(body.message).toStrictEqual("Products fetched successfully!"); + expect(body.data).toBeDefined(); + }); + + it("should fetch single product and return 404 if not found", async () => { + const { body } = await Jest_request.get( + `/api/v1/products/96ff9146-ad09-4dbc-b100-94d3b0c33562`, + ) + .set("Authorization", `Bearer ${seller_token}`) + .expect(404); + + expect(body.status).toStrictEqual("NOT FOUND"); + expect(body.message).toStrictEqual("Product not found or not owned!"); + }); + + it("should fetch single and return 200", async () => { + const { body } = await Jest_request.get(`/api/v1/products/${product_id}`) + .set("Authorization", `Bearer ${seller_token}`) + .expect(200); + + expect(body.status).toStrictEqual("SUCCESS"); + expect(body.message).toStrictEqual("Product fetched successfully!"); + expect(body.data).toBeDefined(); + }); + + it("should return 404 when product doesn't exist", async () => { + const { body } = await Jest_request.get( + `/api/v1/products/97dcfe1e-0686-4876-808a-f3d3ec36c7ff`, + ) + .set("Authorization", `Bearer ${seller_token}`) + .expect(404); + + expect(body.status).toStrictEqual("NOT FOUND"); + expect(body.message).toStrictEqual("Product not found or not owned!"); + }); + + it("should return 400 when product is invalid", async () => { + const { body } = await Jest_request.get( + `/api/v1/products/97dcfe1e-0686-4876-808a-f3d3ec36c`, + ) + .set("Authorization", `Bearer ${seller_token}`) + .expect(400); + + expect(body.status).toStrictEqual("BAD REQUEST"); + expect(body.message).toStrictEqual("You provided Invalid ID!"); + }); + + it("should return 404 if product doesn't exist", async () => { + const { body } = await Jest_request.patch( + `/api/v1/products/96ff9146-ad09-4dbc-b100-94d3b0c33562`, + ) + .set("Authorization", `Bearer ${seller_token}`) + .field("price", new_update_product.price) + .expect(404); + + expect(body.status).toStrictEqual("NOT FOUND"); + expect(body.message).toStrictEqual( + "The product you're trying to update is not found or owned!", + ); + }); + + it("should return 400 if product id is invalid", async () => { + const { body } = await Jest_request.patch( + `/api/v1/products/96ff9146-ad09-4dbc-b100-94d3b0c33`, + ) + .set("Authorization", `Bearer ${seller_token}`) + .field("price", new_update_product.price) + .expect(400); + + expect(body.status).toStrictEqual("BAD REQUEST"); + expect(body.message).toStrictEqual("You provided Invalid ID!"); + }); + + it("should update product and return 200", async () => { + const { body } = await Jest_request.patch(`/api/v1/products/${product_id}`) + .set("Authorization", `Bearer ${seller_token}`) + .field("categoryId", "e4d3f656-7ae6-42a4-bcb4-ec2b993ea80a") + .expect(400); + + expect(body.status).toStrictEqual("BAD REQUEST"); + expect(body.message).toStrictEqual( + "Category doesn't exist, create one and try again!", + ); + }); + + it("should return 400 when seller upddate low number of images", async () => { + const all_images = new_update_product.images; + + let request = Jest_request.patch(`/api/v1/products/${product_id}`) + .set("Authorization", `Bearer ${seller_token}`) + .field("discount", new_update_product.discount); + + for (const image of all_images) { + request = request.attach("images", image); + } + + await request.expect(400); + expect((await request).body.status).toStrictEqual("BAD REQUEST"); + expect((await request).body.message).toBeDefined(); + }); + + it("should update product and return 200", async () => { + const { body } = await Jest_request.patch(`/api/v1/products/${product_id}`) + .set("Authorization", `Bearer ${seller_token}`) + .field("discount", new_update_product.discount) + .field("quantity", new_update_product.quantity) + .field("price", new_update_product.price) + .expect(200); + + expect(body.status).toStrictEqual("SUCCESS"); + expect(body.message).toStrictEqual("Product updated successfully!"); + expect(body.data).toBeDefined(); + }); + + it("should delete a product and return 200", async () => { + const { body } = await Jest_request.delete(`/api/v1/products/${product_id}`) + .set("Authorization", `Bearer ${seller_token}`) + .expect(200); + + expect(body.status).toStrictEqual("SUCCESS"); + expect(body.message).toStrictEqual("Product deleted successfully!"); + }); + + it("should return 404 when the product doesn't exist", async () => { + const { body } = await Jest_request.delete(`/api/v1/products/${product_id}`) + .set("Authorization", `Bearer ${seller_token}`) + .expect(404); + + expect(body.status).toStrictEqual("NOT FOUND"); + expect(body.message).toStrictEqual( + "The product you're trying to delete is not found or owned!", + ); + }); + + it("should return 400 when the seller provided an invalid id", async () => { + const { body } = await Jest_request.delete( + `/api/v1/products/96ff9146-ad09-4dbc-b100-94d3b0c33`, + ) + .set("Authorization", `Bearer ${seller_token}`) + .expect(400); + + expect(body.status).toStrictEqual("BAD REQUEST"); + expect(body.message).toStrictEqual("You provided Invalid ID!"); + }); +}); diff --git a/src/__test__/role.test.ts b/src/__test__/role.test.ts index 3effe3fd..b69e562e 100644 --- a/src/__test__/role.test.ts +++ b/src/__test__/role.test.ts @@ -3,16 +3,9 @@ import request from "supertest"; import { connectionToDatabase } from "../database/config/db.config"; import { deleteTableData } from "../utils/database.utils"; import database_models from "../database/config/db.config"; -import { roleAdmin, mockRole, mockRoleBuyer, NewUser } from "../mock/static"; -import { log } from "console"; -import { ACCESS_TOKEN_SECRET } from "../utils/keys"; -import jwt, { JsonWebTokenError, JwtPayload } from "jsonwebtoken"; +import { roleAdmin, mockRole, NewUser } from "../mock/static"; import { Token } from "../database/models/token"; -interface ExpandedRequest extends Request { - UserId?: JwtPayload; -} - function logErrors( err: { stack: any }, _req: any, @@ -23,10 +16,8 @@ function logErrors( next(err); } const role = database_models["role"]; -const user = database_models["User"]; const Jest_request = request(app.use(logErrors)); let id: string; -let roleId: string; let token = ""; let userId: string; @@ -44,7 +35,7 @@ describe("ROLE API TEST", () => { afterAll(async () => { await deleteTableData(database_models.User, "users"); - // await deleteTableData(database_models.role, "roles"); + await deleteTableData(database_models.Token, "tokens"); }); it("it should register a user and return 201", async () => { @@ -53,7 +44,7 @@ describe("ROLE API TEST", () => { .expect(201); expect(body.status).toStrictEqual("SUCCESS"); expect(body.message).toStrictEqual( - "Account Created successfully, Plase Verify your Account", + "Account Created successfully, Please Verify your Account", ); const tokenRecord = await Token.findOne(); @@ -76,9 +67,8 @@ describe("ROLE API TEST", () => { .expect(200); expect(body.status).toStrictEqual("SUCCESS"); expect(body.message).toStrictEqual("Login successfully!"); - expect(body.token).toBeDefined(); - token = body.token; - console.log(body); + expect(body.data).toBeDefined(); + token = body.data; }); it("it should return all role and return 200 ", async () => { @@ -87,7 +77,7 @@ describe("ROLE API TEST", () => { `Bearer ${token}`, ); expect(body.message).toStrictEqual("we have following roles"); - expect(body.roles).toBeDefined(); + expect(body.data).toBeDefined(); }); it("it create role and return 201 ", async () => { @@ -95,7 +85,7 @@ describe("ROLE API TEST", () => { .set("Authorization", `Bearer ${token}`) .send(mockRole); expect(body.message).toStrictEqual("Role created successfully"); - expect(body.response).toBeDefined(); + expect(body.data).toBeDefined(); }); it("it should return role already exist and return 409 ", async () => { @@ -106,12 +96,12 @@ describe("ROLE API TEST", () => { .set("Authorization", `Bearer ${token}`) .send(mockRoleExist); expect(body.message).toStrictEqual("role already exist"); - expect(body.status).toStrictEqual(409); + expect(body.status).toStrictEqual("CONFLICT"); }); it("it should return role not found and return 404 ", async () => { const roleObj = { - roleId: "ad80d123-dc7d-41b8-928b-b7f51532cacd", + role: "POPPINS", }; userId = "7121d946-7265-45a1-9ce3-3da1789e657e"; const { body } = await Jest_request.post(`/api/v1/users/${userId}/roles`) @@ -122,7 +112,7 @@ describe("ROLE API TEST", () => { it("it should assign user to role and return 201 ", async () => { const roleObj = { - roleId: "11afd4f1-0bed-4a3b-8ad5-0978dabf8fcd", + role: "BUYER", }; userId = "7121d946-7265-45a1-9ce3-3da1789e657e"; const { body } = await Jest_request.post(`/api/v1/users/${userId}/roles`) @@ -133,7 +123,7 @@ describe("ROLE API TEST", () => { it("it should return please login and return 401 ", async () => { const roleObj = { - roleId: "13afd4f1-0bed-4a3b-8ad5-0978dabf8fcd", + role: "SELLER", }; const notuserId = "ad80d123-dc7d-41b8-928b-b7f51532cacd"; const nulltoken = ""; @@ -157,7 +147,6 @@ describe("ROLE API TEST", () => { }); it("it should return updated succefully and return 201 ", async () => { - //const id = "13afd4f1-0bed-4a3b-8ad5-0978dabf8fcd"; const roleNewName = { roleName: "ENDUSER", }; diff --git a/src/__test__/users.test.ts b/src/__test__/users.test.ts index 912dd229..352d761a 100644 --- a/src/__test__/users.test.ts +++ b/src/__test__/users.test.ts @@ -1,13 +1,13 @@ import app from "../app"; import request from "supertest"; -import { connectionToDatabase } from "../database/config/db.config"; import { deleteTableData } from "../utils/database.utils"; import { User } from "../database/models/User"; -import { Token } from "../database/models/token"; import { forgotPassword } from "../controllers/resetPasswort"; import { resetPasswort } from "../controllers/resetPasswort"; -import { Request } from "express"; +import database_models, { + connectionToDatabase, +} from "../database/config/db.config"; import { bad_two_factor_authentication_data, login_user, @@ -18,7 +18,6 @@ import { partial_two_factor_authentication_data, two_factor_authentication_data, user_bad_request, - requestResetBody, newPasswordBody, NotUserrequestBody, sameAsOldPassword, @@ -28,9 +27,6 @@ import { resetPassword } from "../database/models/resetPassword"; jest.setTimeout(30000); -let authenticatetoken: string; -let otp: string; -import database_models from "../database/config/db.config"; const role = database_models["role"]; jest.setTimeout(30000); function logErrors( @@ -59,9 +55,8 @@ describe("USER API TEST", () => { }); afterAll(async () => { - await deleteTableData(User, "users"); - await deleteTableData(Token, "tokens"); - // await deleteTableData(database_models.role, "roles"); + await deleteTableData(database_models.Token, "tokens"); + await deleteTableData(database_models.User, "users"); }); it("Welcome to Hacker's e-commerce backend and return 200", async () => { const { body } = await Jest_request.get("/").expect(200); @@ -78,10 +73,9 @@ describe("USER API TEST", () => { .expect(201); expect(body.status).toStrictEqual("SUCCESS"); expect(body.message).toStrictEqual( - "Account Created successfully, Plase Verify your Account", + "Account Created successfully, Please Verify your Account", ); - - const tokenRecord = await Token.findOne(); + const tokenRecord = await database_models.Token.findOne(); token = tokenRecord?.dataValues.token ?? ""; }); it("it should return a user not found and status 400", async () => { @@ -97,14 +91,11 @@ describe("USER API TEST", () => { }); it("should verify a user's account and return 200", async () => { - // Assuming you have a way to create a user and a corresponding verification token - console.log(token); - const { body } = await Jest_request.get( `/api/v1/users/account/verify/${token}`, ); - expect(body.status).toStrictEqual(200); - expect(body.message).toStrictEqual("Email verified successfull"); + expect(body.status).toStrictEqual("SUCCESS"); + expect(body.message).toStrictEqual("Email verified successfully!"); }); it("should return 400 when the token is invalid", async () => { @@ -112,21 +103,18 @@ describe("USER API TEST", () => { `/api/v1/users/account/verify/${token}`, ).expect(400); - expect(body.status).toStrictEqual(400); + expect(body.status).toStrictEqual("BAD REQUEST"); expect(body.message).toStrictEqual("Invalid link"); }); - /** - * ---------------------------- LOGIN -------------------------------------------- - */ - it("should successfully login a user and return 200", async () => { - await User.update( + await database_models.User.update( { isVerified: true }, { where: { email: login_user.email }, }, ); + const { body } = await Jest_request.post("/api/v1/users/login") .send(login_user) .expect(200); @@ -146,13 +134,9 @@ describe("USER API TEST", () => { const { body } = await Jest_request.post("/api/v1/users/login") .send(login_user) .expect(202); - console.log( - body, - "msbdjbsd sbdjhankjsjdbkjsdja;skjd'laskjd'alsmd'lkasnm;knad'/lkmf/aldksnf'/laskdd", - ); - expect(body.response.status).toStrictEqual("ACCEPTED"); - expect(body.response.message).toStrictEqual( + expect(body.status).toStrictEqual("ACCEPTED"); + expect(body.message).toStrictEqual( "Email sent for verification. Please check your inbox and enter the OTP to complete the authentication process.", ); }); @@ -335,7 +319,7 @@ describe("USER API TEST", () => { ) .send(bad_two_factor_authentication_data) .expect(401); - expect(body.response.message).toStrictEqual("Invalid One Time Password!!"); + expect(body.message).toStrictEqual("Invalid One Time Password!!"); }); it("should return 400 if user add with character < 6 invalid otp", async () => { @@ -372,13 +356,6 @@ describe("USER API TEST", () => { * -----------------------------------------LOG OUT-------------------------------------- */ - it("Should log out a user and return 401", async () => { - const { body } = await Jest_request.post("/api/v1/users/logout").send(); - - expect(401); - expect(body.message).toStrictEqual("Unauthorized"); - }); - it("Should log out a user and return 201", async () => { const { body } = await Jest_request.post("/api/v1/users/logout") .send() @@ -386,6 +363,11 @@ describe("USER API TEST", () => { expect(201); expect(body.status).toStrictEqual("CREATED"); expect(body.message).toStrictEqual("Logged out successfully"); - token = token; + }); + + it("Should alert an error and return 401", async () => { + const { body } = await Jest_request.post("/api/v1/users/logout").send(); + expect(401); + expect(body.status).toStrictEqual("UNAUTHORIZED"); }); }); diff --git a/src/app.ts b/src/app.ts index 6dc8c69f..b2744207 100644 --- a/src/app.ts +++ b/src/app.ts @@ -29,6 +29,7 @@ app.use( app.use(passport.initialize()); app.use(passport.session()); app.use(express.json()); +app.use(express.urlencoded({ extended: true })); app.use( "/api/v1/docs", diff --git a/src/controllers/categoryController.ts b/src/controllers/categoryController.ts new file mode 100644 index 00000000..fc5642d1 --- /dev/null +++ b/src/controllers/categoryController.ts @@ -0,0 +1,199 @@ +import { Request, Response } from "express"; +import { sendResponse } from "../utils/http.exception"; +import database_models from "../database/config/db.config"; +import { insert_function, read_function } from "../utils/db_methods"; +import { category_utils } from "../utils/controller"; +import { CategoryAttributes } from "../types/model"; + +let condition; +let categoryId; + +const add_category = async (req: Request, res: Response) => { + try { + const { name, description } = req.body; + condition = { where: { name } }; + const category_exist = await read_function( + "Category", + "findOne", + condition, + ); + + if (category_exist) { + return sendResponse( + res, + 409, + "CONFLICT", + `Category named ${name} already exist!`, + ); + } + + const category = await insert_function( + "Category", + "create", + { name, description }, + ); + + return sendResponse( + res, + 201, + "SUCCESS", + "Category added successfully!", + category, + ); + } catch (error: unknown) { + return sendResponse( + res, + 500, + "SERVER ERROR", + "Something went wrong!", + error as Error, + ); + } +}; + +const read_all_categories = async (_req: Request, res: Response) => { + try { + condition = { + include: [ + { + model: database_models.Product, + as: "products", + }, + ], + }; + const categories = await read_function( + "Category", + "findAll", + condition, + ); + + return sendResponse( + res, + 200, + "SUCCESS", + "Categories fetched successfully!", + categories, + ); + } catch (error: unknown) { + return sendResponse( + res, + 500, + "SERVER ERROR", + "Something went wrong!", + error as Error, + ); + } +}; + +const read_single_category = async (req: Request, res: Response) => { + try { + categoryId = category_utils(req, res).getId; + const isValidUUID = category_utils(req, res).isValidUUID(categoryId); + if (!isValidUUID) { + return; + } + condition = { + where: { id: categoryId }, + include: [ + { + model: database_models.Product, + as: "products", + }, + ], + }; + + const category = await read_function( + "Category", + "findOne", + condition, + ); + + if (!category) { + return sendResponse(res, 404, "NOT FOUND", "Category not found!"); + } + return sendResponse( + res, + 200, + "SUCCESS", + "Category fetched successfully!", + category, + ); + } catch (error: unknown) { + return sendResponse( + res, + 500, + "SERVER ERROR", + "Something went wrong!", + error as Error, + ); + } +}; + +const update_category = async (req: Request, res: Response) => { + try { + categoryId = category_utils(req, res).getId; + const isValidUUID = category_utils(req, res).isValidUUID(categoryId); + if (!isValidUUID) { + return; + } + condition = { + where: { + id: categoryId, + }, + }; + + const category = await read_function( + "Category", + "findOne", + condition, + ); + if (!category) { + return sendResponse(res, 404, "NOT FOUND", "Category not found!"); + } + + const updated_field = req.body; + if (Object.keys(updated_field).length === 0) { + return sendResponse( + res, + 400, + "BAD REQUEST", + "No field provided to update!", + ); + } + + await insert_function( + "Category", + "update", + updated_field, + condition, + ); + const updated_category = await read_function( + "Category", + "findOne", + condition, + ); + + return sendResponse( + res, + 200, + "SUCCESS", + "Category updated successfully!", + updated_category, + ); + } catch (error: unknown) { + return sendResponse( + res, + 500, + "SERVER ERROR", + "Something went wrong!", + error as Error, + ); + } +}; + +export default { + add_category, + update_category, + read_all_categories, + read_single_category, +}; diff --git a/src/controllers/productController.ts b/src/controllers/productController.ts new file mode 100644 index 00000000..308d47bc --- /dev/null +++ b/src/controllers/productController.ts @@ -0,0 +1,384 @@ +import { Request, Response } from "express"; +import { ExpandedRequest } from "../middlewares/auth"; +import { sendResponse } from "../utils/http.exception"; +import { + ProductAttributes, + ProductCreationAttributes, + CategoryAttributes, +} from "../types/model"; +import { Category } from "../database/models/category"; +import { deleteCloudinaryFile, uploadMultiple } from "../helpers/upload"; +import { User } from "../database/models/User"; +import { insert_function, read_function } from "../utils/db_methods"; +import { category_utils } from "../utils/controller"; +import { Info, Message } from "../types/upload"; + +let product_id; +const include = [ + { + model: User, + as: "seller", + attributes: ["id", "firstName", "lastName", "email", "role"], + }, + { + model: Category, + as: "category", + attributes: ["id", "name", "description"], + }, +]; + +const create_product = async (req: Request, res: Response) => { + try { + const user = (req as ExpandedRequest).user; + const sellerId = user?.id; + const { name, price, discount, quantity, categoryId, expiryDate } = + req.body; + + const product_condition = { where: { name, sellerId } }; + const category_condition = { where: { id: categoryId } }; + + const productExist = await read_function( + "Product", + "findOne", + product_condition, + ); + if (productExist) { + return sendResponse( + res, + 409, + "CONFLICT", + "Product already exist, You can update it instead!", + ); + } + + const categoryExist = await read_function( + "Category", + "findOne", + category_condition, + ); + + if (!categoryExist) { + return sendResponse( + res, + 400, + "BAD REQUEST", + "Category doesn't exist, create one and try again!", + ); + } + + const files = req.files; + const images: string[] = (await uploadMultiple(files, req)) + .images as string[]; + const image_error = (req as Info).info; + + if (image_error) { + return sendResponse(res, 400, "BAD REQUEST", image_error.message); + } + const productData: ProductCreationAttributes = { + name, + images, + price, + discount, + quantity, + categoryId, + sellerId, + expiryDate, + }; + const product = await insert_function( + "Product", + "create", + productData, + ); + + return sendResponse( + res, + 201, + "SUCCESS", + "Product added successfully!", + product, + ); + } catch (error: unknown) { + return sendResponse( + res, + 500, + "SERVER ERROR", + "Something went wrong!", + error as Error, + ); + } +}; + +const read_all_products = async (req: Request, res: Response) => { + try { + const user = (req as ExpandedRequest).user; + const sellerId = user?.id; + const condition_one = { where: { sellerId }, include }; + const condition_two = { include }; + let products; + + if (user?.role === "SELLER") { + products = await read_function( + "Product", + "findAll", + condition_one, + ); + return sendResponse( + res, + 200, + "SUCCESS", + "Products fetched successfully!", + products, + ); + } else { + products = await read_function( + "Product", + "findAll", + condition_two, + ); + return sendResponse( + res, + 200, + "SUCCESS", + "Products fetched successfully!", + products, + ); + } + } catch (error: unknown) { + return sendResponse( + res, + 500, + "SERVER ERROR", + "Something went wrong!", + error as Error, + ); + } +}; + +const read_single_product = async (req: Request, res: Response) => { + try { + product_id = category_utils(req, res).getId; + const isValidUUID = category_utils(req, res).isValidUUID(product_id); + if (!isValidUUID) { + return; + } + const user = (req as ExpandedRequest).user; + const sellerId = user?.id; + const condition_one = { where: { id: product_id, sellerId }, include }; + const condition_two = { where: { id: product_id }, include }; + let product; + + if (user?.role === "SELLER") { + product = await read_function( + "Product", + "findOne", + condition_one, + ); + if (!product) { + return sendResponse( + res, + 404, + "NOT FOUND", + "Product not found or not owned!", + ); + } + return sendResponse( + res, + 200, + "SUCCESS", + "Product fetched successfully!", + product, + ); + } else { + product = await read_function( + "Product", + "findOne", + condition_two, + ); + if (!product) { + return sendResponse( + res, + 404, + "NOT FOUND", + "Product not found or not owned!", + ); + } + return sendResponse( + res, + 200, + "SUCCESS", + "Product fetched successfully!", + product, + ); + } + } catch (error: unknown) { + return sendResponse( + res, + 500, + "SERVER ERROR", + "Something went wrong!", + error as Error, + ); + } +}; + +const update_product = async (req: Request, res: Response) => { + try { + product_id = category_utils(req, res).getId; + const isValidUUID = category_utils(req, res).isValidUUID(product_id); + if (!isValidUUID) { + return; + } + const user = (req as ExpandedRequest).user; + const sellerId = user?.id; + const condition = { + where: { + id: product_id, + sellerId, + }, + }; + + const productExist = await read_function( + "Product", + "findOne", + condition, + ); + if (!productExist) { + return sendResponse( + res, + 404, + "NOT FOUND", + "The product you're trying to update is not found or owned!", + ); + } + + const update_info = req.body; + const { categoryId } = update_info; + + if (categoryId) { + const category = await read_function( + "Category", + "findOne", + { where: { id: categoryId } }, + ); + if (!category) { + return sendResponse( + res, + 400, + "BAD REQUEST", + "Category doesn't exist, create one and try again!", + ); + } + } + + let images: string[]; + let updated_data; + + const files = req.files; + if (files?.length !== 0) { + images = (await uploadMultiple(files, req)).images as string[]; + const image_error = (req as Info).info; + if (image_error) { + return sendResponse(res, 400, "BAD REQUEST", image_error.message); + } + updated_data = { ...update_info, images }; + } else { + updated_data = update_info; + } + + await insert_function( + "Product", + "update", + updated_data, + condition, + ); + const updated_product = await read_function( + "Product", + "findOne", + condition, + ); + return sendResponse( + res, + 200, + "SUCCESS", + "Product updated successfully!", + updated_product, + ); + } catch (error: unknown) { + return sendResponse( + res, + 500, + "SERVER ERROR", + "Something went wrong!", + error as Error, + ); + } +}; + +const delete_product = async (req: Request, res: Response) => { + try { + product_id = category_utils(req, res).getId; + const isValidUUID = category_utils(req, res).isValidUUID(product_id); + if (!isValidUUID) { + return; + } + const user = (req as ExpandedRequest).user; + const sellerId = user?.id; + + const condition = { + where: { + id: product_id, + sellerId, + }, + }; + + const productExist = await read_function( + "Product", + "findOne", + condition, + ); + if (productExist) { + const { images } = productExist; + const deletingImages = images.map(async (image: string) => { + await deleteCloudinaryFile(image.split("/")[7].split(".")[0]); + }); + await Promise.all(deletingImages); + + const result = await read_function( + "Product", + "destroy", + condition, + ); + if (result) { + return sendResponse( + res, + 200, + "SUCCESS", + "Product deleted successfully!", + ); + } + } else { + return sendResponse( + res, + 404, + "NOT FOUND", + "The product you're trying to delete is not found or owned!", + ); + } + } catch (error: unknown) { + return sendResponse( + res, + 500, + "SERVER ERROR", + "Something went wrong!", + error as Error, + ); + } +}; + +export default { + create_product, + update_product, + read_all_products, + read_single_product, + delete_product, +}; diff --git a/src/controllers/resetPasswort.ts b/src/controllers/resetPasswort.ts index 54e0ad1d..20aaba8a 100644 --- a/src/controllers/resetPasswort.ts +++ b/src/controllers/resetPasswort.ts @@ -1,42 +1,50 @@ import { Request, Response } from "express"; -import { User } from "../database/models/User"; import { ACCESS_TOKEN_SECRET, PORT } from "../utils/keys"; import { generateAccessToken } from "../helpers/security.helpers"; -import { resetPassword } from "../database/models/resetPassword"; import Jwt from "jsonwebtoken"; import { sendEmail } from "../helpers/nodemailer"; import { hashPassword } from "../utils/password"; import { resetTokenData } from "../helpers/security.helpers"; import { isValidPassword } from "../utils/password.checks"; +import { insert_function, read_function } from "../utils/db_methods"; +import { + UserModelAttributes, + resetPasswordModelAtributes, +} from "../types/model"; +import { sendResponse } from "../utils/http.exception"; + +let condition; export const forgotPassword = async (req: Request, res: Response) => { try { const { email } = req.body; + condition = { where: { email: email } }; - const isUserExist: User | null = await User.findOne({ - where: { email: email }, - }); - + const isUserExist = await read_function( + "User", + "findOne", + condition, + ); if (!isUserExist) { - return res.status(404).json({ - message: "User not found", - }); + return sendResponse(res, 404, "NOT FOUND", "User not found"); } const resetToken = generateAccessToken({ - id: isUserExist?.dataValues.id, - role: isUserExist?.dataValues.role, - email: isUserExist?.dataValues.email, - }); - - await resetPassword.destroy({ - where: { email: email }, - }); - - await resetPassword.create({ - resetToken: resetToken, - email: email, + id: isUserExist?.id, + role: isUserExist?.role, + email: isUserExist?.email, }); + await read_function( + "resetPassword", + "destroy", + condition, + ); + await insert_function( + "resetPassword", + "create", + { resetToken, email }, + condition, + ); const host = process.env.BASE_URL || `http://localhost:${PORT}`; const confirmlink: string = `${host}/passwordReset?token=${resetToken}`; @@ -50,14 +58,15 @@ export const forgotPassword = async (req: Request, res: Response) => { }; await sendEmail(mailOptions); - - res - .status(200) - .json({ message: "Email sent successfully", status: "SUCCESS" }); + return sendResponse(res, 200, "SUCCESS", "Email sent successfully"); } catch (error) { - res - .status(500) - .json({ message: "An error occurred while requesting password reset." }); + return sendResponse( + res, + 500, + "SERVER ERROR", + "Something went wrong!", + (error as Error).message, + ); } }; @@ -65,44 +74,62 @@ export const resetPasswort = async (req: Request, res: Response) => { try { const { password } = req.body!; const { token } = req.params; + condition = { where: { resetToken: token } }; - const tokenAvailability = await resetPassword.findOne({ - where: { resetToken: token }, - }); + const tokenAvailability = await read_function( + "resetPassword", + "findOne", + condition, + ); if (!tokenAvailability) { - return res.status(400).json({ message: "Invalid link" }); + return sendResponse(res, 400, "BAD REQUEST", "Invalid link"); } const decoded = Jwt.verify(token, ACCESS_TOKEN_SECRET!) as resetTokenData; if (!decoded || !decoded.id) { - return res.status(404).json({ - message: "Invalid link", - }); + return sendResponse(res, 404, "NOT FOUND", "Invalid link"); } - const resettingUser = await User.findOne({ where: { id: decoded.id! } }); - + const resettingUser = await read_function( + "User", + "findOne", + { where: { id: decoded.id! } }, + ); const sameAsOldPassword = await isValidPassword( password, - resettingUser?.dataValues.password as string, + resettingUser?.password as string, ); if (sameAsOldPassword) { - return res - .status(400) - .json({ message: "Password cannot be the same as the old password" }); + return sendResponse( + res, + 400, + "BAD REQUEST", + "Password cannot be the same as the old password", + ); } - const hashedPassword: string = (await hashPassword(password)) as string; - await resettingUser?.update({ password: hashedPassword }); - - await resetPassword.destroy({ where: { resetToken: token } }); - res.status(200).json({ message: "Password reset successfully" }); + await insert_function( + "User", + "update", + { password: hashedPassword }, + { where: { id: decoded.id! } }, + ); + await read_function( + "resetPassword", + "destroy", + condition, + ); + return sendResponse(res, 200, "SUCCESS", "Password reset successfully"); } catch (error) { - res - .status(500) - .json({ message: "An error occurred while resetting password." }); + return sendResponse( + res, + 500, + "SERVER ERROR", + "Something went wrong!", + (error as Error).message, + ); } }; diff --git a/src/controllers/roleConroller.ts b/src/controllers/roleConroller.ts deleted file mode 100644 index d25d472a..00000000 --- a/src/controllers/roleConroller.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { Request, Response } from "express"; -import database_models from "../database/config/db.config"; -const Role = database_models["role"]; -const User = database_models["User"]; -import { roleCreationAttributes } from "../database/models/role"; - -export const allRole = async (req: Request, res: Response) => { - try { - if (req.body) { - const roles = await Role.findAll(); - - return res.status(200).json({ - message: "we have following roles", - roles: roles, - }); - } - } catch (error) { - return res.status(500).json({ - message: "internal server error", - error: error, - }); - } -}; - -export const createRole = async (req: Request, res: Response) => { - try { - if (req.body) { - const exist = await Role.findOne({ - where: { roleName: req.body.roleName }, - raw: true, - }); - if (exist) { - return res.status(409).json({ - status: 409, - message: "role already exist", - }); - } - const data: roleCreationAttributes = { - roleName: req.body.roleName, - }; - const role = await Role.create({ ...data }); - return res.status(201).json({ - message: "Role created successfully", - response: role, - }); - } - } catch (error) { - return res.status(500).json({ - message: "internal server error", - error: error, - }); - } -}; - -export const assignRole = async (req: Request, res: Response) => { - try { - if (req.body) { - const roleId = req.body.roleId; - const userId = req.params.userId; - const role = await Role.findOne({ - where: { id: roleId }, - }); - if (!role) { - return res.status(404).json({ - status: 404, - message: "role not found", - }); - } - await User.update({ role: roleId }, { where: { id: userId } }); - return res.status(201).json({ - message: "Role assigned successfully!", - role: assignRole, - }); - } - } catch (error) { - return res.status(500).json({ - message: "internal server error", - error: error, - }); - } -}; - -export const updateRole = async (req: Request, res: Response) => { - const { id } = req.params; - try { - const role = await Role.findOne({ where: { id }, raw: true }); - if (!role) { - return res.status(404).json({ message: `Role with ${id} doesn't exist` }); - } - await Role.update({ roleName: req.body.roleName }, { where: { id } }); - return res.status(201).json({ message: "Role updated successfully" }); - } catch (error) { - return res.status(500).json({ - message: "Internal server error", - error: error, - }); - } -}; diff --git a/src/controllers/roleController.ts b/src/controllers/roleController.ts new file mode 100644 index 00000000..93ec6915 --- /dev/null +++ b/src/controllers/roleController.ts @@ -0,0 +1,155 @@ +import { Request, Response } from "express"; +import database_models from "../database/config/db.config"; +import { sendResponse } from "../utils/http.exception"; +import { insert_function, read_function } from "../utils/db_methods"; +import { + UserModelAttributes, + roleCreationAttributes, + roleModelAttributes, +} from "../types/model"; + +let condition; + +export const allRole = async (req: Request, res: Response) => { + try { + if (req.body) { + const roles = await read_function("role", "findAll"); + return sendResponse( + res, + 200, + "SUCCESS", + "we have following roles", + roles, + ); + } + } catch (error) { + return sendResponse( + res, + 500, + "SERVER ERROR", + "Something went wrong!", + (error as Error).message, + ); + } +}; + +export const createRole = async (req: Request, res: Response) => { + try { + condition = { where: { roleName: req.body.roleName }, raw: true }; + if (req.body) { + const exist = await read_function( + "role", + "findOne", + condition, + ); + if (exist) { + return sendResponse(res, 409, "CONFLICT", "role already exist"); + } + const data: roleCreationAttributes = { + roleName: req.body.roleName, + }; + const role = await insert_function( + "role", + "create", + { ...data }, + ); + return sendResponse( + res, + 201, + "SUCCESS", + "Role created successfully", + role, + ); + } + } catch (error) { + return sendResponse( + res, + 500, + "SERVER ERROR", + "Something went wrong!", + (error as Error).message, + ); + } +}; + +export const assignRole = async (req: Request, res: Response) => { + try { + if (req.body) { + const { role } = req.body; + const userId = req.params.userId; + condition = { where: { roleName: role } }; + + const assigned_role = await read_function( + "role", + "findOne", + condition, + ); + if (!assigned_role) { + return sendResponse(res, 404, "NOT FOUND", "role not found"); + } + await insert_function( + "User", + "update", + { role: assigned_role.id }, + { where: { id: userId } }, + ); + const user = await read_function("User", "findOne", { + where: { id: userId }, + include: [ + { + model: database_models.role, + as: "Roles", + }, + ], + }); + return sendResponse( + res, + 201, + "SUCCESS", + "Role assigned successfully!", + user, + ); + } + } catch (error) { + return sendResponse( + res, + 500, + "SERVER ERROR", + "Something went wrong!", + (error as Error).message, + ); + } +}; + +export const updateRole = async (req: Request, res: Response) => { + const { id } = req.params; + try { + const role = await read_function("role", "findOne", { + where: { id }, + raw: true, + }); + if (!role) { + return sendResponse( + res, + 404, + "NOT FOUND", + `Role with ${id} doesn't exist`, + ); + } + await insert_function( + "role", + "update", + { roleName: req.body.roleName }, + { where: { id } }, + ); + return sendResponse(res, 201, "SUCCESS", "Role updated successfully"); + } catch (error) { + return sendResponse( + res, + 500, + "SERVER ERROR", + "Something went wrong!", + (error as Error).message, + ); + } +}; diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index 31521ad8..8b2424b4 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -1,25 +1,25 @@ import { NextFunction, Request, Response } from "express"; -import { User, UserModelAttributes } from "../database/models/User"; import { TokenData, generateAccessToken, verifyAccessToken, } from "../helpers/security.helpers"; -import { HttpException } from "../utils/http.exception"; +import { sendResponse } from "../utils/http.exception"; import randomatic from "randomatic"; import HTML_TEMPLATE from "../utils/mail-template"; -import { Token } from "../database/models/token"; import passport from "../middlewares/passport"; -// import sendEmail from "../utils/email"; import { validateToken } from "../utils/token.validation"; import { ACCESS_TOKEN_SECRET } from "../utils/keys"; import { sendEmail } from "../helpers/nodemailer"; -import { Blacklist } from "../database/models/blacklist"; -import database_models from "../database/config/db.config"; +import { + BlacklistModelAtributes, + TokenModelAttributes, + UserModelAttributes, + UserModelInclude, +} from "../types/model"; +import { InfoAttribute } from "../types/passport"; +import { insert_function, read_function } from "../utils/db_methods"; -interface InfoAttribute { - message: string; -} const registerUser = async ( req: Request, res: Response, @@ -30,35 +30,38 @@ const registerUser = async ( passport.authenticate( "signup", (err: Error, user: UserModelAttributes, info: InfoAttribute) => { - if (!user) { - return res - .status(409) - .json(new HttpException("CONFLICT", info.message)); + if (info) { + return sendResponse(res, 409, "CONFLICT", info.message); } req.login(user, async () => { const token = generateAccessToken({ id: user.id, role: user.role }); - await Token.create({ token }); - + await insert_function("Token", "create", { + token, + }); const message = `${process.env.BASE_URL}/users/account/verify/${token}`; await sendEmail({ to: user.email, subject: "Verify Email", html: message, }); - const response = new HttpException( + return sendResponse( + res, + 201, "SUCCESS", - "Account Created successfully, Plase Verify your Account", - ).response(); - - res.status(201).json({ ...response }); + "Account Created successfully, Please Verify your Account", + ); }); }, )(req, res, next); } } catch (error) { - return res - .status(500) - .json(new HttpException("SERVER FAILS", "Something went wrong!")); + return sendResponse( + res, + 500, + "SERVER ERROR", + "Something went wrong!", + (error as Error).message, + ); } }; @@ -67,35 +70,24 @@ const login = async (req: Request, res: Response, next: NextFunction) => { "login", (error: Error, user: UserModelAttributes, info: InfoAttribute) => { if (error) { - return res - .status(400) - .json(new HttpException("BAD REQUEST", "Bad Request!")); + return sendResponse(res, 400, "BAD REQUEST", "Bad Request!"); } if (info) { - return res - .status(401) - .json(new HttpException("UNAUTHORIZED", info.message)); + return sendResponse(res, 401, "UNAUTHORIZED", info.message); } (req as any).login(user, async (err: Error) => { if (err) { - return res - .status(400) - .json(new HttpException("BAD REQUEST", "Bad Request!")); + return sendResponse(res, 400, "BAD REQUEST", "Bad Request!"); } - const { id, role, email, firstName, lastName } = user; + const { id, email, firstName, lastName } = user; + const role = (user as UserModelInclude).Roles?.roleName; let authenticationtoken: string; - authenticationtoken = generateAccessToken({ id, role }); - - const myRole = await database_models.role.findOne({ - where: { id: role }, - }); - - if (myRole?.dataValues.roleName === "SELLER") { + if (role === "SELLER") { const otp = randomatic("0", 6); authenticationtoken = generateAccessToken({ id, role, otp }); @@ -117,23 +109,26 @@ const login = async (req: Request, res: Response, next: NextFunction) => { subject: "Your Login Verification Code", html: HTML_TEMPLATE(message), }; - Token.create({ token: authenticationtoken }); + await insert_function("Token", "create", { + token: authenticationtoken, + }); sendEmail(options); - - const response = new HttpException( + return sendResponse( + res, + 202, "ACCEPTED", "Email sent for verification. Please check your inbox and enter the OTP to complete the authentication process.", - ).response(); - return res.status(202).json({ response }); + ); } else { - const response = new HttpException( + authenticationtoken = generateAccessToken({ id, role }); + return sendResponse( + res, + 200, "SUCCESS", "Login successfully!", - ).response(); - return res - .status(200) - .json({ ...response, token: authenticationtoken }); + authenticationtoken, + ); } }); }, @@ -142,28 +137,37 @@ const login = async (req: Request, res: Response, next: NextFunction) => { const accountVerify = async (req: Request, res: Response) => { try { - const token = await Token.findOne({ where: { token: req.params.token } }); - + const token = await read_function( + "Token", + "findOne", + { where: { token: req.params.token } }, + ); if (!token) { - return res.status(400).json({ status: 400, message: "Invalid link" }); + return sendResponse(res, 400, "BAD REQUEST", "Invalid link"); } - const { user } = validateToken( - token.dataValues.token, - ACCESS_TOKEN_SECRET as string, - ); + const { user } = validateToken(token.token, ACCESS_TOKEN_SECRET as string); if (!user) { - return res.status(400).json({ status: 400, message: "Invalid link" }); + return sendResponse(res, 400, "BAD REQUEST", "Invalid link"); } - await User.update({ isVerified: true }, { where: { id: user.id } }); - await Token.destroy({ where: { id: token.dataValues.id } }); - res - .status(200) - .json({ status: 200, message: "Email verified successfull" }); + await insert_function( + "User", + "update", + { isVerified: true }, + { where: { id: user.id } }, + ); + await read_function("Token", "destroy", { + where: { id: token.id }, + }); + return sendResponse(res, 200, "SUCCESS", "Email verified successfully!"); } catch (error) { - res - .status(400) - .json({ status: 400, message: "Something went wrong", error: error }); + return sendResponse( + res, + 500, + "SERVER ERROR", + "Something went wrong!", + (error as Error).message, + ); } }; @@ -182,39 +186,52 @@ export const handleGoogleAuth = async ( "google", async (err: Error, user: UserModelAttributes) => { const userData = user; - const userExist = await User.findOne({ - where: { email: userData.email }, - }); + const userExist = await read_function( + "User", + "findOne", + { where: { email: userData.email } }, + ); if (userExist) { const token = generateAccessToken({ - id: userExist.dataValues.id, - role: userExist.dataValues.role, + id: userExist.id, + role: userExist.role, }); - const response = new HttpException( + return sendResponse( + res, + 200, "SUCCESS", "Logged in to you account successfully!", - ).response(); - return res.status(200).json({ ...response, token }); + token, + ); } - const newUser = await User.create({ ...userData }); - await newUser.save(); + const newUser = await insert_function( + "User", + "create", + { ...userData }, + ); const token = generateAccessToken({ - id: newUser.dataValues.id, - role: newUser.dataValues.role, + id: newUser.id, + role: newUser.role, }); - const response = new HttpException( + return sendResponse( + res, + 201, "SUCCESS", "Account Created successfully!", - ).response(); - res.status(201).json({ ...response, token }); + token, + ); }, )(req, res, next); } catch (error) { - res - .status(500) - .json(new HttpException("SERVER ERROR", "Something went wrong!")); + return sendResponse( + res, + 500, + "SERVER ERROR", + "Something went wrong!", + (error as Error).message, + ); } }; @@ -224,46 +241,52 @@ const two_factor_authentication = async (req: Request, res: Response) => { const { token } = req.params; const decodedToken = verifyAccessToken(token, res) as TokenData; if (decodedToken && decodedToken.otp && otp === decodedToken.otp) { - Token.destroy({ where: { token: token } }); - const response = new HttpException( + await read_function("Token", "destroy", { + where: { token: token }, + }); + return sendResponse( + res, + 200, "SUCCESS", "Account authentication successfully!", - ).response(); - return res.status(200).json({ ...response, token }); + token, + ); } else { - const response = new HttpException( + return sendResponse( + res, + 401, "Unauthorized", "Invalid One Time Password!!", - ).response(); - return res.status(401).json({ response }); + ); } } catch (error: any) { - return res.status(500).json({ - status: 500, - error: error.message, - }); + return sendResponse( + res, + 500, + "SERVER ERROR", + "Something went wrong!", + (error as Error).message, + ); } }; const logout = async (req: Request, res: Response) => { try { - const token = req.header("Authorization")?.split(" ")[1]; - + const token = req.headers["authorization"]?.split(" ")[1]; if (token) { - await Blacklist.create({ token }); - return res - .status(201) - .json(new HttpException("CREATED", "Logged out successfully")); + await insert_function("Blacklist", "create", { + token, + }); + return sendResponse(res, 201, "CREATED", "Logged out successfully"); } } catch (error) { - return res - .status(500) - .json( - new HttpException( - "INTERNAL_SERVER_ERROR", - "An internal server error occurred", - ), - ); + return sendResponse( + res, + 500, + "SERVER ERROR", + "Something went wrong!", + (error as Error).message, + ); } }; diff --git a/src/database/config/config.js b/src/database/config/config.js index 8f5e9a37..4319732f 100644 --- a/src/database/config/config.js +++ b/src/database/config/config.js @@ -1,6 +1,5 @@ require("dotenv").config(); -//add ssl in this config process.env.DB_HOSTED_MODE == "local" ? (dialect_option = {}) : (dialect_option = { diff --git a/src/database/migrations/20240423144255-create-categories-table.js b/src/database/migrations/20240423144255-create-categories-table.js new file mode 100644 index 00000000..f7dc5127 --- /dev/null +++ b/src/database/migrations/20240423144255-create-categories-table.js @@ -0,0 +1,37 @@ +"use strict"; + +const { UUIDV4 } = require("sequelize"); + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable("categories", { + id: { + type: Sequelize.UUID, + defaultValue: UUIDV4, + primaryKey: true, + allowNull: false, + }, + name: { + type: Sequelize.STRING, + allowNull: false, + }, + description: { + type: Sequelize.STRING, + allowNull: false, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async down(queryInterface, Sequelize) { + await queryInterface.dropTable("categories"); + }, +}; diff --git a/src/database/migrations/20240428090107-create-product-table.js b/src/database/migrations/20240428090107-create-product-table.js new file mode 100644 index 00000000..0d0c1a1c --- /dev/null +++ b/src/database/migrations/20240428090107-create-product-table.js @@ -0,0 +1,71 @@ +"use strict"; + +const { UUIDV4 } = require("sequelize"); + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable("products", { + id: { + type: Sequelize.UUID, + defaultValue: UUIDV4, + primaryKey: true, + allowNull: false, + }, + name: { + type: Sequelize.STRING, + allowNull: false, + }, + price: { + type: Sequelize.FLOAT, + allowNull: false, + }, + images: { + type: Sequelize.ARRAY(Sequelize.STRING), + allowNull: false, + }, + discount: { + type: Sequelize.FLOAT, + allowNull: false, + }, + quantity: { + type: Sequelize.INTEGER, + allowNull: false, + }, + sellerId: { + type: Sequelize.UUID, + allowNull: false, + references: { + model: "users", + key: "id", + onDelete: "CASCADE", + }, + }, + categoryId: { + type: Sequelize.UUID, + allowNull: false, + references: { + model: "categories", + key: "id", + onDelete: "CASCADE", + }, + }, + expiryDate: { + type: Sequelize.DATE, + allowNull: false, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async down(queryInterface, Sequelize) { + await queryInterface.dropTable("products"); + }, +}; diff --git a/src/database/models/User.ts b/src/database/models/User.ts index 520a2a4d..eb55d126 100644 --- a/src/database/models/User.ts +++ b/src/database/models/User.ts @@ -1,24 +1,31 @@ -import { DataTypes, Model, Optional, Sequelize, UUIDV4 } from "sequelize"; +import { DataTypes, Model, Sequelize, UUIDV4 } from "sequelize"; +import { UserCreationAttributes, UserModelAttributes } from "../../types/model"; +import { Product } from "./product"; import database_models from "../config/db.config"; -export interface UserModelAttributes { - id: string; - userName: string; - firstName: string; - lastName: string; - email: string; - password: string; - confirmPassword: string; - role: string; - isVerified: boolean; -} -type UserCreationAttributes = Optional & { - role?: string; -}; + export class User extends Model { - public static associate(models: { role: typeof database_models.role }) { + public id!: string; + public userName!: string; + public firstName!: string; + public lastName!: string; + public email!: string; + public password!: string; + public confirmPassword!: string; + public role!: string; + public isVerified!: boolean; + + public static associate(models: { + Product: typeof Product; + role: typeof database_models.role; + }) { + this.hasOne(models.Product, { + foreignKey: "sellerId", + as: "products", + }); User.belongsTo(models.role, { as: "Roles", foreignKey: "role" }); } } + const user_model = (sequelize: Sequelize) => { User.init( { @@ -73,6 +80,7 @@ const user_model = (sequelize: Sequelize) => { tableName: "users", }, ); + return User; }; export default user_model; diff --git a/src/database/models/blacklist.ts b/src/database/models/blacklist.ts index 9ae2cf78..bc63b28e 100644 --- a/src/database/models/blacklist.ts +++ b/src/database/models/blacklist.ts @@ -1,34 +1,32 @@ -import { DataTypes, Model, Optional } from "sequelize"; +import { DataTypes, Model, Sequelize } from "sequelize"; +import { BlacklistModelAtributes } from "../../types/model"; -import { sequelizeConnection } from "../config/db.config"; - -export interface BlacklistModelAtributes { - id: string; - token: string; +export class Blacklist extends Model { + public id!: string; + public token!: string; } -type BlacklistCreationAttributes = Optional; - -export class Blacklist extends Model< - BlacklistModelAtributes, - BlacklistCreationAttributes -> {} - -Blacklist.init( - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - allowNull: true, +const blacklist_model = (sequelize: Sequelize) => { + Blacklist.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false, + }, + token: { + type: DataTypes.STRING, + allowNull: false, + }, }, - token: { - type: DataTypes.STRING, - allowNull: false, + { + sequelize, + tableName: "blacklisted_tokens", }, - }, - { - sequelize: sequelizeConnection, - tableName: "blacklisted_tokens", - }, -); + ); + + return Blacklist; +}; + +export default blacklist_model; diff --git a/src/database/models/category.ts b/src/database/models/category.ts new file mode 100644 index 00000000..7a49c78a --- /dev/null +++ b/src/database/models/category.ts @@ -0,0 +1,52 @@ +import { DataTypes, Model, Sequelize, UUIDV4 } from "sequelize"; +import { Product } from "./product"; +import { + CategoryAttributes, + CategoryCreationAttributes, +} from "../../types/model"; + +export class Category extends Model< + CategoryAttributes, + CategoryCreationAttributes +> { + declare id: string; + declare name: string; + declare description: string; + + public static associate(models: { Product: typeof Product }) { + this.hasMany(models.Product, { + foreignKey: "categoryId", + as: "products", + // onDelete: "CASCADE", + }); + } +} + +const category_model = (sequelize: Sequelize) => { + Category.init( + { + id: { + type: DataTypes.UUID, + defaultValue: UUIDV4, + primaryKey: true, + allowNull: false, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + description: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + sequelize, + tableName: "categories", + }, + ); + + return Category; +}; + +export default category_model; diff --git a/src/database/models/index.ts b/src/database/models/index.ts index 657cf8fe..e96b9234 100644 --- a/src/database/models/index.ts +++ b/src/database/models/index.ts @@ -1,10 +1,22 @@ import { Sequelize } from "sequelize"; -import Role_model from "./role"; +import product_model from "./product"; +import category_model from "./category"; import user_model from "./User"; +import blacklist_model from "./blacklist"; +import token_model from "./token"; +import Role_model from "./role"; +import reset_model from "./resetPassword"; const Models = (sequelize: Sequelize) => { + const Product = product_model(sequelize); + const Category = category_model(sequelize); const User = user_model(sequelize); + const Blacklist = blacklist_model(sequelize); + const Token = token_model(sequelize); const role = Role_model(sequelize); - return { role, User }; + const resetPassword = reset_model(sequelize); + + return { Product, Category, User, Blacklist, Token, role, resetPassword }; }; + export default Models; diff --git a/src/database/models/product.ts b/src/database/models/product.ts new file mode 100644 index 00000000..4a0a0c79 --- /dev/null +++ b/src/database/models/product.ts @@ -0,0 +1,98 @@ +import { DataTypes, Model, Sequelize, UUIDV4 } from "sequelize"; +import { User } from "./User"; +import { Category } from "./category"; +import { + ProductAttributes, + ProductCreationAttributes, +} from "../../types/model"; + +export class Product extends Model< + ProductAttributes, + ProductCreationAttributes +> { + public id!: string; + public name!: string; + public price!: number; + public images!: string[]; + public discount!: number; + public quantity!: number; + public sellerId!: string; + public categoryId!: string; + public expiryDate!: Date; + + public static associate(models: { + User: typeof User; + Category: typeof Category; + }) { + this.belongsTo(models.User, { + foreignKey: "sellerId", + as: "seller", + }); + + this.belongsTo(models.Category, { + foreignKey: "categoryId", + as: "category", + }); + } +} + +const product_model = (sequelize: Sequelize) => { + Product.init( + { + id: { + type: DataTypes.UUID, + defaultValue: UUIDV4, + primaryKey: true, + allowNull: false, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + price: { + type: DataTypes.FLOAT, + allowNull: false, + }, + images: { + type: DataTypes.ARRAY(DataTypes.STRING), + allowNull: false, + }, + discount: { + type: DataTypes.FLOAT, + allowNull: false, + }, + quantity: { + type: DataTypes.INTEGER, + allowNull: false, + }, + sellerId: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: "users", + key: "id", + }, + }, + categoryId: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: "categories", + key: "id", + }, + }, + expiryDate: { + type: DataTypes.DATE, + allowNull: false, + }, + }, + { + sequelize, + tableName: "products", + }, + ); + + return Product; +}; + +export default product_model; diff --git a/src/database/models/resetPassword.ts b/src/database/models/resetPassword.ts index 9a6c9324..2cc40e90 100644 --- a/src/database/models/resetPassword.ts +++ b/src/database/models/resetPassword.ts @@ -1,33 +1,32 @@ -import { DataTypes, Model } from "sequelize"; -import { sequelizeConnection } from "../config/db.config"; - -export interface resetPasswordModelAtributes { - id?: string; - email: string; - resetToken: string; -} +import { DataTypes, Model, Sequelize } from "sequelize"; +import { resetPasswordModelAtributes } from "../../types/model"; export class resetPassword extends Model {} -resetPassword.init( - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - allowNull: false, - }, - email: { - type: DataTypes.STRING, - allowNull: false, +const reset_model = (sequelize: Sequelize) => { + resetPassword.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false, + }, + email: { + type: DataTypes.STRING, + allowNull: false, + }, + resetToken: { + type: DataTypes.STRING(1000), + allowNull: false, + }, }, - resetToken: { - type: DataTypes.STRING(1000), - allowNull: false, + { + sequelize, + tableName: "resetPassword_tokens", }, - }, - { - sequelize: sequelizeConnection, - tableName: "resetPassword_tokens", - }, -); + ); + return resetPassword; +}; + +export default reset_model; diff --git a/src/database/models/role.ts b/src/database/models/role.ts index 746e7823..b087544c 100644 --- a/src/database/models/role.ts +++ b/src/database/models/role.ts @@ -1,10 +1,6 @@ -import { DataTypes, Sequelize, Model, Optional, UUIDV4 } from "sequelize"; +import { DataTypes, Sequelize, Model, UUIDV4 } from "sequelize"; import database_models from "../config/db.config"; -export interface roleModelAttributes { - id: string; - roleName: string; -} -export type roleCreationAttributes = Optional; +import { roleCreationAttributes, roleModelAttributes } from "../../types/model"; export class Role extends Model { public static associate(models: { User: typeof database_models.User }) { diff --git a/src/database/models/token.ts b/src/database/models/token.ts index 0d79652d..ae9f8bbf 100644 --- a/src/database/models/token.ts +++ b/src/database/models/token.ts @@ -1,31 +1,37 @@ -import { DataTypes, Model, Optional, UUIDV4 } from "sequelize"; -import { sequelizeConnection } from "../config/db.config"; -// token interface -export interface TokenModelAttributes { - id: string; - token: string; -} -type TokenCreationAttributes = Optional; +import { DataTypes, Model, Sequelize, UUIDV4 } from "sequelize"; +import { + TokenModelAttributes, + TokenCreationAttributes, +} from "../../types/model"; + export class Token extends Model< TokenModelAttributes, TokenCreationAttributes -> {} -Token.init( - { - id: { - type: DataTypes.UUID, - defaultValue: UUIDV4, - primaryKey: true, - allowNull: false, +> { + public id!: string; + public token!: string; +} +const token_model = (sequelize: Sequelize) => { + Token.init( + { + id: { + type: DataTypes.UUID, + defaultValue: UUIDV4, + primaryKey: true, + allowNull: false, + }, + token: { + type: DataTypes.STRING(10000), + allowNull: false, + unique: true, + }, }, - token: { - type: DataTypes.STRING(1000), - allowNull: false, - unique: true, + { + sequelize, + tableName: "tokens", }, - }, - { - sequelize: sequelizeConnection, - tableName: "tokens", - }, -); + ); + return Token; +}; + +export default token_model; diff --git a/src/database/seeders/20240423145116-demo-categories.js b/src/database/seeders/20240423145116-demo-categories.js new file mode 100644 index 00000000..99c5d5b7 --- /dev/null +++ b/src/database/seeders/20240423145116-demo-categories.js @@ -0,0 +1,25 @@ +"use strict"; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async up(queryInterface, Sequelize) { + await queryInterface.bulkInsert( + "categories", + [ + { + id: "8dfe453c-b779-453c-b96e-afe656eeebab", + name: "Fruits", + description: "Fruits are amazing", + updatedAt: new Date(), + createdAt: new Date(), + }, + ], + {}, + ); + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async down(queryInterface, Sequelize) { + await queryInterface.bulkDelete("categories", null, {}); + }, +}; diff --git a/src/database/seeders/20240430162648-demo-product.js b/src/database/seeders/20240430162648-demo-product.js new file mode 100644 index 00000000..d802c3c6 --- /dev/null +++ b/src/database/seeders/20240430162648-demo-product.js @@ -0,0 +1,36 @@ +"use strict"; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async up(queryInterface, Sequelize) { + await queryInterface.bulkInsert( + "products", + [ + { + id: "9e555bd6-0f36-454a-a3d5-89edef4ff9d4", + name: "Apple", + price: 29, + images: [ + "https://res.cloudinary.com/dzbxg4xeq/image/upload/v1713877715/e-commerce/cx03adwvxevuvxxeewyv.png", + "https://res.cloudinary.com/dzbxg4xeq/image/upload/v1713877717/e-commerce/r7h5yvc5nbdtjt5yqoua.jpg", + "https://res.cloudinary.com/dzbxg4xeq/image/upload/v1713877719/e-commerce/pegxb75y2c7x6rwtmrho.png", + "https://res.cloudinary.com/dzbxg4xeq/image/upload/v1713877721/e-commerce/biqpzdojtmbv0z55bhew.png", + ], + discount: 0, + quantity: 390, + sellerId: "7321d946-7265-45a1-9ce3-3da1789e657e", + categoryId: "8dfe453c-b779-453c-b96e-afe656eeebab", + expiryDate: "2324-04-30T00:00:00.000Z", + createdAt: new Date(), + updatedAt: new Date(), + }, + ], + {}, + ); + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async down(queryInterface, Sequelize) { + await queryInterface.bulkDelete("products", null, {}); + }, +}; diff --git a/src/documention/basicInfo.ts b/src/documention/basicInfo.ts index 94e26586..a39a217b 100644 --- a/src/documention/basicInfo.ts +++ b/src/documention/basicInfo.ts @@ -1,4 +1,9 @@ -import { GOOGLE_CALLBACK_URL, PORT } from "../utils/keys"; +import { + GOOGLE_CALLBACK_URL, + DEPLOYED_URL, + PORT, + SERVER_URL, +} from "../utils/keys"; const basicInfo = { openapi: "3.0.0", @@ -10,11 +15,11 @@ const basicInfo = { servers: [ { - url: `http://localhost:${PORT}`, + url: SERVER_URL || `http://localhost:${PORT}`, description: "Development server", }, { - url: "https://hackers-ec-be.onrender.com", + url: DEPLOYED_URL, description: "Production server (HTTPS)", }, ], diff --git a/src/documention/category/index.ts b/src/documention/category/index.ts new file mode 100644 index 00000000..931f370e --- /dev/null +++ b/src/documention/category/index.ts @@ -0,0 +1,131 @@ +import { responses } from "../responses"; + +const create_category = { + tags: ["Category"], + summary: "Adding new category", + security: [ + { + bearerAuth: [], + }, + ], + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + description: "Category name", + required: true, + example: "Cars", + }, + description: { + type: "string", + description: "Category description", + required: true, + example: "Cars are awesome", + }, + }, + }, + }, + }, + }, + responses, +}; + +const read_categories = { + all: { + tags: ["Category"], + summary: "Getting all categories", + security: [ + { + bearerAuth: [], + }, + ], + responses, + }, + single: { + tags: ["Category"], + summary: "Getting single category", + security: [ + { + bearerAuth: [], + }, + ], + parameters: [ + { + in: "path", + name: "id", + required: true, + schema: { + type: "string", + format: "uuid", + }, + }, + ], + responses, + }, +}; + +const update_category = { + tags: ["Category"], + summary: "Updating a category", + security: [ + { + bearerAuth: [], + }, + ], + parameters: [ + { + in: "path", + name: "id", + required: true, + schema: { + type: "string", + format: "uuid", + }, + }, + ], + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + description: "Category name", + required: true, + example: "Fancy Cars", + }, + description: { + type: "string", + description: "Category brief description", + required: true, + example: "This cars are awesome", + }, + }, + }, + }, + }, + }, + responses, +}; + +export const categories = { + "/api/v1/categories": { + post: create_category, + }, + "/api/v1/categories/": { + get: read_categories["all"], + }, + "/api/v1/categories/{id}": { + get: read_categories["single"], + }, + "/api/v1/categories/{id}/": { + patch: update_category, + }, +}; diff --git a/src/documention/index.ts b/src/documention/index.ts index 5c588462..da6f78d9 100644 --- a/src/documention/index.ts +++ b/src/documention/index.ts @@ -1,11 +1,15 @@ import basicInfo from "./basicInfo"; import { roles } from "./role"; import { users } from "./user"; +import { categories } from "./category"; +import { products } from "./product"; export default { ...basicInfo, paths: { ...users, ...roles, + ...categories, + ...products, }, }; diff --git a/src/documention/product/index.ts b/src/documention/product/index.ts new file mode 100644 index 00000000..a7823afd --- /dev/null +++ b/src/documention/product/index.ts @@ -0,0 +1,226 @@ +import { responses } from "../responses"; + +const createProduct = { + tags: ["Products"], + security: [ + { + bearerAuth: [], + }, + ], + summary: "Creating product", + requestBody: { + required: true, + content: { + "multipart/form-data": { + schema: { + type: "object", + properties: { + name: { + type: "string", + description: "Product name", + required: true, + example: "BMW", + }, + price: { + type: "number", + description: "Price of product", + required: true, + minimum: 1, + example: 499000, + }, + images: { + type: "array", + items: { + type: "string", + format: "binary", + }, + description: "Product images", + minItems: 3, + maxItems: 8, + }, + discount: { + type: "number", + description: "Discount for a product", + example: 0, + }, + quantity: { + type: "number", + description: "quantity of product", + required: true, + minimum: 1, + example: 123, + }, + categoryId: { + type: "string", + description: "Product category", + required: true, + format: "uuid", + }, + expiryDate: { + type: "string", + format: "date", + description: "Expired date of product", + example: "2121-01-01", + }, + }, + }, + }, + }, + }, + consumes: ["application/json"], + responses, +}; + +const read_products = { + all: { + tags: ["Products"], + security: [ + { + bearerAuth: [], + }, + ], + summary: "List of all the products", + description: "Get all of the products", + responses, + }, + single: { + tags: ["Products"], + security: [ + { + bearerAuth: [], + }, + ], + summary: "Get a single product", + description: "Get a single product", + parameters: [ + { + in: "path", + name: "id", + required: true, + schema: { + type: "string", + format: "uuid", + }, + }, + ], + responses, + }, +}; + +const update_product = { + tags: ["Products"], + security: [ + { + bearerAuth: [], + }, + ], + summary: "Updating a product", + parameters: [ + { + in: "path", + name: "id", + required: true, + schema: { + type: "string", + format: "uuid", + }, + }, + ], + requestBody: { + required: true, + content: { + "multipart/form-data": { + schema: { + type: "object", + properties: { + name: { + type: "string", + description: "Product name", + required: true, + example: "BMW7", + }, + price: { + type: "number", + description: "Price of product", + required: true, + minimum: 1, + example: 499000, + }, + images: { + type: "array", + items: { + minItems: 4, + type: "file", + }, + }, + quantity: { + type: "number", + description: "quantity of product", + required: true, + minimum: 1, + example: 123, + }, + discount: { + type: "number", + description: "Discount for a product", + example: 100, + }, + categoryId: { + type: "string", + description: "Product category", + required: true, + format: "uuid", + }, + expiryDate: { + type: "string", + format: "date", + description: "Expired date of product", + example: "2121-01-01", + }, + }, + }, + }, + }, + }, + responses, +}; + +const delete_product = { + tags: ["Products"], + security: [ + { + bearerAuth: [], + }, + ], + summary: "Deleting a product", + parameters: [ + { + in: "path", + name: "ID", + required: true, + schema: { + type: "string", + format: "uuid", + }, + }, + ], + responses, +}; + +export const products = { + "/api/v1/products": { + post: createProduct, + }, + "/api/v1/products/": { + get: read_products["all"], + }, + "/api/v1/products/{id}": { + get: read_products["single"], + }, + "/api/v1/products/{id}/": { + patch: update_product, + }, + "/api/v1/products/{ID}": { + delete: delete_product, + }, +}; diff --git a/src/documention/role/index.ts b/src/documention/role/index.ts index ea8643c1..b5c48f01 100644 --- a/src/documention/role/index.ts +++ b/src/documention/role/index.ts @@ -60,11 +60,10 @@ const role_routes = { schema: { type: "object", properties: { - roleId: { + role: { type: "string", required: true, - //format:"uuid", - example: "083a197e-ac11-4c62-b190-dad7b05954e", + example: "BUYER", }, }, }, diff --git a/src/documention/swagger.json b/src/documention/swagger.json deleted file mode 100644 index 43df114a..00000000 --- a/src/documention/swagger.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "paths": { - "/": { - "get": { - "tags": ["Default"], - "summary": "Default message on server", - "operationId": "", - "requestBody": { - "description": "default router should return message", - "content": { - "application/json": { - "schema": {} - }, - "application/xml": { - "schema": {} - } - }, - "required": false - }, - "responses": { - "200": { - "description": "Message of successful request", - "content": {} - } - }, - "x-codegen-request-body-name": "body" - } - } - } -} diff --git a/src/documention/user/index.ts b/src/documention/user/index.ts index c2047b59..f63d8ab4 100644 --- a/src/documention/user/index.ts +++ b/src/documention/user/index.ts @@ -110,53 +110,24 @@ const register_login = { }; const userAccount = { - tags: ["User"], - summary: "Verify user account", - parameters: [ - { - in: "path", - name: "token", - required: true, - type: "string", - description: "Verification token", - }, - ], - responses: { - "199": { - description: "Email verified successfully", - schema: { - type: "object", - properties: { - status: { - type: "integer", - example: 199, - }, - message: { - type: "string", - example: "Email verified successfull", - }, - }, + verify: { + tags: ["User"], + security: [ + { + bearerAuth: [], }, - }, - "399": { - description: "Invalid link or something went wrong", - schema: { - type: "object", - properties: { - status: { - type: "integer", - example: 399, - }, - message: { - type: "string", - example: "Invalid link", - }, - error: { - type: "string", - }, - }, + ], + summary: "Verify user account", + parameters: [ + { + in: "path", + name: "token", + required: true, + type: "string", + description: "Verification token", }, - }, + ], + responses, }, }; @@ -168,7 +139,7 @@ const reset2_FA = { bearerAuth: [], }, ], - summary: "Request password reset", + summary: "Two-factor authentication", requestBody: { required: true, content: { @@ -192,7 +163,11 @@ const reset2_FA = { request_password: { tags: ["User"], - security: [{ JWT: [] }], + security: [ + { + bearerAuth: [], + }, + ], summary: "Reset password", parameters: [ @@ -280,7 +255,7 @@ export const users = { }, "/api/v1/users/account/verify/{token}": { - get: userAccount, + get: userAccount["verify"], }, "/api/v1/users/forgot-password": { diff --git a/src/helpers/cloudinary.ts b/src/helpers/cloudinary.ts new file mode 100644 index 00000000..c3eb83d2 --- /dev/null +++ b/src/helpers/cloudinary.ts @@ -0,0 +1,14 @@ +import { v2 as cloudinary } from "cloudinary"; +import { + CLOUDINARY_API_KEY, + CLOUDINARY_API_SECRET, + CLOUDINARY_CLOUD_NAME, +} from "../utils/keys"; + +cloudinary.config({ + cloud_name: CLOUDINARY_CLOUD_NAME, + api_key: CLOUDINARY_API_KEY, + api_secret: CLOUDINARY_API_SECRET, +}); + +export default cloudinary; diff --git a/src/helpers/security.helpers.ts b/src/helpers/security.helpers.ts index a30aaee1..92e6c1ab 100644 --- a/src/helpers/security.helpers.ts +++ b/src/helpers/security.helpers.ts @@ -1,7 +1,7 @@ -import { Response } from "express"; import jwt from "jsonwebtoken"; -import { validateToken } from "../utils/token.validation"; import { ACCESS_TOKEN_SECRET } from "../utils/keys"; +import { validateToken } from "../utils/token.validation"; +import { Response } from "express"; export interface TokenData { id: string | number; @@ -16,7 +16,7 @@ export interface resetTokenData { export const generateAccessToken = (userData: TokenData) => { const token = jwt.sign(userData, ACCESS_TOKEN_SECRET as string, { - expiresIn: "1d", + expiresIn: "1y", }); return token; }; diff --git a/src/helpers/upload.ts b/src/helpers/upload.ts new file mode 100644 index 00000000..6795238a --- /dev/null +++ b/src/helpers/upload.ts @@ -0,0 +1,72 @@ +import cloudinary from "./cloudinary"; +import { CLOUDINARY_FOLDER_NAME } from "../utils/keys"; +import { Info, Message } from "../types/upload"; +import { Request } from "express"; + +const folder = CLOUDINARY_FOLDER_NAME; + +export const uploadSingle = async (image: string) => { + try { + const result = await cloudinary.uploader.upload(image, { + folder, + }); + return result; + } catch (error) { + const err = (error as Error).message; + return { error: err }; + } +}; + +export const uploadMultiple = async (images: any, req: Request) => { + const imageUrls = []; + const errors = []; + + if (images.length < 3 || images.length > 8) { + (req as Info).info = { + message: + images.length < 3 + ? "Product must have at least 3 images!" + : "Product can't have more than 8 images!", + }; + } + + for (const i in images) { + try { + const data = await uploadSingle(images[i].path); + if ("error" in data) { + (req as Info).info = { + message: "Uploading image failed!", + }; + } else { + imageUrls.push(data?.secure_url); + } + } catch (error: any) { + errors.push(error.message); + (req as Info).info = { + message: message(imageUrls.length, errors) as string, + }; + } + } + return { images: imageUrls as string[] }; +}; + +function message(imageLen: number, errors: any) { + if (errors.length > 0) { + return `${ + imageLen === 0 ? "No" : imageLen + } other product images were uploaded, (${ + errors.length + }) went wrong as ${errors + .filter((error: Error, index: number) => errors.indexOf(error) === index) + .join(", ")}!`; + } +} + +export const deleteCloudinaryFile = async (url: string) => { + try { + await cloudinary.uploader.destroy(url); + return true; + } catch (error) { + return error; + } +}; diff --git a/src/middlewares/auth.ts b/src/middlewares/auth.ts index bcdfa65a..02f3908a 100644 --- a/src/middlewares/auth.ts +++ b/src/middlewares/auth.ts @@ -1,26 +1,24 @@ import { Request, Response, NextFunction } from "express"; -import jwt, { JwtPayload } from "jsonwebtoken"; -import { Blacklist } from "../database/models/blacklist"; -// import { userRole } from "../database/models/userroles"; -// import { userRoleModelAttributes } from "../database/models/userroles"; import { ACCESS_TOKEN_SECRET } from "../utils/keys"; import database_models from "../database/config/db.config"; -const Role = database_models["role"]; -const User = database_models["User"]; +import jwt, { JsonWebTokenError, JwtPayload } from "jsonwebtoken"; +import { HttpException } from "../utils/http.exception"; -interface ExpandedRequest extends Request { - UserId?: JwtPayload; +export interface ExpandedRequest extends Request { + user?: JwtPayload; } // only logged in users -export const authenticateUser = async ( +const authenticateUser = async ( req: ExpandedRequest, res: Response, next: NextFunction, ) => { const token = req.headers.authorization?.split(" ")[1]; if (!token) { - return res.status(401).json({ message: "Unauthorized" }); + return res + .status(401) + .json({ status: "UNAUTHORIZED", message: "Please login to continue" }); } //checking token expiration const decoded = jwt.decode(token) as JwtPayload; @@ -35,13 +33,15 @@ export const authenticateUser = async ( token, ACCESS_TOKEN_SECRET as string, ) as JwtPayload; - const isInBlcaklist = await Blacklist.findOne({ where: { token } }); + const isInBlcaklist = await database_models.Blacklist.findOne({ + where: { token }, + }); - if (!verifiedToken && isInBlcaklist) { + if (!verifiedToken || isInBlcaklist) { return res.status(401).json({ message: "please login to continue!" }); } - req.UserId = verifiedToken; + req.user = verifiedToken; next(); } catch (error) { if (error instanceof jwt.TokenExpiredError) { @@ -57,7 +57,7 @@ export const authenticateUser = async ( }; // only buyers -export const isBuyer = async ( +const isBuyer = async ( req: ExpandedRequest, res: Response, next: NextFunction, @@ -76,7 +76,7 @@ export const isBuyer = async ( return res.status(401).json({ message: "please login to continue!" }); } - if (decoded.role !== "buyer") { + if (decoded.role !== "BUYER") { return res.status(403).json({ message: "Forbidden" }); } next(); @@ -85,37 +85,75 @@ export const isBuyer = async ( } }; -//only vendors -export const isVendor = async ( +const isSeller = async ( req: ExpandedRequest, res: Response, next: NextFunction, ) => { const token = req.headers.authorization?.split(" ")[1]; if (!token) { - return res.status(401).json({ message: "Unauthorized" }); + return res + .status(401) + .json(new HttpException("UNAUTHORIZED", "Please login to continue!")); + } + + const decodedToken = jwt.decode(token) as JwtPayload; + if ( + decodedToken && + decodedToken.exp && + Date.now() >= decodedToken.exp * 1000 + ) { + return res + .status(401) + .json( + new HttpException( + "UNAUTHORIZED", + "You have been loggedOut, Please login to continue!", + ), + ); } + try { - const decoded = jwt.verify( + const payLoad = jwt.verify( token, ACCESS_TOKEN_SECRET as string, ) as JwtPayload; - if (!decoded) { - return res.status(401).json({ message: "please login to continue!" }); + if (!payLoad) { + return res + .status(401) + .json(new HttpException("UNAUTHORIZED", "Please login to continue!")); } - if (decoded.role !== "vendor") { - return res.status(403).json({ message: "Forbidden" }); + req.user = payLoad; + + if (req.user?.role != "SELLER") { + return res + .status(403) + .json( + new HttpException( + "FORBIDDEN", + " Only seller can perform this action!", + ), + ); } + next(); } catch (error) { - return res.status(500).json({ message: "Internal server error" }); + if (error instanceof JsonWebTokenError) { + return res + .status(401) + .json(new HttpException("UNAUTHORIZED", "Please login to continue!")); + } else { + return res + .status(401) + .json(new HttpException("UNAUTHORIZED", "Please login to continue!")); + } } }; -//only admins auth -export const isAdmin = async ( +//only admins +const isAdmin = async ( req: ExpandedRequest, res: Response, next: NextFunction, @@ -132,13 +170,9 @@ export const isAdmin = async ( if (!decoded) { return res.status(401).json({ message: "Expired token,Try Login Again" }); } - const role = await User.findOne({ - where: { id: decoded.id }, - include: { model: Role, as: "Roles" }, - }); + const role = decoded.role; if (role) { - const x = role.toJSON() as unknown as { Roles: { roleName: string } }; - if (x.Roles.roleName === "ADMIN") { + if (role === "ADMIN") { next(); } else { return res @@ -156,3 +190,10 @@ export const isAdmin = async ( } } }; + +export default { + authenticateUser, + isBuyer, + isSeller, + isAdmin, +}; diff --git a/src/middlewares/multer.ts b/src/middlewares/multer.ts new file mode 100644 index 00000000..c167759a --- /dev/null +++ b/src/middlewares/multer.ts @@ -0,0 +1,25 @@ +import multer from "multer"; +import path from "path"; + +const fileUpload = multer({ + storage: multer.diskStorage({}), + fileFilter: (_req, file, callback) => { + const ext = path.extname(file.originalname); + if ( + ext !== ".png" && + ext !== ".jpg" && + ext !== ".jpeg" && + ext !== ".gif" && + ext !== ".webp" && + ext !== ".bmp" && + ext !== ".tiff" && + ext !== ".jfif" && + ext !== ".tif" + ) { + return callback(null, false); + } + callback(null, true); + }, +}); + +export default fileUpload; diff --git a/src/middlewares/passport.ts b/src/middlewares/passport.ts index c9c5ca99..ca0017a1 100644 --- a/src/middlewares/passport.ts +++ b/src/middlewares/passport.ts @@ -1,7 +1,6 @@ import { Request } from "express"; import passport from "passport"; import { Strategy as LocalStrategy } from "passport-local"; -import { UserModelAttributes } from "../database/models/User"; import { hashPassword } from "../utils/password"; import { isValidPassword } from "../utils/password.checks"; import GooglePassport, { VerifyCallback } from "passport-google-oauth20"; @@ -14,12 +13,7 @@ import { v4 as uuidv4 } from "uuid"; const GoogleStrategy = GooglePassport.Strategy; import database_models from "../database/config/db.config"; -const User = database_models["User"]; -export interface CustomVerifyOptions { - message: string; - status: string; - statusNumber?: number; -} +import { UserModelAttributes } from "../types/model"; passport.serializeUser(function (user: any, done) { done(null, user); @@ -58,15 +52,15 @@ passport.use( role: role?.dataValues.id as string, isVerified: false, }; - const userEXist = await User.findOne({ + const userExist = await database_models.User.findOne({ where: { email: data.email, }, }); - if (userEXist) { + if (userExist) { return done(null, false, { message: "User already exist!" }); } - const user = await User.create({ ...data }); + const user = await database_models.User.create({ ...data }); done(null, user); } catch (error) { done(error); @@ -85,11 +79,21 @@ passport.use( }, async (_req: Request, email, password, done) => { try { - const user = await User.findOne({ where: { email } }); + const user = await database_models.User.findOne({ + where: { email }, + include: [ + { + model: database_models.role, + as: "Roles", + }, + ], + }); + + const my_user = user?.toJSON(); if (!user) return done(null, false, { message: "Wrong credentials!" }); - const currPassword = user.dataValues.password; + const currPassword = my_user?.password as string; const isValidPass = await isValidPassword(password, currPassword); @@ -100,7 +104,7 @@ passport.use( if (!user.dataValues.isVerified) { return done(null, false, { message: "Verify your Account" }); } - return done(null, user); + return done(null, my_user); } catch (error) { done(error); } diff --git a/src/middlewares/product.middlewares.ts b/src/middlewares/product.middlewares.ts new file mode 100644 index 00000000..f22b6ffc --- /dev/null +++ b/src/middlewares/product.middlewares.ts @@ -0,0 +1,28 @@ +/* eslint-disable no-useless-escape */ +import { NextFunction, Request, Response } from "express"; +import { productValidation } from "../validations/product.validation"; +import { sendResponse } from "../utils/http.exception"; + +const isValidProduct = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + const { error } = productValidation.validate(req.body); + + if (error) { + return sendResponse( + res, + 400, + "BAD REQUEST", + error.details[0].message.replace(/\"/g, "") == "images is required" + ? "Images are required" + : error.details[0].message.replace(/\"/g, ""), + ); + } + next(); +}; + +export default { + isValidProduct, +}; diff --git a/src/middlewares/role.middleware.ts b/src/middlewares/role.middleware.ts index 46221d81..c90b3226 100644 --- a/src/middlewares/role.middleware.ts +++ b/src/middlewares/role.middleware.ts @@ -1,9 +1,10 @@ +/* eslint-disable no-useless-escape */ import { validateNewRole, validateRoleID, } from "../validations/role.validation"; import { NextFunction, Request, Response } from "express"; -import { HttpException } from "../utils/http.exception"; +import { sendResponse } from "../utils/http.exception"; export const roleNameValid = async ( req: Request, @@ -14,23 +15,23 @@ export const roleNameValid = async ( if (req.body) { const error = validateNewRole(req.body); if (error) { - return res - .status(400) - .json( - new HttpException( - "BAD REQUEST", - error.details[0].message.replace(/"/g, " "), - ), - ); + return sendResponse( + res, + 400, + "BAD REQUEST", + error.details[0].message.replace(/"/g, " "), + ); } } next(); } catch (error) { - res.status(500).json({ - status: "SERVER FAIL", - message: "Something went wrong!!", - error: error, - }); + return sendResponse( + res, + 500, + "SERVER ERROR", + "Something went wrong!", + (error as Error).message, + ); } }; @@ -43,22 +44,22 @@ export const roleIdValidations = async ( if (req.body) { const error = validateRoleID(req.body); if (error) { - return res - .status(400) - .json( - new HttpException( - "BAD REQUEST", - error.details[0].message.replace(/"/g, ""), - ), - ); + return sendResponse( + res, + 400, + "BAD REQUEST", + error.details[0].message.replace(/"/g, " "), + ); } } next(); } catch (error) { - res.status(500).json({ - status: "SERVER FAIL", - message: "Something went wrong!!", - error: error, - }); + return sendResponse( + res, + 500, + "SERVER ERROR", + "Something went wrong!", + (error as Error).message, + ); } }; diff --git a/src/middlewares/user.middleware.ts b/src/middlewares/user.middleware.ts index 3393a787..63809382 100644 --- a/src/middlewares/user.middleware.ts +++ b/src/middlewares/user.middleware.ts @@ -3,21 +3,19 @@ import { userValidate } from "../validations/user.valid"; import { NextFunction, Request, Response } from "express"; import validateLogIn from "../validations/login.validation"; import validateReset from "../validations/reset.validation"; -import { HttpException } from "../utils/http.exception"; +import { sendResponse } from "../utils/http.exception"; import validateNewPassword from "../validations/newPassword.validations"; const userValid = async (req: Request, res: Response, next: NextFunction) => { try { if (req.body) { const { error } = userValidate(req.body); if (error) { - return res - .status(400) - .json( - new HttpException( - "BAD REQUEST", - error.details[0].message.replace(/\"/g, ""), - ), - ); + return sendResponse( + res, + 400, + "BAD REQUEST", + error.details[0].message.replace(/"/g, ""), + ); } } next(); @@ -34,14 +32,12 @@ const logInValidated = (req: Request, res: Response, next: NextFunction) => { const error = validateLogIn(req.body); if (error) { - return res - .status(400) - .json( - new HttpException( - "BAD REQUEST", - error.details[0].message.replace(/\"/g, ""), - ), - ); + return sendResponse( + res, + 400, + "BAD REQUEST", + error.details[0].message.replace(/"/g, ""), + ); } next(); @@ -51,14 +47,12 @@ const resetValidated = (req: Request, res: Response, next: NextFunction) => { const error = validateReset(req.body); if (error) { - return res - .status(400) - .json( - new HttpException( - "BAD REQUEST", - error.details[0].message.replace(/\"/g, ""), - ), - ); + return sendResponse( + res, + 400, + "BAD REQUEST", + error.details[0].message.replace(/"/g, ""), + ); } next(); @@ -67,14 +61,12 @@ const isPassword = (req: Request, res: Response, next: NextFunction) => { const error = validateNewPassword(req.body); if (error) { - return res - .status(400) - .json( - new HttpException( - "BAD REQUEST", - error.details[0].message.replace(/\"/g, ""), - ), - ); + return sendResponse( + res, + 400, + "BAD REQUEST", + error.details[0].message.replace(/"/g, ""), + ); } next(); diff --git a/src/mock/images/BMW1.jpeg b/src/mock/images/BMW1.jpeg new file mode 100644 index 00000000..cc3d1c4a Binary files /dev/null and b/src/mock/images/BMW1.jpeg differ diff --git a/src/mock/images/BMW2.jpeg b/src/mock/images/BMW2.jpeg new file mode 100644 index 00000000..5f3683e2 Binary files /dev/null and b/src/mock/images/BMW2.jpeg differ diff --git a/src/mock/images/BMW3.webp b/src/mock/images/BMW3.webp new file mode 100644 index 00000000..14d4121d Binary files /dev/null and b/src/mock/images/BMW3.webp differ diff --git a/src/mock/images/BMW4.webp b/src/mock/images/BMW4.webp new file mode 100644 index 00000000..affb15cb Binary files /dev/null and b/src/mock/images/BMW4.webp differ diff --git a/src/mock/static.ts b/src/mock/static.ts index cacc26f4..bbd546d4 100644 --- a/src/mock/static.ts +++ b/src/mock/static.ts @@ -1,5 +1,24 @@ +import path from "path"; + +export const image_one_path: string = path.resolve( + __dirname, + "images/BMW1.jpeg", +); +export const image_two_path: string = path.resolve( + __dirname, + "images/BMW2.jpeg", +); +export const image_three_path: string = path.resolve( + __dirname, + "images/BMW3.webp", +); +export const image_four_path: string = path.resolve( + __dirname, + "images/BMW4.webp", +); + export const login_user = { - email: "peter234565@gmail.com", + email: "peter23456545@gmail.com", password: "passwordQWE123", }; export const login_user_br = { @@ -18,13 +37,27 @@ export const login_user_invalid_email = { }; export const NewUser = { + firstName: "peter", + lastName: "paul", + email: "peter23456545@gmail.com", + password: "passwordQWE123", + confirmPassword: "passwordQWE123", +}; + +export const new_buyer_user = { + firstName: "mark", + lastName: "mark", + email: "mark234565@gmail.com", + password: "passwordQWE123", + confirmPassword: "passwordQWE123", +}; +export const new_seller_user = { + userName: "peter", firstName: "peter", lastName: "paul", email: "peter234565@gmail.com", password: "passwordQWE123", confirmPassword: "passwordQWE123", - role: "SELLER", - isVerified: true, }; export const exist_user = { @@ -92,3 +125,31 @@ export const mockRole = { export const mockRoleBuyer = { roleName: "BUYER", }; +export const token = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjZjMjUyYWY3LWJhYjYtNGY4MC05YzQ5LTIzZTQ0MWRmMDJjYiIsInJvbGUiOiJTRUxMRVIiLCJpYXQiOjE3MTM5Njg5MDgsImV4cCI6MTc0NTUyNjUwOH0.22hDHx9vHSPw_fQ_yfr-29mUme1LpqFQG-ZIsFjhlH4"; + +export const new_product = { + name: "BMW", + price: 49900, + images: [image_one_path, image_two_path, image_three_path, image_four_path], + discount: 100, + quantity: 356, + expiryDate: "2324-04-30T00:00:00.000Z", +}; +export const new_update_product = { + name: "Ferrari", + price: 49900, + discount: 100, + quantity: 356, + images: [image_one_path], +}; + +export const new_category = { + name: "Cars", + description: "Cars are amazing!", +}; + +export const new_updated_category = { + name: "Fancy Cars", + description: "This cars are highly amazing!", +}; diff --git a/src/routes/categoryRoutes.ts b/src/routes/categoryRoutes.ts new file mode 100644 index 00000000..46a3d7c6 --- /dev/null +++ b/src/routes/categoryRoutes.ts @@ -0,0 +1,20 @@ +import express from "express"; +import userAuthentication from "../middlewares/auth"; +import categoryController from "../controllers/categoryController"; + +const categoryRouter = express.Router(); + +categoryRouter.post( + "/", + userAuthentication.isSeller, + categoryController.add_category, +); +categoryRouter.get("/", categoryController.read_all_categories); +categoryRouter.get("/:id", categoryController.read_single_category); +categoryRouter.patch( + "/:id", + userAuthentication.isSeller, + categoryController.update_category, +); + +export default categoryRouter; diff --git a/src/routes/index.ts b/src/routes/index.ts index 6ad89ba3..2ca0cfd3 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,10 +1,14 @@ import express from "express"; import userRoutes from "./userRoutes"; import { roleRoutes } from "./roleRoutes"; +import productRouter from "./productRoutes"; +import categoryRouter from "./categoryRoutes"; const router = express.Router(); router.use("/users", userRoutes); router.use("/", roleRoutes); +router.use("/products", productRouter); +router.use("/categories", categoryRouter); export default router; diff --git a/src/routes/productRoutes.ts b/src/routes/productRoutes.ts new file mode 100644 index 00000000..60f14cf2 --- /dev/null +++ b/src/routes/productRoutes.ts @@ -0,0 +1,42 @@ +import express from "express"; +import userAuthentication from "../middlewares/auth"; +import productController from "../controllers/productController"; +import productMiddlewares from "../middlewares/product.middlewares"; +import fileUpload from "../middlewares/multer"; + +const productRouter = express.Router(); + +productRouter.post( + "/", + userAuthentication.isSeller, + fileUpload.array("images"), + productMiddlewares.isValidProduct, + productController.create_product, +); + +productRouter.get( + "/", + userAuthentication.authenticateUser, + productController.read_all_products, +); + +productRouter.get( + "/:id", + userAuthentication.authenticateUser, + productController.read_single_product, +); + +productRouter.patch( + "/:id", + userAuthentication.isSeller, + fileUpload.array("images"), + productController.update_product, +); + +productRouter.delete( + "/:id", + userAuthentication.isSeller, + productController.delete_product, +); + +export default productRouter; diff --git a/src/routes/roleRoutes.ts b/src/routes/roleRoutes.ts index 429192af..5589f07d 100644 --- a/src/routes/roleRoutes.ts +++ b/src/routes/roleRoutes.ts @@ -3,16 +3,26 @@ import { assignRole, updateRole, allRole, -} from "../controllers/roleConroller"; +} from "../controllers/roleController"; import express from "express"; import { roleNameValid, roleIdValidations, } from "../middlewares/role.middleware"; -import { isAdmin } from "../middlewares/auth"; +import authentication from "../middlewares/auth"; export const roleRoutes = express.Router(); -roleRoutes.get("/roles/", isAdmin, allRole); -roleRoutes.post("/roles/", isAdmin, roleNameValid, createRole); -roleRoutes.post("/users/:userId/roles", isAdmin, roleIdValidations, assignRole); -roleRoutes.patch("/roles/:id", isAdmin, roleNameValid, updateRole); +roleRoutes.get("/roles/", authentication.isAdmin, allRole); +roleRoutes.post("/roles/", authentication.isAdmin, roleNameValid, createRole); +roleRoutes.post( + "/users/:userId/roles", + authentication.isAdmin, + roleIdValidations, + assignRole, +); +roleRoutes.patch( + "/roles/:id", + authentication.isAdmin, + roleNameValid, + updateRole, +); diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index ba899428..17b8d9b7 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -3,7 +3,7 @@ import userController from "../controllers/userController"; import userMiddleware from "../middlewares/user.middleware"; import otpIsValid from "../middlewares/otp"; import { resetPasswort, forgotPassword } from "../controllers/resetPasswort"; -import { authenticateUser } from "../middlewares/auth"; +import authentication from "../middlewares/auth"; const userRoutes = express.Router(); userRoutes.post( @@ -25,7 +25,11 @@ userRoutes.post( resetPasswort, ); userRoutes.get("/account/verify/:token", userController.accountVerify); -userRoutes.post("/logout", authenticateUser, userController.logout); +userRoutes.post( + "/logout", + authentication.authenticateUser, + userController.logout, +); userRoutes.get("/account/verify/:token", userController.accountVerify); userRoutes.post( diff --git a/src/services/user.services.ts b/src/services/user.services.ts index 908f6053..6ebe10e6 100644 --- a/src/services/user.services.ts +++ b/src/services/user.services.ts @@ -1,24 +1,16 @@ import { hashPassword } from "../utils/password"; -import { User } from "../database/models/User"; -interface UserInt { - userName: string; - firstName: string; - lastName: string; - email: string; - password: string; - confirmPassword: string; - role: string; - isVerified: boolean; -} +import database_models from "../database/config/db.config"; +import { UserInt } from "../types/services"; + export const createUser = async (data: UserInt) => { - return User.create({ + return await database_models.User.create({ email: data.email.trim(), password: await hashPassword(data.password), confirmPassword: await hashPassword(data.password), userName: data.userName == null ? data.email.split("@")[0] : data.userName, firstName: data.firstName, lastName: data.lastName, - role: "BUYER", + role: "SELLER", isVerified: false, }); }; diff --git a/src/types/email.ts b/src/types/email.ts new file mode 100644 index 00000000..dd512ca0 --- /dev/null +++ b/src/types/email.ts @@ -0,0 +1,5 @@ +export type emailAttribute = { + user: string; + subject: string; + message: string; +}; diff --git a/src/types/model.ts b/src/types/model.ts new file mode 100644 index 00000000..6aa21cee --- /dev/null +++ b/src/types/model.ts @@ -0,0 +1,92 @@ +import { Optional } from "sequelize"; + +/** + * -------------- User Model --------------------- + */ + +export interface UserModelAttributes { + id: string; + userName: string; + firstName: string; + lastName: string; + email: string; + password: string; + confirmPassword: string; + role: string; + isVerified: boolean; +} + +export interface UserModelInclude extends UserModelAttributes { + Roles: any; +} + +export type UserCreationAttributes = Optional & { + role?: string; +}; + +/** + * -------------- Token Model --------------------- + */ + +export interface TokenModelAttributes { + id: string; + token: string; +} +export type TokenCreationAttributes = Optional; + +/** + * -------------- Product Model --------------------- + */ + +export interface ProductAttributes { + id: string; + name: string; + price: number; + images: string[]; + discount: number; + quantity: number; + sellerId: string; + categoryId: string; + expiryDate: Date; +} + +export type ProductCreationAttributes = Omit; + +/** + * -------------- Category Model --------------------- + */ + +export interface CategoryAttributes { + id: string; + name: string; + description: string; +} + +export type CategoryCreationAttributes = Omit; + +/** + * ----------------- Blacklist model ---------------------------- + */ +export interface BlacklistModelAtributes { + id?: string; + token: string; +} + +/** + * ----------------- reset model ---------------------------- + */ + +export interface resetPasswordModelAtributes { + id?: string; + email: string; + resetToken: string; +} +/** + * ----------------- role model ---------------------------- + */ + +export interface roleModelAttributes { + id: string; + roleName: string; +} +export type roleCreationAttributes = Optional; diff --git a/src/types/passport.ts b/src/types/passport.ts new file mode 100644 index 00000000..85301b33 --- /dev/null +++ b/src/types/passport.ts @@ -0,0 +1,3 @@ +export type InfoAttribute = { + message: string; +}; diff --git a/src/types/services.ts b/src/types/services.ts new file mode 100644 index 00000000..439008aa --- /dev/null +++ b/src/types/services.ts @@ -0,0 +1,10 @@ +export type UserInt = { + userName: string; + firstName: string; + lastName: string; + email: string; + password: string; + confirmPassword: string; + role: string; + isVerified: boolean; +}; diff --git a/src/types/upload.ts b/src/types/upload.ts new file mode 100644 index 00000000..daa006d4 --- /dev/null +++ b/src/types/upload.ts @@ -0,0 +1,9 @@ +import { Request } from "express"; + +export interface Info extends Request { + info?: T; +} + +export interface Message { + message: string; +} diff --git a/src/utils/controller.ts b/src/utils/controller.ts new file mode 100644 index 00000000..131c4724 --- /dev/null +++ b/src/utils/controller.ts @@ -0,0 +1,18 @@ +import { Request, Response } from "express"; +import { validate as isValidUUID } from "uuid"; +import { sendResponse } from "./http.exception"; + +export const category_utils = (req: Request, res: Response) => { + return { + getId: req.params.id, + isValidUUID: (categoryId: string) => { + const is_valid_uuid = isValidUUID(categoryId); + if (!is_valid_uuid) { + sendResponse(res, 400, "BAD REQUEST", "You provided Invalid ID!"); + return false; + } else { + return true; + } + }, + }; +}; diff --git a/src/utils/database.utils.ts b/src/utils/database.utils.ts index e295b937..77f8c509 100644 --- a/src/utils/database.utils.ts +++ b/src/utils/database.utils.ts @@ -1,3 +1,7 @@ +import database_models from "../database/config/db.config"; +import Jwt, { JwtPayload } from "jsonwebtoken"; +import { ACCESS_TOKEN_SECRET } from "./keys"; + export const deleteTableData = async (Model: any, tableName: string) => { try { const deletedRows = await Model.destroy({ @@ -12,3 +16,19 @@ export const deleteTableData = async (Model: any, tableName: string) => { console.log("Something went wrong in the process:", error); } }; + +export const changeRole = async (token: string): Promise => { + const payLoad = Jwt.verify( + token, + ACCESS_TOKEN_SECRET as string, + ) as JwtPayload; + + await database_models.User.update( + { role: "SELLER" }, + { + where: { + id: payLoad.id, + }, + }, + ); +}; diff --git a/src/utils/db_methods.ts b/src/utils/db_methods.ts new file mode 100644 index 00000000..e428c68c --- /dev/null +++ b/src/utils/db_methods.ts @@ -0,0 +1,66 @@ +/* eslint-disable no-shadow */ +import { CreateOptions, FindOptions, UpdateOptions } from "sequelize"; +import database_models from "../database/config/db.config"; + +type ModelTypes = + | "Product" + | "Category" + | "User" + | "Blacklist" + | "Token" + | "resetPassword" + | "role"; +type MethodTypes = "findAll" | "findOne" | "destroy" | "create" | "update"; + +export const read_function = async ( + model: ModelTypes, + method: MethodTypes, + condition?: FindOptions, +): Promise => { + if (!database_models[model] || !database_models[model][method]) { + // database_models[model] -> database_models.model // invalid modename "" "" + throw new Error( + `Invalid ${!database_models[model] ? "modelName" : ""} ${!database_models[model] && !database_models[model][method] ? "and" : ""} ${!database_models[model][method] ? "method" : ""}`, + ); + } + const result = await ( + database_models[model][method] as (condition: FindOptions) => Promise + )(condition as FindOptions); + return result; +}; + +export const insert_function = async ( + model: ModelTypes, + method: MethodTypes, + data: any, + condition?: FindOptions | UpdateOptions, +): Promise => { + if (!database_models[model] || !database_models[model][method]) { + throw new Error( + `Invalid ${!database_models[model] ? "modelName" : ""} ${!database_models[model] && !database_models[model][method] ? "and" : ""} ${!database_models[model][method] ? "method" : ""}`, + ); + } + + if (method === "create") { + const result = await ( + database_models[model][method] as ( + data: any, + options?: CreateOptions, + ) => Promise + )(data, condition as CreateOptions); + return result; + } else if (method === "update") { + if (!condition) { + throw new Error("Condition is required for update operation"); + } + const result = await ( + database_models[model][method] as ( + values: any, + options?: UpdateOptions, + ) => Promise + )(data, condition as UpdateOptions); + return result; + } else { + throw new Error("Invalid method type"); + } +}; diff --git a/src/utils/email.ts b/src/utils/email.ts index 7747be6f..2c8f3f72 100644 --- a/src/utils/email.ts +++ b/src/utils/email.ts @@ -1,9 +1,6 @@ import nodemailer from "nodemailer"; -export interface emailAttribute { - user: string; - subject: string; - message: string; -} +import { emailAttribute } from "../types/email"; + const sendEmail = async (emailData: emailAttribute) => { try { const transporter = nodemailer.createTransport({ diff --git a/src/utils/http.exception.ts b/src/utils/http.exception.ts index 70b6efeb..847f158b 100644 --- a/src/utils/http.exception.ts +++ b/src/utils/http.exception.ts @@ -1,3 +1,5 @@ +import { Response } from "express"; + export class HttpException { public status: string; public message: string; @@ -13,3 +15,15 @@ export class HttpException { }; } } + +export const sendResponse = ( + res: Response, + statusNumber: number, + status: string, + message: string, + data?: any, +) => { + return res + .status(statusNumber) + .json({ ...new HttpException(status, message), data }); +}; diff --git a/src/utils/keys.ts b/src/utils/keys.ts index 0fb8ef7e..931d3082 100644 --- a/src/utils/keys.ts +++ b/src/utils/keys.ts @@ -11,3 +11,9 @@ export const PASSWORD = process.env.PASSWORD; export const HOST = process.env.HOST; export const SENDGRID_API_KEY = process.env.SENDGRID_API_KEY; export const DEFAULT_ROLE = process.env.DEFAULT_ROLE; +export const CLOUDINARY_CLOUD_NAME = process.env.CLOUDINARY_CLOUD_NAME; +export const CLOUDINARY_API_KEY = process.env.CLOUDINARY_API_KEY; +export const CLOUDINARY_API_SECRET = process.env.CLOUDINARY_API_SECRET; +export const CLOUDINARY_FOLDER_NAME = process.env.CLOUDINARY_FOLDER_NAME; +export const DEPLOYED_URL = process.env.DEPLOYED_URL; +export const SERVER_URL = process.env.SERVER_URL; diff --git a/src/utils/token.validation.ts b/src/utils/token.validation.ts index 1eacf0e9..022d3baf 100644 --- a/src/utils/token.validation.ts +++ b/src/utils/token.validation.ts @@ -1,11 +1,12 @@ import jwt, { JsonWebTokenError, JwtPayload } from "jsonwebtoken"; -interface Result { +type Result = { valid: boolean; id?: string; reason?: string; user?: JwtPayload; -} +}; + export const validateToken = ( token: string | undefined, secretKey: string, diff --git a/src/validations/product.validation.ts b/src/validations/product.validation.ts new file mode 100644 index 00000000..db76feb3 --- /dev/null +++ b/src/validations/product.validation.ts @@ -0,0 +1,32 @@ +import Joi from "joi"; + +export const productValidation = Joi.object({ + name: Joi.string().required().messages({ + "string.empty": "Name field can't be empty!", + }), + images: Joi.array(), + price: Joi.number().required().min(0).messages({ + "any.required": "Price is required!", + "number.base": "Price must be a number!", + "number.min": "Price must be a non-negative number!", + }), + discount: Joi.number().min(0).messages({ + "any.required": "Discount is required!", + "number.base": "Discount must be a number!", + "number.min": "Discount must be a non-negative number!", + }), + quantity: Joi.number().required().min(0).messages({ + "any.required": "Quantity is required!", + "number.base": "Quantity must be a number!", + "number.min": "Quantity must be a non-negative number!", + }), + categoryId: Joi.string().required().messages({ + "string.empty": "CategoryId field can't be empty!", + "any.required": "categoryId is required!", + }), + expiryDate: Joi.date().required().min("now").iso().messages({ + "date.base": "Expiry date must be a valid date", + "date.min": "Expiry date must be in the future", + }), + sellerId: Joi.string(), +}).options({ allowUnknown: false }); diff --git a/src/validations/role.validation.ts b/src/validations/role.validation.ts index 6811dec7..0a26c826 100644 --- a/src/validations/role.validation.ts +++ b/src/validations/role.validation.ts @@ -6,9 +6,9 @@ const createRoleValidation = Joi.object({ }).options({ allowUnknown: false }); const ValidateRoleID = Joi.object({ - roleId: Joi.string().guid({ version: "uuidv4" }).required().messages({ - "string.empty": "roleId field can't be empty!", - "string.guid": "roleId must be a valid UUIDv4 string!", + role: Joi.string().required().messages({ + "string.empty": "role field can't be empty!", + // "string.guid": "roleId must be a valid UUIDv4 string!", }), }).options({ allowUnknown: false });