From e14c4ba1409c0374eb27fb74f100e639b74ea75f Mon Sep 17 00:00:00 2001 From: boghdady Date: 2022年3月17日 20:03:26 +0200 Subject: [PATCH 01/19] Section 11- Reviews And Wishlist --- .vscode/settings.json | 2 +- models/productModel.js | 14 ++++- models/reviewModel.js | 75 ++++++++++++++++++++++++++ models/userModel.js | 17 ++++++ routes/addressRoute.js | 19 +++++++ routes/categoryRoute.js | 1 + routes/productRoute.js | 6 +++ routes/reviewRoute.js | 50 ++++++++++++++++++ routes/wishlistRoute.js | 19 +++++++ server.js | 6 +++ services/addressService.js | 56 ++++++++++++++++++++ services/handlersFactory.js | 17 +++++- services/productService.js | 2 +- services/reviewService.js | 42 +++++++++++++++ services/wishlistService.js | 56 ++++++++++++++++++++ utils/validators/reviewValidator.js | 81 +++++++++++++++++++++++++++++ 16 files changed, 458 insertions(+), 5 deletions(-) create mode 100644 models/reviewModel.js create mode 100644 routes/addressRoute.js create mode 100644 routes/reviewRoute.js create mode 100644 routes/wishlistRoute.js create mode 100644 services/addressService.js create mode 100644 services/reviewService.js create mode 100644 services/wishlistService.js create mode 100644 utils/validators/reviewValidator.js diff --git a/.vscode/settings.json b/.vscode/settings.json index 19f3e17..ce6d5a1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,5 @@ "Namespaces", "Structs" ], - "cSpell.words": ["uuidv"] + "cSpell.words": ["uuidv", "virtuals"] } diff --git a/models/productModel.js b/models/productModel.js index f5c29f3..a25cfb4 100644 --- a/models/productModel.js +++ b/models/productModel.js @@ -62,15 +62,27 @@ const productSchema = new mongoose.Schema( type: Number, min: [1, 'Rating must be above or equal 1.0'], max: [5, 'Rating must be below or equal 5.0'], + // set: (val) => Math.round(val * 10) / 10, // 3.3333 * 10 => 33.333 => 33 => 3.3 }, ratingsQuantity: { type: Number, default: 0, }, }, - { timestamps: true } + { + timestamps: true, + // to enable virtual populate + toJSON: { virtuals: true }, + toObject: { virtuals: true }, + } ); +productSchema.virtual('reviews', { + ref: 'Review', + foreignField: 'product', + localField: '_id', +}); + // Mongoose query middleware productSchema.pre(/^find/, function (next) { this.populate({ diff --git a/models/reviewModel.js b/models/reviewModel.js new file mode 100644 index 0000000..51eb359 --- /dev/null +++ b/models/reviewModel.js @@ -0,0 +1,75 @@ +const mongoose = require('mongoose'); +const Product = require('./productModel'); + +const reviewSchema = new mongoose.Schema( + { + title: { + type: String, + }, + ratings: { + type: Number, + min: [1, 'Min ratings value is 1.0'], + max: [5, 'Max ratings value is 5.0'], + required: [true, 'review ratings required'], + }, + user: { + type: mongoose.Schema.ObjectId, + ref: 'User', + required: [true, 'Review must belong to user'], + }, + // parent reference (one to many) + product: { + type: mongoose.Schema.ObjectId, + ref: 'Product', + required: [true, 'Review must belong to product'], + }, + }, + { timestamps: true } +); + +reviewSchema.pre(/^find/, function (next) { + this.populate({ path: 'user', select: 'name' }); + next(); +}); + +reviewSchema.statics.calcAverageRatingsAndQuantity = async function ( + productId +) { + const result = await this.aggregate([ + // Stage 1 : get all reviews in specific product + { + $match: { product: productId }, + }, + // Stage 2: Grouping reviews based on productID and calc avgRatings, ratingsQuantity + { + $group: { + _id: 'product', + avgRatings: { $avg: '$ratings' }, + ratingsQuantity: { $sum: 1 }, + }, + }, + ]); + + // console.log(result); + if (result.length> 0) { + await Product.findByIdAndUpdate(productId, { + ratingsAverage: result[0].avgRatings, + ratingsQuantity: result[0].ratingsQuantity, + }); + } else { + await Product.findByIdAndUpdate(productId, { + ratingsAverage: 0, + ratingsQuantity: 0, + }); + } +}; + +reviewSchema.post('save', async function () { + await this.constructor.calcAverageRatingsAndQuantity(this.product); +}); + +reviewSchema.post('remove', async function () { + await this.constructor.calcAverageRatingsAndQuantity(this.product); +}); + +module.exports = mongoose.model('Review', reviewSchema); diff --git a/models/userModel.js b/models/userModel.js index 3ccb366..96bb500 100644 --- a/models/userModel.js +++ b/models/userModel.js @@ -39,6 +39,23 @@ const userSchema = new mongoose.Schema( type: Boolean, default: true, }, + // child reference (one to many) + wishlist: [ + { + type: mongoose.Schema.ObjectId, + ref: 'Product', + }, + ], + addresses: [ + { + id: { type: mongoose.Schema.Types.ObjectId }, + alias: String, + details: String, + phone: String, + city: String, + postalCode: String, + }, + ], }, { timestamps: true } ); diff --git a/routes/addressRoute.js b/routes/addressRoute.js new file mode 100644 index 0000000..8340b70 --- /dev/null +++ b/routes/addressRoute.js @@ -0,0 +1,19 @@ +const express = require('express'); + +const authService = require('../services/authService'); + +const { + addAddress, + removeAddress, + getLoggedUserAddresses, +} = require('../services/addressService'); + +const router = express.Router(); + +router.use(authService.protect, authService.allowedTo('user')); + +router.route('/').post(addAddress).get(getLoggedUserAddresses); + +router.delete('/:addressId', removeAddress); + +module.exports = router; diff --git a/routes/categoryRoute.js b/routes/categoryRoute.js index 3172753..5419fd7 100644 --- a/routes/categoryRoute.js +++ b/routes/categoryRoute.js @@ -23,6 +23,7 @@ const subcategoriesRoute = require('./subCategoryRoute'); const router = express.Router(); +// Nested route router.use('/:categoryId/subcategories', subcategoriesRoute); router diff --git a/routes/productRoute.js b/routes/productRoute.js index e5169ac..e7571eb 100644 --- a/routes/productRoute.js +++ b/routes/productRoute.js @@ -16,9 +16,15 @@ const { resizeProductImages, } = require('../services/productService'); const authService = require('../services/authService'); +const reviewsRoute = require('./reviewRoute'); const router = express.Router(); +// POST /products/jkshjhsdjh2332n/reviews +// GET /products/jkshjhsdjh2332n/reviews +// GET /products/jkshjhsdjh2332n/reviews/87487sfww3 +router.use('/:productId/reviews', reviewsRoute); + router .route('/') .get(getProducts) diff --git a/routes/reviewRoute.js b/routes/reviewRoute.js new file mode 100644 index 0000000..654da75 --- /dev/null +++ b/routes/reviewRoute.js @@ -0,0 +1,50 @@ +const express = require('express'); + +const { + createReviewValidator, + updateReviewValidator, + getReviewValidator, + deleteReviewValidator, +} = require('../utils/validators/reviewValidator'); + +const { + getReview, + getReviews, + createReview, + updateReview, + deleteReview, + createFilterObj, + setProductIdAndUserIdToBody, +} = require('../services/reviewService'); + +const authService = require('../services/authService'); + +const router = express.Router({ mergeParams: true }); + +router + .route('/') + .get(createFilterObj, getReviews) + .post( + authService.protect, + authService.allowedTo('user'), + setProductIdAndUserIdToBody, + createReviewValidator, + createReview + ); +router + .route('/:id') + .get(getReviewValidator, getReview) + .put( + authService.protect, + authService.allowedTo('user'), + updateReviewValidator, + updateReview + ) + .delete( + authService.protect, + authService.allowedTo('user', 'manager', 'admin'), + deleteReviewValidator, + deleteReview + ); + +module.exports = router; diff --git a/routes/wishlistRoute.js b/routes/wishlistRoute.js new file mode 100644 index 0000000..5e6a90f --- /dev/null +++ b/routes/wishlistRoute.js @@ -0,0 +1,19 @@ +const express = require('express'); + +const authService = require('../services/authService'); + +const { + addProductToWishlist, + removeProductFromWishlist, + getLoggedUserWishlist, +} = require('../services/wishlistService'); + +const router = express.Router(); + +router.use(authService.protect, authService.allowedTo('user')); + +router.route('/').post(addProductToWishlist).get(getLoggedUserWishlist); + +router.delete('/:productId', removeProductFromWishlist); + +module.exports = router; diff --git a/server.js b/server.js index b3a6ca2..18dede1 100644 --- a/server.js +++ b/server.js @@ -15,6 +15,9 @@ const brandRoute = require('./routes/brandRoute'); const productRoute = require('./routes/productRoute'); const userRoute = require('./routes/userRoute'); const authRoute = require('./routes/authRoute'); +const reviewRoute = require('./routes/reviewRoute'); +const wishlistRoute = require('./routes/wishlistRoute'); +const addressRoute = require('./routes/addressRoute'); // Connect with db dbConnection(); @@ -38,6 +41,9 @@ app.use('/api/v1/brands', brandRoute); app.use('/api/v1/products', productRoute); app.use('/api/v1/users', userRoute); app.use('/api/v1/auth', authRoute); +app.use('/api/v1/reviews', reviewRoute); +app.use('/api/v1/wishlist', wishlistRoute); +app.use('/api/v1/addresses', addressRoute); app.all('*', (req, res, next) => { next(new ApiError(`Can't find this route: ${req.originalUrl}`, 400)); diff --git a/services/addressService.js b/services/addressService.js new file mode 100644 index 0000000..96a0b69 --- /dev/null +++ b/services/addressService.js @@ -0,0 +1,56 @@ +const asyncHandler = require('express-async-handler'); + +const User = require('../models/userModel'); + +// @desc Add address to user addresses list +// @route POST /api/v1/addresses +// @access Protected/User +exports.addAddress = asyncHandler(async (req, res, next) => { + // $addToSet => add address object to user addresses array if address not exist + const user = await User.findByIdAndUpdate( + req.user._id, + { + $addToSet: { addresses: req.body }, + }, + { new: true } + ); + + res.status(200).json({ + status: 'success', + message: 'Address added successfully.', + data: user.addresses, + }); +}); + +// @desc Remove address from user addresses list +// @route DELETE /api/v1/addresses/:addressId +// @access Protected/User +exports.removeAddress = asyncHandler(async (req, res, next) => { + // $pull => remove address object from user addresses array if addressId exist + const user = await User.findByIdAndUpdate( + req.user._id, + { + $pull: { addresses: { _id: req.params.addressId } }, + }, + { new: true } + ); + + res.status(200).json({ + status: 'success', + message: 'Address removed successfully.', + data: user.addresses, + }); +}); + +// @desc Get logged user addresses list +// @route GET /api/v1/addresses +// @access Protected/User +exports.getLoggedUserAddresses = asyncHandler(async (req, res, next) => { + const user = await User.findById(req.user._id).populate('addresses'); + + res.status(200).json({ + status: 'success', + results: user.addresses.length, + data: user.addresses, + }); +}); diff --git a/services/handlersFactory.js b/services/handlersFactory.js index 749718f..e29b07a 100644 --- a/services/handlersFactory.js +++ b/services/handlersFactory.js @@ -10,6 +10,9 @@ exports.deleteOne = (Model) => if (!document) { return next(new ApiError(`No document for this id ${id}`, 404)); } + + // Trigger "remove" event when update document + document.remove(); res.status(204).send(); }); @@ -24,6 +27,8 @@ exports.updateOne = (Model) => new ApiError(`No document for this id ${req.params.id}`, 404) ); } + // Trigger "save" event when update document + document.save(); res.status(200).json({ data: document }); }); @@ -33,10 +38,18 @@ exports.createOne = (Model) => res.status(201).json({ data: newDoc }); }); -exports.getOne = (Model) => +exports.getOne = (Model, populationOpt) => asyncHandler(async (req, res, next) => { const { id } = req.params; - const document = await Model.findById(id); + // 1) Build query + let query = Model.findById(id); + if (populationOpt) { + query = query.populate(populationOpt); + } + + // 2) Execute query + const document = await query; + if (!document) { return next(new ApiError(`No document for this id ${id}`, 404)); } diff --git a/services/productService.js b/services/productService.js index 725aeb3..db48e80 100644 --- a/services/productService.js +++ b/services/productService.js @@ -62,7 +62,7 @@ exports.getProducts = factory.getAll(Product, 'Products'); // @desc Get specific product by id // @route GET /api/v1/products/:id // @access Public -exports.getProduct = factory.getOne(Product); +exports.getProduct = factory.getOne(Product, 'reviews'); // @desc Create product // @route POST /api/v1/products diff --git a/services/reviewService.js b/services/reviewService.js new file mode 100644 index 0000000..0bfb075 --- /dev/null +++ b/services/reviewService.js @@ -0,0 +1,42 @@ +const factory = require('./handlersFactory'); +const Review = require('../models/reviewModel'); + +// Nested route +// GET /api/v1/products/:productId/reviews +exports.createFilterObj = (req, res, next) => { + let filterObject = {}; + if (req.params.productId) filterObject = { product: req.params.productId }; + req.filterObj = filterObject; + next(); +}; + +// @desc Get list of reviews +// @route GET /api/v1/reviews +// @access Public +exports.getReviews = factory.getAll(Review); + +// @desc Get specific review by id +// @route GET /api/v1/reviews/:id +// @access Public +exports.getReview = factory.getOne(Review); + +// Nested route (Create) +exports.setProductIdAndUserIdToBody = (req, res, next) => { + if (!req.body.product) req.body.product = req.params.productId; + if (!req.body.user) req.body.user = req.user._id; + next(); +}; +// @desc Create review +// @route POST /api/v1/reviews +// @access Private/Protect/User +exports.createReview = factory.createOne(Review); + +// @desc Update specific review +// @route PUT /api/v1/reviews/:id +// @access Private/Protect/User +exports.updateReview = factory.updateOne(Review); + +// @desc Delete specific review +// @route DELETE /api/v1/reviews/:id +// @access Private/Protect/User-Admin-Manager +exports.deleteReview = factory.deleteOne(Review); diff --git a/services/wishlistService.js b/services/wishlistService.js new file mode 100644 index 0000000..562d0e9 --- /dev/null +++ b/services/wishlistService.js @@ -0,0 +1,56 @@ +const asyncHandler = require('express-async-handler'); + +const User = require('../models/userModel'); + +// @desc Add product to wishlist +// @route POST /api/v1/wishlist +// @access Protected/User +exports.addProductToWishlist = asyncHandler(async (req, res, next) => { + // $addToSet => add productId to wishlist array if productId not exist + const user = await User.findByIdAndUpdate( + req.user._id, + { + $addToSet: { wishlist: req.body.productId }, + }, + { new: true } + ); + + res.status(200).json({ + status: 'success', + message: 'Product added successfully to your wishlist.', + data: user.wishlist, + }); +}); + +// @desc Remove product from wishlist +// @route DELETE /api/v1/wishlist/:productId +// @access Protected/User +exports.removeProductFromWishlist = asyncHandler(async (req, res, next) => { + // $pull => remove productId from wishlist array if productId exist + const user = await User.findByIdAndUpdate( + req.user._id, + { + $pull: { wishlist: req.params.productId }, + }, + { new: true } + ); + + res.status(200).json({ + status: 'success', + message: 'Product removed successfully from your wishlist.', + data: user.wishlist, + }); +}); + +// @desc Get logged user wishlist +// @route GET /api/v1/wishlist +// @access Protected/User +exports.getLoggedUserWishlist = asyncHandler(async (req, res, next) => { + const user = await User.findById(req.user._id).populate('wishlist'); + + res.status(200).json({ + status: 'success', + results: user.wishlist.length, + data: user.wishlist, + }); +}); diff --git a/utils/validators/reviewValidator.js b/utils/validators/reviewValidator.js new file mode 100644 index 0000000..f434056 --- /dev/null +++ b/utils/validators/reviewValidator.js @@ -0,0 +1,81 @@ +const { check, body } = require('express-validator'); +const validatorMiddleware = require('../../middlewares/validatorMiddleware'); +const Review = require('../../models/reviewModel'); + +exports.createReviewValidator = [ + check('title').optional(), + check('ratings') + .notEmpty() + .withMessage('ratings value required') + .isFloat({ min: 1, max: 5 }) + .withMessage('Ratings value must be between 1 to 5'), + check('user').isMongoId().withMessage('Invalid Review id format'), + check('product') + .isMongoId() + .withMessage('Invalid Review id format') + .custom((val, { req }) => + // Check if logged user create review before + Review.findOne({ user: req.user._id, product: req.body.product }).then( + (review) => { + console.log(review); + if (review) { + return Promise.reject( + new Error('You already created a review before') + ); + } + } + ) + ), + validatorMiddleware, +]; + +exports.getReviewValidator = [ + check('id').isMongoId().withMessage('Invalid Review id format'), + validatorMiddleware, +]; + +exports.updateReviewValidator = [ + check('id') + .isMongoId() + .withMessage('Invalid Review id format') + .custom((val, { req }) => + // Check review ownership before update + Review.findById(val).then((review) => { + if (!review) { + return Promise.reject(new Error(`There is no review with id ${val}`)); + } + + if (review.user._id.toString() !== req.user._id.toString()) { + return Promise.reject( + new Error(`Your are not allowed to perform this action`) + ); + } + }) + ), + validatorMiddleware, +]; + +exports.deleteReviewValidator = [ + check('id') + .isMongoId() + .withMessage('Invalid Review id format') + .custom((val, { req }) => { + // Check review ownership before update + if (req.user.role === 'user') { + return Review.findById(val).then((review) => { + if (!review) { + return Promise.reject( + new Error(`There is no review with id ${val}`) + ); + } + if (review.user._id.toString() !== req.user._id.toString()) { + return Promise.reject( + new Error(`Your are not allowed to perform this action`) + ); + } + }); + } + return true; + }), + validatorMiddleware, +]; From f615c555143915df9ab2581f8543a1b40bd1025b Mon Sep 17 00:00:00 2001 From: boghdady Date: 2022年3月24日 23:30:05 +0200 Subject: [PATCH 02/19] Section 12- Coupons and Shopping Cart --- models/cartModel.js | 29 ++++++ models/couponModel.js | 23 +++++ routes/cartRoute.js | 29 ++++++ routes/couponRoute.js | 20 +++++ routes/index.js | 27 ++++++ server.js | 20 +---- services/cartService.js | 180 ++++++++++++++++++++++++++++++++++++++ services/couponService.js | 27 ++++++ 8 files changed, 337 insertions(+), 18 deletions(-) create mode 100644 models/cartModel.js create mode 100644 models/couponModel.js create mode 100644 routes/cartRoute.js create mode 100644 routes/couponRoute.js create mode 100644 routes/index.js create mode 100644 services/cartService.js create mode 100644 services/couponService.js diff --git a/models/cartModel.js b/models/cartModel.js new file mode 100644 index 0000000..f0b3f35 --- /dev/null +++ b/models/cartModel.js @@ -0,0 +1,29 @@ +const mongoose = require('mongoose'); + +const cartSchema = new mongoose.Schema( + { + cartItems: [ + { + product: { + type: mongoose.Schema.ObjectId, + ref: 'Product', + }, + quantity: { + type: Number, + default: 1, + }, + color: String, + price: Number, + }, + ], + totalCartPrice: Number, + totalPriceAfterDiscount: Number, + user: { + type: mongoose.Schema.ObjectId, + ref: 'User', + }, + }, + { timestamps: true } +); + +module.exports = mongoose.model('Cart', cartSchema); diff --git a/models/couponModel.js b/models/couponModel.js new file mode 100644 index 0000000..024d388 --- /dev/null +++ b/models/couponModel.js @@ -0,0 +1,23 @@ +const mongoose = require('mongoose'); + +const couponSchema = new mongoose.Schema( + { + name: { + type: String, + trim: true, + required: [true, 'Coupon name required'], + unique: true, + }, + expire: { + type: Date, + required: [true, 'Coupon expire time required'], + }, + discount: { + type: Number, + required: [true, 'Coupon discount value required'], + }, + }, + { timestamps: true } +); + +module.exports = mongoose.model('Coupon', couponSchema); diff --git a/routes/cartRoute.js b/routes/cartRoute.js new file mode 100644 index 0000000..5adb092 --- /dev/null +++ b/routes/cartRoute.js @@ -0,0 +1,29 @@ +const express = require('express'); + +const { + addProductToCart, + getLoggedUserCart, + removeSpecificCartItem, + clearCart, + updateCartItemQuantity, + applyCoupon, +} = require('../services/cartService'); +const authService = require('../services/authService'); + +const router = express.Router(); + +router.use(authService.protect, authService.allowedTo('user')); +router + .route('/') + .post(addProductToCart) + .get(getLoggedUserCart) + .delete(clearCart); + +router.put('/applyCoupon', applyCoupon); + +router + .route('/:itemId') + .put(updateCartItemQuantity) + .delete(removeSpecificCartItem); + +module.exports = router; diff --git a/routes/couponRoute.js b/routes/couponRoute.js new file mode 100644 index 0000000..3686260 --- /dev/null +++ b/routes/couponRoute.js @@ -0,0 +1,20 @@ +const express = require('express'); + +const { + getCoupon, + getCoupons, + createCoupon, + updateCoupon, + deleteCoupon, +} = require('../services/couponService'); + +const authService = require('../services/authService'); + +const router = express.Router(); + +router.use(authService.protect, authService.allowedTo('admin', 'manager')); + +router.route('/').get(getCoupons).post(createCoupon); +router.route('/:id').get(getCoupon).put(updateCoupon).delete(deleteCoupon); + +module.exports = router; diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 0000000..80b9124 --- /dev/null +++ b/routes/index.js @@ -0,0 +1,27 @@ +const categoryRoute = require('./categoryRoute'); +const subCategoryRoute = require('./subCategoryRoute'); +const brandRoute = require('./brandRoute'); +const productRoute = require('./productRoute'); +const userRoute = require('./userRoute'); +const authRoute = require('./authRoute'); +const reviewRoute = require('./reviewRoute'); +const wishlistRoute = require('./wishlistRoute'); +const addressRoute = require('./addressRoute'); +const couponRoute = require('./couponRoute'); +const cartRoute = require('./cartRoute'); + +const mountRoutes = (app) => { + app.use('/api/v1/categories', categoryRoute); + app.use('/api/v1/subcategories', subCategoryRoute); + app.use('/api/v1/brands', brandRoute); + app.use('/api/v1/products', productRoute); + app.use('/api/v1/users', userRoute); + app.use('/api/v1/auth', authRoute); + app.use('/api/v1/reviews', reviewRoute); + app.use('/api/v1/wishlist', wishlistRoute); + app.use('/api/v1/addresses', addressRoute); + app.use('/api/v1/coupons', couponRoute); + app.use('/api/v1/cart', cartRoute); +}; + +module.exports = mountRoutes; diff --git a/server.js b/server.js index 18dede1..5f05111 100644 --- a/server.js +++ b/server.js @@ -9,15 +9,7 @@ const ApiError = require('./utils/apiError'); const globalError = require('./middlewares/errorMiddleware'); const dbConnection = require('./config/database'); // Routes -const categoryRoute = require('./routes/categoryRoute'); -const subCategoryRoute = require('./routes/subCategoryRoute'); -const brandRoute = require('./routes/brandRoute'); -const productRoute = require('./routes/productRoute'); -const userRoute = require('./routes/userRoute'); -const authRoute = require('./routes/authRoute'); -const reviewRoute = require('./routes/reviewRoute'); -const wishlistRoute = require('./routes/wishlistRoute'); -const addressRoute = require('./routes/addressRoute'); +const mountRoutes = require('./routes'); // Connect with db dbConnection(); @@ -35,15 +27,7 @@ if (process.env.NODE_ENV === 'development') { } // Mount Routes -app.use('/api/v1/categories', categoryRoute); -app.use('/api/v1/subcategories', subCategoryRoute); -app.use('/api/v1/brands', brandRoute); -app.use('/api/v1/products', productRoute); -app.use('/api/v1/users', userRoute); -app.use('/api/v1/auth', authRoute); -app.use('/api/v1/reviews', reviewRoute); -app.use('/api/v1/wishlist', wishlistRoute); -app.use('/api/v1/addresses', addressRoute); +mountRoutes(app); app.all('*', (req, res, next) => { next(new ApiError(`Can't find this route: ${req.originalUrl}`, 400)); diff --git a/services/cartService.js b/services/cartService.js new file mode 100644 index 0000000..13da5b9 --- /dev/null +++ b/services/cartService.js @@ -0,0 +1,180 @@ +const asyncHandler = require('express-async-handler'); +const ApiError = require('../utils/apiError'); + +const Product = require('../models/productModel'); +const Coupon = require('../models/couponModel'); +const Cart = require('../models/cartModel'); + +const calcTotalCartPrice = (cart) => { + let totalPrice = 0; + cart.cartItems.forEach((item) => { + totalPrice += item.quantity * item.price; + }); + cart.totalCartPrice = totalPrice; + cart.totalPriceAfterDiscount = undefined; + return totalPrice; +}; + +// @desc Add product to cart +// @route POST /api/v1/cart +// @access Private/User +exports.addProductToCart = asyncHandler(async (req, res, next) => { + const { productId, color } = req.body; + const product = await Product.findById(productId); + + // 1) Get Cart for logged user + let cart = await Cart.findOne({ user: req.user._id }); + + if (!cart) { + // create cart fot logged user with product + cart = await Cart.create({ + user: req.user._id, + cartItems: [{ product: productId, color, price: product.price }], + }); + } else { + // product exist in cart, update product quantity + const productIndex = cart.cartItems.findIndex( + (item) => item.product.toString() === productId && item.color === color + ); + + if (productIndex> -1) { + const cartItem = cart.cartItems[productIndex]; + cartItem.quantity += 1; + + cart.cartItems[productIndex] = cartItem; + } else { + // product not exist in cart, push product to cartItems array + cart.cartItems.push({ product: productId, color, price: product.price }); + } + } + + // Calculate total cart price + calcTotalCartPrice(cart); + await cart.save(); + + res.status(200).json({ + status: 'success', + message: 'Product added to cart successfully', + numOfCartItems: cart.cartItems.length, + data: cart, + }); +}); + +// @desc Get logged user cart +// @route GET /api/v1/cart +// @access Private/User +exports.getLoggedUserCart = asyncHandler(async (req, res, next) => { + const cart = await Cart.findOne({ user: req.user._id }); + + if (!cart) { + return next( + new ApiError(`There is no cart for this user id : ${req.user._id}`, 404) + ); + } + + res.status(200).json({ + status: 'success', + numOfCartItems: cart.cartItems.length, + data: cart, + }); +}); + +// @desc Remove specific cart item +// @route DELETE /api/v1/cart/:itemId +// @access Private/User +exports.removeSpecificCartItem = asyncHandler(async (req, res, next) => { + const cart = await Cart.findOneAndUpdate( + { user: req.user._id }, + { + $pull: { cartItems: { _id: req.params.itemId } }, + }, + { new: true } + ); + + calcTotalCartPrice(cart); + cart.save(); + + res.status(200).json({ + status: 'success', + numOfCartItems: cart.cartItems.length, + data: cart, + }); +}); + +// @desc clear logged user cart +// @route DELETE /api/v1/cart +// @access Private/User +exports.clearCart = asyncHandler(async (req, res, next) => { + await Cart.findOneAndDelete({ user: req.user._id }); + res.status(204).send(); +}); + +// @desc Update specific cart item quantity +// @route PUT /api/v1/cart/:itemId +// @access Private/User +exports.updateCartItemQuantity = asyncHandler(async (req, res, next) => { + const { quantity } = req.body; + + const cart = await Cart.findOne({ user: req.user._id }); + if (!cart) { + return next(new ApiError(`there is no cart for user ${req.user._id}`, 404)); + } + + const itemIndex = cart.cartItems.findIndex( + (item) => item._id.toString() === req.params.itemId + ); + if (itemIndex> -1) { + const cartItem = cart.cartItems[itemIndex]; + cartItem.quantity = quantity; + cart.cartItems[itemIndex] = cartItem; + } else { + return next( + new ApiError(`there is no item for this id :${req.params.itemId}`, 404) + ); + } + + calcTotalCartPrice(cart); + + await cart.save(); + + res.status(200).json({ + status: 'success', + numOfCartItems: cart.cartItems.length, + data: cart, + }); +}); + +// @desc Apply coupon on logged user cart +// @route PUT /api/v1/cart/applyCoupon +// @access Private/User +exports.applyCoupon = asyncHandler(async (req, res, next) => { + // 1) Get coupon based on coupon name + const coupon = await Coupon.findOne({ + name: req.body.coupon, + expire: { $gt: Date.now() }, + }); + + if (!coupon) { + return next(new ApiError(`Coupon is invalid or expired`)); + } + + // 2) Get logged user cart to get total cart price + const cart = await Cart.findOne({ user: req.user._id }); + + const totalPrice = cart.totalCartPrice; + + // 3) Calculate price after priceAfterDiscount + const totalPriceAfterDiscount = ( + totalPrice - + (totalPrice * coupon.discount) / 100 + ).toFixed(2); // 99.23 + + cart.totalPriceAfterDiscount = totalPriceAfterDiscount; + await cart.save(); + + res.status(200).json({ + status: 'success', + numOfCartItems: cart.cartItems.length, + data: cart, + }); +}); diff --git a/services/couponService.js b/services/couponService.js new file mode 100644 index 0000000..badc4fb --- /dev/null +++ b/services/couponService.js @@ -0,0 +1,27 @@ +const factory = require('./handlersFactory'); +const Coupon = require('../models/couponModel'); + +// @desc Get list of coupons +// @route GET /api/v1/coupons +// @access Private/Admin-Manager +exports.getCoupons = factory.getAll(Coupon); + +// @desc Get specific coupon by id +// @route GET /api/v1/coupons/:id +// @access Private/Admin-Manager +exports.getCoupon = factory.getOne(Coupon); + +// @desc Create coupon +// @route POST /api/v1/coupons +// @access Private/Admin-Manager +exports.createCoupon = factory.createOne(Coupon); + +// @desc Update specific coupon +// @route PUT /api/v1/coupons/:id +// @access Private/Admin-Manager +exports.updateCoupon = factory.updateOne(Coupon); + +// @desc Delete specific coupon +// @route DELETE /api/v1/coupons/:id +// @access Private/Admin-Manager +exports.deleteCoupon = factory.deleteOne(Coupon); From 36d67b392a03b92f7f83122ba247e5b5df17a1f5 Mon Sep 17 00:00:00 2001 From: boghdady Date: 2022年3月26日 16:50:09 +0200 Subject: [PATCH 03/19] push to heroku --- models/orderModel.js | 73 ++++++++++++++++++ package-lock.json | 146 +++++++++++++++++++++++++++++++++++ package.json | 3 + routes/index.js | 2 + routes/orderRoute.js | 44 +++++++++++ server.js | 9 +++ services/orderService.js | 161 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 438 insertions(+) create mode 100644 models/orderModel.js create mode 100644 routes/orderRoute.js create mode 100644 services/orderService.js diff --git a/models/orderModel.js b/models/orderModel.js new file mode 100644 index 0000000..43eb742 --- /dev/null +++ b/models/orderModel.js @@ -0,0 +1,73 @@ +const mongoose = require('mongoose'); + +const orderSchema = new mongoose.Schema( + { + user: { + type: mongoose.Schema.ObjectId, + ref: 'User', + required: [true, 'Order must be belong to user'], + }, + cartItems: [ + { + product: { + type: mongoose.Schema.ObjectId, + ref: 'Product', + }, + quantity: Number, + color: String, + price: Number, + }, + ], + + taxPrice: { + type: Number, + default: 0, + }, + shippingAddress: { + details: String, + phone: String, + city: String, + postalCode: String, + }, + shippingPrice: { + type: Number, + default: 0, + }, + totalOrderPrice: { + type: Number, + }, + paymentMethodType: { + type: String, + enum: ['card', 'cash'], + default: 'cash', + }, + isPaid: { + type: Boolean, + default: false, + }, + paidAt: Date, + isDelivered: { + type: Boolean, + default: false, + }, + deliveredAt: Date, + }, + { timestamps: true } +); + +orderSchema.pre(/^find/, function (next) { + this.populate({ + path: 'user', + select: 'name profileImg email phone', + }).populate({ + path: 'cartItems.product', + select: 'title imageCover ', + }); + + next(); +}); + +module.exports = mongoose.model('Order', orderSchema); + +// In@in2016 +//progahmedelsayed@gmail.com diff --git a/package-lock.json b/package-lock.json index 269ec12..4dfd3c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,8 @@ "dependencies": { "bcryptjs": "^2.4.3", "colors": "^1.4.0", + "compression": "^1.7.4", + "cors": "^2.8.5", "dotenv": "^10.0.0", "express": "^4.17.1", "express-async-handler": "^1.2.0", @@ -22,6 +24,7 @@ "nodemailer": "^6.7.2", "sharp": "^0.29.3", "slugify": "^1.6.3", + "stripe": "^8.212.0", "uuid": "^8.3.2" }, "devDependencies": { @@ -840,6 +843,60 @@ "node": ">=0.1.90" } }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -968,6 +1025,18 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -4489,6 +4558,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stripe": { + "version": "8.212.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.212.0.tgz", + "integrity": "sha512-xQ2uPMRAmRyOiMZktw3hY8jZ8LFR9lEQRPEaQ5WcDcn51kMyn46GeikOikxiFTHEN8PeKRdwtpz4yNArAvu/Kg==", + "dependencies": { + "@types/node": ">=8.1.0", + "qs": "^6.6.0" + }, + "engines": { + "node": "^8.1 ||>=10.*" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5582,6 +5663,53 @@ "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5692,6 +5820,15 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -8342,6 +8479,15 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "stripe": { + "version": "8.212.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.212.0.tgz", + "integrity": "sha512-xQ2uPMRAmRyOiMZktw3hY8jZ8LFR9lEQRPEaQ5WcDcn51kMyn46GeikOikxiFTHEN8PeKRdwtpz4yNArAvu/Kg==", + "requires": { + "@types/node": ">=8.1.0", + "qs": "^6.6.0" + } + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/package.json b/package.json index 6b59b27..2d9df3d 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "dependencies": { "bcryptjs": "^2.4.3", "colors": "^1.4.0", + "compression": "^1.7.4", + "cors": "^2.8.5", "dotenv": "^10.0.0", "express": "^4.17.1", "express-async-handler": "^1.2.0", @@ -24,6 +26,7 @@ "nodemailer": "^6.7.2", "sharp": "^0.29.3", "slugify": "^1.6.3", + "stripe": "^8.212.0", "uuid": "^8.3.2" }, "devDependencies": { diff --git a/routes/index.js b/routes/index.js index 80b9124..13987ff 100644 --- a/routes/index.js +++ b/routes/index.js @@ -9,6 +9,7 @@ const wishlistRoute = require('./wishlistRoute'); const addressRoute = require('./addressRoute'); const couponRoute = require('./couponRoute'); const cartRoute = require('./cartRoute'); +const orderRoute = require('./orderRoute'); const mountRoutes = (app) => { app.use('/api/v1/categories', categoryRoute); @@ -22,6 +23,7 @@ const mountRoutes = (app) => { app.use('/api/v1/addresses', addressRoute); app.use('/api/v1/coupons', couponRoute); app.use('/api/v1/cart', cartRoute); + app.use('/api/v1/orders', orderRoute); }; module.exports = mountRoutes; diff --git a/routes/orderRoute.js b/routes/orderRoute.js new file mode 100644 index 0000000..ee81d59 --- /dev/null +++ b/routes/orderRoute.js @@ -0,0 +1,44 @@ +const express = require('express'); +const { + createCashOrder, + findAllOrders, + findSpecificOrder, + filterOrderForLoggedUser, + updateOrderToPaid, + updateOrderToDelivered, + checkoutSession, +} = require('../services/orderService'); + +const authService = require('../services/authService'); + +const router = express.Router(); + +router.use(authService.protect); + +router.get( + '/checkout-session/:cartId', + authService.allowedTo('user'), + checkoutSession +); + +router.route('/:cartId').post(authService.allowedTo('user'), createCashOrder); +router.get( + '/', + authService.allowedTo('user', 'admin', 'manager'), + filterOrderForLoggedUser, + findAllOrders +); +router.get('/:id', findSpecificOrder); + +router.put( + '/:id/pay', + authService.allowedTo('admin', 'manager'), + updateOrderToPaid +); +router.put( + '/:id/deliver', + authService.allowedTo('admin', 'manager'), + updateOrderToDelivered +); + +module.exports = router; diff --git a/server.js b/server.js index 5f05111..f8a4ecd 100644 --- a/server.js +++ b/server.js @@ -3,6 +3,8 @@ const path = require('path'); const express = require('express'); const dotenv = require('dotenv'); const morgan = require('morgan'); +const cors = require('cors'); +const compression = require('compression'); dotenv.config({ path: 'config.env' }); const ApiError = require('./utils/apiError'); @@ -17,6 +19,13 @@ dbConnection(); // express app const app = express(); +// Enable other domains to access your application +app.use(cors()); +app.options('*', cors()); + +// compress all responses +app.use(compression()); + // Middlewares app.use(express.json()); app.use(express.static(path.join(__dirname, 'uploads'))); diff --git a/services/orderService.js b/services/orderService.js new file mode 100644 index 0000000..03c34a3 --- /dev/null +++ b/services/orderService.js @@ -0,0 +1,161 @@ +const stripe = require('stripe')(process.env.STRIPE_SECRET); +const asyncHandler = require('express-async-handler'); +const factory = require('./handlersFactory'); +const ApiError = require('../utils/apiError'); + +const Product = require('../models/productModel'); +const Cart = require('../models/cartModel'); +const Order = require('../models/orderModel'); + +// @desc create cash order +// @route POST /api/v1/orders/cartId +// @access Protected/User +exports.createCashOrder = asyncHandler(async (req, res, next) => { + // app settings + const taxPrice = 0; + const shippingPrice = 0; + + // 1) Get cart depend on cartId + const cart = await Cart.findById(req.params.cartId); + if (!cart) { + return next( + new ApiError(`There is no such cart with id ${req.params.cartId}`, 404) + ); + } + + // 2) Get order price depend on cart price "Check if coupon apply" + const cartPrice = cart.totalPriceAfterDiscount + ? cart.totalPriceAfterDiscount + : cart.totalCartPrice; + + const totalOrderPrice = cartPrice + taxPrice + shippingPrice; + + // 3) Create order with default paymentMethodType cash + const order = await Order.create({ + user: req.user._id, + cartItems: cart.cartItems, + shippingAddress: req.body.shippingAddress, + totalOrderPrice, + }); + + // 4) After creating order, decrement product quantity, increment product sold + if (order) { + const bulkOption = cart.cartItems.map((item) => ({ + updateOne: { + filter: { _id: item.product }, + update: { $inc: { quantity: -item.quantity, sold: +item.quantity } }, + }, + })); + await Product.bulkWrite(bulkOption, {}); + + // 5) Clear cart depend on cartId + await Cart.findByIdAndDelete(req.params.cartId); + } + + res.status(201).json({ status: 'success', data: order }); +}); + +exports.filterOrderForLoggedUser = asyncHandler(async (req, res, next) => { + if (req.user.role === 'user') req.filterObj = { user: req.user._id }; + next(); +}); +// @desc Get all orders +// @route POST /api/v1/orders +// @access Protected/User-Admin-Manager +exports.findAllOrders = factory.getAll(Order); + +// @desc Get all orders +// @route POST /api/v1/orders +// @access Protected/User-Admin-Manager +exports.findSpecificOrder = factory.getOne(Order); + +// @desc Update order paid status to paid +// @route PUT /api/v1/orders/:id/pay +// @access Protected/Admin-Manager +exports.updateOrderToPaid = asyncHandler(async (req, res, next) => { + const order = await Order.findById(req.params.id); + if (!order) { + return next( + new ApiError( + `There is no such a order with this id:${req.params.id}`, + 404 + ) + ); + } + + // update order to paid + order.isPaid = true; + order.paidAt = Date.now(); + + const updatedOrder = await order.save(); + + res.status(200).json({ status: 'success', data: updatedOrder }); +}); + +// @desc Update order delivered status +// @route PUT /api/v1/orders/:id/deliver +// @access Protected/Admin-Manager +exports.updateOrderToDelivered = asyncHandler(async (req, res, next) => { + const order = await Order.findById(req.params.id); + if (!order) { + return next( + new ApiError( + `There is no such a order with this id:${req.params.id}`, + 404 + ) + ); + } + + // update order to paid + order.isDelivered = true; + order.deliveredAt = Date.now(); + + const updatedOrder = await order.save(); + + res.status(200).json({ status: 'success', data: updatedOrder }); +}); + +// @desc Get checkout session from stripe and send it as response +// @route GET /api/v1/orders/checkout-session/cartId +// @access Protected/User +exports.checkoutSession = asyncHandler(async (req, res, next) => { + // app settings + const taxPrice = 0; + const shippingPrice = 0; + + // 1) Get cart depend on cartId + const cart = await Cart.findById(req.params.cartId); + if (!cart) { + return next( + new ApiError(`There is no such cart with id ${req.params.cartId}`, 404) + ); + } + + // 2) Get order price depend on cart price "Check if coupon apply" + const cartPrice = cart.totalPriceAfterDiscount + ? cart.totalPriceAfterDiscount + : cart.totalCartPrice; + + const totalOrderPrice = cartPrice + taxPrice + shippingPrice; + + // 3) Create stripe checkout session + const session = await stripe.checkout.sessions.create({ + line_items: [ + { + name: req.user.name, + amount: totalOrderPrice * 100, + currency: 'egp', + quantity: 1, + }, + ], + mode: 'payment', + success_url: `${req.protocol}://${req.get('host')}/orders`, + cancel_url: `${req.protocol}://${req.get('host')}/cart`, + customer_email: req.user.email, + client_reference_id: req.params.cartId, + metadata: req.body.shippingAddress, + }); + + // 4) send session to response + res.status(200).json({ status: 'success', session }); +}); From 6b833da90d2a06229c3cf9abcd589e83ce2e1912 Mon Sep 17 00:00:00 2001 From: boghdady Date: 2022年3月26日 17:03:49 +0200 Subject: [PATCH 04/19] push to heroku --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 2d9df3d..70a3e22 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "start:dev": "nodemon server.js", - "start:prod": "NODE_ENV=production node server.js" + "start": "NODE_ENV=production node server.js" }, "keywords": [], "author": "", @@ -40,5 +40,8 @@ "eslint-plugin-react": "^7.28.0", "nodemon": "^2.0.15", "prettier": "^2.5.1" + }, + "engines": { + "node": "^16.13.0" } } From cf317063e951df34e46edb728bad1b3b4ee20a2e Mon Sep 17 00:00:00 2001 From: boghdady Date: 2022年3月27日 00:12:02 +0200 Subject: [PATCH 05/19] add webhook --- server.js | 8 ++++++++ services/orderService.js | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/server.js b/server.js index f8a4ecd..446777f 100644 --- a/server.js +++ b/server.js @@ -12,6 +12,7 @@ const globalError = require('./middlewares/errorMiddleware'); const dbConnection = require('./config/database'); // Routes const mountRoutes = require('./routes'); +const { webhookCheckout } = require('./services/orderService'); // Connect with db dbConnection(); @@ -26,6 +27,13 @@ app.options('*', cors()); // compress all responses app.use(compression()); +// Checkout webhook +app.post( + '/webhook-checkout', + express.raw({ type: 'application/json' }), + webhookCheckout +); + // Middlewares app.use(express.json()); app.use(express.static(path.join(__dirname, 'uploads'))); diff --git a/services/orderService.js b/services/orderService.js index 03c34a3..61f0140 100644 --- a/services/orderService.js +++ b/services/orderService.js @@ -159,3 +159,22 @@ exports.checkoutSession = asyncHandler(async (req, res, next) => { // 4) send session to response res.status(200).json({ status: 'success', session }); }); + +exports.webhookCheckout = asyncHandler(async (req, res, next) => { + const sig = req.headers['stripe-signature']; + + let event; + + try { + event = stripe.webhooks.constructEvent( + req.body, + sig, + process.env.STRIPE_WEBHOOK_SECRET + ); + } catch (err) { + return res.status(400).send(`Webhook Error: ${err.message}`); + } + if (event.type === 'checkout.session.completed') { + console.log('Create Order Here......'); + } +}); From bfa4deab52b107d61a1bb35f7a2c73b17ab16c2f Mon Sep 17 00:00:00 2001 From: boghdady Date: 2022年3月27日 00:15:49 +0200 Subject: [PATCH 06/19] add webhook --- services/orderService.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/orderService.js b/services/orderService.js index 61f0140..81f115f 100644 --- a/services/orderService.js +++ b/services/orderService.js @@ -176,5 +176,6 @@ exports.webhookCheckout = asyncHandler(async (req, res, next) => { } if (event.type === 'checkout.session.completed') { console.log('Create Order Here......'); + console.log(event.data.object.client_reference_id); } }); From f6a21e0977157fe81d0e583be4ea020a50eaa4bc Mon Sep 17 00:00:00 2001 From: boghdady Date: 2022年3月27日 00:31:56 +0200 Subject: [PATCH 07/19] add webhook --- services/orderService.js | 44 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/services/orderService.js b/services/orderService.js index 81f115f..0e1b53a 100644 --- a/services/orderService.js +++ b/services/orderService.js @@ -3,6 +3,7 @@ const asyncHandler = require('express-async-handler'); const factory = require('./handlersFactory'); const ApiError = require('../utils/apiError'); +const User = require('../models/userModel'); const Product = require('../models/productModel'); const Cart = require('../models/cartModel'); const Order = require('../models/orderModel'); @@ -160,6 +161,43 @@ exports.checkoutSession = asyncHandler(async (req, res, next) => { res.status(200).json({ status: 'success', session }); }); +const createCardOrder = async (session) => { + const cartId = session.client_reference_id; + const shippingAddress = session.metadata; + const oderPrice = session.display_items[0].amount / 100; + + const cart = await Cart.findById(cartId); + const user = await User.findOne({ email: session.customer_email }); + + // 3) Create order with default paymentMethodType card + const order = await Order.create({ + user: user._id, + cartItems: cart.cartItems, + shippingAddress, + totalOrderPrice: oderPrice, + isPaid: true, + paidAt: Date.now(), + paymentMethodType: 'card', + }); + + // 4) After creating order, decrement product quantity, increment product sold + if (order) { + const bulkOption = cart.cartItems.map((item) => ({ + updateOne: { + filter: { _id: item.product }, + update: { $inc: { quantity: -item.quantity, sold: +item.quantity } }, + }, + })); + await Product.bulkWrite(bulkOption, {}); + + // 5) Clear cart depend on cartId + await Cart.findByIdAndDelete(cartId); + } +}; + +// @desc This webhook will run when stripe payment success paid +// @route POST /webhook-checkout +// @access Protected/User exports.webhookCheckout = asyncHandler(async (req, res, next) => { const sig = req.headers['stripe-signature']; @@ -175,7 +213,9 @@ exports.webhookCheckout = asyncHandler(async (req, res, next) => { return res.status(400).send(`Webhook Error: ${err.message}`); } if (event.type === 'checkout.session.completed') { - console.log('Create Order Here......'); - console.log(event.data.object.client_reference_id); + // Create order + createCardOrder(event.data.object); } + + res.status(200).json({ received: true }); }); From 9c50f793ffd1ce9ad48827454bff575bc45b4ff9 Mon Sep 17 00:00:00 2001 From: boghdady Date: 2022年3月27日 00:39:39 +0200 Subject: [PATCH 08/19] add webhook --- services/orderService.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/orderService.js b/services/orderService.js index 0e1b53a..6fa71a8 100644 --- a/services/orderService.js +++ b/services/orderService.js @@ -169,6 +169,8 @@ const createCardOrder = async (session) => { const cart = await Cart.findById(cartId); const user = await User.findOne({ email: session.customer_email }); + console.log(cart, user); + // 3) Create order with default paymentMethodType card const order = await Order.create({ user: user._id, From 3cc84674420669e4bc47bdf42e64d4d193b58ebf Mon Sep 17 00:00:00 2001 From: boghdady Date: 2022年3月27日 00:53:53 +0200 Subject: [PATCH 09/19] add webhook --- services/orderService.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/orderService.js b/services/orderService.js index 6fa71a8..0caf417 100644 --- a/services/orderService.js +++ b/services/orderService.js @@ -216,6 +216,8 @@ exports.webhookCheckout = asyncHandler(async (req, res, next) => { } if (event.type === 'checkout.session.completed') { // Create order + console.log('Create order.....'); + console.log(event.data.object); createCardOrder(event.data.object); } From d03a1ca8ef37b1479c6f79326e4ba9cb7a0f5a2f Mon Sep 17 00:00:00 2001 From: boghdady Date: 2022年3月27日 00:59:57 +0200 Subject: [PATCH 10/19] add webhook --- services/orderService.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/services/orderService.js b/services/orderService.js index 0caf417..e30dfe7 100644 --- a/services/orderService.js +++ b/services/orderService.js @@ -164,13 +164,11 @@ exports.checkoutSession = asyncHandler(async (req, res, next) => { const createCardOrder = async (session) => { const cartId = session.client_reference_id; const shippingAddress = session.metadata; - const oderPrice = session.display_items[0].amount / 100; + const oderPrice = session.amount_total / 100; const cart = await Cart.findById(cartId); const user = await User.findOne({ email: session.customer_email }); - console.log(cart, user); - // 3) Create order with default paymentMethodType card const order = await Order.create({ user: user._id, @@ -216,8 +214,6 @@ exports.webhookCheckout = asyncHandler(async (req, res, next) => { } if (event.type === 'checkout.session.completed') { // Create order - console.log('Create order.....'); - console.log(event.data.object); createCardOrder(event.data.object); } From b26cb2dd58397d2a6ae9eb23df640d4f771d6627 Mon Sep 17 00:00:00 2001 From: boghdady Date: 2022年3月27日 01:35:45 +0200 Subject: [PATCH 11/19] All Source code --- models/orderModel.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/models/orderModel.js b/models/orderModel.js index 43eb742..1dba1f7 100644 --- a/models/orderModel.js +++ b/models/orderModel.js @@ -69,5 +69,3 @@ orderSchema.pre(/^find/, function (next) { module.exports = mongoose.model('Order', orderSchema); -// In@in2016 -//progahmedelsayed@gmail.com From e1e936375136376a6da720e6a4a53126dc8c303a Mon Sep 17 00:00:00 2001 From: Ahmed Boghdady Date: Fri, 1 Apr 2022 13:17:58 +0200 Subject: [PATCH 12/19] Create README.md --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..f597524 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Course Material and FAQ for my NodeJS - Build a Full E-Commerce RESTful APIs (بالعربي) + +This repo contains starter files and the finished project files for all the projects contained in the course (complete repo size is **288MB**). + +Use starter code to start each section, and **final code to compare it with your own code whenever something doesn't work**! + + +👇 **_Please read the following Frequently Asked Questions (FAQ) carefully before starting the course_** 👇 + +## FAQ + +### Q1: How do I download the files? + +**A:** If you're new to GitHub and just want to download the entire code, hit the green button saying "Code", and then choose the "Download ZIP" option. + + +### Q2: I'm stuck in one of the projects. Where do I get help? + +**A:** Have you actually tried to fix the problem on your own? Have you compared your code to the final code? If you failed fixing your problem, please **post a detailed description of the problem to the Q&A area of that video over at Udemy**, along with a [codepen](https://codepen.io/pen/) containing your code. You will get help there. Please don't send me a personal message or email to fix coding problems. + + +### Q3: I want to put the project in my portfolio. Is that allowed? + +**A:** Absolutely! Just make sure you actually built it yourself by following the course, and that you understand what you did. What is **not allowed** is that you create your own course/videos/articles based on this course's content! + + +### Q4: Do you accept pull requests? + +**A:** No, for the simple reason that I want this repository to contain the _exact_ same code that is shown in the videos. However, please feel free to add an issue if you found one. From 6b5bdbcdf92e96d27e41f6ecaf2cfbde7905a2a6 Mon Sep 17 00:00:00 2001 From: Ahmed Boghdady Date: Fri, 1 Apr 2022 13:18:49 +0200 Subject: [PATCH 13/19] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f597524..267a104 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Course Material and FAQ for my NodeJS - Build a Full E-Commerce RESTful APIs (بالعربي) -This repo contains starter files and the finished project files for all the projects contained in the course (complete repo size is **288MB**). +This repo contains starter files and the finished project files for all the projects contained in the course Use starter code to start each section, and **final code to compare it with your own code whenever something doesn't work**! From 47c52818e39e854a109901490b814a9e40d3fc95 Mon Sep 17 00:00:00 2001 From: Ahmed Boghdady Date: Fri, 1 Apr 2022 13:21:33 +0200 Subject: [PATCH 14/19] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 267a104..ff7e21b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Course Material and FAQ for my NodeJS - Build a Full E-Commerce RESTful APIs (بالعربي) -This repo contains starter files and the finished project files for all the projects contained in the course +This repo contains every course section in branche and the finished project files for all the projects contained in the master branch -Use starter code to start each section, and **final code to compare it with your own code whenever something doesn't work**! +Choose the section branch that you study, and **final code to compare it with your own code whenever something doesn't work**! 👇 **_Please read the following Frequently Asked Questions (FAQ) carefully before starting the course_** 👇 From 220fbc8c72fc390e1cf685b932feb2b948c61757 Mon Sep 17 00:00:00 2001 From: Ahmed Boghdady Date: Fri, 1 Apr 2022 13:22:17 +0200 Subject: [PATCH 15/19] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ff7e21b..9a7cb88 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Course Material and FAQ for my NodeJS - Build a Full E-Commerce RESTful APIs (بالعربي) -This repo contains every course section in branche and the finished project files for all the projects contained in the master branch +This repo contains every course section in a single branch and the finished project files for all the projects contained in the master branch Choose the section branch that you study, and **final code to compare it with your own code whenever something doesn't work**! From b12ee5a2a479fcbb1ed88cd489dec48fc4a95187 Mon Sep 17 00:00:00 2001 From: Ahmed Boghdady Date: Fri, 1 Apr 2022 14:06:12 +0200 Subject: [PATCH 16/19] Update README.md --- README.md | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/README.md b/README.md index 9a7cb88..e935c72 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,72 @@ Choose the section branch that you study, and **final code to compare it with yo ### Q4: Do you accept pull requests? **A:** No, for the simple reason that I want this repository to contain the _exact_ same code that is shown in the videos. However, please feel free to add an issue if you found one. + + +## Course Highlights + +Project Overview + +خلال هذا القسم هيتم استعراض مشروع المتجر الإلكتروني اللي هيتم تنفيذه خلال هذا الكورس ... مهم جدا تتفرج عليه بتركيز عشان تكون عارف ايه المميزات اللي هتتنفذ خلال المشروع ده + +How Web Work + +خلال القسم ده هنتكلم شويه عن اساسيات النتورك وازاي الويب بيشتغل عشان كله يكون عنده الاساسيات اللي هنبني عليها اللي جاي وفي نفس الوقت نكون عارف احنا مكانا فين بالظبط وايه دورنا واحنا بنكتب كود + +Preparing Tools And Environment + +خلال القسم ده هنبدأ نجهز بيئة العمل بتاعتنا والمحرر اللي هنبدأ نشتغل عليه + +Preparing Express Server And Mongodb + +خلال القسم ده هنبدأ نجهز الاكسبريس اب بتاعنا ونبدأ ننشأ السيرفر ونربط التطبيق بتاعنا بالداتا بيز وكمان هنشرح الستراكشر بتاع الملفات اللي هنشتغل بيه خلال المشروع اللي هننفذه + +Categories CRUD Operations + +خلال القسم ده هنبدأ التنفيذ الفعل لفيتشر الاقسام داخل المتجر الالكتروني الاقسام دي ممكن تكون ملابس او الكترونيات ..إلى آخره. + +Advanced Error Handling & Adding Validation Layer + +من السكاشن المهمة جدا اللي هنشرح فيها ازاي اكسبريس بيتعامل مع الايرورز وهنبدأ نشوف ازاي نمسك الايرورز دي ونتحكم في شكلها والشكل النهائي اللي هيرجع للمستخدم وكمان هنشوف ازاي نمسك باقي الايرورز اللي ممكن تحصل في باقي التطبيق غير اكسبريس + +SubCategories CRUD & Brands CRUD Operations + +خلال القسم ده هنبدأ ننفذ الاقسام الفرعية اللي هتكون بتنتمي للاقسام الرئيسية بمعني ان القسم الرئيسي ينتمي ليه قسم او اكثر فرعي .. بالاضافه للعمل علي فيشتر البراندات + +Products CRUD Operations + +خلال القسم ده هنبدأ نشتغل علي فيتشر المنتج وهنشوف ازاي نعمل انشاء وتعديل وحذف للمنتج .. بالاضافة ازاي نعمل بحث وازاي نعمل ترتيب للمنتج سواء بسعره او عدد المبيعات للمنتج او غيره .. ازاي كمان نعمل فلتر للمنتج سواء بالقسم اللي بينتمي ليه واو العلامة التجارية وغيره + +Upload Single And Multiple Images And Image Processing + +خلال القسم ده هنشوف ازاي نعمل رفع لصوره واحدة او اكتر من صورة .. وهنشوف ازاي نحسن من العمليات اللي هتم علي الصورة عشان يحسن من الاداء .. وهنتعامل مع الايرورز اللي ممكن تظهرك لما ترفع فايل غير الصور .. وهنبدأ نضيف الصور للمنتج بتاعنا + +Authentication And Authorization + +خلال القسم ده هنشرح عمليه المصادقة بشكل تفصيلي وهنشوف ازاي تسجيل الدخول وانشاء الحساب ونسيت كلمه المرور وازاي بتعمل التوكن وازاي بنعمل عمليه التحقق عليه ..كمان هنشتغل علي صلاحيات المستخدمين وهيكون عندنا ادمن ومانجر ويوزر عادي وكل واحد ليه صلاحيات مختلفة عن التاني... القسم ده مهم جدا وهتستفاد منه جدا + +Reviews, Wishlist And User Addresses + +خلال القسم ده هنبدأ نشتغل علي التقييمات وهنشوف ازاي هنمكن المتسخدم انه يضيف تقييم علي المنتجات وكمان هنحسب متوسط عدد التقييمات علي المنتج الواحد بالاضافة للعدد الكلي للتقيمات علي المنتج الواحد ، كمان هنشرح ازاي نمكن المسخدم انه يضيف منتج لقائمة المفضلة وفي نفس الوقت يقدر يحذفه ، كمان هنمكن المستخدم من انه يضيف عنوان لدفتر العناوين بتاعه يقدر يستخدمه لما يجي يطلب اوردر . + +Coupons And Shopping Cart + +خلال القسم ده هنبدأ نمكن الادمن من انه ينشأ الكوبونات وكل كوبون بيكون ليه تاريخ معين ينتهي فيه ونسبة خصم معينة بيحددها الادمن ... والمستخدم هيقدر يستخدم الكوبون ده عشان يتسفاد من الخصم .. كمان هنمكن المستخدم من انه ينشأ سلة المنتجات اللي هيبدأ يضيف فيها المنتجات اللي عايز يشتريها ويعدل يختار ويعدل في كمية المنتجات لو متاح كمية منها في المخزن بالاضافة انه يقدر يضيف كوبون خصم علي السلة . + +Cash And Online Orders, Online Payments And Deployments + +خلال القسم ده هنبدأ نشتغل علي الاورد ر او الطلبية سواء الاوردر ده هيتم دفعه كاش او عند الاستلام او الاوردر ده هيتم دفعه من خلال بطاقة دفع او محفظة الكترنية زي ابل باي او غيره .. هيتم الربط مع بوابة الدفع ونشوف ايه وسائل الدفع اللي بتوفرها بوابة الدفع وهنعمل عميلة الدفع من خلالها ... وهنشوف ازاي بنشوف عملية الدفع نجحت ولا لا .. وازاي نعمل اوردر في حالة نجاح عملية الدفع .. هنتكلم بالتفصيل عن الدفع الكاش والدفع الالكتروني .. وفي الاخر هنرفع التطبيق علي هيروكو عشان تقدر تشاركه مع الفرونت اند او تحط اللينك في البرورتفوليو بتاعك + +Security + +خلال القسم ده هنتكلم شويه عن وسائل الامان اللي ممكن تستخدمها عشان تأمن التطبيق بتاعك + +Enhancements + +خلال القسم ده هنضيف فيه التحسينات اللي هتتضاف في الكورس ... بالاضافة لو فيه مشاكل ظهرت هنسجلها فيديو ونضيفه في السكشن ده + +Appendix + +خلال القسم ده هضفلكم شويه دروس عن الجافا سكريبت عشان ترجعو ليها لو عايز تتاسس فيها عشان تساعدك وانت شغال في الكورس + + From db4de4c8c2c07e2f9b309d47586a923020ca4c86 Mon Sep 17 00:00:00 2001 From: Ahmed Boghdady Date: Fri, 1 Apr 2022 14:08:35 +0200 Subject: [PATCH 17/19] Update README.md --- README.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index e935c72..21ce3b8 100644 --- a/README.md +++ b/README.md @@ -31,67 +31,67 @@ Choose the section branch that you study, and **final code to compare it with yo ## Course Highlights -Project Overview +1- Project Overview خلال هذا القسم هيتم استعراض مشروع المتجر الإلكتروني اللي هيتم تنفيذه خلال هذا الكورس ... مهم جدا تتفرج عليه بتركيز عشان تكون عارف ايه المميزات اللي هتتنفذ خلال المشروع ده -How Web Work +2- How Web Work خلال القسم ده هنتكلم شويه عن اساسيات النتورك وازاي الويب بيشتغل عشان كله يكون عنده الاساسيات اللي هنبني عليها اللي جاي وفي نفس الوقت نكون عارف احنا مكانا فين بالظبط وايه دورنا واحنا بنكتب كود -Preparing Tools And Environment +3- Preparing Tools And Environment خلال القسم ده هنبدأ نجهز بيئة العمل بتاعتنا والمحرر اللي هنبدأ نشتغل عليه -Preparing Express Server And Mongodb +4- Preparing Express Server And Mongodb خلال القسم ده هنبدأ نجهز الاكسبريس اب بتاعنا ونبدأ ننشأ السيرفر ونربط التطبيق بتاعنا بالداتا بيز وكمان هنشرح الستراكشر بتاع الملفات اللي هنشتغل بيه خلال المشروع اللي هننفذه -Categories CRUD Operations +5- Categories CRUD Operations خلال القسم ده هنبدأ التنفيذ الفعل لفيتشر الاقسام داخل المتجر الالكتروني الاقسام دي ممكن تكون ملابس او الكترونيات ..إلى آخره. -Advanced Error Handling & Adding Validation Layer +6- Advanced Error Handling & Adding Validation Layer من السكاشن المهمة جدا اللي هنشرح فيها ازاي اكسبريس بيتعامل مع الايرورز وهنبدأ نشوف ازاي نمسك الايرورز دي ونتحكم في شكلها والشكل النهائي اللي هيرجع للمستخدم وكمان هنشوف ازاي نمسك باقي الايرورز اللي ممكن تحصل في باقي التطبيق غير اكسبريس -SubCategories CRUD & Brands CRUD Operations +7- SubCategories CRUD & Brands CRUD Operations خلال القسم ده هنبدأ ننفذ الاقسام الفرعية اللي هتكون بتنتمي للاقسام الرئيسية بمعني ان القسم الرئيسي ينتمي ليه قسم او اكثر فرعي .. بالاضافه للعمل علي فيشتر البراندات -Products CRUD Operations +8- Products CRUD Operations خلال القسم ده هنبدأ نشتغل علي فيتشر المنتج وهنشوف ازاي نعمل انشاء وتعديل وحذف للمنتج .. بالاضافة ازاي نعمل بحث وازاي نعمل ترتيب للمنتج سواء بسعره او عدد المبيعات للمنتج او غيره .. ازاي كمان نعمل فلتر للمنتج سواء بالقسم اللي بينتمي ليه واو العلامة التجارية وغيره -Upload Single And Multiple Images And Image Processing +9- Upload Single And Multiple Images And Image Processing خلال القسم ده هنشوف ازاي نعمل رفع لصوره واحدة او اكتر من صورة .. وهنشوف ازاي نحسن من العمليات اللي هتم علي الصورة عشان يحسن من الاداء .. وهنتعامل مع الايرورز اللي ممكن تظهرك لما ترفع فايل غير الصور .. وهنبدأ نضيف الصور للمنتج بتاعنا -Authentication And Authorization +10- Authentication And Authorization خلال القسم ده هنشرح عمليه المصادقة بشكل تفصيلي وهنشوف ازاي تسجيل الدخول وانشاء الحساب ونسيت كلمه المرور وازاي بتعمل التوكن وازاي بنعمل عمليه التحقق عليه ..كمان هنشتغل علي صلاحيات المستخدمين وهيكون عندنا ادمن ومانجر ويوزر عادي وكل واحد ليه صلاحيات مختلفة عن التاني... القسم ده مهم جدا وهتستفاد منه جدا -Reviews, Wishlist And User Addresses +11- Reviews, Wishlist And User Addresses خلال القسم ده هنبدأ نشتغل علي التقييمات وهنشوف ازاي هنمكن المتسخدم انه يضيف تقييم علي المنتجات وكمان هنحسب متوسط عدد التقييمات علي المنتج الواحد بالاضافة للعدد الكلي للتقيمات علي المنتج الواحد ، كمان هنشرح ازاي نمكن المسخدم انه يضيف منتج لقائمة المفضلة وفي نفس الوقت يقدر يحذفه ، كمان هنمكن المستخدم من انه يضيف عنوان لدفتر العناوين بتاعه يقدر يستخدمه لما يجي يطلب اوردر . -Coupons And Shopping Cart +12- Coupons And Shopping Cart خلال القسم ده هنبدأ نمكن الادمن من انه ينشأ الكوبونات وكل كوبون بيكون ليه تاريخ معين ينتهي فيه ونسبة خصم معينة بيحددها الادمن ... والمستخدم هيقدر يستخدم الكوبون ده عشان يتسفاد من الخصم .. كمان هنمكن المستخدم من انه ينشأ سلة المنتجات اللي هيبدأ يضيف فيها المنتجات اللي عايز يشتريها ويعدل يختار ويعدل في كمية المنتجات لو متاح كمية منها في المخزن بالاضافة انه يقدر يضيف كوبون خصم علي السلة . -Cash And Online Orders, Online Payments And Deployments +13- Cash And Online Orders, Online Payments And Deployments خلال القسم ده هنبدأ نشتغل علي الاورد ر او الطلبية سواء الاوردر ده هيتم دفعه كاش او عند الاستلام او الاوردر ده هيتم دفعه من خلال بطاقة دفع او محفظة الكترنية زي ابل باي او غيره .. هيتم الربط مع بوابة الدفع ونشوف ايه وسائل الدفع اللي بتوفرها بوابة الدفع وهنعمل عميلة الدفع من خلالها ... وهنشوف ازاي بنشوف عملية الدفع نجحت ولا لا .. وازاي نعمل اوردر في حالة نجاح عملية الدفع .. هنتكلم بالتفصيل عن الدفع الكاش والدفع الالكتروني .. وفي الاخر هنرفع التطبيق علي هيروكو عشان تقدر تشاركه مع الفرونت اند او تحط اللينك في البرورتفوليو بتاعك -Security +14- Security خلال القسم ده هنتكلم شويه عن وسائل الامان اللي ممكن تستخدمها عشان تأمن التطبيق بتاعك -Enhancements +15- Enhancements خلال القسم ده هنضيف فيه التحسينات اللي هتتضاف في الكورس ... بالاضافة لو فيه مشاكل ظهرت هنسجلها فيديو ونضيفه في السكشن ده -Appendix +16- Appendix خلال القسم ده هضفلكم شويه دروس عن الجافا سكريبت عشان ترجعو ليها لو عايز تتاسس فيها عشان تساعدك وانت شغال في الكورس From 6956e29f1f1ae85cb9d5d4c39f4ad5077f3f3235 Mon Sep 17 00:00:00 2001 From: Ahmed Boghdady Date: Mon, 4 Apr 2022 00:57:54 +0200 Subject: [PATCH 18/19] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 21ce3b8..aaba21f 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ This repo contains every course section in a single branch and the finished pro Choose the section branch that you study, and **final code to compare it with your own code whenever something doesn't work**! +## Join To Discord Channel For Updates [discord](https://discord.gg/e2nwBNU2q9) + 👇 **_Please read the following Frequently Asked Questions (FAQ) carefully before starting the course_** 👇 From c3a7e13e49fcee7d6f633dd6bf007206eee04910 Mon Sep 17 00:00:00 2001 From: boghdady Date: Fri, 8 Apr 2022 02:14:38 +0200 Subject: [PATCH 19/19] Section 14- Security Best Practice And Recomentations --- package-lock.json | 43 +++++++++++++++++++++++++++++++++++++++++++ package.json | 2 ++ server.js | 28 +++++++++++++++++++++++++++- utils/apiFeatures.js | 2 +- 4 files changed, 73 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4dfd3c0..e4b6ad1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,9 @@ "dotenv": "^10.0.0", "express": "^4.17.1", "express-async-handler": "^1.2.0", + "express-rate-limit": "^6.3.0", "express-validator": "^6.14.0", + "hpp": "^0.2.3", "jsonwebtoken": "^8.5.1", "mongoose": "^6.0.13", "morgan": "^1.10.0", @@ -38,6 +40,9 @@ "eslint-plugin-react": "^7.28.0", "nodemon": "^2.0.15", "prettier": "^2.5.1" + }, + "engines": { + "node": "^16.13.0" } }, "node_modules/@babel/runtime": { @@ -1942,6 +1947,17 @@ "resolved": "https://registry.npmjs.org/express-async-handler/-/express-async-handler-1.2.0.tgz", "integrity": "sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w==" }, + "node_modules/express-rate-limit": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.3.0.tgz", + "integrity": "sha512-932Io1VGKjM3ppi7xW9sb1J5nVkEJSUiOtHw2oE+JyHks1e+AXuOBSXbJKM0mcXwEnW1TibJibQ455Ow1YFjfg==", + "engines": { + "node": ">= 12.9.0" + }, + "peerDependencies": { + "express": "^4" + } + }, "node_modules/express-validator": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.14.0.tgz", @@ -2395,6 +2411,18 @@ "node": ">=8" } }, + "node_modules/hpp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hpp/-/hpp-0.2.3.tgz", + "integrity": "sha512-4zDZypjQcxK/8pfFNR7jaON7zEUpXZxz4viyFmqjb3kWNWAHsLEUmWXcdn25c5l76ISvnD6hbOGO97cXUI3Ryw==", + "dependencies": { + "lodash": "^4.17.12", + "type-is": "^1.6.12" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", @@ -6526,6 +6554,12 @@ "resolved": "https://registry.npmjs.org/express-async-handler/-/express-async-handler-1.2.0.tgz", "integrity": "sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w==" }, + "express-rate-limit": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.3.0.tgz", + "integrity": "sha512-932Io1VGKjM3ppi7xW9sb1J5nVkEJSUiOtHw2oE+JyHks1e+AXuOBSXbJKM0mcXwEnW1TibJibQ455Ow1YFjfg==", + "requires": {} + }, "express-validator": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.14.0.tgz", @@ -6867,6 +6901,15 @@ "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", "dev": true }, + "hpp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hpp/-/hpp-0.2.3.tgz", + "integrity": "sha512-4zDZypjQcxK/8pfFNR7jaON7zEUpXZxz4viyFmqjb3kWNWAHsLEUmWXcdn25c5l76ISvnD6hbOGO97cXUI3Ryw==", + "requires": { + "lodash": "^4.17.12", + "type-is": "^1.6.12" + } + }, "http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", diff --git a/package.json b/package.json index 70a3e22..8ff5848 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,9 @@ "dotenv": "^10.0.0", "express": "^4.17.1", "express-async-handler": "^1.2.0", + "express-rate-limit": "^6.3.0", "express-validator": "^6.14.0", + "hpp": "^0.2.3", "jsonwebtoken": "^8.5.1", "mongoose": "^6.0.13", "morgan": "^1.10.0", diff --git a/server.js b/server.js index 446777f..5647f56 100644 --- a/server.js +++ b/server.js @@ -5,6 +5,8 @@ const dotenv = require('dotenv'); const morgan = require('morgan'); const cors = require('cors'); const compression = require('compression'); +const rateLimit = require('express-rate-limit'); +const hpp = require('hpp'); dotenv.config({ path: 'config.env' }); const ApiError = require('./utils/apiError'); @@ -35,7 +37,7 @@ app.post( ); // Middlewares -app.use(express.json()); +app.use(express.json({ limit: '20kb' })); app.use(express.static(path.join(__dirname, 'uploads'))); if (process.env.NODE_ENV === 'development') { @@ -43,6 +45,30 @@ if (process.env.NODE_ENV === 'development') { console.log(`mode: ${process.env.NODE_ENV}`); } +// Limit each IP to 100 requests per `window` (here, per 15 minutes) +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, + message: + 'Too many accounts created from this IP, please try again after an hour', +}); + +// Apply the rate limiting middleware to all requests +app.use('/api', limiter); + +// Middleware to protect against HTTP Parameter Pollution attacks +app.use( + hpp({ + whitelist: [ + 'price', + 'sold', + 'quantity', + 'ratingsAverage', + 'ratingsQuantity', + ], + }) +); + // Mount Routes mountRoutes(app); diff --git a/utils/apiFeatures.js b/utils/apiFeatures.js index af51e2e..e2edc59 100644 --- a/utils/apiFeatures.js +++ b/utils/apiFeatures.js @@ -45,7 +45,7 @@ class ApiFeatures { { title: { $regex: this.queryString.keyword, $options: 'i' } }, { description: { $regex: this.queryString.keyword, $options: 'i' } }, ]; - } else { + } else { query = { name: { $regex: this.queryString.keyword, $options: 'i' } }; }

AltStyle によって変換されたページ (->オリジナル) /