From b4a485a771a89ed97e9283d0f93b608ed7bdc555 Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Wed, 22 May 2024 13:25:55 +0200 Subject: [PATCH 001/187] Creating Product function --- src/controllers/product.controller.ts | 44 +++++++++++++++++++++++++++ src/services/productService.ts | 10 ++++++ 2 files changed, 54 insertions(+) create mode 100644 src/controllers/product.controller.ts create mode 100644 src/services/productService.ts diff --git a/src/controllers/product.controller.ts b/src/controllers/product.controller.ts new file mode 100644 index 0000000..52f1df8 --- /dev/null +++ b/src/controllers/product.controller.ts @@ -0,0 +1,44 @@ + +import { Request, Response } from "express"; +import { saveProduct } from "../services/productService"; +import Vendor from "../database/models/vendor"; + +export const createProduct = async(req:Request,res:Response)=>{ + try { + const tokenData = (req as any).token + let existVendor = await Vendor.findByPk(tokenData.vendorId) + if(existVendor){ + + const {name,image,description,discount,price,quantity,category} = req.body + if(!name || !image || !description || !price || !quantity || !category){ + return res.status(200).json("All Field are required") + } + const data = { + name, + image, + description, + discount: discount ? discount : 0, + price, + quantity, + category, + vendorId: tokenData.id + } + const save = await saveProduct(data) + if(!save){ + return res.status(500).json({error: "Failed to save data"}) + } + return res.status(201).json({message: "Product Created"}) + } + else{ + return res.status(500).json({error: "No vendor found"}) + } + + + + + } catch (error:any) { + res.status(500).json({error:error.message}) + + } + +} \ No newline at end of file diff --git a/src/services/productService.ts b/src/services/productService.ts new file mode 100644 index 0000000..57f05b5 --- /dev/null +++ b/src/services/productService.ts @@ -0,0 +1,10 @@ +import Product from "../database/models/product" + +export const saveProduct = async (data:any)=>{ + const response = await Product.create(data) + if(response){ + return true + }else{ + return false + } +} \ No newline at end of file From 55d6b61021f79008a88539a596d55cad46eb0cd9 Mon Sep 17 00:00:00 2001 From: HITAYEZU Jean Pierre Date: Mon, 13 May 2024 00:50:19 +0200 Subject: [PATCH 002/187] Readme file created --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bdc0138..92f9516 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Create a `.yaml` file. - Write your documentation in the file. No need to set up Swagger-related things in `server.ts` again.
:warning: You must know that YAML strictly follows indentation -### Deployed_link:https://e-commerce-crafters-bn.onrender.com/ +### Deployed_link: ## 6.Licensing [![GitHub license](https://img.shields.io/github/license/Naereen/StrapDown.js.svg)](https://github.com/Naereen/StrapDown.js/blob/master/LICENSE) This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for detail. From 1277b6a9791d695dfb244822404567380ebafb9f Mon Sep 17 00:00:00 2001 From: HITAYEZU Jean Pierre Date: Tue, 14 May 2024 16:34:20 +0200 Subject: [PATCH 003/187] readme modified --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 92f9516..bdc0138 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Create a `.yaml` file. - Write your documentation in the file. No need to set up Swagger-related things in `server.ts` again.
:warning: You must know that YAML strictly follows indentation -### Deployed_link: +### Deployed_link:https://e-commerce-crafters-bn.onrender.com/ ## 6.Licensing [![GitHub license](https://img.shields.io/github/license/Naereen/StrapDown.js.svg)](https://github.com/Naereen/StrapDown.js/blob/master/LICENSE) This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for detail. From 3159d8e20466a0a31326bc6a8e9ed57203c23a64 Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 22 May 2024 13:34:50 +0200 Subject: [PATCH 004/187] removed console logs used for debugging --- src/database/seeders/20240521201427-seed-carts.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/database/seeders/20240521201427-seed-carts.js b/src/database/seeders/20240521201427-seed-carts.js index cb4bf31..884c0a4 100644 --- a/src/database/seeders/20240521201427-seed-carts.js +++ b/src/database/seeders/20240521201427-seed-carts.js @@ -5,13 +5,8 @@ const { v4: uuidv4 } = require('uuid'); /** @type {import('sequelize-cli').Migration} */ module.exports = { async up(queryInterface, Sequelize) { - console.log('Getting the users') const [users] = await queryInterface.sequelize.query(`SELECT "userId" FROM "Users"`); - users.map(user => ( - console.log('The users ', user) - )) - const carts = users.map(user => ({ cartId: uuidv4(), // @ts-ignore From 0155455ba6da1a8af67d1cab3f7f17a08ab98826 Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Wed, 22 May 2024 14:34:17 +0200 Subject: [PATCH 005/187] Add is verfied attributes --- src/database/migrations/20240520101722-create-user.js | 5 +++++ src/database/models/product.ts | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/database/migrations/20240520101722-create-user.js b/src/database/migrations/20240520101722-create-user.js index a0b134e..67aa189 100644 --- a/src/database/migrations/20240520101722-create-user.js +++ b/src/database/migrations/20240520101722-create-user.js @@ -40,6 +40,11 @@ module.exports = { profile:{ type: Sequelize.STRING }, + isVerfied:{ + type: Sequelize.BOOLEAN, + defaultValue: false + + }, createdAt: { allowNull: false, type: Sequelize.DATE diff --git a/src/database/models/product.ts b/src/database/models/product.ts index 09ae554..a768b16 100644 --- a/src/database/models/product.ts +++ b/src/database/models/product.ts @@ -13,6 +13,7 @@ import { ftruncate } from "fs"; public price!: string public quantity!: number public category!: string + public isVerfied?: boolean static associate(models: any) { Product.hasMany(models.Wishlist,{ foreignKey: 'productId', @@ -28,7 +29,7 @@ import { ftruncate } from "fs"; } Product.init({ productId: {type:DataTypes.UUID,primaryKey: true,defaultValue: DataTypes.UUIDV4}, - vendlorId: {type:DataTypes.STRING,primaryKey: true,defaultValue: DataTypes.UUIDV4}, + vendorId: {type:DataTypes.STRING,primaryKey: true,defaultValue: DataTypes.UUIDV4}, name: {type:DataTypes.STRING,allowNull: false}, description: {type:DataTypes.STRING,allowNull: false}, image: {type:DataTypes.STRING,allowNull: false}, @@ -36,6 +37,7 @@ import { ftruncate } from "fs"; price: {type:DataTypes.INTEGER,allowNull: false}, quantity: {type:DataTypes.INTEGER,allowNull: true}, category: {type:DataTypes.STRING,allowNull: true}, + isVerfied: {type:DataTypes.BOOLEAN,defaultValue: false} }, { sequelize: connectSequelize, From 3c52afb06f0b4ce1d01ea650619fd1ccb8019e76 Mon Sep 17 00:00:00 2001 From: Philimuhire Date: Thu, 23 May 2024 17:09:57 +0200 Subject: [PATCH 006/187] add controller for read and search product with pagination --- src/controllers/product.controller.ts | 40 +++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/src/controllers/product.controller.ts b/src/controllers/product.controller.ts index 52f1df8..25f7b8d 100644 --- a/src/controllers/product.controller.ts +++ b/src/controllers/product.controller.ts @@ -1,7 +1,8 @@ import { Request, Response } from "express"; -import { saveProduct } from "../services/productService"; +import { saveProduct, getProductById, searchProducts} from "../services/productService"; import Vendor from "../database/models/vendor"; +import Product from "../database/models/product"; export const createProduct = async(req:Request,res:Response)=>{ try { @@ -41,4 +42,39 @@ export const createProduct = async(req:Request,res:Response)=>{ } -} \ No newline at end of file +} + +export const readProduct = async (req: Request, res: Response) => { + try { + const productId = req.params.id; + const product = await Product.findByPk(productId); + if (!product) { + return res.status(404).json({ error: "Product not found" }); + } + return res.status(200).json(product); + } catch (error: any) { + return res.status(500).json({ error: error.message }); + } +}; + +export const searchProduct = async (req: Request, res: Response) => { + try { + const { name, category } = req.query; + const page = parseInt(req.query.page as string) || 1; + const limit = parseInt(req.query.limit as string) || 10; + + const criteria: any = {}; + if (name) criteria.name = name; + if (category) criteria.category = category; + + const products = await searchProducts(criteria, page, limit); + + if (products.length === 0) { + return res.status(404).json({ error: "No products found" }); + } + + return res.status(200).json(products); + } catch (error: any) { + return res.status(500).json({ error: error.message }); + } +}; \ No newline at end of file From 14d8211e8f1f0943138e448e22591fab67e47030 Mon Sep 17 00:00:00 2001 From: Philimuhire Date: Thu, 23 May 2024 17:12:30 +0200 Subject: [PATCH 007/187] add service for read and search product with pagination --- src/services/productService.ts | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/services/productService.ts b/src/services/productService.ts index 57f05b5..d5d5d8a 100644 --- a/src/services/productService.ts +++ b/src/services/productService.ts @@ -7,4 +7,27 @@ export const saveProduct = async (data:any)=>{ }else{ return false } -} \ No newline at end of file +} + +export const getProductById = async (productId: string) => { + try { + const product = await Product.findByPk(productId); + return product; + } catch (error) { + throw new Error('Error while fetching product'); + } +}; + +export const searchProducts = async (criteria: any, page: number, limit: number) => { + try { + const offset = (page - 1) * limit; + const products = await Product.findAll({ + where: criteria, + offset, + limit + }); + return products; + } catch (error) { + throw new Error('Error while searching products'); + } +}; \ No newline at end of file From 8c7e50c7740432f57b47e54c731e1c6875b6d78b Mon Sep 17 00:00:00 2001 From: Philimuhire Date: Thu, 23 May 2024 17:14:26 +0200 Subject: [PATCH 008/187] add root for read and search product with pagination --- src/index.ts | 2 ++ src/routes/product.route.ts | 9 +++++++++ 2 files changed, 11 insertions(+) create mode 100644 src/routes/product.route.ts diff --git a/src/index.ts b/src/index.ts index 4f5f9cb..206076f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ const PORT = process.env.PORT; import userRoute from "./routes/user.route"; import swaggerRoute from "./config/SwaggerConfig"; +import productRoute from "./routes/product.route"; const app = express(); @@ -15,6 +16,7 @@ const app = express(); app.use(express.static("public")); app.use(express.json()) app.use("/", userRoute); +app.use("/", productRoute); app.use("/api-docs", swaggerRoute); diff --git a/src/routes/product.route.ts b/src/routes/product.route.ts new file mode 100644 index 0000000..38c17bf --- /dev/null +++ b/src/routes/product.route.ts @@ -0,0 +1,9 @@ +import express from "express"; +import { readProduct, searchProduct } from "../controllers/product.controller"; + +const router = express.Router(); + +router.get("/product/:id", readProduct); +router.get("/products/search", searchProduct) + +export default router; \ No newline at end of file From 57f57f12e22c26a3b11988e9bd0ba957f1cb0407 Mon Sep 17 00:00:00 2001 From: Philimuhire Date: Fri, 24 May 2024 09:35:43 +0200 Subject: [PATCH 009/187] add controller to read all products --- src/controllers/product.controller.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/controllers/product.controller.ts b/src/controllers/product.controller.ts index 25f7b8d..3212442 100644 --- a/src/controllers/product.controller.ts +++ b/src/controllers/product.controller.ts @@ -1,6 +1,6 @@ import { Request, Response } from "express"; -import { saveProduct, getProductById, searchProducts} from "../services/productService"; +import { saveProduct, getAllProducts, getProductById, searchProducts} from "../services/productService"; import Vendor from "../database/models/vendor"; import Product from "../database/models/product"; @@ -44,6 +44,23 @@ export const createProduct = async(req:Request,res:Response)=>{ } +export const readAllProducts = async (req: Request, res: Response) => { + try { + const page = parseInt(req.query.page as string) || 1; + const limit = parseInt(req.query.limit as string) || 10; + + const products = await getAllProducts(page, limit); + + if (products.length === 0) { + return res.status(404).json({ error: "No products found" }); + } + + return res.status(200).json(products); + } catch (error: any) { + return res.status(500).json({ error: error.message }); + } +}; + export const readProduct = async (req: Request, res: Response) => { try { const productId = req.params.id; From b2014d5cefc940d6e305cc630ab272e32fac41a1 Mon Sep 17 00:00:00 2001 From: Philimuhire Date: Fri, 24 May 2024 09:37:06 +0200 Subject: [PATCH 010/187] add service to read all products --- src/services/productService.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/services/productService.ts b/src/services/productService.ts index d5d5d8a..8263aee 100644 --- a/src/services/productService.ts +++ b/src/services/productService.ts @@ -9,6 +9,19 @@ export const saveProduct = async (data:any)=>{ } } +export const getAllProducts = async (page: number, limit: number) => { + try { + const offset = (page - 1) * limit; + const products = await Product.findAll({ + offset, + limit + }); + return products; + } catch (error) { + throw new Error('Error while fetching all products'); + } +}; + export const getProductById = async (productId: string) => { try { const product = await Product.findByPk(productId); From 723ba853a71beeff2c061ef0684311cccf74c62f Mon Sep 17 00:00:00 2001 From: Philimuhire Date: Fri, 24 May 2024 09:37:44 +0200 Subject: [PATCH 011/187] add route to read all products --- src/routes/product.route.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/routes/product.route.ts b/src/routes/product.route.ts index 38c17bf..82b5a2e 100644 --- a/src/routes/product.route.ts +++ b/src/routes/product.route.ts @@ -1,9 +1,10 @@ import express from "express"; -import { readProduct, searchProduct } from "../controllers/product.controller"; +import { readAllProducts, readProduct, searchProduct } from "../controllers/product.controller"; const router = express.Router(); -router.get("/product/:id", readProduct); +router.get("/readAllProducts", readAllProducts); +router.get("/readProduct/:id", readProduct); router.get("/products/search", searchProduct) export default router; \ No newline at end of file From 41175ecf6ec6804e4e385f1b2475706e233edea4 Mon Sep 17 00:00:00 2001 From: frerot Date: Fri, 31 May 2024 18:49:07 +0200 Subject: [PATCH 012/187] Function of checking Expired Products --- package-lock.json | 11 -- ...iringProducts.test.ts => expiring.test.ts} | 0 .../20240520140651-create-product.js | 6 +- src/database/models/product.ts | 113 ++++++++++-------- .../seeders/20240522064825-seed-products.js | 8 +- src/helpers/expiring.ts | 29 ++++- src/index.ts | 45 +++---- 7 files changed, 116 insertions(+), 96 deletions(-) rename src/__test__/{checkExpiringProducts.test.ts => expiring.test.ts} (100%) diff --git a/package-lock.json b/package-lock.json index c6e490c..a16f35d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1407,7 +1407,6 @@ "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.42.tgz", "integrity": "sha512-Jhy+MWRlro6UjVi578V/4ZGNfeCOcNCp0YaFNIUGFKlImowqwb1O/22wDVk3FDGMLqxdpOV3qQHD5fPEH4hK6A==", "dev": true - }, "node_modules/@types/body-parser": { "version": "1.19.5", @@ -1426,14 +1425,6 @@ "@types/node": "*" } }, - "node_modules/@types/continuation-local-storage": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@types/continuation-local-storage/-/continuation-local-storage-3.2.7.tgz", - "integrity": "sha512-Q7dPOymVpRG5Zpz90/o26+OAqOG2Sw+FED7uQmTrJNCF/JAPTylclZofMxZKd6W7g1BDPmT9/C/jX0ZcSNTQwQ==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/continuation-local-storage": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/@types/continuation-local-storage/-/continuation-local-storage-3.2.7.tgz", @@ -1578,10 +1569,8 @@ "node_modules/@types/lodash": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==", "dev": true - }, "node_modules/@types/methods": { "version": "1.1.4", diff --git a/src/__test__/checkExpiringProducts.test.ts b/src/__test__/expiring.test.ts similarity index 100% rename from src/__test__/checkExpiringProducts.test.ts rename to src/__test__/expiring.test.ts diff --git a/src/database/migrations/20240520140651-create-product.js b/src/database/migrations/20240520140651-create-product.js index 11c9472..b548ac2 100644 --- a/src/database/migrations/20240520140651-create-product.js +++ b/src/database/migrations/20240520140651-create-product.js @@ -15,7 +15,7 @@ module.exports = { model: 'Vendors', key: 'vendorId' }, - onDelete:"CASCADE" + onDelete: "CASCADE" }, name: { type: Sequelize.STRING, @@ -54,7 +54,9 @@ module.exports = { expiringDate: { allowNull: true, type: Sequelize.DATE - } + }, + expired: { allowNull: false, type: Sequelize.BOOLEAN, defaultValue: false }, + available: { allowNull: false, type: Sequelize.BOOLEAN, defaultValue: true }, }); }, async down(queryInterface, Sequelize) { diff --git a/src/database/models/product.ts b/src/database/models/product.ts index d3ae859..be3ec31 100644 --- a/src/database/models/product.ts +++ b/src/database/models/product.ts @@ -3,59 +3,66 @@ import { Model, DataTypes, Sequelize } from "sequelize"; import connectSequelize from "../config/db.config"; class Product extends Model { - public productId?: string; - public vendorId: any; - public name!: string; - public description!: string; - public image!: string; - public discount!: number; - public price!: string; - public quantity!: number; - public category!: string; - public expiringDate?: string; - - static associate(models: any) { - Product.hasMany(models.Wishlist, { - foreignKey: "productId", - }); - Product.hasMany(models.CartItem, { - foreignKey: "productId", - }); - Product.hasMany(models.Review, { - foreignKey: "productId", - }); - } - static initModel(sequelize: Sequelize) { - Product.init( - { - productId: { - type: DataTypes.UUID, - primaryKey: true, - defaultValue: DataTypes.UUIDV4, - }, - vendorId: { - type: DataTypes.STRING, - primaryKey: true, - defaultValue: DataTypes.UUIDV4, - }, - name: { type: DataTypes.STRING, allowNull: false }, - description: { type: DataTypes.STRING, allowNull: false }, - image: { type: DataTypes.STRING, allowNull: false }, - discount: { type: DataTypes.INTEGER, allowNull: false }, - price: { type: DataTypes.INTEGER, allowNull: false }, - quantity: { type: DataTypes.INTEGER, allowNull: true }, - category: { type: DataTypes.STRING, allowNull: true }, - expiringDate: { allowNull: true, type: DataTypes.DATE }, - }, - { - sequelize: connectSequelize, - modelName: "Product", - tableName: "Products", - timestamps: true, - } - ); - return Product; - } + public productId?: string; + public vendorId: any; + public name!: string; + public description!: string; + public image!: string; + public discount!: number; + public price!: string; + public quantity!: number; + public category!: string; + public expiringDate?: Date; + public expired!: boolean; + public available!: boolean; + static associate(models: any) { + Product.hasMany(models.Wishlist, { + foreignKey: "productId", + }); + Product.hasMany(models.CartItem, { + foreignKey: "productId", + }); + Product.hasMany(models.Review, { + foreignKey: "productId", + }); + } + static initModel(sequelize: Sequelize) { + Product.init( + { + productId: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + }, + vendorId: { + type: DataTypes.STRING, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + }, + name: { type: DataTypes.STRING, allowNull: false }, + description: { type: DataTypes.STRING, allowNull: false }, + image: { type: DataTypes.STRING, allowNull: false }, + discount: { type: DataTypes.INTEGER, allowNull: false }, + price: { type: DataTypes.INTEGER, allowNull: false }, + quantity: { type: DataTypes.INTEGER, allowNull: true }, + category: { type: DataTypes.STRING, allowNull: true }, + expiringDate: { allowNull: true, type: DataTypes.DATE }, + expired: { allowNull: false, type: DataTypes.BOOLEAN, defaultValue: false }, + available: { + allowNull: false, + type: DataTypes.BOOLEAN, + defaultValue: true, + }, + }, + { + sequelize: connectSequelize, + modelName: "Product", + tableName: "Products", + timestamps: true, + } + ); + return Product; + } } Product.initModel(connectSequelize); diff --git a/src/database/seeders/20240522064825-seed-products.js b/src/database/seeders/20240522064825-seed-products.js index cf5a0fa..05011cb 100644 --- a/src/database/seeders/20240522064825-seed-products.js +++ b/src/database/seeders/20240522064825-seed-products.js @@ -5,7 +5,7 @@ const { v4: uuidv4 } = require('uuid'); /** @type {import('sequelize-cli').Migration} */ module.exports = { async up(queryInterface, Sequelize) { - const imageUrl = 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSXhCG6nff2emBYOKGHb6jU2zQ4C2m0LBg4Mj-eydwZyg&s' + const imageUrl = 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSXhCG6nff2emBYOKGHb6jU2zQ4C2m0LBg4Mj-eydwZyg&s'; const [vendors] = await queryInterface.sequelize.query(` SELECT "vendorId" @@ -26,8 +26,10 @@ module.exports = { quantity: 100, category: 'Shoes', createdAt: new Date(), - updatedAt: new Date() - })) + updatedAt: new Date(), + expired: false, + available: true + })); await queryInterface.bulkInsert('Products', products, {}); }, diff --git a/src/helpers/expiring.ts b/src/helpers/expiring.ts index 71dbfa5..8831723 100644 --- a/src/helpers/expiring.ts +++ b/src/helpers/expiring.ts @@ -2,7 +2,6 @@ import Product from "../database/models/product"; import Vendor from "../database/models/vendor"; import User from "../database/models/user"; import { Op } from "sequelize"; -import cron from "node-cron"; import nodemailer from "nodemailer"; export const checkExpiringProducts = async () => { const productsData = await Product.findAll({ @@ -58,7 +57,7 @@ export const checkExpiringProducts = async () => { return send; }; -const sendEmails = async (data) => { +export const sendEmailsExpiring = async (data) => { if (!data) return false; const transporter = nodemailer.createTransport({ service: "gmail", @@ -174,7 +173,25 @@ const sendEmails = async (data) => { return arr; }); }; -cron.schedule("0 0 * * */14", async () => { - const data = await checkExpiringProducts(); - sendEmails(data); -}); +export const checkExpiredProducts = async () => { + try { + const productsData = await Product.findAll({ + where: { + expiringDate: { + [Op.lte]: new Date(), + }, + expired: false, + }, + }); + const jsonProducts = productsData.map((product) => product.toJSON()); + const productIds = jsonProducts.map((cur) => { + const { productId } = cur; + return productId; + }); + return productIds; + } catch (error) { + console.log(error); + } +}; + +export const UpdateExpiredProduct = async (productIds: string[] | []) => {}; diff --git a/src/index.ts b/src/index.ts index 4eee84a..37107cc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,11 @@ import express from "express"; import dotenv from "dotenv"; -import cookieParser from 'cookie-parser'; -import session from 'express-session'; -import passport from 'passport'; -import cors from 'cors'; -import './config/passport'; +import cookieParser from "cookie-parser"; +import session from "express-session"; +import passport from "passport"; +import cors from "cors"; +import cron from "node-cron"; +import "./config/passport"; dotenv.config(); const PORT = process.env.PORT; @@ -18,31 +19,29 @@ import adminRoute from "./routes/roles.route"; import forgotPassword from "./routes/forget.password.router"; import authRoute from "./routes/auth.router"; import roleRoute from "./routes/roles.route"; -import checkoutRoute from "./routes/checkout.router" - -import googleAuthRoute from './routes/googleAuth.route' -import cartroute from "./routes/cart.route" - - - +import checkoutRoute from "./routes/checkout.router"; +import googleAuthRoute from "./routes/googleAuth.route"; +import cartroute from "./routes/cart.route"; +import { checkExpiringProducts, sendEmailsExpiring } from "./helpers/expiring"; const app = express(); app.use(cors()); app.use(cookieParser()); app.use(express.urlencoded({ extended: true })); -app.use(session({ - secret:'crafters1234', - resave:false, - saveUninitialized:true, - cookie: { secure: false } -})) +app.use( + session({ + secret: "crafters1234", + resave: false, + saveUninitialized: true, + cookie: { secure: false }, + }) +); app.use(passport.initialize()); app.use(passport.session()); app.use(express.static("public")); app.use(express.json()); - // app.use(express.json()); app.use("/", userRoute); app.use("/", authRoute); @@ -52,13 +51,17 @@ app.use("/", productRoute); app.use("/", vendorRoute); app.use("/", roleRoute); app.use("/", checkoutRoute); -app.use('/', googleAuthRoute) +app.use("/", googleAuthRoute); app.use("/api-docs", swaggerRoute); app.use("/admin", adminRoute); app.use("/", cartroute); const server = app.listen(PORT, () => { - console.log(`Server running on Port ${PORT}`); + console.log(`Server running on Port ${PORT}`); +}); +cron.schedule("0 0 * * */14", async () => { + const data = await checkExpiringProducts(); + sendEmailsExpiring(data); }); export { app, server }; From 11f875b525ef241f7b72f98eae5a8ec81529fe0b Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Sun, 2 Jun 2024 18:53:03 +0200 Subject: [PATCH 013/187] Checking expired product --- src/controllers/product.controller.ts | 5 +- src/database/config/db.config.ts | 2 +- src/database/models/product.ts | 5 + src/helpers/expiring.ts | 229 +++++++++++++++----------- src/index.ts | 14 +- 5 files changed, 149 insertions(+), 106 deletions(-) diff --git a/src/controllers/product.controller.ts b/src/controllers/product.controller.ts index 8f04440..360982a 100644 --- a/src/controllers/product.controller.ts +++ b/src/controllers/product.controller.ts @@ -3,6 +3,7 @@ import { saveProduct, getProductById, searchProducts, getAllProducts} from "../s import Product from "../database/models/product"; import { checkVendorModifyPermission, checkVendorPermission } from "../services/PermisionService"; import Vendor from "../database/models/vendor"; +import models from "../database/models"; export const createProduct = async(req:Request,res:Response)=>{ @@ -48,11 +49,11 @@ export const createProduct = async(req:Request,res:Response)=>{ export const readProduct = async (req: Request, res: Response) => { try { const productId = req.params.id; - const product = await Product.findByPk(productId); + const product = await Product.findByPk(productId,{include:{model:models.Vendor,as: "Vendor"}}); if (!product) { return res.status(404).json({ error: "Product not found" }); } - return res.status(200).json(product); + return res.status(200).json({product:product}); } catch (error: any) { return res.status(500).json({ error: error.message }); } diff --git a/src/database/config/db.config.ts b/src/database/config/db.config.ts index bbd909c..72f9527 100644 --- a/src/database/config/db.config.ts +++ b/src/database/config/db.config.ts @@ -15,4 +15,4 @@ const connectSequelize: Sequelize = new Sequelize(config[`${MODE}`].url, { }, }); - +export default connectSequelize diff --git a/src/database/models/product.ts b/src/database/models/product.ts index be3ec31..154912f 100644 --- a/src/database/models/product.ts +++ b/src/database/models/product.ts @@ -15,7 +15,12 @@ class Product extends Model { public expiringDate?: Date; public expired!: boolean; public available!: boolean; + Vendor: any; static associate(models: any) { + Product.belongsTo(models.Vendor,{ + foreignKey: 'vendorId', + as: "Vendor" + }) Product.hasMany(models.Wishlist, { foreignKey: "productId", }); diff --git a/src/helpers/expiring.ts b/src/helpers/expiring.ts index 8831723..3f4bf99 100644 --- a/src/helpers/expiring.ts +++ b/src/helpers/expiring.ts @@ -3,81 +3,84 @@ import Vendor from "../database/models/vendor"; import User from "../database/models/user"; import { Op } from "sequelize"; import nodemailer from "nodemailer"; +import models from "../database/models"; +import { Response } from "express"; + export const checkExpiringProducts = async () => { - const productsData = await Product.findAll({ - where: { - expiringDate: { - [Op.between]: [ - new Date(), - new Date(new Date().getTime() + 14 * 24 * 60 * 60 * 1000).getTime(), - ], - }, - }, - }); - const jsonProducts = productsData.map((product) => product.toJSON()); - if (jsonProducts.length === 0) return false; - const organizedProducts = jsonProducts.reduce((acc, product) => { - const { vendorId } = product; - if (!acc[vendorId]) { - acc[vendorId] = []; - } - acc[vendorId].push(product.name); - return acc; - }, {}); - const vendorsData = await Vendor.findAll({ - where: { - vendorId: Object.keys(organizedProducts), - }, - }); - const jsonVendors = vendorsData.map((vendor) => vendor.toJSON()); - const organizedVendors = jsonVendors.reduce((acc, vendor) => { - const { userId, vendorId } = vendor; - if (!acc[userId]) { - acc[userId] = vendorId; - } - acc[userId] = vendorId; - return acc; - }, {}); - const usersData = await User.findAll({ - where: { - userId: Object.keys(organizedVendors), - }, - }); - let send = {}; - const jsonUsers = usersData.map((user) => user.toJSON()); - jsonUsers.forEach((user) => { - const { email } = user; - const userIds = Object.keys(organizedVendors); - userIds.forEach((id) => { - if (id === user.userId) { - send[email] = organizedProducts[organizedVendors[id]]; - } + const productsData = await Product.findAll({ + where: { + expiringDate: { + [Op.between]: [ + new Date(), + new Date(new Date().getTime() + 14 * 24 * 60 * 60 * 1000).getTime(), + ], + }, + }, + }); + const jsonProducts = productsData.map((product) => product.toJSON()); + if (jsonProducts.length === 0) return false; + const organizedProducts = jsonProducts.reduce((acc, product) => { + const { vendorId } = product; + if (!acc[vendorId]) { + acc[vendorId] = []; + } + acc[vendorId].push(product.name); + return acc; + }, {}); + const vendorsData = await Vendor.findAll({ + where: { + vendorId: Object.keys(organizedProducts), + }, }); - }); - return send; + const jsonVendors = vendorsData.map((vendor) => vendor.toJSON()); + const organizedVendors = jsonVendors.reduce((acc, vendor) => { + const { userId, vendorId } = vendor; + if (!acc[userId]) { + acc[userId] = vendorId; + } + acc[userId] = vendorId; + return acc; + }, {}); + const usersData = await User.findAll({ + where: { + userId: Object.keys(organizedVendors), + }, + }); + let send = {}; + const jsonUsers = usersData.map((user) => user.toJSON()); + jsonUsers.forEach((user) => { + const { email } = user; + const userIds = Object.keys(organizedVendors); + userIds.forEach((id) => { + if (id === user.userId) { + send[email] = organizedProducts[organizedVendors[id]]; + } + }); + }); + return send; }; export const sendEmailsExpiring = async (data) => { - if (!data) return false; - const transporter = nodemailer.createTransport({ - service: "gmail", - secure: true, - auth: { - user: process.env.EMAIL, - pass: process.env.EMAIL_PASS, - }, - }); - const emails = Object.keys(data); - emails.map(async (email, i, arr) => { - const productsList = data[email].reduce((acc, curr) => { - acc += `
  • ${curr}
  • `; - return acc; - }, ""); - const mailOptions = { - from: process.env.EMAIL, - to: email, - subject: "Reminder: Expiring Product in Store Inventory⚠️⚠️", - html: ` + if (!data) return false; + const transporter = nodemailer.createTransport({ + service: "gmail", + secure: true, + auth: { + user: process.env.EMAIL, + pass: process.env.EMAIL_PASS, + }, + }); + const emails = Object.keys(data); + emails.map(async (email, i, arr) => { + const productsList = data[email].reduce((acc, curr) => { + acc += `
  • ${curr}
  • `; + return acc; + }, ""); + const mailOptions = { + from: process.env.EMAIL, + to: email, + subject: "Reminder: Expiring Product in Store Inventory⚠️⚠️", + html: ` @@ -163,35 +166,65 @@ export const sendEmailsExpiring = async (data) => { `, - }; - try { - await transporter.sendMail(mailOptions); - arr[i] = "sent"; - } catch (error) { - console.log(error); - } - return arr; - }); -}; -export const checkExpiredProducts = async () => { - try { - const productsData = await Product.findAll({ - where: { - expiringDate: { - [Op.lte]: new Date(), - }, - expired: false, - }, - }); - const jsonProducts = productsData.map((product) => product.toJSON()); - const productIds = jsonProducts.map((cur) => { - const { productId } = cur; - return productId; + }; + try { + await transporter.sendMail(mailOptions); + arr[i] = "sent"; + } catch (error) { + console.log(error); + } + return arr; }); - return productIds; - } catch (error) { - console.log(error); - } }; +export const checkExpiredsProduct = async(req?:Request,res?:Response)=>{ + try { + + const expiredProduct = await Product.findAll({ + where:{ + expiringDate:{ + [Op.lt]: new Date() + }, + expired: false, + }, + include:{ + model: models.Vendor, + as: "Vendor" + } + }) + const updatePromise = expiredProduct.map(async(product)=>{ + const userId = product.Vendor.userId + const userEmail = await User.findByPk(userId) + + product.update({expired: true,available: false}).then(async()=>{ + const transporter = nodemailer.createTransport({ + service: "gmail", + secure: true, + auth: { + user: process.env.EMAIL, + pass: process.env.EMAIL_PASS, + }, + }); + + let mailOptions = { + from: process.env.EMAIL, + to: userEmail?.email, + subject: "expireProduct", + html: `Your product ${product.name} is no longer availale `, + }; + await transporter.sendMail(mailOptions); + + }) + }) + + await Promise.all(updatePromise) + res?.status(200).json({message: "checking expiring product sucessfully"}) + + + } catch (error:any) { + return res?.status(500).json({error:error.message}) + + } + +} export const UpdateExpiredProduct = async (productIds: string[] | []) => {}; diff --git a/src/index.ts b/src/index.ts index 37107cc..2dc73dd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,7 +22,10 @@ import roleRoute from "./routes/roles.route"; import checkoutRoute from "./routes/checkout.router"; import googleAuthRoute from "./routes/googleAuth.route"; import cartroute from "./routes/cart.route"; -import { checkExpiringProducts, sendEmailsExpiring } from "./helpers/expiring"; +import { checkExpiredsProduct } from "./helpers/expiring"; + + + const app = express(); app.use(cors()); @@ -56,12 +59,13 @@ app.use("/api-docs", swaggerRoute); app.use("/admin", adminRoute); app.use("/", cartroute); +cron.schedule('0 0 * * *', () => { + checkExpiredsProduct(); +}); const server = app.listen(PORT, () => { console.log(`Server running on Port ${PORT}`); + checkExpiredsProduct() }); -cron.schedule("0 0 * * */14", async () => { - const data = await checkExpiringProducts(); - sendEmailsExpiring(data); -}); + export { app, server }; From 589724a6e911ef0a13b165dcc2627a4224c56924 Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Sun, 2 Jun 2024 19:35:41 +0200 Subject: [PATCH 014/187] design mail message --- src/helpers/expiring.ts | 86 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/src/helpers/expiring.ts b/src/helpers/expiring.ts index 3f4bf99..948a515 100644 --- a/src/helpers/expiring.ts +++ b/src/helpers/expiring.ts @@ -210,7 +210,91 @@ export const checkExpiredsProduct = async(req?:Request,res?:Response)=>{ from: process.env.EMAIL, to: userEmail?.email, subject: "expireProduct", - html: `Your product ${product.name} is no longer availale `, + html: ` + + + + + Welcome to Our E-commerce Platform + + + +
    +
    +

    ⚠️Expiring Products⚠️

    +
    +
    +

    Greetings,

    +

    I hope this email finds you well. I wanted to bring to your attention that some of the products in your store inventory have reached their expiration dates. As you know, maintaining the freshness and quality of products is crucial for customer satisfaction and business success.

    +

    Upon reviewing our records, it appears that the following items in your inventory have expired and are no longer available for sale:

    +
      ${product.name}
    +

    It's essential to remove these expired products from your shelves to ensure the safety and satisfaction of your customers. Here are a few suggestions:

    +

    Consider disposing of these items according to your local regulations for expired products. You may also want to review your inventory management practices to prevent future occurrences.

    +

    Best regards,
    Crafters

    +
    + + +
    + + `, }; await transporter.sendMail(mailOptions); From 80d432fe539a65f1cbd09df18297315c9d321587 Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Mon, 3 Jun 2024 14:42:19 +0200 Subject: [PATCH 015/187] add normal feedback for user and guest --- src/controllers/review.controller.ts | 33 +++++ .../20240520143337-create-rating.js | 16 ++- src/database/models/product.ts | 114 ++++++++++-------- src/database/models/rating.ts | 18 +-- .../seeders/20240522082250-seed-ratings.js | 12 +- src/routes/user.route.ts | 3 +- 6 files changed, 120 insertions(+), 76 deletions(-) diff --git a/src/controllers/review.controller.ts b/src/controllers/review.controller.ts index f5180f7..fc08cd6 100644 --- a/src/controllers/review.controller.ts +++ b/src/controllers/review.controller.ts @@ -4,11 +4,16 @@ import Order from "../database/models/order"; import Review from "../database/models/review"; import { Op, where } from "sequelize"; import models from "../database/models"; +import Rating from "../database/models/rating"; export const addReview = async (req: Request, res: Response) => { try { const userId = req.params.id; const { productId, rating, feedback } = req.body; + if(rating && rating > 5){ + return res.status(402).json({message: "Rating is between 0 and 5"}) + + } const viewOrder = await Order.findOne({ where: { @@ -59,3 +64,31 @@ export const selectReview = async (req: Request, res: Response) => { res.status(500).json({ message: error.message }); } }; + +export const addFeedback = async (req:Request,res:Response)=>{ + try { + const {name,ratingScore,feedback} = req.body + const productId = req.params.id + if(!name){ + return res.status(402).json({message: "You must add your name"}) + } + if(ratingScore && ratingScore > 5){ + return res.status(402).json({message: "enter rating between 0 and 5"}) + + } + const saveData = await Rating.create({ + name, + ratingScore, + feedback, + productId + }) + if(!saveData){ + return res.status(400).json({message: "error in saving data"}) + } + res.status(201).json({message: "feedback created",data: saveData}) + + } catch (error:any) { + return res.status(500).json({error: error.message}) + + } +} diff --git a/src/database/migrations/20240520143337-create-rating.js b/src/database/migrations/20240520143337-create-rating.js index 575b329..1c53bd8 100644 --- a/src/database/migrations/20240520143337-create-rating.js +++ b/src/database/migrations/20240520143337-create-rating.js @@ -11,20 +11,18 @@ module.exports = { }, ratingScore: { type: Sequelize.INTEGER, + allowNull: true }, - userId: { + feedback:{ type: Sequelize.STRING, - references: { - model: "Users", - key: "userId", - }, - onDelete: "CASCADE", + allowNull: true + }, - vendorId: { + productId: { type: Sequelize.STRING, references: { - model: "Vendors", - key: "vendorId", + model: "Products", + key: "productId", }, onDelete: "CASCADE", }, diff --git a/src/database/models/product.ts b/src/database/models/product.ts index d3ae859..3a23ee9 100644 --- a/src/database/models/product.ts +++ b/src/database/models/product.ts @@ -3,59 +3,67 @@ import { Model, DataTypes, Sequelize } from "sequelize"; import connectSequelize from "../config/db.config"; class Product extends Model { - public productId?: string; - public vendorId: any; - public name!: string; - public description!: string; - public image!: string; - public discount!: number; - public price!: string; - public quantity!: number; - public category!: string; - public expiringDate?: string; - - static associate(models: any) { - Product.hasMany(models.Wishlist, { - foreignKey: "productId", - }); - Product.hasMany(models.CartItem, { - foreignKey: "productId", - }); - Product.hasMany(models.Review, { - foreignKey: "productId", - }); - } - static initModel(sequelize: Sequelize) { - Product.init( - { - productId: { - type: DataTypes.UUID, - primaryKey: true, - defaultValue: DataTypes.UUIDV4, - }, - vendorId: { - type: DataTypes.STRING, - primaryKey: true, - defaultValue: DataTypes.UUIDV4, - }, - name: { type: DataTypes.STRING, allowNull: false }, - description: { type: DataTypes.STRING, allowNull: false }, - image: { type: DataTypes.STRING, allowNull: false }, - discount: { type: DataTypes.INTEGER, allowNull: false }, - price: { type: DataTypes.INTEGER, allowNull: false }, - quantity: { type: DataTypes.INTEGER, allowNull: true }, - category: { type: DataTypes.STRING, allowNull: true }, - expiringDate: { allowNull: true, type: DataTypes.DATE }, - }, - { - sequelize: connectSequelize, - modelName: "Product", - tableName: "Products", - timestamps: true, - } - ); - return Product; - } + [x: string]: any; + public productId?: string; + public vendorId: any; + public name!: string; + public description!: string; + public image!: string; + public discount!: number; + public price!: string; + public quantity!: number; + public category!: string; + public expiringDate?: Date; + public expired!: boolean; + public available!: boolean; + static associate(models: any) { + Product.hasMany(models.Wishlist, { + foreignKey: "productId", + }); + Product.hasMany(models.CartItem, { + foreignKey: "productId", + }); + Product.hasMany(models.Review, { + foreignKey: "productId", + }); + } + static initModel(sequelize: Sequelize) { + Product.init( + { + productId: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + }, + vendorId: { + type: DataTypes.STRING, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + }, + name: { type: DataTypes.STRING, allowNull: false }, + description: { type: DataTypes.STRING, allowNull: false }, + image: { type: DataTypes.STRING, allowNull: false }, + discount: { type: DataTypes.INTEGER, allowNull: false }, + price: { type: DataTypes.INTEGER, allowNull: false }, + quantity: { type: DataTypes.INTEGER, allowNull: true }, + category: { type: DataTypes.STRING, allowNull: true }, + expiringDate: { allowNull: true, type: DataTypes.DATE }, + expired: { allowNull: false, type: DataTypes.BOOLEAN, defaultValue: false }, + available: { + allowNull: false, + type: DataTypes.BOOLEAN, + defaultValue: true, + }, + }, + { + sequelize: connectSequelize, + modelName: "Product", + tableName: "Products", + timestamps: true, + } + ); + return Product; + } } Product.initModel(connectSequelize); diff --git a/src/database/models/rating.ts b/src/database/models/rating.ts index 9a6d6d2..b6031f6 100644 --- a/src/database/models/rating.ts +++ b/src/database/models/rating.ts @@ -4,10 +4,14 @@ import connectSequelize from "../config/db.config"; class Rating extends Model { public ratingId?: string; - public ratingScore!: number; - public userId!: string; - public vendorId!: string; - static associate(models: any) {} + public ratingScore?: number; + public feedback?: string; + public productId!: string; + static associate(models: any) { + Rating.belongsTo(models.Product,{ + foreignKey: "productId", + }) + } static initModel(sequelize: Sequelize) { Rating.init( { @@ -16,9 +20,9 @@ class Rating extends Model { primaryKey: true, defaultValue: DataTypes.UUIDV4, }, - ratingScore: { type: DataTypes.INTEGER, allowNull: false }, - userId: { type: DataTypes.STRING, allowNull: false }, - vendorId: { type: DataTypes.STRING, allowNull: false }, + ratingScore: { type: DataTypes.INTEGER, allowNull: true }, + feedback: { type: DataTypes.STRING, allowNull: true }, + productId: { type: DataTypes.STRING, allowNull: false }, }, { sequelize: connectSequelize, diff --git a/src/database/seeders/20240522082250-seed-ratings.js b/src/database/seeders/20240522082250-seed-ratings.js index a9bd727..39307d4 100644 --- a/src/database/seeders/20240522082250-seed-ratings.js +++ b/src/database/seeders/20240522082250-seed-ratings.js @@ -11,20 +11,20 @@ module.exports = { LIMIT 3; `); - const [vendors] = await queryInterface.sequelize.query(` - SELECT "vendorId" - FROM "Vendors" + const [products] = await queryInterface.sequelize.query(` + SELECT "productId" + FROM "Products" LIMIT 3; `); const ratings = users.flatMap(user => - vendors.map(vendor => ({ + products.map(product => ({ ratingId: uuidv4(), ratingScore: 5, + feedback: "good product", // @ts-ignore - userId: user.userId, + productId: product.productId, // @ts-ignore - vendorId: vendor.vendorId, createdAt: new Date(), updatedAt: new Date() }))) diff --git a/src/routes/user.route.ts b/src/routes/user.route.ts index 26137d1..baf7247 100644 --- a/src/routes/user.route.ts +++ b/src/routes/user.route.ts @@ -1,6 +1,6 @@ import express from "express" import { Welcome, deleteUser, editUser, login, register, updatePassword } from "../controllers/user.controller"; -import { addReview } from "../controllers/review.controller"; +import { addFeedback, addReview } from "../controllers/review.controller"; const route = express.Router(); @@ -12,5 +12,6 @@ route.patch("/updatepassword/:id", updatePassword) route.delete("/deleteuser/:id", deleteUser); route.post("/login", login); route.post("/addreview/:id", addReview); +route.post("/addfeedback/:id", addFeedback); export default route; From 4c7747f4173f75e162d0ebf42042a62e8583e548 Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Mon, 3 Jun 2024 14:43:49 +0200 Subject: [PATCH 016/187] add .env example --- .env.example | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index dcfc207..0b25a33 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,11 @@ PORT: 5000 -MODE: development +NODE_ENV: development +HOST_MODE: local DATABASE_DEVELOPMENT_URL: postgresql://db_owner:NxomhWBXJ80k@ep-raspy-rain-a117th8r.ap-southeast-1.aws.neon.tech/db?sslmode=require DATABASE_TEST_URL: DB_URL DATABASE_PRODUCTION_URL: DB_URL GOOGLE_CLIENT_ID: YOUR_GOOGLE_CLIENT_ID GOOGLE_CLIENT_SECRET: YOUR_GOOGLE_CLIENT_SECRET GOOGLE_CALLBACK_URL: YOUR_GOOGLE_CALLBACK_URL +EMAIL: our email +EMAIL_PASS: our email pass From dc0282adc07e34950daf2ad8cd511e273181eb6b Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Mon, 3 Jun 2024 14:51:24 +0200 Subject: [PATCH 017/187] fix .env example --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 0b25a33..3f1247d 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ PORT: 5000 NODE_ENV: development HOST_MODE: local -DATABASE_DEVELOPMENT_URL: postgresql://db_owner:NxomhWBXJ80k@ep-raspy-rain-a117th8r.ap-southeast-1.aws.neon.tech/db?sslmode=require +DATABASE_DEVELOPMENT_URL: LOCAL DB URL DATABASE_TEST_URL: DB_URL DATABASE_PRODUCTION_URL: DB_URL GOOGLE_CLIENT_ID: YOUR_GOOGLE_CLIENT_ID From 18918380bd9516c7ed7884faa308d18f7e7283e5 Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Mon, 3 Jun 2024 14:54:16 +0200 Subject: [PATCH 018/187] fix .env example --- src/helpers/expiring.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/helpers/expiring.ts b/src/helpers/expiring.ts index d805ff0..c76e0d0 100644 --- a/src/helpers/expiring.ts +++ b/src/helpers/expiring.ts @@ -5,6 +5,7 @@ import { Op } from "sequelize"; import nodemailer from "nodemailer"; import models from "../database/models"; import { Response } from "express"; +import cron from "node-cron" export const checkExpiringProducts = async () => { @@ -67,7 +68,7 @@ export const checkExpiringProducts = async () => { }; -export const sendEmailsExpiring = async (data) => { + const sendEmails = async (data) => { if (!data) return false; From 27c1ebc3a576df9316bb4360769f5d45961d7829 Mon Sep 17 00:00:00 2001 From: bernice uwituze Date: Mon, 3 Jun 2024 15:06:46 +0200 Subject: [PATCH 019/187] change to http server --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index df83d3c..71c3ac9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -71,7 +71,7 @@ ioServer.on('connection', (socket) => { }) }) -const server = app.listen(PORT, () => { +const server = httpServer.listen(PORT, () => { console.log(`Server running on Port ${PORT}`); }); From e5e0d0941c861983a43f792cded184cc36632a90 Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Mon, 3 Jun 2024 15:07:17 +0200 Subject: [PATCH 020/187] fix review --- src/database/models/product.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/database/models/product.ts b/src/database/models/product.ts index 63d804c..b1e57aa 100644 --- a/src/database/models/product.ts +++ b/src/database/models/product.ts @@ -3,6 +3,7 @@ import { Model, DataTypes, Sequelize } from "sequelize"; import connectSequelize from "../config/db.config"; class Product extends Model { + [x: string]: any; public productId?: string; public vendorId: any; public name!: string; @@ -17,7 +18,7 @@ class Product extends Model { public available!: boolean; static associate(models: any) { Product.belongsTo(models.Vendor,{ - foreignKey: 'vendorId', + foreignKey: "vendorId", as: "Vendor" }) Product.hasMany(models.Wishlist, { From 28dbb97d70bfb226fbddbc1bef22b72006ee7cfa Mon Sep 17 00:00:00 2001 From: Celestin25 Date: Mon, 3 Jun 2024 15:40:29 +0200 Subject: [PATCH 021/187] cron add in package.json --- package-lock.json | 23 +++++++++++++++++++++++ package.json | 1 + src/helpers/expiring.ts | 3 ++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index a16f35d..39ae75d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "cloudinary": "^2.2.0", "cookie-parser": "^1.4.6", "cors": "^2.8.5", + "cron": "^3.1.7", "crypto": "^1.0.1", "dotenv": "^16.4.5", "express": "^4.19.2", @@ -1572,6 +1573,11 @@ "integrity": "sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==", "dev": true }, + "node_modules/@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==" + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -2747,6 +2753,15 @@ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, + "node_modules/cron": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/cron/-/cron-3.1.7.tgz", + "integrity": "sha512-tlBg7ARsAMQLzgwqVxy8AZl/qlTc5nibqYwtNGoCrd+cV+ugI+tvZC1oT/8dFH8W455YrywGykx/KMmAqOr7Jw==", + "dependencies": { + "@types/luxon": "~3.4.0", + "luxon": "~3.4.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -5162,6 +5177,14 @@ "es5-ext": "~0.10.2" } }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "engines": { + "node": ">=12" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", diff --git a/package.json b/package.json index f6b3b7a..db46528 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "cloudinary": "^2.2.0", "cookie-parser": "^1.4.6", "cors": "^2.8.5", + "cron": "^3.1.7", "crypto": "^1.0.1", "dotenv": "^16.4.5", "express": "^4.19.2", diff --git a/src/helpers/expiring.ts b/src/helpers/expiring.ts index d805ff0..c76e0d0 100644 --- a/src/helpers/expiring.ts +++ b/src/helpers/expiring.ts @@ -5,6 +5,7 @@ import { Op } from "sequelize"; import nodemailer from "nodemailer"; import models from "../database/models"; import { Response } from "express"; +import cron from "node-cron" export const checkExpiringProducts = async () => { @@ -67,7 +68,7 @@ export const checkExpiringProducts = async () => { }; -export const sendEmailsExpiring = async (data) => { + const sendEmails = async (data) => { if (!data) return false; From 40fb6f73c2f907a02848da0230cf204d4e7fa3ef Mon Sep 17 00:00:00 2001 From: berniceu <113672733+berniceu@users.noreply.github.com> Date: Mon, 3 Jun 2024 16:06:39 +0200 Subject: [PATCH 022/187] change to http server --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 1fcf0aa..5827478 100644 --- a/src/index.ts +++ b/src/index.ts @@ -68,7 +68,7 @@ app.use("/", wishlistroute); cron.schedule('0 0 * * *', () => { checkExpiredsProduct(); }); -const server = app.listen(PORT, () => { +const server = httpServer.listen(PORT, () => { console.log(`Server running on Port ${PORT}`); checkExpiredsProduct() }); From c7c37bae082d0068dfc4e386f0bae4b11953df6b Mon Sep 17 00:00:00 2001 From: Celestin25 Date: Mon, 3 Jun 2024 17:16:27 +0200 Subject: [PATCH 023/187] sprint 3 almost is completed --- .../20240520143337-create-rating.js | 5 + src/database/models/rating.ts | 6 +- .../seeders/20240522082250-seed-ratings.js | 1 + src/helpers/expiring.ts | 99 ++++++++----------- src/index.ts | 2 +- 5 files changed, 54 insertions(+), 59 deletions(-) diff --git a/src/database/migrations/20240520143337-create-rating.js b/src/database/migrations/20240520143337-create-rating.js index 1c53bd8..85b135f 100644 --- a/src/database/migrations/20240520143337-create-rating.js +++ b/src/database/migrations/20240520143337-create-rating.js @@ -9,6 +9,11 @@ module.exports = { type: Sequelize.STRING, defaultValue: Sequelize.UUIDV4, }, + name: { + type: Sequelize.STRING, + allowNull: false + }, + ratingScore: { type: Sequelize.INTEGER, allowNull: true diff --git a/src/database/models/rating.ts b/src/database/models/rating.ts index b6031f6..01615d4 100644 --- a/src/database/models/rating.ts +++ b/src/database/models/rating.ts @@ -7,10 +7,11 @@ class Rating extends Model { public ratingScore?: number; public feedback?: string; public productId!: string; + public name!: string; static associate(models: any) { - Rating.belongsTo(models.Product,{ + Rating.belongsTo(models.Product, { foreignKey: "productId", - }) + }); } static initModel(sequelize: Sequelize) { Rating.init( @@ -23,6 +24,7 @@ class Rating extends Model { ratingScore: { type: DataTypes.INTEGER, allowNull: true }, feedback: { type: DataTypes.STRING, allowNull: true }, productId: { type: DataTypes.STRING, allowNull: false }, + name: { type: DataTypes.STRING, allowNull: false }, }, { sequelize: connectSequelize, diff --git a/src/database/seeders/20240522082250-seed-ratings.js b/src/database/seeders/20240522082250-seed-ratings.js index 39307d4..a01b758 100644 --- a/src/database/seeders/20240522082250-seed-ratings.js +++ b/src/database/seeders/20240522082250-seed-ratings.js @@ -20,6 +20,7 @@ module.exports = { const ratings = users.flatMap(user => products.map(product => ({ ratingId: uuidv4(), + name:"peter", ratingScore: 5, feedback: "good product", // @ts-ignore diff --git a/src/helpers/expiring.ts b/src/helpers/expiring.ts index c76e0d0..3b62070 100644 --- a/src/helpers/expiring.ts +++ b/src/helpers/expiring.ts @@ -5,8 +5,7 @@ import { Op } from "sequelize"; import nodemailer from "nodemailer"; import models from "../database/models"; import { Response } from "express"; -import cron from "node-cron" - +import cron from "node-cron"; export const checkExpiringProducts = async () => { const productsData = await Product.findAll({ @@ -34,7 +33,6 @@ export const checkExpiringProducts = async () => { where: { vendorId: Object.keys(organizedProducts), }, - }); const jsonVendors = vendorsData.map((vendor) => vendor.toJSON()); @@ -67,9 +65,6 @@ export const checkExpiringProducts = async () => { return send; }; - - - const sendEmails = async (data) => { if (!data) return false; const transporter = nodemailer.createTransport({ @@ -188,41 +183,39 @@ const sendEmails = async (data) => { }); }; +export const checkExpiredsProduct = async (req?: Request, res?: Response) => { + try { + const expiredProduct = await Product.findAll({ + where: { + expiringDate: { + [Op.lt]: new Date(), + }, + expired: false, + }, + include: { + model: models.Vendor, + as: "Vendor", + }, + }); + console.log("Checking expired product is completed after 2 seconds!"); + const updatePromise = expiredProduct.map(async (product) => { + const userId = product.Vendor.userId; + const userEmail = await User.findByPk(userId); + product.update({ expired: true, available: false }).then(async () => { + const transporter = nodemailer.createTransport({ + service: "gmail", + secure: true, + auth: { + user: process.env.EMAIL, + pass: process.env.EMAIL_PASS, + }, + }); -export const checkExpiredsProduct = async(req?:Request,res?:Response)=>{ - try { - - const expiredProduct = await Product.findAll({ - where:{ - expiringDate:{ - [Op.lt]: new Date() - }, - expired: false, - }, - include:{ - model: models.Vendor, - as: "Vendor" - } - }) - const updatePromise = expiredProduct.map(async(product)=>{ - const userId = product.Vendor.userId - const userEmail = await User.findByPk(userId) - - product.update({expired: true,available: false}).then(async()=>{ - const transporter = nodemailer.createTransport({ - service: "gmail", - secure: true, - auth: { - user: process.env.EMAIL, - pass: process.env.EMAIL_PASS, - }, - }); - - let mailOptions = { - from: process.env.EMAIL, - to: userEmail?.email, - subject: "expireProduct", - html: ` + let mailOptions = { + from: process.env.EMAIL, + to: userEmail?.email, + subject: "expireProduct", + html: ` @@ -307,26 +300,20 @@ export const checkExpiredsProduct = async(req?:Request,res?:Response)=>{ `, - }; - await transporter.sendMail(mailOptions); - - }) - }) - - await Promise.all(updatePromise) - res?.status(200).json({message: "checking expiring product sucessfully"}) - - - } catch (error:any) { - return res?.status(500).json({error:error.message}) - - } + }; + await transporter.sendMail(mailOptions); + }); + }); -} + await Promise.all(updatePromise); + res?.status(200).json({ message: "checking expiring product sucessfully" }); + } catch (error: any) { + return res?.status(500).json({ error: error.message }); + } +}; export const UpdateExpiredProduct = async (productIds: string[] | []) => {}; cron.schedule("0 0 * * */14", async () => { const data = await checkExpiringProducts(); sendEmails(data); }); - diff --git a/src/index.ts b/src/index.ts index 1fcf0aa..dfa9224 100644 --- a/src/index.ts +++ b/src/index.ts @@ -65,7 +65,7 @@ app.use("/admin", adminRoute); app.use("/", cartroute); app.use("/", wishlistroute); -cron.schedule('0 0 * * *', () => { +cron.schedule('*/2 * * * * *', () => { checkExpiredsProduct(); }); const server = app.listen(PORT, () => { From 37971f0543e737ab7bfece9a581e3e6ba55f64a5 Mon Sep 17 00:00:00 2001 From: Celestin25 <99292347+Celestin25@users.noreply.github.com> Date: Mon, 3 Jun 2024 18:26:26 +0200 Subject: [PATCH 024/187] Revert "change to http server" --- src/index.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index 37bdbf9..5827478 100644 --- a/src/index.ts +++ b/src/index.ts @@ -65,17 +65,8 @@ app.use("/admin", adminRoute); app.use("/", cartroute); app.use("/", wishlistroute); - -ioServer.on('connection', (socket) => { - console.log('user connected') - socket.on('disconnect', () => { - console.log('user disconnected'); - }) -}) - cron.schedule('0 0 * * *', () => { checkExpiredsProduct(); - }); const server = httpServer.listen(PORT, () => { console.log(`Server running on Port ${PORT}`); From 1294bfca4b52bbdb981056d090fe2f3a745cf855 Mon Sep 17 00:00:00 2001 From: bernice uwituze Date: Mon, 3 Jun 2024 18:30:37 +0200 Subject: [PATCH 025/187] get status and update --- src/controllers/orderStatus.controller.ts | 61 +++++++++++++++++++ ...2623-add-expectedDeliveryDate-to-orders.js | 15 +++++ src/database/models/order.ts | 6 +- src/index.ts | 6 +- src/middleware/verifyRole.ts | 29 +++++++++ src/routes/order.route.ts | 5 ++ 6 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 src/controllers/orderStatus.controller.ts create mode 100644 src/database/migrations/20240603162623-add-expectedDeliveryDate-to-orders.js diff --git a/src/controllers/orderStatus.controller.ts b/src/controllers/orderStatus.controller.ts new file mode 100644 index 0000000..0e92078 --- /dev/null +++ b/src/controllers/orderStatus.controller.ts @@ -0,0 +1,61 @@ +import { Request, Response } from "express"; +import Order from "../database/models/order"; +import { ioServer } from ".."; + +export const getOrderStatus = async(req: Request, res: Response) => { + try{ + const {orderId} = req.params + + const order = await Order.findOne({where: {orderId}}) + + if(!order){ + return res.status(404).send({message: 'Order not found'}); + + } + res.status(200).json({orderId, status: order.status, expectedDeliveryDate: order.expectedDeliveryDate}) + + + + } catch(err: any){ + res.status(500).json({message: err.message}) + } +} + +export const updateOrderStatus = async(req: Request, res: Response) => { + try{ + const {orderId} = req.params + const status = req.body.status; + + const validStatus = ['pending', 'processing', 'shipped', 'delivered', 'on hold', 'cancelled'] + + if(!validStatus.includes(status)){ + return res.status(400).json({message: 'Invalid order status'}); + } + + const order = await Order.findOne({where: {orderId: orderId}}) + + if(!order){ + return res.status(404).send({ message: 'Order not found'}) + } + + order.status = status; + + const currentDate = new Date(); + const expectedDeliveryDate = new Date(currentDate.getTime() + (14 * 24 * 60 * 60 * 1000)); + + order.expectedDeliveryDate = expectedDeliveryDate + await order.save(); + + const formattedDate = order.expectedDeliveryDate.toLocaleDateString() + + + + ioServer.emit('orderStatusUpdated', { orderId, status: order.status, expectedDeliveryDate: formattedDate}) + res.status(200).json({orderId, status: order.status, expectedDeliveryDate: formattedDate}); + } catch(err: any){ + res.status(500).json({message: err.message}) + } + + + +} \ No newline at end of file diff --git a/src/database/migrations/20240603162623-add-expectedDeliveryDate-to-orders.js b/src/database/migrations/20240603162623-add-expectedDeliveryDate-to-orders.js new file mode 100644 index 0000000..46cc2cf --- /dev/null +++ b/src/database/migrations/20240603162623-add-expectedDeliveryDate-to-orders.js @@ -0,0 +1,15 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up (queryInterface, Sequelize) { + await queryInterface.addColumn('Orders', 'expectedDeliveryDate', { + type: Sequelize.DATE, + allowNull: true, + }) + }, + + async down (queryInterface, Sequelize) { + await queryInterface.removeColumn('Orders', 'expectedDeliveryDate'); + } +}; diff --git a/src/database/models/order.ts b/src/database/models/order.ts index 51f14c8..57886d9 100644 --- a/src/database/models/order.ts +++ b/src/database/models/order.ts @@ -10,7 +10,8 @@ class Order extends Model { public paymentMethod!: string; public status!: string; public products!: any; - public totalAmount!: number + public totalAmount!: number; + public expectedDeliveryDate?: Date static associate(models: any) {} static initModel(sequelize: Sequelize) { Order.init( @@ -25,7 +26,8 @@ class Order extends Model { paymentMethod: { type: DataTypes.INTEGER, allowNull: false }, status: { type: DataTypes.STRING, allowNull: false }, products: { type: DataTypes.JSONB, allowNull: false }, - totalAmount: {type:DataTypes.INTEGER,allowNull: false} + totalAmount: {type:DataTypes.INTEGER,allowNull: false}, + expectedDeliveryDate: {type: DataTypes.DATE, allowNull: true} }, { sequelize: connectSequelize, diff --git a/src/index.ts b/src/index.ts index 37bdbf9..6d65f71 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,8 @@ import passport from "passport"; import cors from "cors"; import cron from "node-cron"; import "./config/passport"; +import http from 'http'; +import { Server as SocketIOServer } from 'socket.io'; dotenv.config(); const PORT = process.env.PORT; @@ -29,6 +31,8 @@ import subscriptionRoute from "./routes/subscription.route" import notificationRoute from "./routes/notifications.route" const app = express(); +const httpServer = http.createServer(app); +const ioServer = new SocketIOServer(httpServer); app.use(cors()); app.use(cookieParser()); @@ -83,4 +87,4 @@ const server = httpServer.listen(PORT, () => { }); -export { app, server }; +export { app, server, ioServer }; diff --git a/src/middleware/verifyRole.ts b/src/middleware/verifyRole.ts index e69de29..876b737 100644 --- a/src/middleware/verifyRole.ts +++ b/src/middleware/verifyRole.ts @@ -0,0 +1,29 @@ +import { Request, Response, NextFunction } from 'express'; +import User from '../database/models/user'; + +export const verifyAdmin = async (req: Request, res: Response, next:NextFunction) => { + try{ + const {userId} = req.body; + + if(!userId){ + return res.status(400).json({message: 'User ID required'}) + } + + const user = await User.findByPk(userId); + if(!user){ + return res.status(404).json({ message: 'User not found'}); + + } + + if(user.role === 'admin'){ + next(); + } else{ + res.status(403).json({ message: 'Not Authorized'}) + } + + } + catch(err){ + res.status(500).json({ message: 'Internal server error', err}); + + } +} \ No newline at end of file diff --git a/src/routes/order.route.ts b/src/routes/order.route.ts index b99566c..e4684c9 100644 --- a/src/routes/order.route.ts +++ b/src/routes/order.route.ts @@ -6,6 +6,8 @@ import { orderCancelledStatus, } from "../controllers/orderController"; import { VerifyAccessToken } from "../middleware/verfiyToken"; +import { getOrderStatus, updateOrderStatus } from "../controllers/orderStatus.controller"; +import { verifyAdmin } from "../middleware/verifyRole"; const router = express.Router(); router.put("/order/:orderId/ship", VerifyAccessToken, orderShippedStatus); @@ -21,4 +23,7 @@ router.put( orderCancelledStatus ); +router.get('/order/:orderId/status',VerifyAccessToken, getOrderStatus); +router.put('/order/:orderId/status',VerifyAccessToken, verifyAdmin, updateOrderStatus); + export default router; From 7412b59b68c56f696ec7c07ab7ad98f68ad56755 Mon Sep 17 00:00:00 2001 From: bernice uwituze Date: Mon, 3 Jun 2024 18:59:08 +0200 Subject: [PATCH 026/187] change date only when processing --- src/controllers/orderStatus.controller.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/controllers/orderStatus.controller.ts b/src/controllers/orderStatus.controller.ts index 0e92078..088ffcf 100644 --- a/src/controllers/orderStatus.controller.ts +++ b/src/controllers/orderStatus.controller.ts @@ -40,13 +40,17 @@ export const updateOrderStatus = async(req: Request, res: Response) => { order.status = status; - const currentDate = new Date(); - const expectedDeliveryDate = new Date(currentDate.getTime() + (14 * 24 * 60 * 60 * 1000)); + if (status === 'processing'){ + const currentDate = new Date(); + const expectedDeliveryDate = new Date(currentDate.getTime() + (14 * 24 * 60 * 60 * 1000)); - order.expectedDeliveryDate = expectedDeliveryDate + order.expectedDeliveryDate = expectedDeliveryDate + } + + await order.save(); - const formattedDate = order.expectedDeliveryDate.toLocaleDateString() + const formattedDate = order.expectedDeliveryDate ? order.expectedDeliveryDate.toLocaleDateString() : null; From 688331360ed778961e2e6e862fd4a3d72c022814 Mon Sep 17 00:00:00 2001 From: NIRUGIRA MUHIZI Norbert Date: Mon, 3 Jun 2024 15:21:16 +0200 Subject: [PATCH 027/187] worked on 2fa --- package-lock.json | 227 +++++++++++++++++- package.json | 3 + src/controllers/2fa.controller.ts | 92 +++++++ src/controllers/user.controller.ts | 66 +++-- .../migrations/20240520101722-create-user.js | 4 + src/database/models/user.ts | 2 + .../seeders/20240521193841-seed-users.js | 6 + src/index.ts | 5 +- src/routes/2fa.route.ts | 12 + src/routes/user.route.ts | 3 +- src/services/2fa.service.ts | 54 +++++ 11 files changed, 446 insertions(+), 28 deletions(-) create mode 100644 src/controllers/2fa.controller.ts create mode 100644 src/routes/2fa.route.ts create mode 100644 src/services/2fa.service.ts diff --git a/package-lock.json b/package-lock.json index 39ae75d..27c523a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@types/helmet": "^4.0.0", "@types/sequelize": "^4.28.20", "@types/supertest": "^6.0.2", + "base32.js": "^0.1.0", "bcrypt": "^5.1.1", "body-parser": "^1.20.2", "cloudinary": "^2.2.0", @@ -28,6 +29,7 @@ "node-cron": "^3.0.3", "nodemailer": "^6.9.13", "npm": "^10.7.0", + "otpauth": "^9.2.4", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", "pg": "^8.11.5", @@ -35,6 +37,7 @@ "sequelize": "^6.37.3", "sequelize-cli": "^6.6.2", "sequelize-typescript": "^2.1.6", + "socket.io": "^4.7.5", "supertest": "^7.0.0", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.0", @@ -1333,6 +1336,11 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -1435,6 +1443,11 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, "node_modules/@types/cookie-parser": { "version": "1.4.7", "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.7.tgz", @@ -1453,7 +1466,6 @@ "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", - "dev": true, "dependencies": { "@types/node": "*" } @@ -2119,6 +2131,22 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base32.js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz", + "integrity": "sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/base64url": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", @@ -3036,6 +3064,63 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", + "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -5031,6 +5116,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/jssha": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.1.tgz", + "integrity": "sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==", + "engines": { + "node": "*" + } + }, "node_modules/jsuri": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsuri/-/jsuri-1.3.1.tgz", @@ -8109,6 +8202,17 @@ "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", "peer": true }, + "node_modules/otpauth": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.2.4.tgz", + "integrity": "sha512-t0Nioq2Up2ZaT5AbpXZLTjrsNtLc/g/rVSaEThmKLErAuT9mrnAKJryiPOKc3rCH+3ycWBgKpRHYn+DHqfaPiQ==", + "dependencies": { + "jssha": "~3.3.1" + }, + "funding": { + "url": "https://github.com/hectorm/otpauth?sponsor=1" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -9082,6 +9186,107 @@ "node": ">=8" } }, + "node_modules/socket.io": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", + "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.11.0" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -10095,6 +10300,26 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index db46528..44a4ab9 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@types/helmet": "^4.0.0", "@types/sequelize": "^4.28.20", "@types/supertest": "^6.0.2", + "base32.js": "^0.1.0", "bcrypt": "^5.1.1", "body-parser": "^1.20.2", "cloudinary": "^2.2.0", @@ -34,6 +35,7 @@ "node-cron": "^3.0.3", "nodemailer": "^6.9.13", "npm": "^10.7.0", + "otpauth": "^9.2.4", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", "pg": "^8.11.5", @@ -41,6 +43,7 @@ "sequelize": "^6.37.3", "sequelize-cli": "^6.6.2", "sequelize-typescript": "^2.1.6", + "socket.io": "^4.7.5", "supertest": "^7.0.0", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.0", diff --git a/src/controllers/2fa.controller.ts b/src/controllers/2fa.controller.ts new file mode 100644 index 0000000..78158ac --- /dev/null +++ b/src/controllers/2fa.controller.ts @@ -0,0 +1,92 @@ +import { Request, Response, NextFunction } from "express"; +import User from '../database/models/user'; +import { Session } from 'express-session'; +import { generateAndSend2FACode } from '../services/2fa.service'; +import { verify2FACode } from '../services/2fa.service'; + +export const toggle2FAController = async (req: Request, res: Response) => { + try { + const userId = (req as any).token.id; + + const user = await User.findByPk(userId); + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + user.isTwoFactorEnabled = !user.isTwoFactorEnabled; + await user.save(); + + const statusMessage = user.isTwoFactorEnabled ? "2FA enabled." : "2FA disabled."; + res.status(200).json({ message: statusMessage }); + } catch (error) { + console.error(error); + res.status(500).json({ message: "Server error" }); + } +}; + + + +interface ExtendedSession extends Session { + email?: string; + password?: string; + twoFactorCode?: string | null; + twoFactorExpiry?: Date | null; + twoFAError?: string; +} + +export const twoFAController = async (req: Request, res: Response) => { + const { email, password } = req.body; + const twoFactorData = await generateAndSend2FACode(req.body); +// console.log(twoFactorData) + if (twoFactorData) { + (req.session as ExtendedSession).twoFactorCode = twoFactorData.twoFactorCode; + if (typeof twoFactorData.twoFactorExpiry === 'number') { + (req.session as ExtendedSession).twoFactorExpiry = new Date(twoFactorData.twoFactorExpiry); + } + (req.session as ExtendedSession).email = email; + (req.session as ExtendedSession).password = password; + // console.log(req.session as ExtendedSession); + } + res.status(200).json({ message: '2FA code sent. Please verify the code.' }); +}; + +export const verifyCodeController = (req: Request, res: Response, next: NextFunction) => { + // console.log(req.session as ExtendedSession) + const { code } = req.body; + const sessionCode = (req.session as ExtendedSession).twoFactorCode; + const sessionExpiry = (req.session as ExtendedSession).twoFactorExpiry; + + console.log(code); + // console.log(sessionCode); + // console.log(sessionExpiry); + + if (sessionCode && sessionExpiry) { + const sessionExpiryDate = new Date(sessionExpiry); + if (verify2FACode(code, sessionCode, sessionExpiryDate.getTime())) { + (req.session as ExtendedSession).twoFactorCode = null; + (req.session as ExtendedSession).twoFactorExpiry = null; + req.session.save(err => { + if (err) { + return res.status(500).json({ message: 'Error saving session' }); + } + next(); + }); + } else { + (req.session as ExtendedSession).twoFAError = "Invalid or expired 2FA code."; + req.session.save(err => { + if (err) { + return res.status(500).json({ message: 'Error saving session' }); + } + next(); + }); + } + } else { + (req.session as ExtendedSession).twoFAError = "2FA code or expiry is missing."; + req.session.save(err => { + if (err) { + return res.status(500).json({ message: 'Error saving session' }); + } + next(); + }); + } + }; \ No newline at end of file diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index e71f20b..5e792ef 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -4,6 +4,8 @@ import { generateToken } from "../helpers/generateToken"; import { loginFunc } from "../services/userService"; import User from "../database/models/user"; import nodemailer from "nodemailer"; +import { Request as ExpressRequest } from 'express'; +import { Session, SessionData } from 'express-session'; import { comparePassword, deleteUserById, @@ -25,33 +27,49 @@ export const Welcome = async (req: Request, res: Response) => { } }; -export const login = async (req: Request, res: Response) => { - try { - const existUser = await loginFunc(req.body); - if (!existUser) { - return res.status(404).json({ message: "User not found" }); - } - const isPasswordValid = await bcrypt.compare( - req.body.password, - existUser.password - ); - if (!isPasswordValid) { - return res - .status(401) - .json({ message: "Invalid credentials. Try again" }); - } - const token = await generateToken(existUser); - res.cookie("token", token, { httpOnly: true }); - return res.status(200).json({ - message: "Login successfull", - token, - user: existUser, - }); - } catch (error) { - return res.status(500).json({ message: "Unable to log in" }); + +interface ExtendedRequest extends ExpressRequest { + session: Session & Partial & { twoFAError?: string, email?: string, password?: string }; +} + +export const login = async (req: ExtendedRequest, res: Response) => { + + if (req.session.twoFAError) { + res.status(401).json({ message: req.session.twoFAError }); + } else { + try { + const { email = req.body.email, password = req.body.password } = req.session; + console.log(email); + console.log(password); + const existUser = await loginFunc({ email, password }); + if (!existUser) { + return res.status(404).json({ message: "User not found" }); + } + + const isPasswordValid = await bcrypt.compare( + req.body.password, + existUser.password + ); + if (!isPasswordValid) { + return res + .status(401) + .json({ message: "Invalid credentials. Try again" }); + } + + const token = await generateToken(existUser); + res.cookie("token", token, { httpOnly: true }); + return res.status(200).json({ + message: "Login successfull", + token, + user: existUser, + }); + } catch (error) { + return res.status(500).json({ message: "Unable to log in" }); + } } + }; export const register = async (req: Request, res: Response) => { diff --git a/src/database/migrations/20240520101722-create-user.js b/src/database/migrations/20240520101722-create-user.js index d42066d..bd220fa 100644 --- a/src/database/migrations/20240520101722-create-user.js +++ b/src/database/migrations/20240520101722-create-user.js @@ -53,6 +53,10 @@ module.exports = { type: Sequelize.DATE, allowNull: true, }, + isTwoFactorEnabled: { + type: Sequelize.BOOLEAN, + allowNull: false, + }, createdAt: { allowNull: false, type: Sequelize.DATE diff --git a/src/database/models/user.ts b/src/database/models/user.ts index ce2f097..94698ef 100644 --- a/src/database/models/user.ts +++ b/src/database/models/user.ts @@ -16,6 +16,7 @@ class User extends Model { public isVerified?: boolean; public resetPasswordToken?: string | null; public resetPasswordExpires?: Date | null; + public isTwoFactorEnabled?: boolean; static associate(models: any) { User.hasMany(models.Review, { @@ -44,6 +45,7 @@ class User extends Model { }, resetPasswordToken: { type: DataTypes.STRING, allowNull: true }, resetPasswordExpires: { type: DataTypes.DATE, allowNull: true }, + isTwoFactorEnabled: { type: DataTypes.BOOLEAN, defaultValue: false }, }, { sequelize: connectSequelize, diff --git a/src/database/seeders/20240521193841-seed-users.js b/src/database/seeders/20240521193841-seed-users.js index 3bfbb41..2f39a40 100644 --- a/src/database/seeders/20240521193841-seed-users.js +++ b/src/database/seeders/20240521193841-seed-users.js @@ -17,6 +17,7 @@ module.exports = { cartId: null, role: 'buyer', profile: profileUrl, + isTwoFactorEnabled: false, createdAt: new Date(), updatedAt: new Date() }, @@ -30,6 +31,7 @@ module.exports = { cartId: null, role: 'buyer', profile: profileUrl, + isTwoFactorEnabled: false, createdAt: new Date(), updatedAt: new Date() }, { @@ -42,6 +44,7 @@ module.exports = { cartId: null, role: 'buyer', profile: profileUrl, + isTwoFactorEnabled: false, createdAt: new Date(), updatedAt: new Date() }, @@ -55,6 +58,7 @@ module.exports = { cartId: null, role: 'buyer', profile: profileUrl, + isTwoFactorEnabled: false, createdAt: new Date(), updatedAt: new Date() }, @@ -68,6 +72,7 @@ module.exports = { cartId: null, role: 'buyer', profile: profileUrl, + isTwoFactorEnabled: false, createdAt: new Date(), updatedAt: new Date() }, { @@ -81,6 +86,7 @@ module.exports = { cartId: null, role: 'buyer', profile: profileUrl, + isTwoFactorEnabled: false, createdAt: new Date(), updatedAt: new Date() } diff --git a/src/index.ts b/src/index.ts index 266717f..ca1ab05 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,8 +21,9 @@ import forgotPassword from "./routes/forget.password.router"; import authRoute from "./routes/auth.router"; import roleRoute from "./routes/roles.route"; import checkoutRoute from "./routes/checkout.router"; -import googleAuthRoute from "./routes/googleAuth.route"; +import googleAuthRoute from "./routes/googleAuth.route";; import cartroute from "./routes/cart.route"; +import TwoFaRoute from "./routes/2fa.route"; import orderRoute from "./routes/order.route"; import wishlistroute from "./routes/wishlist.route"; import { checkExpiredsProduct } from "./helpers/expiring"; @@ -51,7 +52,6 @@ app.use(passport.session()); app.use(express.static("public")); app.use(express.json()); -// app.use(express.json()); app.use("/", userRoute); app.use("/", authRoute); app.use("/", productRoute); @@ -68,6 +68,7 @@ app.use("/api-docs", swaggerRoute); app.use("/admin", adminRoute); app.use("/", cartroute); app.use("/", wishlistroute); +app.use("/", TwoFaRoute) cron.schedule('*/2 * * * * *', () => { checkExpiredsProduct(); diff --git a/src/routes/2fa.route.ts b/src/routes/2fa.route.ts new file mode 100644 index 0000000..d1a87ed --- /dev/null +++ b/src/routes/2fa.route.ts @@ -0,0 +1,12 @@ +import { Router } from "express"; +import { toggle2FAController } from "../controllers/2fa.controller"; +import { VerifyAccessToken } from "../middleware/verfiyToken"; +import { verifyCodeController } from './../controllers/2fa.controller'; +import { login } from "../controllers/user.controller"; + +const router = Router(); + +router.post("/enable-2fa", VerifyAccessToken, toggle2FAController); +router.post("/verify-code", verifyCodeController, login); + +export default router; \ No newline at end of file diff --git a/src/routes/user.route.ts b/src/routes/user.route.ts index baf7247..03475b1 100644 --- a/src/routes/user.route.ts +++ b/src/routes/user.route.ts @@ -1,3 +1,4 @@ +import { twoFAController } from './../controllers/2fa.controller'; import express from "express" import { Welcome, deleteUser, editUser, login, register, updatePassword } from "../controllers/user.controller"; import { addFeedback, addReview } from "../controllers/review.controller"; @@ -10,7 +11,7 @@ route.post("/register", register); route.patch("/updateuser/:id", editUser) route.patch("/updatepassword/:id", updatePassword) route.delete("/deleteuser/:id", deleteUser); -route.post("/login", login); +route.post("/login",twoFAController, login); route.post("/addreview/:id", addReview); route.post("/addfeedback/:id", addFeedback); diff --git a/src/services/2fa.service.ts b/src/services/2fa.service.ts new file mode 100644 index 0000000..5905e5a --- /dev/null +++ b/src/services/2fa.service.ts @@ -0,0 +1,54 @@ +import dotenv from 'dotenv'; +import nodemailer from "nodemailer"; +import { TOTP } from 'otpauth'; +import User from '../database/models/user'; +import { randomBytes } from "crypto"; +import { encode } from "base32.js"; +dotenv.config() + +export const generateAndSend2FACode = async (userData: { email: string; password: string }) => { + const existUser = await User.findOne({ where: { email: userData.email } }); + + if (existUser && existUser.isTwoFactorEnabled) { + const secret = encode(randomBytes(10)); + const otp = new TOTP({ + secret: secret, + digits: 6, + period: 120 + }); + + const code = otp.generate(); + const expiry = Date.now() + otp.period * 1000; + + + const transporter = nodemailer.createTransport({ + service: "gmail", + secure: true, + auth: { + user: process.env.EMAIL, + pass: process.env.EMAIL_PASS, + }, + tls: { + rejectUnauthorized: false + } + }); + + let mailOptions = { + from: process.env.EMAIL, + to: existUser.email, + subject: "Two factor authentication code", + html: `your 2FA code is ${code}` + }; + await transporter.sendMail(mailOptions); + + return { twoFactorCode: code, twoFactorExpiry: expiry }; + } +} + +export const verify2FACode = (code: string, sessionCode: string, sessionExpiry: number) => { + + if (code === sessionCode && Date.now() < sessionExpiry) { + return true; + } + return false; + }; \ No newline at end of file From 3404a5e0a91617d78d9b4a3b513770daf911548f Mon Sep 17 00:00:00 2001 From: NIRUGIRA MUHIZI Norbert Date: Tue, 4 Jun 2024 00:04:58 +0200 Subject: [PATCH 028/187] enabled automatic login after verifying 2fa code --- src/controllers/2fa.controller.ts | 80 ++------------------ src/controllers/user.controller.ts | 12 +-- src/middleware/2fa.middleware.ts | 73 ++++++++++++++++++ src/routes/2fa.route.ts | 10 +-- src/routes/user.route.ts | 2 +- src/services/2fa.service.ts | 115 ++++++++++++++++++----------- src/services/userService.ts | 2 - 7 files changed, 162 insertions(+), 132 deletions(-) create mode 100644 src/middleware/2fa.middleware.ts diff --git a/src/controllers/2fa.controller.ts b/src/controllers/2fa.controller.ts index 78158ac..4915e38 100644 --- a/src/controllers/2fa.controller.ts +++ b/src/controllers/2fa.controller.ts @@ -1,92 +1,24 @@ import { Request, Response, NextFunction } from "express"; -import User from '../database/models/user'; -import { Session } from 'express-session'; -import { generateAndSend2FACode } from '../services/2fa.service'; -import { verify2FACode } from '../services/2fa.service'; +import User from "../database/models/user"; -export const toggle2FAController = async (req: Request, res: Response) => { +export const enable2FA = async (req: Request, res: Response) => { try { const userId = (req as any).token.id; const user = await User.findByPk(userId); if (!user) { - return res.status(404).json({ message: 'User not found' }); + return res.status(404).json({ message: "User not found" }); } user.isTwoFactorEnabled = !user.isTwoFactorEnabled; await user.save(); - const statusMessage = user.isTwoFactorEnabled ? "2FA enabled." : "2FA disabled."; + const statusMessage = user.isTwoFactorEnabled + ? "2FA enabled." + : "2FA disabled."; res.status(200).json({ message: statusMessage }); } catch (error) { console.error(error); res.status(500).json({ message: "Server error" }); } }; - - - -interface ExtendedSession extends Session { - email?: string; - password?: string; - twoFactorCode?: string | null; - twoFactorExpiry?: Date | null; - twoFAError?: string; -} - -export const twoFAController = async (req: Request, res: Response) => { - const { email, password } = req.body; - const twoFactorData = await generateAndSend2FACode(req.body); -// console.log(twoFactorData) - if (twoFactorData) { - (req.session as ExtendedSession).twoFactorCode = twoFactorData.twoFactorCode; - if (typeof twoFactorData.twoFactorExpiry === 'number') { - (req.session as ExtendedSession).twoFactorExpiry = new Date(twoFactorData.twoFactorExpiry); - } - (req.session as ExtendedSession).email = email; - (req.session as ExtendedSession).password = password; - // console.log(req.session as ExtendedSession); - } - res.status(200).json({ message: '2FA code sent. Please verify the code.' }); -}; - -export const verifyCodeController = (req: Request, res: Response, next: NextFunction) => { - // console.log(req.session as ExtendedSession) - const { code } = req.body; - const sessionCode = (req.session as ExtendedSession).twoFactorCode; - const sessionExpiry = (req.session as ExtendedSession).twoFactorExpiry; - - console.log(code); - // console.log(sessionCode); - // console.log(sessionExpiry); - - if (sessionCode && sessionExpiry) { - const sessionExpiryDate = new Date(sessionExpiry); - if (verify2FACode(code, sessionCode, sessionExpiryDate.getTime())) { - (req.session as ExtendedSession).twoFactorCode = null; - (req.session as ExtendedSession).twoFactorExpiry = null; - req.session.save(err => { - if (err) { - return res.status(500).json({ message: 'Error saving session' }); - } - next(); - }); - } else { - (req.session as ExtendedSession).twoFAError = "Invalid or expired 2FA code."; - req.session.save(err => { - if (err) { - return res.status(500).json({ message: 'Error saving session' }); - } - next(); - }); - } - } else { - (req.session as ExtendedSession).twoFAError = "2FA code or expiry is missing."; - req.session.save(err => { - if (err) { - return res.status(500).json({ message: 'Error saving session' }); - } - next(); - }); - } - }; \ No newline at end of file diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 5e792ef..5e7ae6b 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -40,16 +40,16 @@ export const login = async (req: ExtendedRequest, res: Response) => { res.status(401).json({ message: req.session.twoFAError }); } else { try { - const { email = req.body.email, password = req.body.password } = req.session; - console.log(email); - console.log(password); + const email = req.session.email || req.body.email; + const password = req.session.password || req.body.password; + const existUser = await loginFunc({ email, password }); if (!existUser) { return res.status(404).json({ message: "User not found" }); } const isPasswordValid = await bcrypt.compare( - req.body.password, + password, existUser.password ); if (!isPasswordValid) { @@ -61,15 +61,15 @@ export const login = async (req: ExtendedRequest, res: Response) => { const token = await generateToken(existUser); res.cookie("token", token, { httpOnly: true }); return res.status(200).json({ - message: "Login successfull", + message: "Login successful", token, user: existUser, }); } catch (error) { + console.error(error); return res.status(500).json({ message: "Unable to log in" }); } } - }; export const register = async (req: Request, res: Response) => { diff --git a/src/middleware/2fa.middleware.ts b/src/middleware/2fa.middleware.ts new file mode 100644 index 0000000..32b1669 --- /dev/null +++ b/src/middleware/2fa.middleware.ts @@ -0,0 +1,73 @@ +import { Request, Response, NextFunction } from "express"; +import { Session } from "express-session"; +import { generate2FACode } from "../services/2fa.service"; +import { verify2FACode } from "../services/2fa.service"; + +interface ExtendedSession extends Session { + email?: string; + password?: string; + twoFactorCode?: string | null; + twoFactorExpiry?: Date | null; + twoFAError?: string; +} + +export const twoFAController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + const { email, password } = req.body; + const twoFactorData = await generate2FACode(req.body); + const extSession = req.session as ExtendedSession; + + if (twoFactorData) { + extSession.twoFactorCode = twoFactorData.twoFactorCode; + if (typeof twoFactorData.twoFactorExpiry === "number") { + extSession.twoFactorExpiry = new Date(twoFactorData.twoFactorExpiry); + } + extSession.email = email; + extSession.password = password; + return res.status(200).json({ message: "2FA code sent. Please verify the code." }); + } else { + next(); + } +}; + +export const verifyCode = async ( + req: Request, + res: Response, + next: NextFunction +) => { + const extendedSession = req.session as ExtendedSession; + const { code } = req.body; + const sessionCode = extendedSession.twoFactorCode; + const sessionExpiry = extendedSession.twoFactorExpiry; + + if (sessionCode && sessionExpiry) { + const sessionExpiryDate = new Date(sessionExpiry); + + if (verify2FACode(code, sessionCode, sessionExpiryDate.getTime())) { + extendedSession.twoFactorCode = null; + extendedSession.twoFactorExpiry = null; + } else { + extendedSession.twoFAError = "Invalid or expired 2FA code."; + } + } else { + extendedSession.twoFAError = "2FA code or expiring time is missing."; + } + + try { + await new Promise((resolve, reject) => { + req.session.save((err) => { + if (err) { + reject(err); + } else { + resolve(null); + } + }); + }); + next(); + } catch (err) { + return res.status(500).json({ message: "Error saving session" }); + } +}; diff --git a/src/routes/2fa.route.ts b/src/routes/2fa.route.ts index d1a87ed..248ad29 100644 --- a/src/routes/2fa.route.ts +++ b/src/routes/2fa.route.ts @@ -1,12 +1,12 @@ import { Router } from "express"; -import { toggle2FAController } from "../controllers/2fa.controller"; +import { enable2FA } from "../controllers/2fa.controller"; import { VerifyAccessToken } from "../middleware/verfiyToken"; -import { verifyCodeController } from './../controllers/2fa.controller'; +import { verifyCode } from "./../middleware/2fa.middleware"; import { login } from "../controllers/user.controller"; const router = Router(); -router.post("/enable-2fa", VerifyAccessToken, toggle2FAController); -router.post("/verify-code", verifyCodeController, login); +router.post("/enable-2fa", VerifyAccessToken, enable2FA); +router.post("/verify-code", verifyCode, login); -export default router; \ No newline at end of file +export default router; diff --git a/src/routes/user.route.ts b/src/routes/user.route.ts index 03475b1..1eac8dd 100644 --- a/src/routes/user.route.ts +++ b/src/routes/user.route.ts @@ -1,4 +1,4 @@ -import { twoFAController } from './../controllers/2fa.controller'; +import { twoFAController } from './../middleware/2fa.middleware'; import express from "express" import { Welcome, deleteUser, editUser, login, register, updatePassword } from "../controllers/user.controller"; import { addFeedback, addReview } from "../controllers/review.controller"; diff --git a/src/services/2fa.service.ts b/src/services/2fa.service.ts index 5905e5a..6bdc21c 100644 --- a/src/services/2fa.service.ts +++ b/src/services/2fa.service.ts @@ -1,54 +1,81 @@ -import dotenv from 'dotenv'; +import dotenv from "dotenv"; import nodemailer from "nodemailer"; -import { TOTP } from 'otpauth'; -import User from '../database/models/user'; +import { TOTP } from "otpauth"; +import User from "../database/models/user"; import { randomBytes } from "crypto"; import { encode } from "base32.js"; -dotenv.config() +dotenv.config(); -export const generateAndSend2FACode = async (userData: { email: string; password: string }) => { - const existUser = await User.findOne({ where: { email: userData.email } }); - - if (existUser && existUser.isTwoFactorEnabled) { - const secret = encode(randomBytes(10)); - const otp = new TOTP({ - secret: secret, - digits: 6, - period: 120 - }); +const transporter = nodemailer.createTransport({ + service: "gmail", + secure: true, + auth: { + user: process.env.EMAIL, + pass: process.env.EMAIL_PASS, + }, + tls: { + rejectUnauthorized: false, + }, +}); - const code = otp.generate(); - const expiry = Date.now() + otp.period * 1000; +export const generate2FACode = async (userData: { + email: string; + password: string; +}) => { + const existUser = await User.findOne({ where: { email: userData.email } }); + if (existUser && existUser.isTwoFactorEnabled) { + const secret = encode(randomBytes(10)); + const otp = new TOTP({ + secret: secret, + digits: 6, + period: 120, + }); - const transporter = nodemailer.createTransport({ - service: "gmail", - secure: true, - auth: { - user: process.env.EMAIL, - pass: process.env.EMAIL_PASS, - }, - tls: { - rejectUnauthorized: false - } - }); + const code = otp.generate(); + const expiry = Date.now() + otp.period * 1000; - let mailOptions = { - from: process.env.EMAIL, - to: existUser.email, - subject: "Two factor authentication code", - html: `your 2FA code is ${code}` - }; - await transporter.sendMail(mailOptions); + let mailOptions = { + from: process.env.EMAIL, + to: existUser.email, + subject: "Two factor authentication code", + html: ` +
    +

    Two Factor Authentication Code

    +

    + Dear user, +

    +

    + Your 2FA code is: +

    +

    + ${code} +

    +

    + This code will expire in 2 minutes. Please use it promptly. +

    +

    + Regards, +

    +

    + Crafters Team +

    +
    + `, + }; + await transporter.sendMail(mailOptions); - return { twoFactorCode: code, twoFactorExpiry: expiry }; - } -} + return { twoFactorCode: code, twoFactorExpiry: expiry }; + } +}; -export const verify2FACode = (code: string, sessionCode: string, sessionExpiry: number) => { - - if (code === sessionCode && Date.now() < sessionExpiry) { - return true; - } - return false; - }; \ No newline at end of file +export const verify2FACode = ( + code: string, + sessionCode: string, + sessionExpiry: number +) => { + if (code === sessionCode && Date.now() < sessionExpiry) { + return true; + } + return false; +}; diff --git a/src/services/userService.ts b/src/services/userService.ts index 22c033d..f49c3f2 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -18,10 +18,8 @@ export const loginFunc = async (userData: { email: string; password: string }) = const { email} = userData; try { const existUser = await User.findOne({ where: { email } }); - console.log(existUser) return existUser; } catch (error) { - throw new Error('Unable to log in, may be user not found'); } }; From 298ad7b758e8f26810e5e68f8f11c814c1bb3cac Mon Sep 17 00:00:00 2001 From: frerot Date: Tue, 4 Jun 2024 15:39:33 +0200 Subject: [PATCH 029/187] Added checkExpirng Products On index.ts/app and it's Node Cron --- src/helpers/expiring.ts | 266 ++++++++++++++++++---------------------- src/index.ts | 29 +++-- 2 files changed, 137 insertions(+), 158 deletions(-) diff --git a/src/helpers/expiring.ts b/src/helpers/expiring.ts index d805ff0..25b09bb 100644 --- a/src/helpers/expiring.ts +++ b/src/helpers/expiring.ts @@ -6,90 +6,84 @@ import nodemailer from "nodemailer"; import models from "../database/models"; import { Response } from "express"; - export const checkExpiringProducts = async () => { - const productsData = await Product.findAll({ - where: { - expiringDate: { - [Op.between]: [ - new Date(), - new Date(new Date().getTime() + 14 * 24 * 60 * 60 * 1000).getTime(), - ], - }, - }, - }); - const jsonProducts = productsData.map((product) => product.toJSON()); - if (jsonProducts.length === 0) return false; - const organizedProducts = jsonProducts.reduce((acc, product) => { - const { vendorId } = product; - if (!acc[vendorId]) { - acc[vendorId] = []; - } - acc[vendorId].push(product.name); - return acc; - }, {}); + const productsData = await Product.findAll({ + where: { + expiringDate: { + [Op.between]: [ + new Date(), + new Date(new Date().getTime() + 14 * 24 * 60 * 60 * 1000).getTime(), + ], + }, + }, + }); + const jsonProducts = productsData.map((product) => product.toJSON()); + if (jsonProducts.length === 0) return false; + const organizedProducts = jsonProducts.reduce((acc, product) => { + const { vendorId } = product; + if (!acc[vendorId]) { + acc[vendorId] = []; + } + acc[vendorId].push(product.name); + return acc; + }, {}); - const vendorsData = await Vendor.findAll({ - where: { - vendorId: Object.keys(organizedProducts), - }, + const vendorsData = await Vendor.findAll({ + where: { + vendorId: Object.keys(organizedProducts), + }, + }); - }); + const jsonVendors = vendorsData.map((vendor) => vendor.toJSON()); + const organizedVendors = jsonVendors.reduce((acc, vendor) => { + const { userId, vendorId } = vendor; + if (!acc[userId]) { + acc[userId] = vendorId; + } + acc[userId] = vendorId; + return acc; + }, {}); - const jsonVendors = vendorsData.map((vendor) => vendor.toJSON()); - const organizedVendors = jsonVendors.reduce((acc, vendor) => { - const { userId, vendorId } = vendor; - if (!acc[userId]) { - acc[userId] = vendorId; - } - acc[userId] = vendorId; - return acc; - }, {}); + const usersData = await User.findAll({ + where: { + userId: Object.keys(organizedVendors), + }, + }); - const usersData = await User.findAll({ - where: { - userId: Object.keys(organizedVendors), - }, - }); - - let send = {}; - const jsonUsers = usersData.map((user) => user.toJSON()); - jsonUsers.forEach((user) => { - const { email } = user; - const userIds = Object.keys(organizedVendors); - userIds.forEach((id) => { - if (id === user.userId) { - send[email] = organizedProducts[organizedVendors[id]]; - } - }); + let send = {}; + const jsonUsers = usersData.map((user) => user.toJSON()); + jsonUsers.forEach((user) => { + const { email } = user; + const userIds = Object.keys(organizedVendors); + userIds.forEach((id) => { + if (id === user.userId) { + send[email] = organizedProducts[organizedVendors[id]]; + } }); - return send; + }); + return send; }; - - export const sendEmailsExpiring = async (data) => { - -const sendEmails = async (data) => { - if (!data) return false; - const transporter = nodemailer.createTransport({ - service: "gmail", - secure: true, - auth: { - user: process.env.EMAIL, - pass: process.env.EMAIL_PASS, - }, - }); - const emails = Object.keys(data); - emails.map(async (email, i, arr) => { - const productsList = data[email].reduce((acc, curr) => { - acc += `
  • ${curr}
  • `; - return acc; - }, ""); - const mailOptions = { - from: process.env.EMAIL, - to: email, - subject: "Reminder: Expiring Product in Store Inventory⚠️⚠️", - html: ` + if (!data) return false; + const transporter = nodemailer.createTransport({ + service: "gmail", + secure: true, + auth: { + user: process.env.EMAIL, + pass: process.env.EMAIL_PASS, + }, + }); + const emails = Object.keys(data); + emails.map(async (email, i, arr) => { + const productsList = data[email].reduce((acc, curr) => { + acc += `
  • ${curr}
  • `; + return acc; + }, ""); + const mailOptions = { + from: process.env.EMAIL, + to: email, + subject: "Reminder: Expiring Product in Store Inventory⚠️⚠️", + html: ` @@ -175,53 +169,48 @@ const sendEmails = async (data) => { `, - }; + }; - try { - await transporter.sendMail(mailOptions); - arr[i] = "sent"; - } catch (error) { - console.log(error); - } - return arr; - }); + try { + await transporter.sendMail(mailOptions); + arr[i] = "sent"; + } catch (error) { + console.log(error); + } + return arr; + }); }; - - -export const checkExpiredsProduct = async(req?:Request,res?:Response)=>{ - try { - - const expiredProduct = await Product.findAll({ - where:{ - expiringDate:{ - [Op.lt]: new Date() - }, - expired: false, - }, - include:{ - model: models.Vendor, - as: "Vendor" - } - }) - const updatePromise = expiredProduct.map(async(product)=>{ - const userId = product.Vendor.userId - const userEmail = await User.findByPk(userId) - - product.update({expired: true,available: false}).then(async()=>{ - const transporter = nodemailer.createTransport({ - service: "gmail", - secure: true, - auth: { - user: process.env.EMAIL, - pass: process.env.EMAIL_PASS, - }, - }); - - let mailOptions = { - from: process.env.EMAIL, - to: userEmail?.email, - subject: "expireProduct", - html: ` +export const checkExpiredProducts = async (req?: Request, res?: Response) => { + try { + const expiredProduct = await Product.findAll({ + where: { + expiringDate: { + [Op.lt]: new Date(), + }, + expired: false, + }, + include: { + model: models.Vendor, + as: "Vendor", + }, + }); + const updatePromise = expiredProduct.map(async (product) => { + const userId = product.Vendor.userId; + const userEmail = await User.findByPk(userId); + product.update({ expired: true, available: false }).then(async () => { + const transporter = nodemailer.createTransport({ + service: "gmail", + secure: true, + auth: { + user: process.env.EMAIL, + pass: process.env.EMAIL_PASS, + }, + }); + let mailOptions = { + from: process.env.EMAIL, + to: userEmail?.email, + subject: "expireProduct", + html: ` @@ -287,7 +276,7 @@ export const checkExpiredsProduct = async(req?:Request,res?:Response)=>{
    -

    ⚠️Expiring Products⚠️

    +

    ⚠️Expired Products⚠️

    Greetings,

    @@ -306,26 +295,13 @@ export const checkExpiredsProduct = async(req?:Request,res?:Response)=>{
    `, - }; - await transporter.sendMail(mailOptions); - - }) - }) - - await Promise.all(updatePromise) - res?.status(200).json({message: "checking expiring product sucessfully"}) - - - } catch (error:any) { - return res?.status(500).json({error:error.message}) - - } - -} -export const UpdateExpiredProduct = async (productIds: string[] | []) => {}; - -cron.schedule("0 0 * * */14", async () => { - const data = await checkExpiringProducts(); - sendEmails(data); -}); - + }; + await transporter.sendMail(mailOptions); + }); + }); + await Promise.all(updatePromise); + res?.status(200).json({ message: "Check Expired Product Successfully" }); + } catch (error: any) { + return res?.status(500).json({ error: error.message }); + } +}; diff --git a/src/index.ts b/src/index.ts index 1fcf0aa..c84825e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,11 +23,13 @@ import googleAuthRoute from "./routes/googleAuth.route"; import cartroute from "./routes/cart.route"; import orderRoute from "./routes/order.route"; import wishlistroute from "./routes/wishlist.route"; -import { checkExpiredsProduct } from "./helpers/expiring"; - -import subscriptionRoute from "./routes/subscription.route" - -import notificationRoute from "./routes/notifications.route" +import { + checkExpiredProducts, + checkExpiringProducts, + sendEmailsExpiring, +} from "./helpers/expiring"; +import subscriptionRoute from "./routes/subscription.route"; +import notificationRoute from "./routes/notifications.route"; const app = express(); app.use(cors()); @@ -57,21 +59,22 @@ app.use("/", vendorRoute); app.use("/", roleRoute); app.use("/", orderRoute); app.use("/", checkoutRoute); -app.use('/', googleAuthRoute); -app.use('/', subscriptionRoute); -app.use('/', notificationRoute) +app.use("/", googleAuthRoute); +app.use("/", subscriptionRoute); +app.use("/", notificationRoute); app.use("/api-docs", swaggerRoute); app.use("/admin", adminRoute); app.use("/", cartroute); app.use("/", wishlistroute); - -cron.schedule('0 0 * * *', () => { - checkExpiredsProduct(); +cron.schedule("0 0 * * *", () => { + checkExpiredProducts(); +}); +cron.schedule("0 0 * * */14", async () => { + const data = await checkExpiringProducts(); + sendEmailsExpiring(data); }); const server = app.listen(PORT, () => { console.log(`Server running on Port ${PORT}`); - checkExpiredsProduct() }); - export { app, server }; From 2d638c480d93cb231e38481138876883ffb6a9ad Mon Sep 17 00:00:00 2001 From: sevelinCa <142162886+sevelinCa@users.noreply.github.com> Date: Tue, 4 Jun 2024 17:25:57 +0200 Subject: [PATCH 030/187] Update order.route.ts --- src/routes/order.route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/order.route.ts b/src/routes/order.route.ts index 11381f8..a135fa8 100644 --- a/src/routes/order.route.ts +++ b/src/routes/order.route.ts @@ -5,7 +5,7 @@ import { getOrderStatus, updateOrderStatus } from "../controllers/orderStatus.co import { verifyAdmin } from "../middleware/verifyRole"; const router = express.Router(); -router.put("/order/:orderId/status", VerifyAccessToken, updateOrderStatus); +router.put("/order/:orderId/order-status", VerifyAccessToken, updateOrderStatus); router.get('/order/:orderId/status',VerifyAccessToken, getOrderStatus); router.put('/order/:orderId/status',VerifyAccessToken, verifyAdmin, updateOrderStatus); From be5e6e375a2f8de18e5eed534cc80835aca57283 Mon Sep 17 00:00:00 2001 From: sevelinCa <142162886+sevelinCa@users.noreply.github.com> Date: Tue, 4 Jun 2024 17:28:12 +0200 Subject: [PATCH 031/187] Update order.route.ts --- src/routes/order.route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/order.route.ts b/src/routes/order.route.ts index a135fa8..c1f47d6 100644 --- a/src/routes/order.route.ts +++ b/src/routes/order.route.ts @@ -1,5 +1,5 @@ import express from "express"; -import { updateOrderStatus } from "../controllers/orderController"; +import { modifyOrderStatus } from "../controllers/orderController"; import { VerifyAccessToken } from "../middleware/verfiyToken"; import { getOrderStatus, updateOrderStatus } from "../controllers/orderStatus.controller"; import { verifyAdmin } from "../middleware/verifyRole"; From 90559d1a828896183e1a331457efe5919984763a Mon Sep 17 00:00:00 2001 From: sevelinCa <142162886+sevelinCa@users.noreply.github.com> Date: Tue, 4 Jun 2024 17:28:55 +0200 Subject: [PATCH 032/187] Update orderController.ts --- src/controllers/orderController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/orderController.ts b/src/controllers/orderController.ts index 7bb1456..c71b729 100644 --- a/src/controllers/orderController.ts +++ b/src/controllers/orderController.ts @@ -4,7 +4,7 @@ import Order from "../database/models/order"; const allowedStatuses = ["Pending", "Shipped", "Delivered", "Cancelled"]; -export const updateOrderStatus = async (req: Request, res: Response) => { +export const modifyOrderStatus = async (req: Request, res: Response) => { try { const { orderId } = req.params; const { status } = req.body; @@ -36,4 +36,4 @@ export const updateOrderStatus = async (req: Request, res: Response) => { console.error(`Failed to update order status: ${error}`); res.status(500).json({ error: error.message }); } -}; \ No newline at end of file +}; From cb7ecb80df490d410bd70e4fec4872b910d4d6c8 Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Thu, 6 Jun 2024 12:29:26 +0200 Subject: [PATCH 033/187] Add documentation and test --- package-lock.json | 108 ++++++++++++++- package.json | 1 + src/__test__/expiring.test.ts | 25 ---- src/__test__/xeview.test.ts | 200 ++++++++++++++++++++++++++++ src/docs/product.swagger.yaml | 103 ++++++++++++++ src/docs/review.swagger.yaml | 244 ++++++++++++++++++++++++++++++++++ 6 files changed, 654 insertions(+), 27 deletions(-) delete mode 100644 src/__test__/expiring.test.ts create mode 100644 src/__test__/xeview.test.ts create mode 100644 src/docs/product.swagger.yaml create mode 100644 src/docs/review.swagger.yaml diff --git a/package-lock.json b/package-lock.json index a16f35d..7c068f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "sequelize": "^6.37.3", "sequelize-cli": "^6.6.2", "sequelize-typescript": "^2.1.6", + "sinon": "^18.0.0", "supertest": "^7.0.0", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.0", @@ -1318,7 +1319,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, "dependencies": { "type-detect": "4.0.8" } @@ -1332,6 +1332,29 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==" + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -5025,6 +5048,11 @@ "node": "*" } }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==" + }, "node_modules/jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -5434,6 +5462,31 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" }, + "node_modules/nise": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.0.0.tgz", + "integrity": "sha512-K8ePqo9BFvN31HXwEtTNGzgrPpmvgciDsFz8aztFjt4LqKO/JeFD8tBOeuDiCMXrIl/m1YvfH8auSpxfaD09wg==", + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==" + }, "node_modules/node-addon-api": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", @@ -9044,6 +9097,58 @@ "node": ">=10" } }, + "node_modules/sinon": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.0.tgz", + "integrity": "sha512-+dXDXzD1sBO6HlmZDd7mXZCR/y5ECiEiGCBSGuFD/kZ0bDTofPYc6JaeGmPSF+1j1MejGUWkORbYOLDyvqCWpA==", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.2.0", + "nise": "^6.0.0", + "supports-color": "^7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -9674,7 +9779,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, "engines": { "node": ">=4" } diff --git a/package.json b/package.json index f6b3b7a..5e3de19 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "sequelize": "^6.37.3", "sequelize-cli": "^6.6.2", "sequelize-typescript": "^2.1.6", + "sinon": "^18.0.0", "supertest": "^7.0.0", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.0", diff --git a/src/__test__/expiring.test.ts b/src/__test__/expiring.test.ts deleted file mode 100644 index f86707c..0000000 --- a/src/__test__/expiring.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { checkExpiringProducts } from "../helpers/expiring"; - -describe("CheckExpringProducts", () => { - test("Should Return An Object or false ", async () => { - const result = await checkExpiringProducts(); - expect(typeof result === "boolean" || typeof result === "object").toBe(true); - }); - test("Check if the Keys are emails and values is array of string of products ", async () => { - const result = await checkExpiringProducts(); - if (typeof result === "object") { - for (const key in result) { - expect(validateEmail(key)).toBe(true); - expect(Array.isArray(result[key])).toBe(true); - result[key].forEach((product) => { - expect(typeof product).toBe("string"); - }); - } - } - }); -}); - -function validateEmail(email: string): boolean { - const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return emailPattern.test(email); -} diff --git a/src/__test__/xeview.test.ts b/src/__test__/xeview.test.ts new file mode 100644 index 0000000..2c275ca --- /dev/null +++ b/src/__test__/xeview.test.ts @@ -0,0 +1,200 @@ +import request from 'supertest'; +import sinon from 'sinon'; +import {app} from '../index'; +import Rating from '../database/models/rating'; +import Review from '../database/models/review'; +import Order from '../database/models/order'; + + +describe('addReview', () => { + let findOneOrderStub: sinon.SinonStub; + let createReviewStub: sinon.SinonStub; + + beforeAll(() => { + findOneOrderStub = sinon.stub(Order, 'findOne'); + createReviewStub = sinon.stub(Review, 'create'); + }); + + afterAll(() => { + findOneOrderStub.restore(); + createReviewStub.restore(); + }); + + it('should return 402 if rating is above 5', async () => { + const res = await request(app) + .post('/addreview/123') + .send({ productId: '456', rating: 6, feedback: 'Great product!' }); + + expect(res.status).toBe(402); + expect(res.body.message).toBe('Rating is between 0 and 5'); + }); + + it('should return 400 if user has not bought the product', async () => { + findOneOrderStub.resolves(null); + + const res = await request(app) + .post('/addreview/123') + .send({ productId: '456', rating: 4, feedback: 'Great product!' }); + + expect(res.status).toBe(400); + expect(res.body.message).toBe(' User has not bougth this product'); + }); + + it('should return 200 and create review successfully', async () => { + const order = { userId: '123', products: [{ productId: '456' }] }; + findOneOrderStub.resolves(order); + + const review = { + userId: '123', + productId: '456', + rating: 4, + feedback: 'Great product!', + }; + createReviewStub.resolves(review); + + const res = await request(app) + .post('/addreview/123') + .send({ productId: '456', rating: 4, feedback: 'Great product!' }); + + expect(res.status).toBe(200); + expect(res.body.message).toBe('Review created successfully'); + expect(res.body.review).toEqual(review); + }); + + it('should return 500 if there is an internal server error', async () => { + findOneOrderStub.rejects(new Error('Internal server error')); + + const res = await request(app) + .post('/addreview/123') + .send({ productId: '456', rating: 4, feedback: 'Great product!' }); + + expect(res.status).toBe(500); + expect(res.body.error).toBe('Internal server error'); + }); +}); + +describe('addFeedback', () => { + let createStub: sinon.SinonStub; + + beforeAll(() => { + createStub = sinon.stub(Rating, 'create'); + }); + + afterAll(() => { + createStub.restore(); + }); + + it('should return 402 if name is not provided', async () => { + const res = await request(app) + .post('/addfeedback/123') + .send({ ratingScore: 4, feedback: 'Great product!' }); + + expect(res.status).toBe(402); + expect(res.body.message).toBe('You must add your name'); + }); + + it('should return 402 if ratingScore is greater than 5', async () => { + const res = await request(app) + .post('/addfeedback/123') + .send({ name: 'John Doe', ratingScore: 6, feedback: 'Great product!' }); + + expect(res.status).toBe(402); + expect(res.body.message).toBe('enter rating between 0 and 5'); + }); + + it('should return 400 if saving data fails', async () => { + createStub.resolves(null); + + const res = await request(app) + .post('/addfeedback/123') + .send({ name: 'John Doe', ratingScore: 4, feedback: 'Great product!' }); + + expect(res.status).toBe(400); + expect(res.body.message).toBe('error in saving data'); + }); + + it('should return 201 and save data successfully', async () => { + const feedbackData = { + name: 'John Doe', + ratingScore: 4, + feedback: 'Great product!', + productId: '123', + }; + createStub.resolves(feedbackData); + + const res = await request(app) + .post('/addfeedback/123') + .send({ name: 'John Doe', ratingScore: 4, feedback: 'Great product!' }); + + expect(res.status).toBe(201); + expect(res.body.message).toBe('feedback created'); + expect(res.body.data).toEqual(feedbackData); + }); + + it('should return 500 if there is an internal server error', async () => { + createStub.rejects(new Error('Internal server error')); + + const res = await request(app) + .post('/addfeedback/123') + .send({ name: 'John Doe', ratingScore: 4, feedback: 'Great product!' }); + + expect(res.status).toBe(500); + expect(res.body.error).toBe('Internal server error'); + }); +}); + +describe('selectReview', () => { + let findAllReviewStub: sinon.SinonStub; + + beforeAll(() => { + findAllReviewStub = sinon.stub(Review, 'findAll'); + }); + + afterAll(() => { + findAllReviewStub.restore(); + }); + + it('should return 400 if no reviews are found', async () => { + findAllReviewStub.resolves([]); + + const res = await request(app) + .get('/select-review/123'); + + expect(res.status).toBe(400); + expect(res.body.message).toBe('There in no review in your products'); + }); + + it('should return 200 and reviews if they exist', async () => { + const reviews = [ + { + id: 1, + content: 'Great product!', + Product: { + vendorId: '123', + name: 'Product1', + }, + }, + ]; + findAllReviewStub.resolves(reviews); + + const res = await request(app) + .get('/select-review/123'); // Adjust the URL as needed + + expect(res.status).toBe(200); + expect(res.body.review).toEqual(reviews); + }); + + it('should return 500 if there is an internal server error', async () => { + findAllReviewStub.rejects(new Error('Internal server error')); + + const res = await request(app) + .get('/select-review/123'); + + expect(res.status).toBe(500); + expect(res.body.message).toBe('Internal server error'); + }); +}); + + + + diff --git a/src/docs/product.swagger.yaml b/src/docs/product.swagger.yaml new file mode 100644 index 0000000..0ed4c42 --- /dev/null +++ b/src/docs/product.swagger.yaml @@ -0,0 +1,103 @@ +paths: + /create/product/{vendorId}: + post: + summary: Create product by vendor + description: Vendor can create product + tags: + - Product + parameters: + - in: path + name: vendorId + required: true + schema: + type: string + description: ID of the vendor in order to create product + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + example: air force 1 shoes + image: + type: string + example: http://indopic.com/public/airforce.png + description: + type: string + example: amazing shoes + price: + type: integer + example: 20000 + quantity: + type: integer + example: 12 + category: + type: string + example: shoes + expiringDate: + type: string + example: 08/10/2025 + responses: + 201: + description: Product created successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "Product created" + data: + type: object + properties: + id: + type: string + example: 1wsssd625283ad + name: + type: string + example: air force 1 shoes + image: + type: string + example: http://indopic.com/public/airforce.png + description: + type: string + example: amazing shoes + price: + type: integer + example: 20000 + quantity: + type: integer + example: 12 + category: + type: string + example: shoes + expiringDate: + type: string + example: 08/10/2025 + 422: + description: Fail to save data + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Failed to save data + 500: + description: internal server error + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: internal server error + \ No newline at end of file diff --git a/src/docs/review.swagger.yaml b/src/docs/review.swagger.yaml new file mode 100644 index 0000000..d862101 --- /dev/null +++ b/src/docs/review.swagger.yaml @@ -0,0 +1,244 @@ +paths: + /addreview/{userId}: + post: + summary: Add a review for product buyer bought successfully + description: Add a review for product buyer bought successfully + tags: + - review + security: + - bearerAuth: [] + parameters: + - in: path + name: userId + required: true + schema: + type: string + description: ID of the user + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + productId: + type: string + example: "12345" + description: ID of the product + rating: + type: integer + example: 4 + description: Rating of the product + feedback: + type: string + example: "Great product!" + description: Feedback about the product + responses: + 200: + description: Review created successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Review created successfully + review: + type: object + properties: + userId: + type: string + example: "user123" + productId: + type: string + example: "product123" + rating: + type: integer + example: 4 + feedback: + type: string + example: "Great product" + 400: + description: User has not bought this product + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: User has not bought this product + 402: + description: Rating is between 0 and 5 + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Rating is between 0 and 5 + 500: + description: Internal server error + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: internal server error + + /selectreview/{vendorId}: + get: + summary: Get all reviews for a vendor's products + description: Get all reviews for a vendor's products + tags: + - review + security: + - bearerAuth: [] + parameters: + - in: path + name: vendorId + required: true + schema: + type: string + description: ID of the vendor + responses: + 200: + description: Successfully retrieved reviews + content: + application/json: + schema: + type: object + properties: + review: + type: array + items: + type: object + properties: + userId: + type: string + example: "user123" + productId: + type: string + example: "product123" + rating: + type: integer + example: 4 + feedback: + type: string + example: "Great product" + 400: + description: No reviews found for the vendor's products + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: There is no review for your products + 500: + description: Internal server error + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: internal server error + /addfeedback/{productId}: + post: + summary: Add feedback for a product + description: Add feedback for a product + tags: + - review + parameters: + - in: path + name: productId + required: true + schema: + type: string + description: ID of the product + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + example: "John Doe" + description: Name of the person giving feedback + ratingScore: + type: integer + example: 4 + description: Rating score for the product + feedback: + type: string + example: "Great product" + description: Feedback about the product + responses: + 201: + description: Feedback created successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Feedback created + data: + type: object + properties: + id: + type: string + example: "feedback123" + name: + type: string + example: "John Doe" + ratingScore: + type: integer + example: 4 + feedback: + type: string + example: "Great product" + productId: + type: string + example: "product123" + 400: + description: Error in saving data + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Error in saving data + 402: + description: Validation error + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "You must add your name or enter rating between 0 and 5" + 500: + description: Internal server error + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: internal server error + From e46bfe40a014acd00ebd3c4b9df3d255ab5f0e80 Mon Sep 17 00:00:00 2001 From: sevelinCa <142162886+sevelinCa@users.noreply.github.com> Date: Thu, 6 Jun 2024 14:04:36 +0200 Subject: [PATCH 034/187] Update index.ts --- src/index.ts | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1fcf0aa..1e9a3ee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,8 @@ import passport from "passport"; import cors from "cors"; import cron from "node-cron"; import "./config/passport"; +import http from 'http'; +import { Server as SocketIOServer } from 'socket.io'; dotenv.config(); const PORT = process.env.PORT; @@ -19,27 +21,32 @@ import forgotPassword from "./routes/forget.password.router"; import authRoute from "./routes/auth.router"; import roleRoute from "./routes/roles.route"; import checkoutRoute from "./routes/checkout.router"; -import googleAuthRoute from "./routes/googleAuth.route"; +import googleAuthRoute from "./routes/googleAuth.route";; import cartroute from "./routes/cart.route"; +import TwoFaRoute from "./routes/2fa.route"; import orderRoute from "./routes/order.route"; + import wishlistroute from "./routes/wishlist.route"; +import subscriptionRoute from "./routes/subscription.route"; +import notificationRoute from "./routes/notifications.route"; import { checkExpiredsProduct } from "./helpers/expiring"; -import subscriptionRoute from "./routes/subscription.route" - -import notificationRoute from "./routes/notifications.route" const app = express(); +const httpServer = http.createServer(app); +const ioServer = new SocketIOServer(httpServer); app.use(cors()); app.use(cookieParser()); app.use(express.urlencoded({ extended: true })); app.use( + session({ secret: "crafters1234", resave: false, saveUninitialized: true, cookie: { secure: false }, }) + ); app.use(passport.initialize()); app.use(passport.session()); @@ -47,7 +54,6 @@ app.use(passport.session()); app.use(express.static("public")); app.use(express.json()); -// app.use(express.json()); app.use("/", userRoute); app.use("/", authRoute); app.use("/", productRoute); @@ -57,21 +63,22 @@ app.use("/", vendorRoute); app.use("/", roleRoute); app.use("/", orderRoute); app.use("/", checkoutRoute); -app.use('/', googleAuthRoute); -app.use('/', subscriptionRoute); -app.use('/', notificationRoute) +app.use("/", googleAuthRoute); +app.use("/", subscriptionRoute); +app.use("/", notificationRoute); app.use("/api-docs", swaggerRoute); app.use("/admin", adminRoute); app.use("/", cartroute); app.use("/", wishlistroute); +app.use("/", TwoFaRoute) -cron.schedule('0 0 * * *', () => { +cron.schedule('*/2 * * * * *', () => { checkExpiredsProduct(); }); -const server = app.listen(PORT, () => { +const server = httpServer.listen(PORT, () => { console.log(`Server running on Port ${PORT}`); checkExpiredsProduct() }); -export { app, server }; +export { app, server, ioServer }; From 50c9dacfef39cc9ec49ea7edaefdd4c44446d97f Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Thu, 6 Jun 2024 14:14:20 +0200 Subject: [PATCH 035/187] Add documentation and test --- package-lock.json | 244 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 5 +- src/index.ts | 2 - 3 files changed, 247 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7c068f1..0bfbe78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "sequelize-cli": "^6.6.2", "sequelize-typescript": "^2.1.6", "sinon": "^18.0.0", + "socket.io": "^4.7.5", "supertest": "^7.0.0", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.0", @@ -56,8 +57,11 @@ "@types/passport-google-oauth20": "^2.0.16", "@types/pg": "^8.11.6", "@types/sequelize": "^4.28.20", + "@types/sinon": "^17.0.3", + "@types/socket.io": "^3.0.2", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.6", + "cross-env": "^7.0.3", "jest": "^29.7.0", "jest-globals": "^0.1.7", "nodemon": "^3.1.0", @@ -1355,6 +1359,11 @@ "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==" }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -1457,6 +1466,11 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, "node_modules/@types/cookie-parser": { "version": "1.4.7", "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.7.tgz", @@ -1475,7 +1489,6 @@ "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", - "dev": true, "dependencies": { "@types/node": "*" } @@ -1782,6 +1795,31 @@ "@types/send": "*" } }, + "node_modules/@types/sinon": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", + "dev": true, + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true + }, + "node_modules/@types/socket.io": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.2.tgz", + "integrity": "sha512-pu0sN9m5VjCxBZVK8hW37ZcMe8rjn4HHggBN5CbaRTvFwv5jOmuIRZEuddsBPa9Th0ts0SIo3Niukq+95cMBbQ==", + "deprecated": "This is a stub types definition. socket.io provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "socket.io": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -2136,6 +2174,14 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/base64url": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", @@ -2770,6 +2816,24 @@ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -3044,6 +3108,63 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", + "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -9164,6 +9285,107 @@ "node": ">=8" } }, + "node_modules/socket.io": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", + "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.11.0" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -10176,6 +10398,26 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 5e3de19..4d8de2c 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "scripts": { "dev": "nodemon src/index.ts", "start": "node dist/index.js", - "build": "tsc -p .", "test": "jest", "migrate": "sequelize-cli db:migrate", "seed": "sequelize-cli db:seed:all" @@ -41,6 +40,7 @@ "sequelize-cli": "^6.6.2", "sequelize-typescript": "^2.1.6", "sinon": "^18.0.0", + "socket.io": "^4.7.5", "supertest": "^7.0.0", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.0", @@ -62,8 +62,11 @@ "@types/passport-google-oauth20": "^2.0.16", "@types/pg": "^8.11.6", "@types/sequelize": "^4.28.20", + "@types/sinon": "^17.0.3", + "@types/socket.io": "^3.0.2", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.6", + "cross-env": "^7.0.3", "jest": "^29.7.0", "jest-globals": "^0.1.7", "nodemon": "^3.1.0", diff --git a/src/index.ts b/src/index.ts index 1e9a3ee..7f5e8ad 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,7 +23,6 @@ import roleRoute from "./routes/roles.route"; import checkoutRoute from "./routes/checkout.router"; import googleAuthRoute from "./routes/googleAuth.route";; import cartroute from "./routes/cart.route"; -import TwoFaRoute from "./routes/2fa.route"; import orderRoute from "./routes/order.route"; import wishlistroute from "./routes/wishlist.route"; @@ -70,7 +69,6 @@ app.use("/api-docs", swaggerRoute); app.use("/admin", adminRoute); app.use("/", cartroute); app.use("/", wishlistroute); -app.use("/", TwoFaRoute) cron.schedule('*/2 * * * * *', () => { checkExpiredsProduct(); From 3eba273faeea3ebe45f12304db9f9d63058d3b1d Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Thu, 6 Jun 2024 14:36:50 +0200 Subject: [PATCH 036/187] Add documentation and test --- .github/workflows/node.js.yml | 4 ++-- src/index.ts | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 8a8487c..70f3e93 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -9,7 +9,7 @@ jobs: env: DATABASE_TEST_URL: ${{ secrets.DATABASE_TEST_URL }} - PORT: 5000 + PORT: 5001 GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} GOOGLE_CALLBACK_URL: ${{ secrets.GOOGLE_CALLBACK_URL }} @@ -38,4 +38,4 @@ jobs: - name: Run tests - run: npm run test + run: PORT=5001 npm run test diff --git a/src/index.ts b/src/index.ts index 7f5e8ad..32e8b09 100644 --- a/src/index.ts +++ b/src/index.ts @@ -78,5 +78,11 @@ const server = httpServer.listen(PORT, () => { checkExpiredsProduct() }); +if (process.env.NODE_ENV === 'test') { + afterAll(() => { + server.close(); + }); + } + export { app, server, ioServer }; From b58f0efd59bc59f3d657b748a3d249221a33860a Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Thu, 6 Jun 2024 14:39:18 +0200 Subject: [PATCH 037/187] Add documentation and test --- src/__test__/xeview.test.ts | 116 ------------------------------------ 1 file changed, 116 deletions(-) diff --git a/src/__test__/xeview.test.ts b/src/__test__/xeview.test.ts index 2c275ca..46d8362 100644 --- a/src/__test__/xeview.test.ts +++ b/src/__test__/xeview.test.ts @@ -6,72 +6,6 @@ import Review from '../database/models/review'; import Order from '../database/models/order'; -describe('addReview', () => { - let findOneOrderStub: sinon.SinonStub; - let createReviewStub: sinon.SinonStub; - - beforeAll(() => { - findOneOrderStub = sinon.stub(Order, 'findOne'); - createReviewStub = sinon.stub(Review, 'create'); - }); - - afterAll(() => { - findOneOrderStub.restore(); - createReviewStub.restore(); - }); - - it('should return 402 if rating is above 5', async () => { - const res = await request(app) - .post('/addreview/123') - .send({ productId: '456', rating: 6, feedback: 'Great product!' }); - - expect(res.status).toBe(402); - expect(res.body.message).toBe('Rating is between 0 and 5'); - }); - - it('should return 400 if user has not bought the product', async () => { - findOneOrderStub.resolves(null); - - const res = await request(app) - .post('/addreview/123') - .send({ productId: '456', rating: 4, feedback: 'Great product!' }); - - expect(res.status).toBe(400); - expect(res.body.message).toBe(' User has not bougth this product'); - }); - - it('should return 200 and create review successfully', async () => { - const order = { userId: '123', products: [{ productId: '456' }] }; - findOneOrderStub.resolves(order); - - const review = { - userId: '123', - productId: '456', - rating: 4, - feedback: 'Great product!', - }; - createReviewStub.resolves(review); - - const res = await request(app) - .post('/addreview/123') - .send({ productId: '456', rating: 4, feedback: 'Great product!' }); - - expect(res.status).toBe(200); - expect(res.body.message).toBe('Review created successfully'); - expect(res.body.review).toEqual(review); - }); - - it('should return 500 if there is an internal server error', async () => { - findOneOrderStub.rejects(new Error('Internal server error')); - - const res = await request(app) - .post('/addreview/123') - .send({ productId: '456', rating: 4, feedback: 'Great product!' }); - - expect(res.status).toBe(500); - expect(res.body.error).toBe('Internal server error'); - }); -}); describe('addFeedback', () => { let createStub: sinon.SinonStub; @@ -143,57 +77,7 @@ describe('addFeedback', () => { }); }); -describe('selectReview', () => { - let findAllReviewStub: sinon.SinonStub; - - beforeAll(() => { - findAllReviewStub = sinon.stub(Review, 'findAll'); - }); - afterAll(() => { - findAllReviewStub.restore(); - }); - - it('should return 400 if no reviews are found', async () => { - findAllReviewStub.resolves([]); - - const res = await request(app) - .get('/select-review/123'); - - expect(res.status).toBe(400); - expect(res.body.message).toBe('There in no review in your products'); - }); - - it('should return 200 and reviews if they exist', async () => { - const reviews = [ - { - id: 1, - content: 'Great product!', - Product: { - vendorId: '123', - name: 'Product1', - }, - }, - ]; - findAllReviewStub.resolves(reviews); - - const res = await request(app) - .get('/select-review/123'); // Adjust the URL as needed - - expect(res.status).toBe(200); - expect(res.body.review).toEqual(reviews); - }); - - it('should return 500 if there is an internal server error', async () => { - findAllReviewStub.rejects(new Error('Internal server error')); - - const res = await request(app) - .get('/select-review/123'); - - expect(res.status).toBe(500); - expect(res.body.message).toBe('Internal server error'); - }); -}); From 36d786a55c534f2a317d4321f2e647e671575a9c Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Thu, 6 Jun 2024 14:40:18 +0200 Subject: [PATCH 038/187] Add documentation and test --- src/__test__/user.test.ts | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 src/__test__/user.test.ts diff --git a/src/__test__/user.test.ts b/src/__test__/user.test.ts deleted file mode 100644 index 0856423..0000000 --- a/src/__test__/user.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import request from 'supertest'; -import {app,server} from '..' - -describe("Welcome endpoint",()=>{ - beforeAll((done) => { - done(); - }); - - afterAll((done) => { - server.close(done); - }); - it('should return welcome message and status 200 ', async()=>{ - const response = await request(app).get('/'); - expect(response.status).toBe(200); - expect(response.text).toContain("

    Welcome to our backend as code crafters team

    "); - - }); -}) \ No newline at end of file From ea0bff67ecb625b0f8bb0e531a88b150b84936ff Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Thu, 6 Jun 2024 15:21:28 +0200 Subject: [PATCH 039/187] Add documentation and test --- jest.config.js | 3 +- package.json | 2 +- src/__test__/xeview.test.ts | 116 ++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 2 deletions(-) diff --git a/jest.config.js b/jest.config.js index 6f865d6..a8fb7d4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,5 +1,6 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', - setupFilesAfterEnv: ['./jest.config.js'] // Assuming the setup file is called jest.setup.js + verbose: true, + forceExit: true, }; diff --git a/package.json b/package.json index 4d8de2c..e9a30fb 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "nodemon src/index.ts", "start": "node dist/index.js", - "test": "jest", + "test": "jest ", "migrate": "sequelize-cli db:migrate", "seed": "sequelize-cli db:seed:all" }, diff --git a/src/__test__/xeview.test.ts b/src/__test__/xeview.test.ts index 46d8362..2c275ca 100644 --- a/src/__test__/xeview.test.ts +++ b/src/__test__/xeview.test.ts @@ -6,6 +6,72 @@ import Review from '../database/models/review'; import Order from '../database/models/order'; +describe('addReview', () => { + let findOneOrderStub: sinon.SinonStub; + let createReviewStub: sinon.SinonStub; + + beforeAll(() => { + findOneOrderStub = sinon.stub(Order, 'findOne'); + createReviewStub = sinon.stub(Review, 'create'); + }); + + afterAll(() => { + findOneOrderStub.restore(); + createReviewStub.restore(); + }); + + it('should return 402 if rating is above 5', async () => { + const res = await request(app) + .post('/addreview/123') + .send({ productId: '456', rating: 6, feedback: 'Great product!' }); + + expect(res.status).toBe(402); + expect(res.body.message).toBe('Rating is between 0 and 5'); + }); + + it('should return 400 if user has not bought the product', async () => { + findOneOrderStub.resolves(null); + + const res = await request(app) + .post('/addreview/123') + .send({ productId: '456', rating: 4, feedback: 'Great product!' }); + + expect(res.status).toBe(400); + expect(res.body.message).toBe(' User has not bougth this product'); + }); + + it('should return 200 and create review successfully', async () => { + const order = { userId: '123', products: [{ productId: '456' }] }; + findOneOrderStub.resolves(order); + + const review = { + userId: '123', + productId: '456', + rating: 4, + feedback: 'Great product!', + }; + createReviewStub.resolves(review); + + const res = await request(app) + .post('/addreview/123') + .send({ productId: '456', rating: 4, feedback: 'Great product!' }); + + expect(res.status).toBe(200); + expect(res.body.message).toBe('Review created successfully'); + expect(res.body.review).toEqual(review); + }); + + it('should return 500 if there is an internal server error', async () => { + findOneOrderStub.rejects(new Error('Internal server error')); + + const res = await request(app) + .post('/addreview/123') + .send({ productId: '456', rating: 4, feedback: 'Great product!' }); + + expect(res.status).toBe(500); + expect(res.body.error).toBe('Internal server error'); + }); +}); describe('addFeedback', () => { let createStub: sinon.SinonStub; @@ -77,7 +143,57 @@ describe('addFeedback', () => { }); }); +describe('selectReview', () => { + let findAllReviewStub: sinon.SinonStub; + + beforeAll(() => { + findAllReviewStub = sinon.stub(Review, 'findAll'); + }); + afterAll(() => { + findAllReviewStub.restore(); + }); + + it('should return 400 if no reviews are found', async () => { + findAllReviewStub.resolves([]); + + const res = await request(app) + .get('/select-review/123'); + + expect(res.status).toBe(400); + expect(res.body.message).toBe('There in no review in your products'); + }); + + it('should return 200 and reviews if they exist', async () => { + const reviews = [ + { + id: 1, + content: 'Great product!', + Product: { + vendorId: '123', + name: 'Product1', + }, + }, + ]; + findAllReviewStub.resolves(reviews); + + const res = await request(app) + .get('/select-review/123'); // Adjust the URL as needed + + expect(res.status).toBe(200); + expect(res.body.review).toEqual(reviews); + }); + + it('should return 500 if there is an internal server error', async () => { + findAllReviewStub.rejects(new Error('Internal server error')); + + const res = await request(app) + .get('/select-review/123'); + + expect(res.status).toBe(500); + expect(res.body.message).toBe('Internal server error'); + }); +}); From 02d3e48f235837afaed085af185db4cc762e070c Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Thu, 6 Jun 2024 15:24:10 +0200 Subject: [PATCH 040/187] Add documentation and test --- .../{xeview.test.ts => review.test.ts} | 0 src/__test__/user.test.ts | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+) rename src/__test__/{xeview.test.ts => review.test.ts} (100%) create mode 100644 src/__test__/user.test.ts diff --git a/src/__test__/xeview.test.ts b/src/__test__/review.test.ts similarity index 100% rename from src/__test__/xeview.test.ts rename to src/__test__/review.test.ts diff --git a/src/__test__/user.test.ts b/src/__test__/user.test.ts new file mode 100644 index 0000000..0856423 --- /dev/null +++ b/src/__test__/user.test.ts @@ -0,0 +1,18 @@ +import request from 'supertest'; +import {app,server} from '..' + +describe("Welcome endpoint",()=>{ + beforeAll((done) => { + done(); + }); + + afterAll((done) => { + server.close(done); + }); + it('should return welcome message and status 200 ', async()=>{ + const response = await request(app).get('/'); + expect(response.status).toBe(200); + expect(response.text).toContain("

    Welcome to our backend as code crafters team

    "); + + }); +}) \ No newline at end of file From 79691197dafe8b7759e709dcfd584a72ad30d39a Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Thu, 6 Jun 2024 15:33:02 +0200 Subject: [PATCH 041/187] Add documentation and test --- src/__test__/review.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/__test__/review.test.ts b/src/__test__/review.test.ts index 2c275ca..e4764e5 100644 --- a/src/__test__/review.test.ts +++ b/src/__test__/review.test.ts @@ -15,9 +15,10 @@ describe('addReview', () => { createReviewStub = sinon.stub(Review, 'create'); }); - afterAll(() => { + afterAll((done) => { findOneOrderStub.restore(); createReviewStub.restore(); + done() }); it('should return 402 if rating is above 5', async () => { @@ -80,8 +81,9 @@ describe('addFeedback', () => { createStub = sinon.stub(Rating, 'create'); }); - afterAll(() => { + afterAll((done) => { createStub.restore(); + done() }); it('should return 402 if name is not provided', async () => { @@ -150,8 +152,9 @@ describe('selectReview', () => { findAllReviewStub = sinon.stub(Review, 'findAll'); }); - afterAll(() => { + afterAll((done) => { findAllReviewStub.restore(); + done() }); it('should return 400 if no reviews are found', async () => { From 293a65a257ab567d33f31c6275804e0290709a09 Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Thu, 6 Jun 2024 15:34:38 +0200 Subject: [PATCH 042/187] Add documentation and test --- src/__test__/product.test.ts | 203 +++++++++++++++++++++++++++++++++++ src/__test__/user.test.ts | 18 ---- 2 files changed, 203 insertions(+), 18 deletions(-) create mode 100644 src/__test__/product.test.ts delete mode 100644 src/__test__/user.test.ts diff --git a/src/__test__/product.test.ts b/src/__test__/product.test.ts new file mode 100644 index 0000000..e4764e5 --- /dev/null +++ b/src/__test__/product.test.ts @@ -0,0 +1,203 @@ +import request from 'supertest'; +import sinon from 'sinon'; +import {app} from '../index'; +import Rating from '../database/models/rating'; +import Review from '../database/models/review'; +import Order from '../database/models/order'; + + +describe('addReview', () => { + let findOneOrderStub: sinon.SinonStub; + let createReviewStub: sinon.SinonStub; + + beforeAll(() => { + findOneOrderStub = sinon.stub(Order, 'findOne'); + createReviewStub = sinon.stub(Review, 'create'); + }); + + afterAll((done) => { + findOneOrderStub.restore(); + createReviewStub.restore(); + done() + }); + + it('should return 402 if rating is above 5', async () => { + const res = await request(app) + .post('/addreview/123') + .send({ productId: '456', rating: 6, feedback: 'Great product!' }); + + expect(res.status).toBe(402); + expect(res.body.message).toBe('Rating is between 0 and 5'); + }); + + it('should return 400 if user has not bought the product', async () => { + findOneOrderStub.resolves(null); + + const res = await request(app) + .post('/addreview/123') + .send({ productId: '456', rating: 4, feedback: 'Great product!' }); + + expect(res.status).toBe(400); + expect(res.body.message).toBe(' User has not bougth this product'); + }); + + it('should return 200 and create review successfully', async () => { + const order = { userId: '123', products: [{ productId: '456' }] }; + findOneOrderStub.resolves(order); + + const review = { + userId: '123', + productId: '456', + rating: 4, + feedback: 'Great product!', + }; + createReviewStub.resolves(review); + + const res = await request(app) + .post('/addreview/123') + .send({ productId: '456', rating: 4, feedback: 'Great product!' }); + + expect(res.status).toBe(200); + expect(res.body.message).toBe('Review created successfully'); + expect(res.body.review).toEqual(review); + }); + + it('should return 500 if there is an internal server error', async () => { + findOneOrderStub.rejects(new Error('Internal server error')); + + const res = await request(app) + .post('/addreview/123') + .send({ productId: '456', rating: 4, feedback: 'Great product!' }); + + expect(res.status).toBe(500); + expect(res.body.error).toBe('Internal server error'); + }); +}); + +describe('addFeedback', () => { + let createStub: sinon.SinonStub; + + beforeAll(() => { + createStub = sinon.stub(Rating, 'create'); + }); + + afterAll((done) => { + createStub.restore(); + done() + }); + + it('should return 402 if name is not provided', async () => { + const res = await request(app) + .post('/addfeedback/123') + .send({ ratingScore: 4, feedback: 'Great product!' }); + + expect(res.status).toBe(402); + expect(res.body.message).toBe('You must add your name'); + }); + + it('should return 402 if ratingScore is greater than 5', async () => { + const res = await request(app) + .post('/addfeedback/123') + .send({ name: 'John Doe', ratingScore: 6, feedback: 'Great product!' }); + + expect(res.status).toBe(402); + expect(res.body.message).toBe('enter rating between 0 and 5'); + }); + + it('should return 400 if saving data fails', async () => { + createStub.resolves(null); + + const res = await request(app) + .post('/addfeedback/123') + .send({ name: 'John Doe', ratingScore: 4, feedback: 'Great product!' }); + + expect(res.status).toBe(400); + expect(res.body.message).toBe('error in saving data'); + }); + + it('should return 201 and save data successfully', async () => { + const feedbackData = { + name: 'John Doe', + ratingScore: 4, + feedback: 'Great product!', + productId: '123', + }; + createStub.resolves(feedbackData); + + const res = await request(app) + .post('/addfeedback/123') + .send({ name: 'John Doe', ratingScore: 4, feedback: 'Great product!' }); + + expect(res.status).toBe(201); + expect(res.body.message).toBe('feedback created'); + expect(res.body.data).toEqual(feedbackData); + }); + + it('should return 500 if there is an internal server error', async () => { + createStub.rejects(new Error('Internal server error')); + + const res = await request(app) + .post('/addfeedback/123') + .send({ name: 'John Doe', ratingScore: 4, feedback: 'Great product!' }); + + expect(res.status).toBe(500); + expect(res.body.error).toBe('Internal server error'); + }); +}); + +describe('selectReview', () => { + let findAllReviewStub: sinon.SinonStub; + + beforeAll(() => { + findAllReviewStub = sinon.stub(Review, 'findAll'); + }); + + afterAll((done) => { + findAllReviewStub.restore(); + done() + }); + + it('should return 400 if no reviews are found', async () => { + findAllReviewStub.resolves([]); + + const res = await request(app) + .get('/select-review/123'); + + expect(res.status).toBe(400); + expect(res.body.message).toBe('There in no review in your products'); + }); + + it('should return 200 and reviews if they exist', async () => { + const reviews = [ + { + id: 1, + content: 'Great product!', + Product: { + vendorId: '123', + name: 'Product1', + }, + }, + ]; + findAllReviewStub.resolves(reviews); + + const res = await request(app) + .get('/select-review/123'); // Adjust the URL as needed + + expect(res.status).toBe(200); + expect(res.body.review).toEqual(reviews); + }); + + it('should return 500 if there is an internal server error', async () => { + findAllReviewStub.rejects(new Error('Internal server error')); + + const res = await request(app) + .get('/select-review/123'); + + expect(res.status).toBe(500); + expect(res.body.message).toBe('Internal server error'); + }); +}); + + + + diff --git a/src/__test__/user.test.ts b/src/__test__/user.test.ts deleted file mode 100644 index 0856423..0000000 --- a/src/__test__/user.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import request from 'supertest'; -import {app,server} from '..' - -describe("Welcome endpoint",()=>{ - beforeAll((done) => { - done(); - }); - - afterAll((done) => { - server.close(done); - }); - it('should return welcome message and status 200 ', async()=>{ - const response = await request(app).get('/'); - expect(response.status).toBe(200); - expect(response.text).toContain("

    Welcome to our backend as code crafters team

    "); - - }); -}) \ No newline at end of file From 4f8a9c715bc8441ab01ce1b13386dd580d250887 Mon Sep 17 00:00:00 2001 From: sevelinCa <142162886+sevelinCa@users.noreply.github.com> Date: Thu, 6 Jun 2024 15:55:55 +0200 Subject: [PATCH 043/187] Update node.js.yml --- .github/workflows/node.js.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 70f3e93..b0e40b1 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -38,4 +38,4 @@ jobs: - name: Run tests - run: PORT=5001 npm run test + run: npm run test From 76a53b532ec195dc7347f4f148f6e09a885ddec1 Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Thu, 6 Jun 2024 18:06:53 +0200 Subject: [PATCH 044/187] Fix server already running --- package.json | 2 +- src/__test__/product.test.ts | 203 ----------------------------------- src/__test__/review.test.ts | 14 ++- src/__test__/user.test.ts | 22 ++++ src/index.ts | 39 +++++-- 5 files changed, 64 insertions(+), 216 deletions(-) delete mode 100644 src/__test__/product.test.ts create mode 100644 src/__test__/user.test.ts diff --git a/package.json b/package.json index e9a30fb..4d8de2c 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "nodemon src/index.ts", "start": "node dist/index.js", - "test": "jest ", + "test": "jest", "migrate": "sequelize-cli db:migrate", "seed": "sequelize-cli db:seed:all" }, diff --git a/src/__test__/product.test.ts b/src/__test__/product.test.ts deleted file mode 100644 index e4764e5..0000000 --- a/src/__test__/product.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -import request from 'supertest'; -import sinon from 'sinon'; -import {app} from '../index'; -import Rating from '../database/models/rating'; -import Review from '../database/models/review'; -import Order from '../database/models/order'; - - -describe('addReview', () => { - let findOneOrderStub: sinon.SinonStub; - let createReviewStub: sinon.SinonStub; - - beforeAll(() => { - findOneOrderStub = sinon.stub(Order, 'findOne'); - createReviewStub = sinon.stub(Review, 'create'); - }); - - afterAll((done) => { - findOneOrderStub.restore(); - createReviewStub.restore(); - done() - }); - - it('should return 402 if rating is above 5', async () => { - const res = await request(app) - .post('/addreview/123') - .send({ productId: '456', rating: 6, feedback: 'Great product!' }); - - expect(res.status).toBe(402); - expect(res.body.message).toBe('Rating is between 0 and 5'); - }); - - it('should return 400 if user has not bought the product', async () => { - findOneOrderStub.resolves(null); - - const res = await request(app) - .post('/addreview/123') - .send({ productId: '456', rating: 4, feedback: 'Great product!' }); - - expect(res.status).toBe(400); - expect(res.body.message).toBe(' User has not bougth this product'); - }); - - it('should return 200 and create review successfully', async () => { - const order = { userId: '123', products: [{ productId: '456' }] }; - findOneOrderStub.resolves(order); - - const review = { - userId: '123', - productId: '456', - rating: 4, - feedback: 'Great product!', - }; - createReviewStub.resolves(review); - - const res = await request(app) - .post('/addreview/123') - .send({ productId: '456', rating: 4, feedback: 'Great product!' }); - - expect(res.status).toBe(200); - expect(res.body.message).toBe('Review created successfully'); - expect(res.body.review).toEqual(review); - }); - - it('should return 500 if there is an internal server error', async () => { - findOneOrderStub.rejects(new Error('Internal server error')); - - const res = await request(app) - .post('/addreview/123') - .send({ productId: '456', rating: 4, feedback: 'Great product!' }); - - expect(res.status).toBe(500); - expect(res.body.error).toBe('Internal server error'); - }); -}); - -describe('addFeedback', () => { - let createStub: sinon.SinonStub; - - beforeAll(() => { - createStub = sinon.stub(Rating, 'create'); - }); - - afterAll((done) => { - createStub.restore(); - done() - }); - - it('should return 402 if name is not provided', async () => { - const res = await request(app) - .post('/addfeedback/123') - .send({ ratingScore: 4, feedback: 'Great product!' }); - - expect(res.status).toBe(402); - expect(res.body.message).toBe('You must add your name'); - }); - - it('should return 402 if ratingScore is greater than 5', async () => { - const res = await request(app) - .post('/addfeedback/123') - .send({ name: 'John Doe', ratingScore: 6, feedback: 'Great product!' }); - - expect(res.status).toBe(402); - expect(res.body.message).toBe('enter rating between 0 and 5'); - }); - - it('should return 400 if saving data fails', async () => { - createStub.resolves(null); - - const res = await request(app) - .post('/addfeedback/123') - .send({ name: 'John Doe', ratingScore: 4, feedback: 'Great product!' }); - - expect(res.status).toBe(400); - expect(res.body.message).toBe('error in saving data'); - }); - - it('should return 201 and save data successfully', async () => { - const feedbackData = { - name: 'John Doe', - ratingScore: 4, - feedback: 'Great product!', - productId: '123', - }; - createStub.resolves(feedbackData); - - const res = await request(app) - .post('/addfeedback/123') - .send({ name: 'John Doe', ratingScore: 4, feedback: 'Great product!' }); - - expect(res.status).toBe(201); - expect(res.body.message).toBe('feedback created'); - expect(res.body.data).toEqual(feedbackData); - }); - - it('should return 500 if there is an internal server error', async () => { - createStub.rejects(new Error('Internal server error')); - - const res = await request(app) - .post('/addfeedback/123') - .send({ name: 'John Doe', ratingScore: 4, feedback: 'Great product!' }); - - expect(res.status).toBe(500); - expect(res.body.error).toBe('Internal server error'); - }); -}); - -describe('selectReview', () => { - let findAllReviewStub: sinon.SinonStub; - - beforeAll(() => { - findAllReviewStub = sinon.stub(Review, 'findAll'); - }); - - afterAll((done) => { - findAllReviewStub.restore(); - done() - }); - - it('should return 400 if no reviews are found', async () => { - findAllReviewStub.resolves([]); - - const res = await request(app) - .get('/select-review/123'); - - expect(res.status).toBe(400); - expect(res.body.message).toBe('There in no review in your products'); - }); - - it('should return 200 and reviews if they exist', async () => { - const reviews = [ - { - id: 1, - content: 'Great product!', - Product: { - vendorId: '123', - name: 'Product1', - }, - }, - ]; - findAllReviewStub.resolves(reviews); - - const res = await request(app) - .get('/select-review/123'); // Adjust the URL as needed - - expect(res.status).toBe(200); - expect(res.body.review).toEqual(reviews); - }); - - it('should return 500 if there is an internal server error', async () => { - findAllReviewStub.rejects(new Error('Internal server error')); - - const res = await request(app) - .get('/select-review/123'); - - expect(res.status).toBe(500); - expect(res.body.message).toBe('Internal server error'); - }); -}); - - - - diff --git a/src/__test__/review.test.ts b/src/__test__/review.test.ts index e4764e5..8eacfb3 100644 --- a/src/__test__/review.test.ts +++ b/src/__test__/review.test.ts @@ -1,10 +1,19 @@ import request from 'supertest'; import sinon from 'sinon'; -import {app} from '../index'; +import {app, closeServer, startServer} from '../index'; import Rating from '../database/models/rating'; import Review from '../database/models/review'; import Order from '../database/models/order'; +beforeAll(async () => { + console.log("test starting .........."); + await startServer(); +}); + +afterAll(async () => { + await closeServer(); + console.log("server stop.........."); +}); describe('addReview', () => { let findOneOrderStub: sinon.SinonStub; @@ -22,8 +31,7 @@ describe('addReview', () => { }); it('should return 402 if rating is above 5', async () => { - const res = await request(app) - .post('/addreview/123') + const res = await request(app).post('/addreview/123') .send({ productId: '456', rating: 6, feedback: 'Great product!' }); expect(res.status).toBe(402); diff --git a/src/__test__/user.test.ts b/src/__test__/user.test.ts new file mode 100644 index 0000000..2779573 --- /dev/null +++ b/src/__test__/user.test.ts @@ -0,0 +1,22 @@ +import request from 'supertest'; +import {app, closeServer, startServer} from '..' + +beforeAll(async () => { + console.log("test starting .........."); + await startServer(); +}); + +afterAll(async () => { + await closeServer(); + console.log("server stop.........."); +}); + +describe("Welcome endpoint",()=>{ + + it('should return welcome message and status 200 ', async()=>{ + const response = await request(app).get('/'); + expect(response.status).toBe(200); + expect(response.text).toContain("

    Welcome to our backend as code crafters team

    "); + + }); +}) \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 32e8b09..0227355 100644 --- a/src/index.ts +++ b/src/index.ts @@ -73,16 +73,37 @@ app.use("/", wishlistroute); cron.schedule('*/2 * * * * *', () => { checkExpiredsProduct(); }); -const server = httpServer.listen(PORT, () => { - console.log(`Server running on Port ${PORT}`); - checkExpiredsProduct() -}); -if (process.env.NODE_ENV === 'test') { - afterAll(() => { - server.close(); +const startServer = () => { + return new Promise((resolve, reject) => { + const server = httpServer.listen(PORT, () => { + console.log(`Server running on Port ${PORT}`); + checkExpiredsProduct(); + resolve(); + }); + + server.on('error', reject); + }); +}; + +const closeServer = () => { + return new Promise((resolve, reject) => { + httpServer.close((err) => { + if (err) { + reject(err); + } else { + console.log("Server stopped"); + resolve(); + } + }); }); - } +}; + +if (process.env.NODE_ENV !== 'test') { + console.log(process.env.NODE_ENV) + startServer(); +} + -export { app, server, ioServer }; +export { app, ioServer,startServer,closeServer }; From be2bbfa9307c6ed792937829d2cd9d75587e1b96 Mon Sep 17 00:00:00 2001 From: sevelinCa <142162886+sevelinCa@users.noreply.github.com> Date: Thu, 6 Jun 2024 18:15:22 +0200 Subject: [PATCH 045/187] Update node.js.yml --- .github/workflows/node.js.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index b0e40b1..e6c6a44 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -9,11 +9,12 @@ jobs: env: DATABASE_TEST_URL: ${{ secrets.DATABASE_TEST_URL }} - PORT: 5001 + PORT: 5000 GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} GOOGLE_CALLBACK_URL: ${{ secrets.GOOGLE_CALLBACK_URL }} - MODE: ${{ secrets.HOST_MODE }} + HOST_MODE: ${{ secrets.HOST_MODE }} + NODE_ENV: ${{ secrets.NODE_ENV }} steps: - uses: actions/checkout@v4 From d5a4e138829aee8c1f01004e773d5a52e935bd48 Mon Sep 17 00:00:00 2001 From: sevelinCa <142162886+sevelinCa@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:18:40 +0200 Subject: [PATCH 046/187] Update node.js.yml --- .github/workflows/node.js.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index e6c6a44..09f55f4 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -13,8 +13,8 @@ jobs: GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} GOOGLE_CALLBACK_URL: ${{ secrets.GOOGLE_CALLBACK_URL }} - HOST_MODE: ${{ secrets.HOST_MODE }} - NODE_ENV: ${{ secrets.NODE_ENV }} + HOST_MODE: remote + NODE_ENV: test steps: - uses: actions/checkout@v4 @@ -36,6 +36,7 @@ jobs: echo "DATABASE_TEST_URL=$DATABASE_TEST_URL" echo "GOOGLE_CALLBACK_URL=$GOOGLE_CALLBACK_URL" echo "GOOGLE_CALLBACK_ID=$GOOGLE_CLIENT_ID" + echo "NODE_ENV=$NODE_ENV" - name: Run tests From 1f9b05b8beb235b2c4509ab5383e47e517696b9c Mon Sep 17 00:00:00 2001 From: HITAYEZU Jean Pierre Date: Mon, 13 May 2024 00:50:19 +0200 Subject: [PATCH 047/187] Readme file created --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bdc0138..92f9516 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Create a `.yaml` file. - Write your documentation in the file. No need to set up Swagger-related things in `server.ts` again.
    :warning: You must know that YAML strictly follows indentation -### Deployed_link:https://e-commerce-crafters-bn.onrender.com/ +### Deployed_link: ## 6.Licensing [![GitHub license](https://img.shields.io/github/license/Naereen/StrapDown.js.svg)](https://github.com/Naereen/StrapDown.js/blob/master/LICENSE) This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for detail. From 06b48641cd319d3a4dd44764749d9ddaedb027b8 Mon Sep 17 00:00:00 2001 From: HITAYEZU Jean Pierre Date: Tue, 14 May 2024 16:34:20 +0200 Subject: [PATCH 048/187] readme modified --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 92f9516..bdc0138 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Create a `.yaml` file. - Write your documentation in the file. No need to set up Swagger-related things in `server.ts` again.
    :warning: You must know that YAML strictly follows indentation -### Deployed_link: +### Deployed_link:https://e-commerce-crafters-bn.onrender.com/ ## 6.Licensing [![GitHub license](https://img.shields.io/github/license/Naereen/StrapDown.js.svg)](https://github.com/Naereen/StrapDown.js/blob/master/LICENSE) This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for detail. From f7be4d640fb1cb8b06b19319b03d1fc42ad761af Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 22 May 2024 13:34:50 +0200 Subject: [PATCH 049/187] removed console logs used for debugging --- src/database/seeders/20240521201427-seed-carts.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/database/seeders/20240521201427-seed-carts.js b/src/database/seeders/20240521201427-seed-carts.js index cb4bf31..884c0a4 100644 --- a/src/database/seeders/20240521201427-seed-carts.js +++ b/src/database/seeders/20240521201427-seed-carts.js @@ -5,13 +5,8 @@ const { v4: uuidv4 } = require('uuid'); /** @type {import('sequelize-cli').Migration} */ module.exports = { async up(queryInterface, Sequelize) { - console.log('Getting the users') const [users] = await queryInterface.sequelize.query(`SELECT "userId" FROM "Users"`); - users.map(user => ( - console.log('The users ', user) - )) - const carts = users.map(user => ({ cartId: uuidv4(), // @ts-ignore From e3863648fd87c403a2409f8e355c2ceea92a7ab6 Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Fri, 7 Jun 2024 10:35:36 +0200 Subject: [PATCH 050/187] Fix server already running --- src/__test__/product.test.ts | 83 ++++++++++++++++++++++++++++++++++++ src/index.ts | 1 - 2 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 src/__test__/product.test.ts diff --git a/src/__test__/product.test.ts b/src/__test__/product.test.ts new file mode 100644 index 0000000..a725eb9 --- /dev/null +++ b/src/__test__/product.test.ts @@ -0,0 +1,83 @@ +import request from 'supertest'; +import sinon from 'sinon'; +import { app, closeServer, startServer } from '../index'; +import { productLifecycleEmitter, PRODUCT_ADDED } from '../helpers/events'; +import { NextFunction,Request,Response } from 'express'; + +jest.mock('../middleware/verfiyToken', () => { + return { + VerifyAccessToken: (req:Request, res:Response, next:NextFunction) => { + (req as any).token = { userId: 'test-user' }; + next(); + } + }; +}); + +beforeAll(async () => { + await startServer(); +}); + +afterAll(async () => { + await closeServer(); +}); + +describe('createProduct', () => { + let checkVendorPermissionStub: sinon.SinonStub; + let saveProductStub: sinon.SinonStub; + let productLifecycleEmitterStub: sinon.SinonStub; + + beforeEach(() => { + checkVendorPermissionStub = sinon.stub(require('../services/PermisionService'), 'checkVendorPermission'); + saveProductStub = sinon.stub(require('../services/productService'), 'saveProduct'); + productLifecycleEmitterStub = sinon.stub(productLifecycleEmitter, 'emit'); + }); + + afterEach(() => { + checkVendorPermissionStub.restore(); + saveProductStub.restore(); + productLifecycleEmitterStub.restore(); + }); + + it('should return 403 if vendor permission is not allowed', async () => { + checkVendorPermissionStub.resolves({ allowed: false, status: 403, message: 'Permission denied' }); + + const res = await request(app).post('/create/product/123') + .send({ name: 'Product1', image: 'image.png', description: 'A great product', price: 100, quantity: 10, category: 'Electronics' }); + + expect(res.status).toBe(403); + expect(res.body.message).toBe('Permission denied'); + }); + + it('should return 500 if saving product fails', async () => { + checkVendorPermissionStub.resolves({ allowed: true }); + saveProductStub.resolves(null); + + const res = await request(app).post('/create/product/123') + .send({ name: 'Product1', image: 'image.png', description: 'A great product', price: 100, quantity: 10, category: 'Electronics' }); + + expect(res.status).toBe(500); + expect(res.body.error).toBe('Failed to save data'); + }); + + it('should return 201 if product is created successfully', async () => { + checkVendorPermissionStub.resolves({ allowed: true }); + saveProductStub.resolves({ id: '1', name: 'Product1', image: 'image.png', description: 'A great product', price: 100, quantity: 10, category: 'Electronics' }); + + const res = await request(app).post('/create/product/123') + .send({ name: 'Product1', image: 'image.png', description: 'A great product', price: 100, quantity: 10, category: 'Electronics' }); + + expect(res.status).toBe(201); + expect(res.body.message).toBe('Product Created'); + expect(res.body.data).toEqual({ id: '1', name: 'Product1', image: 'image.png', description: 'A great product', price: 100, quantity: 10, category: 'Electronics' }); + }); + + it('should return 500 if there is an internal server error', async () => { + checkVendorPermissionStub.rejects(new Error('Internal server error')); + + const res = await request(app).post('/create/product/123') + .send({ name: 'Product1', image: 'image.png', description: 'A great product', price: 100, quantity: 10, category: 'Electronics' }); + + expect(res.status).toBe(500); + expect(res.body.error).toBe('Internal server error'); + }); +}); diff --git a/src/index.ts b/src/index.ts index 0227355..68bf9fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -73,7 +73,6 @@ app.use("/", wishlistroute); cron.schedule('*/2 * * * * *', () => { checkExpiredsProduct(); }); - const startServer = () => { return new Promise((resolve, reject) => { const server = httpServer.listen(PORT, () => { From fb73932a10130003ed5e66de7f0e34bc2d26cf9c Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Fri, 7 Jun 2024 12:38:54 +0200 Subject: [PATCH 051/187] fix port already running --- jest.config.js | 1 + package.json | 2 +- src/__test__/product.test.ts | 31 +++++----- src/__test__/review.test.ts | 16 ++++-- src/__test__/user.test.ts | 11 ++-- src/helpers/createServer.ts | 70 +++++++++++++++++++++++ src/index.ts | 107 +++-------------------------------- 7 files changed, 108 insertions(+), 130 deletions(-) create mode 100644 src/helpers/createServer.ts diff --git a/jest.config.js b/jest.config.js index a8fb7d4..e49a805 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,4 +3,5 @@ module.exports = { testEnvironment: 'node', verbose: true, forceExit: true, + clearMocks: true }; diff --git a/package.json b/package.json index 4d8de2c..c21e9d4 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "nodemon src/index.ts", "start": "node dist/index.js", - "test": "jest", + "test": "jest --detectOpenHandles", "migrate": "sequelize-cli db:migrate", "seed": "sequelize-cli db:seed:all" }, diff --git a/src/__test__/product.test.ts b/src/__test__/product.test.ts index a725eb9..945a710 100644 --- a/src/__test__/product.test.ts +++ b/src/__test__/product.test.ts @@ -1,8 +1,21 @@ import request from 'supertest'; import sinon from 'sinon'; -import { app, closeServer, startServer } from '../index'; import { productLifecycleEmitter, PRODUCT_ADDED } from '../helpers/events'; import { NextFunction,Request,Response } from 'express'; +import setupServer from '../helpers/createServer'; +import { server } from '..'; +jest.setTimeout(50000); +let app = setupServer() + + +beforeAll(() => { + +}); + +afterAll(async () => { + await new Promise(resolve => server.close(resolve)); // Close the server after all tests have finished +}); + jest.mock('../middleware/verfiyToken', () => { return { @@ -13,13 +26,6 @@ jest.mock('../middleware/verfiyToken', () => { }; }); -beforeAll(async () => { - await startServer(); -}); - -afterAll(async () => { - await closeServer(); -}); describe('createProduct', () => { let checkVendorPermissionStub: sinon.SinonStub; @@ -38,15 +44,6 @@ describe('createProduct', () => { productLifecycleEmitterStub.restore(); }); - it('should return 403 if vendor permission is not allowed', async () => { - checkVendorPermissionStub.resolves({ allowed: false, status: 403, message: 'Permission denied' }); - - const res = await request(app).post('/create/product/123') - .send({ name: 'Product1', image: 'image.png', description: 'A great product', price: 100, quantity: 10, category: 'Electronics' }); - - expect(res.status).toBe(403); - expect(res.body.message).toBe('Permission denied'); - }); it('should return 500 if saving product fails', async () => { checkVendorPermissionStub.resolves({ allowed: true }); diff --git a/src/__test__/review.test.ts b/src/__test__/review.test.ts index 8eacfb3..81c995f 100644 --- a/src/__test__/review.test.ts +++ b/src/__test__/review.test.ts @@ -1,20 +1,24 @@ import request from 'supertest'; import sinon from 'sinon'; -import {app, closeServer, startServer} from '../index'; + import Rating from '../database/models/rating'; import Review from '../database/models/review'; import Order from '../database/models/order'; +import setupServer from '../helpers/createServer'; +import { server } from '..'; + +let app = setupServer() + + +beforeAll(() => { -beforeAll(async () => { - console.log("test starting .........."); - await startServer(); }); afterAll(async () => { - await closeServer(); - console.log("server stop.........."); + await new Promise(resolve => server.close(resolve)); // Close the server after all tests have finished }); + describe('addReview', () => { let findOneOrderStub: sinon.SinonStub; let createReviewStub: sinon.SinonStub; diff --git a/src/__test__/user.test.ts b/src/__test__/user.test.ts index 2779573..49c0855 100644 --- a/src/__test__/user.test.ts +++ b/src/__test__/user.test.ts @@ -1,16 +1,13 @@ import request from 'supertest'; -import {app, closeServer, startServer} from '..' +import {app, server} from '..' + +beforeAll(() => { -beforeAll(async () => { - console.log("test starting .........."); - await startServer(); }); afterAll(async () => { - await closeServer(); - console.log("server stop.........."); + await new Promise(resolve => server.close(resolve)); // Close the server after all tests have finished }); - describe("Welcome endpoint",()=>{ it('should return welcome message and status 200 ', async()=>{ diff --git a/src/helpers/createServer.ts b/src/helpers/createServer.ts new file mode 100644 index 0000000..024f5a6 --- /dev/null +++ b/src/helpers/createServer.ts @@ -0,0 +1,70 @@ +// helpers/serverSetup.js + +import express from "express"; +import dotenv from "dotenv"; +import cookieParser from "cookie-parser"; +import session from "express-session"; +import passport from "passport"; +import cors from "cors"; +import cron from "node-cron"; +import userRoute from "../routes/user.route"; +import vendorRoute from "../routes/vendor.route"; +import swaggerRoute from "../config/SwaggerConfig"; +import productRoute from "../routes/product.route"; +import adminRoute from "../routes/roles.route"; +import forgotPassword from "../routes/forget.password.router"; +import authRoute from "../routes/auth.router"; +import roleRoute from "../routes/roles.route"; +import checkoutRoute from "../routes/checkout.router"; +import googleAuthRoute from "../routes/googleAuth.route";; +import cartroute from "../routes/cart.route"; +import orderRoute from "../routes/order.route"; +import wishlistroute from "../routes/wishlist.route"; +import subscriptionRoute from "../routes/subscription.route"; +import notificationRoute from "../routes/notifications.route"; +import { checkExpiredsProduct } from "./expiring"; + +dotenv.config(); + +const setupServer = () => { + const app = express(); + + app.use(cors()); + app.use(cookieParser()); + app.use(express.urlencoded({ extended: true })); + app.use( + session({ + secret: "crafters1234", + resave: false, + saveUninitialized: true, + cookie: { secure: false }, + }) + ); + app.use(passport.initialize()); + app.use(passport.session()); + app.use(express.static("public")); + app.use(express.json()); + + // Add routes + app.use("/", userRoute); + app.use("/", authRoute); + app.use("/", productRoute); + app.use("/", forgotPassword); + app.use("/", productRoute); + app.use("/", vendorRoute); + app.use("/", roleRoute); + app.use("/", orderRoute); + app.use("/", checkoutRoute); + app.use("/", googleAuthRoute); + app.use("/", subscriptionRoute); + app.use("/", notificationRoute); + app.use("/api-docs", swaggerRoute); + app.use("/admin", adminRoute); + app.use("/", cartroute); + app.use("/", wishlistroute); + + + return app; +}; + +export default setupServer; diff --git a/src/index.ts b/src/index.ts index 68bf9fb..cbdbe07 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,108 +1,17 @@ -import express from "express"; -import dotenv from "dotenv"; -import cookieParser from "cookie-parser"; -import session from "express-session"; -import passport from "passport"; -import cors from "cors"; -import cron from "node-cron"; -import "./config/passport"; +// index.js + import http from 'http'; import { Server as SocketIOServer } from 'socket.io'; +import setupServer from './helpers/createServer'; -dotenv.config(); -const PORT = process.env.PORT; - -import userRoute from "./routes/user.route"; -import vendorRoute from "./routes/vendor.route"; -import swaggerRoute from "./config/SwaggerConfig"; -import productRoute from "./routes/product.route"; -import adminRoute from "./routes/roles.route"; -import forgotPassword from "./routes/forget.password.router"; -import authRoute from "./routes/auth.router"; -import roleRoute from "./routes/roles.route"; -import checkoutRoute from "./routes/checkout.router"; -import googleAuthRoute from "./routes/googleAuth.route";; -import cartroute from "./routes/cart.route"; -import orderRoute from "./routes/order.route"; +const PORT = process.env.PORT || 3000; -import wishlistroute from "./routes/wishlist.route"; -import subscriptionRoute from "./routes/subscription.route"; -import notificationRoute from "./routes/notifications.route"; -import { checkExpiredsProduct } from "./helpers/expiring"; - -const app = express(); +const app = setupServer(); const httpServer = http.createServer(app); const ioServer = new SocketIOServer(httpServer); -app.use(cors()); -app.use(cookieParser()); -app.use(express.urlencoded({ extended: true })); -app.use( - - session({ - secret: "crafters1234", - resave: false, - saveUninitialized: true, - cookie: { secure: false }, - }) - -); -app.use(passport.initialize()); -app.use(passport.session()); - -app.use(express.static("public")); -app.use(express.json()); - -app.use("/", userRoute); -app.use("/", authRoute); -app.use("/", productRoute); -app.use("/", forgotPassword); -app.use("/", productRoute); -app.use("/", vendorRoute); -app.use("/", roleRoute); -app.use("/", orderRoute); -app.use("/", checkoutRoute); -app.use("/", googleAuthRoute); -app.use("/", subscriptionRoute); -app.use("/", notificationRoute); -app.use("/api-docs", swaggerRoute); -app.use("/admin", adminRoute); -app.use("/", cartroute); -app.use("/", wishlistroute); - -cron.schedule('*/2 * * * * *', () => { - checkExpiredsProduct(); +const server = httpServer.listen(PORT, () => { + console.log(`Server running on Port ${PORT}`); }); -const startServer = () => { - return new Promise((resolve, reject) => { - const server = httpServer.listen(PORT, () => { - console.log(`Server running on Port ${PORT}`); - checkExpiredsProduct(); - resolve(); - }); - - server.on('error', reject); - }); -}; - -const closeServer = () => { - return new Promise((resolve, reject) => { - httpServer.close((err) => { - if (err) { - reject(err); - } else { - console.log("Server stopped"); - resolve(); - } - }); - }); -}; - -if (process.env.NODE_ENV !== 'test') { - console.log(process.env.NODE_ENV) - startServer(); -} - - -export { app, ioServer,startServer,closeServer }; +export { app, ioServer, server }; From a21bf8718a02cdbe55e4a5e591229e7e2dff27ea Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Fri, 7 Jun 2024 12:41:41 +0200 Subject: [PATCH 052/187] fix port already running --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c21e9d4..4d8de2c 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "nodemon src/index.ts", "start": "node dist/index.js", - "test": "jest --detectOpenHandles", + "test": "jest", "migrate": "sequelize-cli db:migrate", "seed": "sequelize-cli db:seed:all" }, From a333665297fdc63db18ea65636020ee8426f2b2b Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Fri, 7 Jun 2024 12:43:01 +0200 Subject: [PATCH 053/187] fix port already running --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4d8de2c..c21e9d4 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "nodemon src/index.ts", "start": "node dist/index.js", - "test": "jest", + "test": "jest --detectOpenHandles", "migrate": "sequelize-cli db:migrate", "seed": "sequelize-cli db:seed:all" }, From a82b7ea1397d454f1fba5033be17a9180b577ad9 Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Fri, 7 Jun 2024 12:53:01 +0200 Subject: [PATCH 054/187] fix port already running --- src/index.ts | 77 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index cbdbe07..de8aefa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,17 +1,80 @@ -// index.js +import express from "express"; +import dotenv from "dotenv"; +import cookieParser from "cookie-parser"; +import session from "express-session"; +import passport from "passport"; +import cors from "cors"; +import cron from "node-cron"; +import "./config/passport"; +import http from "http"; +import { Server as SocketIOServer } from "socket.io"; -import http from 'http'; -import { Server as SocketIOServer } from 'socket.io'; -import setupServer from './helpers/createServer'; +dotenv.config(); +const PORT = process.env.PORT; -const PORT = process.env.PORT || 3000; +import userRoute from "./routes/user.route"; +import vendorRoute from "./routes/vendor.route"; +import swaggerRoute from "./config/SwaggerConfig"; +import productRoute from "./routes/product.route"; +import adminRoute from "./routes/roles.route"; +import forgotPassword from "./routes/forget.password.router"; +import authRoute from "./routes/auth.router"; +import roleRoute from "./routes/roles.route"; +import checkoutRoute from "./routes/checkout.router"; +import googleAuthRoute from "./routes/googleAuth.route"; +import cartroute from "./routes/cart.route"; +import orderRoute from "./routes/order.route"; -const app = setupServer(); +import wishlistroute from "./routes/wishlist.route"; +import subscriptionRoute from "./routes/subscription.route"; +import notificationRoute from "./routes/notifications.route"; +import { checkExpiredsProduct } from "./helpers/expiring"; + +const app = express(); const httpServer = http.createServer(app); const ioServer = new SocketIOServer(httpServer); +app.use(cors()); +app.use(cookieParser()); +app.use(express.urlencoded({ extended: true })); +app.use( + session({ + secret: "crafters1234", + resave: false, + saveUninitialized: true, + cookie: { secure: false }, + }) +); +app.use(passport.initialize()); +app.use(passport.session()); + +app.use(express.static("public")); +app.use(express.json()); + +app.use("/", userRoute); +app.use("/", authRoute); +app.use("/", productRoute); +app.use("/", forgotPassword); +app.use("/", productRoute); +app.use("/", vendorRoute); +app.use("/", roleRoute); +app.use("/", orderRoute); +app.use("/", checkoutRoute); +app.use("/", googleAuthRoute); +app.use("/", subscriptionRoute); +app.use("/", notificationRoute); +app.use("/api-docs", swaggerRoute); +app.use("/admin", adminRoute); +app.use("/", cartroute); +app.use("/", wishlistroute); + +cron.schedule("*/2 * * * * *", () => { + checkExpiredsProduct(); +}); + const server = httpServer.listen(PORT, () => { - console.log(`Server running on Port ${PORT}`); + console.log(`Server running on Port ${PORT}`); + checkExpiredsProduct(); }); export { app, ioServer, server }; From 2c4dcd9e310d583fa0e304c75a8984b8cfd3a3b4 Mon Sep 17 00:00:00 2001 From: sevelinCA Date: Fri, 7 Jun 2024 16:17:33 +0200 Subject: [PATCH 055/187] Add product tests --- src/__test__/cart.test.ts | 152 ++++++++++++++++++ src/__test__/product.test.ts | 6 +- src/__test__/review.test.ts | 5 +- src/__test__/user.test.ts | 2 +- src/config/SwaggerConfig.ts | 14 +- src/database/config/config.js | 9 +- ...2623-add-expectedDeliveryDate-to-orders.js | 15 ++ 7 files changed, 193 insertions(+), 10 deletions(-) create mode 100644 src/__test__/cart.test.ts create mode 100644 src/database/migrations/20240603162623-add-expectedDeliveryDate-to-orders.js diff --git a/src/__test__/cart.test.ts b/src/__test__/cart.test.ts new file mode 100644 index 0000000..94ae547 --- /dev/null +++ b/src/__test__/cart.test.ts @@ -0,0 +1,152 @@ +import request from 'supertest'; +import { app, server } from '../index'; +import sinon from 'sinon'; +import Cart from '../database/models/cart'; +import CartItem from '../database/models/cartitem'; +import User from '../database/models/user'; +import Product from '../database/models/product'; + +jest.setTimeout(50000); + +afterAll(async () => { + await new Promise(resolve => server.close(resolve)); +}); + +describe('Cart Controller - addToCart', () => { + let findOneCartStub:sinon.SinonStub, createCartStub:sinon.SinonStub, updateCartStub:sinon.SinonStub, createCartItemStub:sinon.SinonStub; + + beforeEach(() => { + findOneCartStub = sinon.stub(Cart, 'findOne'); + createCartStub = sinon.stub(Cart, 'create'); + updateCartStub = sinon.stub(User, 'update'); + createCartItemStub = sinon.stub(CartItem, 'create'); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should add a product to the cart', async () => { + const userId = 'user1'; + const productId = 'product1'; + const quantity = 2; + const price = 10; + + findOneCartStub.resolves(null); + createCartStub.resolves({ cartId: 'cart1', userId }); + updateCartStub.resolves([1]); + createCartItemStub.resolves({ cartId: 'cart1', productId, quantity, price }); + + const response = await request(app) + .post('/addcart') + .send({ userId, productId, quantity, price }); + + console.log('Response:', response.body); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('message', 'cart added successfully!'); + expect(response.body).toHaveProperty('cart'); + }); +}); + + + + +describe('Cart Controller - getCart', () => { + let findOneCartStub:sinon.SinonStub, findAllCartItemStub:sinon.SinonStub; + + beforeEach(() => { + findOneCartStub = sinon.stub(Cart, 'findOne'); + findAllCartItemStub = sinon.stub(CartItem, 'findAll'); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should retrieve the cart for a user', async () => { + const userId = 'user1'; + + findOneCartStub.resolves({ cartId: 'cart1', userId }); + findAllCartItemStub.resolves([{ cartId: 'cart1', productId: 'product1', quantity: 2, price: 10 }]); + + const response = await request(app).get(`/getcart/${userId}`); + + console.log('getCart Response:', response.body); // Log the response for debugging + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('cartitem'); + }); +}); + + + + + +describe('Cart Controller - clearCart', () => { + let findOneCartStub:sinon.SinonStub, destroyCartItemStub:sinon.SinonStub; + + beforeEach(() => { + findOneCartStub = sinon.stub(Cart, 'findOne'); + destroyCartItemStub = sinon.stub(CartItem, 'destroy'); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should clear the cart for a user', async () => { + const userId = 'user1'; + + findOneCartStub.resolves({ cartId: 'cart1', userId }); + destroyCartItemStub.resolves(1); + + const response = await request(app).delete(`/clearcart/${userId}`); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('message', 'Cart cleared successfully'); + }); +}); + + + +describe('Cart Controller - updateCart', () => { + let findOneProductStub:sinon.SinonStub, findOneCartItemStub:sinon.SinonStub, updateCartItemStub:sinon.SinonStub, findAllCartItemStub:sinon.SinonStub, sumCartItemStub:sinon.SinonStub; + + beforeEach(() => { + findOneProductStub = sinon.stub(Product, 'findOne'); + findOneCartItemStub = sinon.stub(CartItem, 'findOne'); + updateCartItemStub = sinon.stub(CartItem, 'update'); + findAllCartItemStub = sinon.stub(CartItem, 'findAll'); + sumCartItemStub = sinon.stub(CartItem, 'sum'); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should update the quantities of products in the cart', async () => { + const updates = [ + { cartId: 'cart1', productId: 'product1', quantity: 3, price: 15 }, + { cartId: 'cart1', productId: 'product2', quantity: 2, price: 20 }, + ]; + + findOneProductStub.withArgs({ where: { productId: 'product1' } }).resolves({ productId: 'product1', quantity: 10, name: 'Product 1' }); + findOneProductStub.withArgs({ where: { productId: 'product2' } }).resolves({ productId: 'product2', quantity: 10, name: 'Product 2' }); + findOneCartItemStub.withArgs({ where: { cartId: 'cart1', productId: 'product1' } }).resolves({ cartId: 'cart1', productId: 'product1' }); + findOneCartItemStub.withArgs({ where: { cartId: 'cart1', productId: 'product2' } }).resolves({ cartId: 'cart1', productId: 'product2' }); + updateCartItemStub.resolves([1]); + findAllCartItemStub.resolves(updates); + sumCartItemStub.resolves(45); + + const response = await request(app) + .post('/updatecart') + .send({ updates }); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('message', 'Cart items updated successfully!'); + expect(response.body).toHaveProperty('cartItems'); + expect(response.body).toHaveProperty('total'); + }); +}); + diff --git a/src/__test__/product.test.ts b/src/__test__/product.test.ts index 945a710..b745f41 100644 --- a/src/__test__/product.test.ts +++ b/src/__test__/product.test.ts @@ -3,9 +3,9 @@ import sinon from 'sinon'; import { productLifecycleEmitter, PRODUCT_ADDED } from '../helpers/events'; import { NextFunction,Request,Response } from 'express'; import setupServer from '../helpers/createServer'; -import { server } from '..'; +import { app, server } from '..'; jest.setTimeout(50000); -let app = setupServer() + beforeAll(() => { @@ -13,7 +13,7 @@ beforeAll(() => { }); afterAll(async () => { - await new Promise(resolve => server.close(resolve)); // Close the server after all tests have finished + await new Promise(resolve => server.close(resolve)); }); diff --git a/src/__test__/review.test.ts b/src/__test__/review.test.ts index 81c995f..6629edb 100644 --- a/src/__test__/review.test.ts +++ b/src/__test__/review.test.ts @@ -5,9 +5,8 @@ import Rating from '../database/models/rating'; import Review from '../database/models/review'; import Order from '../database/models/order'; import setupServer from '../helpers/createServer'; -import { server } from '..'; +import { app, server } from '..'; -let app = setupServer() beforeAll(() => { @@ -15,7 +14,7 @@ beforeAll(() => { }); afterAll(async () => { - await new Promise(resolve => server.close(resolve)); // Close the server after all tests have finished + await new Promise(resolve => server.close(resolve)); }); diff --git a/src/__test__/user.test.ts b/src/__test__/user.test.ts index 49c0855..c76fac3 100644 --- a/src/__test__/user.test.ts +++ b/src/__test__/user.test.ts @@ -6,7 +6,7 @@ beforeAll(() => { }); afterAll(async () => { - await new Promise(resolve => server.close(resolve)); // Close the server after all tests have finished + await new Promise(resolve => server.close(resolve)); }); describe("Welcome endpoint",()=>{ diff --git a/src/config/SwaggerConfig.ts b/src/config/SwaggerConfig.ts index bc324db..a05e861 100644 --- a/src/config/SwaggerConfig.ts +++ b/src/config/SwaggerConfig.ts @@ -16,9 +16,19 @@ const options = { description: "Multi vendor ecommerce api docs", }, components: { - securitySchemes: {}, + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + }, + }, }, - + security: [ + { + bearerAuth: [], + }, + ], servers: [ { url: "http://localhost:5000", diff --git a/src/database/config/config.js b/src/database/config/config.js index b1a3ebe..62e34e5 100644 --- a/src/database/config/config.js +++ b/src/database/config/config.js @@ -3,7 +3,14 @@ require('dotenv').config(); const config = { development: { url: process.env.DATABASE_DEVELOPMENT_URL, - dialect: 'postgres' + dialect: 'postgres', + dialectOptions: { + ssl: { + require: true, + rejectUnauthorized: false, + }, + + } }, test: { url: process.env.DATABASE_TEST_URL, diff --git a/src/database/migrations/20240603162623-add-expectedDeliveryDate-to-orders.js b/src/database/migrations/20240603162623-add-expectedDeliveryDate-to-orders.js new file mode 100644 index 0000000..9212dcd --- /dev/null +++ b/src/database/migrations/20240603162623-add-expectedDeliveryDate-to-orders.js @@ -0,0 +1,15 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up (queryInterface, Sequelize) { + await queryInterface.addColumn('Orders', 'expectedDeliveryDate', { + type: Sequelize.DATE, + allowNull: true, + }) + }, + + async down (queryInterface, Sequelize) { + await queryInterface.removeColumn('Orders', 'expectedDeliveryDate'); + } +}; \ No newline at end of file From d2eb5f7c28dbcb5f0f1571883c8d3ca5ec61de81 Mon Sep 17 00:00:00 2001 From: Prince-Kid Date: Fri, 7 Jun 2024 17:00:35 +0200 Subject: [PATCH 056/187] Roles Test and swagger Documentation --- .DS_Store | Bin 0 -> 6148 bytes jest.config.js | 6 +- package-lock.json | 108 +++++++++++++++++- package.json | 3 +- src/.DS_Store | Bin 0 -> 6148 bytes src/__test__/.DS_Store | Bin 0 -> 6148 bytes src/__test__/roles.test.ts | 91 +++++++++++++++ src/__test__/user.test.ts | 35 +++--- src/config/SwaggerConfig.ts | 50 ++++---- src/controllers/cart.controller.ts | 2 +- src/database/config/config.js | 12 +- src/database/config/db.config.ts | 2 +- .../migrations/20240520101722-create-user.js | 46 ++++---- src/database/models/user.ts | 2 +- src/docs/role.swagger.yaml | 31 +++++ src/docs/user.swagger.yaml | 3 - src/index.ts | 39 +++---- src/services/rolesService.ts | 8 +- 18 files changed, 327 insertions(+), 111 deletions(-) create mode 100644 .DS_Store create mode 100644 src/.DS_Store create mode 100644 src/__test__/.DS_Store create mode 100644 src/__test__/roles.test.ts create mode 100644 src/docs/role.swagger.yaml diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..20b47fde9c91c4e988f02627fcc805c288232670 GIT binary patch literal 6148 zcmeHKL2uJA6n<_CP1%Nu1kx@@k+_c1!9Yx0vaUT$0uBp;1EA70rDV(Es!8dgs+2SQ z2mS(AehL4D6MWD1q$Xv@AwYhT{XN_7v*Rz09TSo0%)(uwCJ{MEjJ0dXeq-FuY0WC8 zhIvBu(V`(qO6WaBYtgpCDqt1(+Z5n$_d1=>h*HXE_4^&8bVBJ-EQdiX1H|zrae&nN zsSF-bou1MW9a4{;lTW=;^w(1KCiVtNAx7)dnet{7^Vmh6xLxAipa~7Ix11s$BmOQ} zQkB&+@}5)b;d7~n%~B5+@qUze4LZi$4ya4dfGs?ASRAP`E-|W5a!SKul+x|Xif$$< zn5Pq??zdnPM`_k>e;1W%?aI}unJfO{xt=7e{hi)J%hPMwRIp$ees=zN>54ger literal 0 HcmV?d00001 diff --git a/jest.config.js b/jest.config.js index 6f865d6..675873e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,5 +1,5 @@ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - setupFilesAfterEnv: ['./jest.config.js'] // Assuming the setup file is called jest.setup.js + preset: "ts-jest", + testEnvironment: "node", + setupFilesAfterEnv: ["./jest.config.js"], // Assuming the setup file is called jest.setup.js }; diff --git a/package-lock.json b/package-lock.json index 27c523a..2d164d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "sequelize": "^6.37.3", "sequelize-cli": "^6.6.2", "sequelize-typescript": "^2.1.6", + "sinon": "^18.0.0", "socket.io": "^4.7.5", "supertest": "^7.0.0", "swagger-jsdoc": "^6.2.8", @@ -1322,7 +1323,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, "dependencies": { "type-detect": "4.0.8" } @@ -1336,6 +1336,29 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==" + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -5133,6 +5156,11 @@ "node": "*" } }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==" + }, "node_modules/jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -5550,6 +5578,31 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" }, + "node_modules/nise": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.0.0.tgz", + "integrity": "sha512-K8ePqo9BFvN31HXwEtTNGzgrPpmvgciDsFz8aztFjt4LqKO/JeFD8tBOeuDiCMXrIl/m1YvfH8auSpxfaD09wg==", + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==" + }, "node_modules/node-addon-api": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", @@ -9171,6 +9224,58 @@ "node": ">=10" } }, + "node_modules/sinon": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.0.tgz", + "integrity": "sha512-+dXDXzD1sBO6HlmZDd7mXZCR/y5ECiEiGCBSGuFD/kZ0bDTofPYc6JaeGmPSF+1j1MejGUWkORbYOLDyvqCWpA==", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.2.0", + "nise": "^6.0.0", + "supports-color": "^7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -9902,7 +10007,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, "engines": { "node": ">=4" } diff --git a/package.json b/package.json index 44a4ab9..e97e258 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dev": "nodemon src/index.ts", "start": "node dist/index.js", "build": "tsc -p .", - "test": "jest", + "test": "jest --detectOpenHandles", "migrate": "sequelize-cli db:migrate", "seed": "sequelize-cli db:seed:all" }, @@ -43,6 +43,7 @@ "sequelize": "^6.37.3", "sequelize-cli": "^6.6.2", "sequelize-typescript": "^2.1.6", + "sinon": "^18.0.0", "socket.io": "^4.7.5", "supertest": "^7.0.0", "swagger-jsdoc": "^6.2.8", diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..9061f054b1eee512b2626bdf009c624e43a157f2 GIT binary patch literal 6148 zcmeHKO>fgM7=GQA)?GpB0i<1!B5|EY2Lo;5(v@-GN)Q|Xm81z5;~x*%1RdLI9Q zzrdAW!hhie&ue>9nigp%guriPKX2^k^~clLu8BwtXVE@UkBB4^V{;qX2IG28Yu2zO zH-SR8kyAk<8a+?dc#Z7}oB~dPzfA#NyOP%|R&Gp{^&6%tL>+Go$4GlWwZYrK56gUF z`QO4xn&d^l|6Me;nm1Z)*_K`T&OcM7Uj)T09|qGm{Pt3*G`#YI@Kq9*qu%YODl38{ zi>EpuiDFRRzD}}8l|wblqFl%Nrb2dPXVlx7&mSK4cfI|iM~hu=e)ItT!Q+F)q9gCz zyZ_|H=wo`GsY^X_64Qh# zuiC1_{4%s0(oPN6U2T@EN|h^F z3Ggq_%NSm5Jc3oJ^6~2I8VcC8AiLM#I*L=kDe!+O!25%T#5l0HG^n=@WcmsKY@%2j zeE!S8F`mVN#ic>iz=R0}nowb{7{Y|3+_Sua#icG{wW1S^Tao literal 0 HcmV?d00001 diff --git a/src/__test__/.DS_Store b/src/__test__/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..9052733fee14e943578a2eee65b0736c030bcd1b GIT binary patch literal 6148 zcmeHKO-lnY5Pi`iq7{1dBJ3Y%ga2Ty^&p4{DtOcGwn}x|E!{16+<)Xx)Hg}|XsgzP zh?JQn^RkmnCi9jg8vv#~?d<~%0Glj=wJoYoMB1e+$Xc*$5RDt7g%W)f)UAcyYETB0 zf&a#Utlc_NbTM(O_I>@9D2}r{O|m>;AK4%dSX$3cSQA?xj~HV>-vLIri_1J!M}@ zY~qlXAs-ppk0#c1&MKA(N4a(R$sC$U7{#(Ceiqk!UUQea{RuzJ?=DHz{(?AD8{=Iy{idV4CCEtk3zpVVCB)f!-UI+36V{>p%@pP z<43MLOzcs$GN261GO+Gn8*=_1eSQC*b<#IwKp9vm228DU-f1(XaJDWaCueQMa>F7b o{VI>ng(@7!t|CYAE{hYkMN%Qg0V|KRQ1nN@(V$8h_)`X601YUEc>n+a literal 0 HcmV?d00001 diff --git a/src/__test__/roles.test.ts b/src/__test__/roles.test.ts new file mode 100644 index 0000000..8e72c0d --- /dev/null +++ b/src/__test__/roles.test.ts @@ -0,0 +1,91 @@ +import request from "supertest"; +import sinon from "sinon"; +import { app, server } from "../index"; +import * as rolesService from "../services/rolesService"; +import User from "../database/models/user"; +import Vendor from "../database/models/vendor"; +import nodemailer from "nodemailer"; + +beforeAll(() => {}); + +afterAll(async () => { + await new Promise((resolve) => server.close(resolve)); +}); + +describe("Vendor Approval Endpoint", () => { + let approveVendorRequestStub: sinon.SinonStub; + let vendorFindOneStub: sinon.SinonStub; + let userFindOneStub: sinon.SinonStub; + let vendorUpdateStub: sinon.SinonStub; + let userUpdateStub: sinon.SinonStub; + let transporter: any; + + beforeEach(() => { + approveVendorRequestStub = sinon.stub(rolesService, "approveVendorRequest"); + vendorFindOneStub = sinon.stub(Vendor, "findOne"); + userFindOneStub = sinon.stub(User, "findOne"); + vendorUpdateStub = sinon.stub(Vendor, "update"); + userUpdateStub = sinon.stub(User, "update"); + + transporter = { + sendMail: sinon.stub().resolves(), + }; + sinon.stub(nodemailer, "createTransport").returns(transporter); + }); + + afterEach(() => { + approveVendorRequestStub.restore(); + vendorFindOneStub.restore(); + userFindOneStub.restore(); + vendorUpdateStub.restore(); + userUpdateStub.restore(); + (nodemailer.createTransport as sinon.SinonStub).restore(); + sinon.restore(); + }); + + it("should approve vendor request successfully", async () => { + vendorFindOneStub.resolves({ userId: "123" }); + userFindOneStub.resolves({ + userId: "123", + email: "test@example.com", + name: "Test User", + }); + vendorUpdateStub.resolves([1]); + userUpdateStub.resolves([1]); + + approveVendorRequestStub.resolves({ + status: 200, + message: "Vendor Request Approved", + }); + + const response = await request(app).put("/approve-vendor/123"); + + expect(response.status).toBe(200); + expect(response.body.message).toBe("Vendor Request Approved"); + }); + + it("should return 404 if vendor request is not found", async () => { + vendorFindOneStub.resolves(null); + + approveVendorRequestStub.resolves({ + status: 404, + message: "Vendor Request Not Found", + }); + + const response = await request(app).put("/approve-vendor/123"); + + expect(response.status).toBe(404); + expect(response.body.message).toBe("Vendor Request Not Found"); + }); + + it("should return 500 if there is an internal server error", async () => { + vendorFindOneStub.rejects(new Error("Internal server error")); + + approveVendorRequestStub.rejects(new Error("Internal server error")); + + const response = await request(app).put("/approve-vendor/123"); + + expect(response.status).toBe(500); + expect(response.body.message).toBe("Internal Server Error"); + }); +}); diff --git a/src/__test__/user.test.ts b/src/__test__/user.test.ts index 0856423..fa6538d 100644 --- a/src/__test__/user.test.ts +++ b/src/__test__/user.test.ts @@ -1,18 +1,19 @@ -import request from 'supertest'; -import {app,server} from '..' +import request from "supertest"; +import { app, server } from ".."; -describe("Welcome endpoint",()=>{ - beforeAll((done) => { - done(); - }); - - afterAll((done) => { - server.close(done); - }); - it('should return welcome message and status 200 ', async()=>{ - const response = await request(app).get('/'); - expect(response.status).toBe(200); - expect(response.text).toContain("

    Welcome to our backend as code crafters team

    "); - - }); -}) \ No newline at end of file +describe("Welcome endpoint", () => { + beforeAll((done) => { + done(); + }); + + afterAll((done) => { + server.close(done); + }); + it("should return welcome message and status 200 ", async () => { + const response = await request(app).get("/"); + expect(response.status).toBe(200); + expect(response.text).toContain( + "

    Welcome to our backend as code crafters team

    " + ); + }); +}); diff --git a/src/config/SwaggerConfig.ts b/src/config/SwaggerConfig.ts index bc324db..59bb489 100644 --- a/src/config/SwaggerConfig.ts +++ b/src/config/SwaggerConfig.ts @@ -1,36 +1,32 @@ -import express = require('express') +import express = require("express"); -import swaggerJSDoc from 'swagger-jsdoc' -import swaggerUi from "swagger-ui-express" - - -const router = express.Router() +import swaggerJSDoc from "swagger-jsdoc"; +import swaggerUi from "swagger-ui-express"; +const router = express.Router(); const options = { - definition: { - openapi: "3.0.0", - info: { - title: "Code crafters api documentation", - version: "1.0.0", - description: "Multi vendor ecommerce api docs", - }, - components: { - securitySchemes: {}, - }, - - servers: [ - { - url: "http://localhost:5000", - }, - ], + definition: { + openapi: "3.0.0", + info: { + title: "Code crafters api documentation", + version: "1.0.0", + description: "Multi vendor ecommerce api docs", + }, + components: { + securitySchemes: {}, }, - apis: ["./src/docs/*.yaml"], - }; - const swaggerSpec = swaggerJSDoc(options) - + servers: [ + { + url: "http://localhost:5000", + }, + ], + }, + apis: ["./src/docs/*.yaml"], +}; +const swaggerSpec = swaggerJSDoc(options); router.use("/", swaggerUi.serve, swaggerUi.setup(swaggerSpec)); -export default router \ No newline at end of file +export default router; diff --git a/src/controllers/cart.controller.ts b/src/controllers/cart.controller.ts index ce63a04..38130ae 100644 --- a/src/controllers/cart.controller.ts +++ b/src/controllers/cart.controller.ts @@ -66,7 +66,7 @@ export const getCart = async (req: Request, res: Response) => { if (!cart) { return res.status(404).json({ message: "Cart not found" }); } - const cartitem = await CartItem.findAll({where:{cartId:cart.cartId}}) + const cartitem = await CartItem.findAll({ where: { cartId: cart.cartId } }); return res.status(200).json({ cartitem }); } catch (error: any) { diff --git a/src/database/config/config.js b/src/database/config/config.js index b1a3ebe..ea7f392 100644 --- a/src/database/config/config.js +++ b/src/database/config/config.js @@ -1,18 +1,18 @@ -require('dotenv').config(); +require("dotenv").config(); const config = { development: { url: process.env.DATABASE_DEVELOPMENT_URL, - dialect: 'postgres' + dialect: "postgres", }, test: { url: process.env.DATABASE_TEST_URL, - dialect: 'postgres' + dialect: "postgres", }, production: { url: process.env.DATABASE_PRODUCTION_URL, - dialect: 'postgres' - } + dialect: "postgres", + }, }; -module.exports = config; \ No newline at end of file +module.exports = config; diff --git a/src/database/config/db.config.ts b/src/database/config/db.config.ts index 3e148a6..3428a5a 100644 --- a/src/database/config/db.config.ts +++ b/src/database/config/db.config.ts @@ -33,4 +33,4 @@ const connectSequelize: Sequelize = new Sequelize(getURL(), { logging: false, }); -export default connectSequelize +export default connectSequelize; diff --git a/src/database/migrations/20240520101722-create-user.js b/src/database/migrations/20240520101722-create-user.js index bd220fa..5a1ad45 100644 --- a/src/database/migrations/20240520101722-create-user.js +++ b/src/database/migrations/20240520101722-create-user.js @@ -1,49 +1,47 @@ -'use strict'; +"use strict"; /** @type {import('sequelize-cli').Migration} */ module.exports = { async up(queryInterface, Sequelize) { - await queryInterface.createTable('Users', { + await queryInterface.createTable("Users", { userId: { allowNull: false, primaryKey: true, type: Sequelize.STRING, - defaultValue: Sequelize.UUIDV4 + defaultValue: Sequelize.UUIDV4, }, name: { type: Sequelize.STRING, - allowNull: false + allowNull: false, }, - email:{ + email: { type: Sequelize.STRING, allowNull: false, - unique: true + unique: true, }, - password:{ + password: { type: Sequelize.STRING, - allowNull: false + allowNull: false, }, - status:{ + status: { type: Sequelize.STRING, - defaultValue: 'active' + defaultValue: "active", }, - wishlistId:{ + wishlistId: { type: Sequelize.STRING, }, cartId: { - type: Sequelize.STRING + type: Sequelize.STRING, }, role: { type: Sequelize.STRING, - defaultValue: 'buyer' - + defaultValue: "buyer", }, - profile:{ - type: Sequelize.STRING + profile: { + type: Sequelize.STRING, }, - isVerfied:{ + isVerfied: { type: Sequelize.BOOLEAN, - defaultValue: false - + defaultValue: false, }, resetPasswordToken: { type: Sequelize.STRING, @@ -59,15 +57,15 @@ module.exports = { }, createdAt: { allowNull: false, - type: Sequelize.DATE + type: Sequelize.DATE, }, updatedAt: { allowNull: false, - type: Sequelize.DATE - } + type: Sequelize.DATE, + }, }); }, async down(queryInterface, Sequelize) { - await queryInterface.dropTable('Users'); - } + await queryInterface.dropTable("Users"); + }, }; diff --git a/src/database/models/user.ts b/src/database/models/user.ts index 94698ef..b6d6748 100644 --- a/src/database/models/user.ts +++ b/src/database/models/user.ts @@ -16,7 +16,7 @@ class User extends Model { public isVerified?: boolean; public resetPasswordToken?: string | null; public resetPasswordExpires?: Date | null; - public isTwoFactorEnabled?: boolean; + public isTwoFactorEnabled?: boolean; static associate(models: any) { User.hasMany(models.Review, { diff --git a/src/docs/role.swagger.yaml b/src/docs/role.swagger.yaml new file mode 100644 index 0000000..2341381 --- /dev/null +++ b/src/docs/role.swagger.yaml @@ -0,0 +1,31 @@ +/approve-vendor/{userId}: + put: + summary: Approve a vendor request + parameters: + - in: path + name: userId + schema: + type: string + required: true + description: The ID of the user to approve + responses: + 200: + description: Vendor Request Approved + 404: + description: Vendor Request Not Found + +/reject-vendor/{userId}: + put: + summary: Reject a vendor request + parameters: + - in: path + name: userId + schema: + type: string + required: true + description: The ID of the user to reject + responses: + 200: + description: Vendor Request Rejected + 404: + description: Vendor Request Not Found diff --git a/src/docs/user.swagger.yaml b/src/docs/user.swagger.yaml index 619c9f0..9f3f7c3 100644 --- a/src/docs/user.swagger.yaml +++ b/src/docs/user.swagger.yaml @@ -6,6 +6,3 @@ description: server running 500: description: sorry - - - \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index ca1ab05..42335f4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,8 +6,8 @@ import passport from "passport"; import cors from "cors"; import cron from "node-cron"; import "./config/passport"; -import http from 'http'; -import { Server as SocketIOServer } from 'socket.io'; +import http from "http"; +import { Server as SocketIOServer } from "socket.io"; dotenv.config(); const PORT = process.env.PORT; @@ -21,16 +21,16 @@ import forgotPassword from "./routes/forget.password.router"; import authRoute from "./routes/auth.router"; import roleRoute from "./routes/roles.route"; import checkoutRoute from "./routes/checkout.router"; -import googleAuthRoute from "./routes/googleAuth.route";; +import googleAuthRoute from "./routes/googleAuth.route"; import cartroute from "./routes/cart.route"; import TwoFaRoute from "./routes/2fa.route"; import orderRoute from "./routes/order.route"; import wishlistroute from "./routes/wishlist.route"; import { checkExpiredsProduct } from "./helpers/expiring"; -import subscriptionRoute from "./routes/subscription.route" +import subscriptionRoute from "./routes/subscription.route"; -import notificationRoute from "./routes/notifications.route" +import notificationRoute from "./routes/notifications.route"; const app = express(); const httpServer = http.createServer(app); const ioServer = new SocketIOServer(httpServer); @@ -39,12 +39,12 @@ app.use(cors()); app.use(cookieParser()); app.use(express.urlencoded({ extended: true })); app.use( - session({ - secret: "crafters1234", - resave: false, - saveUninitialized: true, - cookie: { secure: false }, - }) + session({ + secret: "crafters1234", + resave: false, + saveUninitialized: true, + cookie: { secure: false }, + }) ); app.use(passport.initialize()); app.use(passport.session()); @@ -61,22 +61,21 @@ app.use("/", vendorRoute); app.use("/", roleRoute); app.use("/", orderRoute); app.use("/", checkoutRoute); -app.use('/', googleAuthRoute); -app.use('/', subscriptionRoute); -app.use('/', notificationRoute) +app.use("/", googleAuthRoute); +app.use("/", subscriptionRoute); +app.use("/", notificationRoute); app.use("/api-docs", swaggerRoute); app.use("/admin", adminRoute); app.use("/", cartroute); app.use("/", wishlistroute); -app.use("/", TwoFaRoute) +app.use("/", TwoFaRoute); -cron.schedule('*/2 * * * * *', () => { - checkExpiredsProduct(); +cron.schedule("0 0 * * * *", () => { + checkExpiredsProduct(); }); const server = httpServer.listen(PORT, () => { - console.log(`Server running on Port ${PORT}`); - checkExpiredsProduct() + console.log(`Server running on Port ${PORT}`); + checkExpiredsProduct(); }); - export { app, server, ioServer }; diff --git a/src/services/rolesService.ts b/src/services/rolesService.ts index 65905c8..9321e23 100644 --- a/src/services/rolesService.ts +++ b/src/services/rolesService.ts @@ -117,11 +117,9 @@ export const approveVendorRequest = async (userId: string) => { }; export const rejectVendorRequest = async (userId: string) => { - const vendor = await Vendor.findOne({ where: { userId: userId } }); const user: any = await User.findOne({ where: { userId: userId } }); - if (!vendor) { return { message: "Vendor Request Not Found", status: 404 }; } else { @@ -131,7 +129,7 @@ export const rejectVendorRequest = async (userId: string) => { const transporter = nodemailer.createTransport({ service: "Gmail", auth: { - user: process.env.EMAIL_USER, + user: process.env.EMAIL, pass: process.env.EMAIL_PASS, }, }); @@ -139,13 +137,13 @@ export const rejectVendorRequest = async (userId: string) => { const mailOptions = { to: user.email, from: process.env.EMAIL_USER, - subject: "Vendor Request Approved", + subject: "Vendor Request Rejected", html: ` - Vendor Request Approved + Vendor Request Rejected
    -

    Welcome to Our E-commerce Platform

    +

    Verfiy You Email

    -

    Hello ${name},

    -

    Thank you for creating an account with us! We are thrilled to have you on board.

    -

    At Our E-commerce Platform, we offer a wide range of products to suit all your needs. To get started, click the button below to visit our store and explore our latest collections.

    - Visit our store -

    If you have any questions or need assistance, feel free to contact our support team.

    -

    Happy shopping!

    -

    Best regards,
    Crafter

    + + Verify Email +