├── .gitignore ├── .gitattributes ├── middlewares ├── not_found.js └── auth.js ├── utils ├── createTokenUser.js ├── fakeStripeApi.js ├── checkPermissions.js └── jwt.js ├── routes ├── auth.js ├── review.js ├── user.js ├── order.js └── product.js ├── db └── mongoDb.js ├── package.json ├── models ├── order.js ├── user.js ├── review.js └── product.js ├── app.js ├── controllers ├── user.js ├── auth.js ├── order.js ├── product.js └── review.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules/ 3 | 4 | todo.txt 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /middlewares/not_found.js: -------------------------------------------------------------------------------- 1 | const notFound = (req, res) => res.status(404).send('Route does not exist') 2 | 3 | module.exports = notFound -------------------------------------------------------------------------------- /utils/createTokenUser.js: -------------------------------------------------------------------------------- 1 | const createTokenUser = (user) => { 2 | return { name: user.name, userId: user._id, role: user.role }; 3 | }; 4 | 5 | module.exports = createTokenUser; -------------------------------------------------------------------------------- /utils/fakeStripeApi.js: -------------------------------------------------------------------------------- 1 | // just a fake stripe api to get the client secret and amount to pay 2 | const fakeStripeAPI = async ({ amount, currency }) => { 3 | const client_secret = 'someRandomValue'; 4 | return { client_secret, amount }; 5 | }; 6 | 7 | 8 | module.exports = fakeStripeAPI; -------------------------------------------------------------------------------- /utils/checkPermissions.js: -------------------------------------------------------------------------------- 1 | 2 | const checkPermissions = (requestUser, resourceUserId) => { 3 | if (requestUser.role === 'admin') return; 4 | if (requestUser.userId === resourceUserId.toString()) return; 5 | return res.send(403).json({ msg: 'you are not allowed to do this' }); 6 | }; 7 | 8 | module.exports = checkPermissions; -------------------------------------------------------------------------------- /routes/auth.js: -------------------------------------------------------------------------------- 1 | 2 | const authRouter = require("express").Router(); 3 | const { login, register, logout } = require("../controllers/auth"); 4 | 5 | 6 | authRouter.route("/login").post(login); 7 | authRouter.route("/register").post(register); 8 | authRouter.route("/logout").get(logout); 9 | 10 | 11 | module.exports = authRouter; -------------------------------------------------------------------------------- /db/mongoDb.js: -------------------------------------------------------------------------------- 1 | const {connect,set} = require( 'mongoose'); 2 | 3 | set('strictQuery', true); 4 | const connectDb = (dbURI)=>{ 5 | connect(dbURI,{useNewUrlParser:true, useUnifiedTopology:true}).then((result)=>console.log("connected to db " + result.connections[0].name) 6 | ).catch((err)=>console.log(err)); 7 | } 8 | 9 | module.exports = { connectDb }; 10 | -------------------------------------------------------------------------------- /routes/review.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { authMiddleware } = require('../middlewares/auth'); 4 | 5 | const { 6 | createReview, 7 | getAllReviews, 8 | getSingleReview, 9 | updateReview, 10 | deleteReview, 11 | } = require('../controllers/review'); 12 | 13 | router.route('/').post(authMiddleware, createReview).get(getAllReviews); 14 | 15 | router 16 | .route('/:id') 17 | .get(getSingleReview) 18 | .patch(authMiddleware, updateReview) 19 | .delete(authMiddleware, deleteReview); 20 | 21 | module.exports = router; -------------------------------------------------------------------------------- /routes/user.js: -------------------------------------------------------------------------------- 1 | const { getAllUsers,getUserById,UpdateUser,UpdateUserPassword,getCurrentUser } = require("../controllers/user"); 2 | const { authMiddleware, authorizeRoles } = require("../middlewares/auth"); 3 | 4 | const userRouter = require("express").Router(); 5 | 6 | userRouter.route("/").get(authorizeRoles("admin"),getAllUsers); 7 | userRouter.route("/me").get(getCurrentUser); 8 | userRouter.route("/update").patch(UpdateUser); 9 | userRouter.route("/update/password").patch(UpdateUserPassword); 10 | userRouter.route("/:id").get(getUserById) 11 | 12 | 13 | module.exports = userRouter; -------------------------------------------------------------------------------- /routes/order.js: -------------------------------------------------------------------------------- 1 | 2 | const router = require('express').Router(); 3 | const { 4 | authMiddleware, 5 | authorizeRoles, 6 | } = require('../middlewares/auth'); 7 | 8 | const { 9 | getAllOrders, 10 | getSingleOrder, 11 | getCurrentUserOrders, 12 | createOrder, 13 | updateOrder, 14 | } = require('../controllers/order'); 15 | 16 | router 17 | .route('/') 18 | .post(authMiddleware, createOrder) 19 | .get(authMiddleware, authorizeRoles('admin'), getAllOrders); 20 | 21 | router.route('/me').get(authMiddleware, getCurrentUserOrders); 22 | 23 | router 24 | .route('/:id') 25 | .get(authMiddleware, getSingleOrder) 26 | .patch(authMiddleware, updateOrder); 27 | 28 | module.exports = router; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecommerce-api-nodejs", 3 | "version": "1.0.0", 4 | "description": "an ecommerce api", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "fares bekkouche", 10 | "license": "ISC", 11 | "dependencies": { 12 | "bcryptjs": "^2.4.3", 13 | "cookie-parser": "^1.4.6", 14 | "cookies-parser": "^1.2.0", 15 | "cors": "^2.8.5", 16 | "dotenv": "^16.1.0", 17 | "express": "^4.18.2", 18 | "express-fileupload": "^1.4.0", 19 | "express-mongo-sanitize": "^2.2.0", 20 | "express-rate-limit": "^6.7.0", 21 | "express-rate-limiter": "^1.3.1", 22 | "helmet": "^7.0.0", 23 | "jsonwebtoken": "^9.0.0", 24 | "mongoose": "^7.2.1", 25 | "validator": "^13.9.0", 26 | "xss-clean": "^0.1.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /utils/jwt.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken") 2 | 3 | const isTokenValid = (tk) => jwt.verify(tk,process.env.JWT_SECRET); 4 | 5 | const createJwt = ({payload})=>{ 6 | return jwt.sign(payload,process.env.JWT_SECRET,{ 7 | expiresIn: process.env.JWT_LIFETIME, 8 | }); 9 | } 10 | 11 | // this is for setup the cookies for web , with cookies , 12 | const attachCookiesToResponse = ({ res, user }) => { 13 | const token = createJwt({ payload: user }); 14 | 15 | const oneDay = 1000 * 60 * 60 * 24; 16 | 17 | res.cookie('token', token, { 18 | httpOnly: true, 19 | expires: new Date(Date.now() + oneDay), 20 | secure: process.env.NODE_ENV === 'production', 21 | signed: true, 22 | }); 23 | }; 24 | 25 | module.exports = { 26 | isTokenValid, 27 | createJwt, 28 | attachCookiesToResponse 29 | } -------------------------------------------------------------------------------- /routes/product.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | 3 | const {createProduct,getAllProducts,uploadImage,getSingleProduct,updateProduct,deleteProduct} = require("../controllers/product"); 4 | const { authorizeRoles, authMiddleware } = require("../middlewares/auth"); 5 | 6 | // const { getSingleProductReviews } = require('../controllers/reviewController'); 7 | 8 | router 9 | .route('/') 10 | .get(getAllProducts) 11 | .post([authMiddleware, authorizeRoles('admin')], createProduct) 12 | 13 | router 14 | .route('/:id') 15 | .get(getSingleProduct) 16 | .patch([authMiddleware, authorizeRoles('admin')], updateProduct) 17 | .delete([authMiddleware, authorizeRoles('admin')], deleteProduct); 18 | 19 | router 20 | .route('/uploadImage') 21 | .post([authMiddleware, authorizeRoles('admin')],uploadImage); 22 | // router.route('/:id/reviews').get(getSingleProductReviews); 23 | 24 | module.exports = router; -------------------------------------------------------------------------------- /middlewares/auth.js: -------------------------------------------------------------------------------- 1 | const { isTokenValid } = require('../utils/jwt'); 2 | 3 | const authMiddleware = async (req, res, next) => { 4 | 5 | // check first if send in header 6 | let token; 7 | const authHeader = req.headers.authorization; 8 | if (authHeader && authHeader.startsWith('Bearer')) { 9 | token = authHeader.split(' ')[1]; 10 | } 11 | 12 | // otherwise check if sent in cookies 13 | else if (req.cookies.token) { 14 | token = req.cookies.token; 15 | } 16 | 17 | 18 | 19 | if (!token) { 20 | return res.status(401).json({ msg: 'you are not authorized' }); 21 | } 22 | try { 23 | const payload = isTokenValid(token); 24 | 25 | // do something if not valid 26 | 27 | req.user = { 28 | userId: payload.user.userId, 29 | role: payload.user.role, 30 | }; 31 | 32 | next(); 33 | } catch (error) { 34 | console.log(error); 35 | res.status(401).json({ msg: 'something went wrong ',error }); 36 | } 37 | }; 38 | 39 | const authorizeRoles = (...roles) => { 40 | return (req, res, next) => { 41 | if (!roles.includes(req.user.role)) { 42 | return res.status(403).json({ msg: 'you are not allowed to do this' }); 43 | } 44 | next(); 45 | }; 46 | }; 47 | 48 | module.exports = { authMiddleware, authorizeRoles }; -------------------------------------------------------------------------------- /models/order.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const SingleOrderItemSchema = new mongoose.Schema({ 4 | name: { type: String, required: true }, 5 | image: { type: String, required: true }, 6 | price: { type: Number, required: true }, 7 | amount: { type: Number, required: true }, 8 | product: { 9 | type: mongoose.Schema.ObjectId, 10 | ref: 'Product', 11 | required: true, 12 | }, 13 | }); 14 | 15 | const OrderSchema = new mongoose.Schema( 16 | { 17 | tax: { 18 | type: Number, 19 | required: true, 20 | }, 21 | shippingFee: { 22 | type: Number, 23 | required: true, 24 | }, 25 | subtotal: { 26 | type: Number, 27 | required: true, 28 | }, 29 | total: { 30 | type: Number, 31 | required: true, 32 | }, 33 | orderItems: [SingleOrderItemSchema], 34 | status: { 35 | type: String, 36 | enum: ['pending', 'failed', 'paid', 'delivered', 'canceled'], 37 | default: 'pending', 38 | }, 39 | user: { 40 | type: mongoose.Schema.ObjectId, 41 | ref: 'User', 42 | required: true, 43 | }, 44 | clientSecret: { 45 | type: String, 46 | required: true, 47 | }, 48 | paymentIntentId: { 49 | type: String, 50 | }, 51 | }, 52 | { timestamps: true } 53 | ); 54 | 55 | module.exports = mongoose.model('Order', OrderSchema); -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const validatorJs = require('validator'); 3 | const bcrypt = require('bcryptjs'); 4 | 5 | const UserSchema = new mongoose.Schema( 6 | { 7 | name : { 8 | type:String, 9 | required: [true, 'Please provide name'], 10 | maxlength: 50, 11 | minlength: 3, 12 | }, 13 | email :{ 14 | type:String, 15 | required: [true,"pls provide email"], 16 | unique: true, 17 | validator:{ 18 | validator:validatorJs.isEmail, 19 | message:"please provide valid email" 20 | } 21 | }, 22 | password:{ 23 | type:String, 24 | required :[true,"pls provide password"], 25 | minlength:8, 26 | }, 27 | role: { 28 | type: String, 29 | enum: ['admin', 'user'], 30 | default: 'user', 31 | }, 32 | } 33 | ); 34 | UserSchema.pre('save', async function () { 35 | if (!this.isModified('password')) return; // check if the password got changed if so we hash it 36 | const salt = await bcrypt.genSalt(10); 37 | this.password = await bcrypt.hash(this.password, salt); 38 | }); 39 | 40 | 41 | UserSchema.methods.comparePassword = async function (canditatePassword) { // we can check this in controller but this better for better SOLID 42 | const isMatch = await bcrypt.compare(canditatePassword, this.password); 43 | return isMatch; 44 | }; 45 | 46 | module.exports = mongoose.model("User",UserSchema); -------------------------------------------------------------------------------- /models/review.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const ReviewSchema = new mongoose.Schema( 4 | { 5 | rating: { 6 | type: Number, 7 | min: 1, 8 | max: 5, 9 | required: [true, 'Please provide rating'], 10 | }, 11 | title: { 12 | type: String, 13 | trim: true, 14 | required: [true, 'Please provide review title'], 15 | maxlength: 100, 16 | }, 17 | comment: { 18 | type: String, 19 | required: [true, 'Please provide review text'], 20 | }, 21 | user: { 22 | type: mongoose.Schema.ObjectId, 23 | ref: 'User', 24 | required: true, 25 | }, 26 | product: { 27 | type: mongoose.Schema.ObjectId, 28 | ref: 'Product', 29 | required: true, 30 | }, 31 | }, 32 | { timestamps: true } 33 | ); 34 | 35 | 36 | // $ 37 | ReviewSchema.index({ product: 1, user: 1 }, { unique: true }); 38 | 39 | ReviewSchema.statics.calculateAverageRating = async function (productId) { 40 | const result = await this.aggregate([ 41 | { $match: { product: productId } }, 42 | { 43 | $group: { 44 | _id: null, 45 | averageRating: { $avg: '$rating' }, 46 | numOfReviews: { $sum: 1 }, 47 | }, 48 | }, 49 | ]); 50 | 51 | try { 52 | await this.model('Product').findOneAndUpdate( 53 | { _id: productId }, 54 | { 55 | averageRating: Math.ceil(result[0]?.averageRating || 0), 56 | numOfReviews: result[0]?.numOfReviews || 0, 57 | } 58 | ); 59 | } catch (error) { 60 | console.log(error); 61 | } 62 | }; 63 | 64 | ReviewSchema.post('save', async function () { 65 | await this.constructor.calculateAverageRating(this.product); 66 | }); 67 | 68 | ReviewSchema.post('remove', async function () { 69 | await this.constructor.calculateAverageRating(this.product); 70 | }); 71 | 72 | module.exports = mongoose.model('Review', ReviewSchema); -------------------------------------------------------------------------------- /models/product.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const ProductSchema = new mongoose.Schema( 4 | { 5 | name: { 6 | type: String, 7 | trim: true, 8 | required: [true, 'Please provide product name'], 9 | maxlength: [100, 'Name can not be more than 100 characters'], 10 | }, 11 | price: { 12 | type: Number, 13 | required: [true, 'Please provide product price'], 14 | default: 0, 15 | }, 16 | description: { 17 | type: String, 18 | required: [true, 'Please provide product description'], 19 | maxlength: [1000, 'Description can not be more than 1000 characters'], 20 | }, 21 | image: { 22 | type: String, 23 | default: '/uploads/example.jpeg', 24 | }, 25 | category: { 26 | type: String, 27 | required: [true, 'Please provide product category'], 28 | enum: ['office', 'kitchen', 'bedroom'], 29 | }, 30 | company: { 31 | type: String, 32 | required: [true, 'Please provide company'], 33 | enum: { 34 | values: ['ikea', 'liddy', 'marcos'], 35 | message: '{VALUE} is not supported', 36 | }, 37 | }, 38 | colors: { 39 | type: [String], 40 | default: ['#222'], 41 | required: true, 42 | }, 43 | featured: { 44 | type: Boolean, 45 | default: false, 46 | }, 47 | freeShipping: { 48 | type: Boolean, 49 | default: false, 50 | }, 51 | inventory: { 52 | type: Number, 53 | required: true, 54 | default: 15, 55 | }, 56 | averageRating: { 57 | type: Number, 58 | default: 0, 59 | }, 60 | numOfReviews: { 61 | type: Number, 62 | default: 0, 63 | }, 64 | user: { 65 | type: mongoose.Types.ObjectId, 66 | ref: 'User', 67 | required: true, 68 | }, 69 | }, 70 | { timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } } 71 | ); 72 | 73 | 74 | // the fuck is this, ##todo 75 | ProductSchema.virtual('reviews', { 76 | ref: 'Review', 77 | localField: '_id', 78 | foreignField: 'product', 79 | justOne: false, 80 | }); 81 | 82 | 83 | // when deleting a product we go to the review db and remove it from reviews 84 | ProductSchema.pre('remove', async function (next) { 85 | await this.model('Review').deleteMany({ product: this._id }); 86 | }); 87 | 88 | module.exports = mongoose.model('Product', ProductSchema); -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const express = require('express'); 3 | const { connectDb } = require('./db/mongoDb'); 4 | const notFound = require('./middlewares/not_found'); 5 | const authRouter = require('./routes/auth'); 6 | const orderRouter = require('./routes/order'); 7 | const productRouter = require('./routes/product'); 8 | const reviewRouter = require('./routes/review'); 9 | const userRouter = require('./routes/user'); 10 | const cors = require('cors'); 11 | const xss = require('xss-clean'); 12 | const helmet = require('helmet'); 13 | const cookieParser = require('cookie-parser'); 14 | const fieUpload = require('express-fileupload'); 15 | const rateLimiter = require('express-rate-limit'); 16 | const { authMiddleware } = require('./middlewares/auth'); 17 | const mongoSanitize = require('express-mongo-sanitize'); 18 | 19 | 20 | // initliaze express app 21 | const app = express(); 22 | 23 | 24 | // middlewares 25 | 26 | // setting our trust proxy to true to allow heroku to trust our proxy 27 | app.set('trust proxy', 1); 28 | 29 | // limiting the number of requests to our api 30 | app.use( 31 | rateLimiter({ 32 | windowMs: 15 * 60 * 1000, 33 | max: 60, 34 | }) 35 | ); 36 | 37 | // security 38 | app.use(helmet()); 39 | app.use(cors()); 40 | app.use(xss()); 41 | app.use(mongoSanitize()); 42 | 43 | // serving static files 44 | app.use(express.static('./views')); 45 | 46 | // parsing json data 47 | app.use(express.json()); 48 | 49 | 50 | // parsing form data 51 | app.use(express.urlencoded({extended:true})); 52 | 53 | // cookie parser 54 | app.use(cookieParser(process.env.JWT_SECRET)); 55 | 56 | // file upload middleware to upload images 57 | app.use(fieUpload()); 58 | 59 | 60 | // routes 61 | app.get("/",(req,res)=>{ 62 | return res.send("Welcome to Ecommerce Api"); 63 | }) 64 | 65 | // routes 66 | app.use("/api/v1/auth",authRouter); 67 | app.use("/api/v1/orders",orderRouter); 68 | app.use("/api/v1/products",productRouter); 69 | app.use("/api/v1/reviews",reviewRouter); 70 | app.use("/api/v1/users",authMiddleware,userRouter); 71 | 72 | 73 | // not found middleware 74 | app.use(notFound); 75 | 76 | const port = process.env.PORT || 5000; 77 | 78 | 79 | // starting point of the server 80 | let main = async ()=>{ 81 | try { 82 | await connectDb(process.env.MONGO_URI); 83 | app.listen(port, () => { 84 | console.log('Server is running on http://localhost:' + port); 85 | } 86 | ); 87 | } catch (error) { 88 | console.log(error); 89 | } 90 | } 91 | 92 | main(); 93 | 94 | 95 | -------------------------------------------------------------------------------- /controllers/user.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/user'); 2 | const createTokenUser = require('../utils/createTokenUser'); 3 | const { attachCookiesToResponse } = require('../utils/jwt'); 4 | 5 | let getAllUsers = async (req,res)=>{ 6 | try { 7 | const users = await User.find({role:"user"}).select("-password"); 8 | return res.status(200).json({users}); 9 | } catch (error) { 10 | return res.status(500).json({msg:"server error" , error:error}); 11 | } 12 | } 13 | 14 | let getUserById = async (req,res)=>{ 15 | try { 16 | const user = await User.findById(req.params.id).select("-password"); 17 | return res.status(200).json({user}); 18 | } catch (error) { 19 | return res.status(500).json({msg:"server error" , error:error}); 20 | } 21 | } 22 | 23 | 24 | let UpdateUserPassword = async (req,res)=>{ 25 | try { 26 | const { oldPassword, newPassword } = req.body; 27 | if(!oldPassword || !newPassword){ 28 | return res.status(400).json({ msg: 'oldpassword or new password invalid' }) 29 | } 30 | const user = await User.findById(req.user.userId); 31 | if(!user){ 32 | return res.status(400).json({ msg: 'no user found' }) 33 | } 34 | if (!user.comparePassword(oldPassword)) { 35 | return res.status(400).json({ msg: 'incorrect password' }) 36 | } 37 | user.password = newPassword; 38 | await user.save(); 39 | } catch (error) { 40 | return res.status(500).json({msg:"server error" , error:error}); 41 | } 42 | 43 | } 44 | 45 | let UpdateUser = async (req,res)=>{ 46 | try { 47 | const { name, email } = req.body; 48 | const user = await User.findById(req.user.userId); 49 | if(!user){ 50 | return res.status(400).json({ msg: 'no user found' }) 51 | } 52 | user.name = name; 53 | user.email = email; 54 | await user.save(); 55 | const tokenUser = createTokenUser(user); 56 | attachCookiesToResponse({ res, user: tokenUser }); 57 | return res.status(200).json({msg:"user updated successfully",user:tokenUser}); 58 | 59 | } catch (error) { 60 | return res.status(500).json({msg:"server error" , error:error}); 61 | } 62 | } 63 | 64 | let getCurrentUser = async (req,res)=>{ 65 | try { 66 | return res.status(200).json({user:req.user}); 67 | } catch (error) { 68 | return res.status(500).json({msg:"server error" , error:error}); 69 | } 70 | } 71 | 72 | 73 | 74 | 75 | module.exports = { 76 | getAllUsers, 77 | getUserById, 78 | UpdateUserPassword, 79 | UpdateUser, 80 | getCurrentUser 81 | } 82 | -------------------------------------------------------------------------------- /controllers/auth.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/user'); 2 | const { createJwt, attachCookiesToResponse } = require('../utils/jwt'); 3 | const createTokenUser = require('../utils/createTokenUser'); 4 | 5 | 6 | let register = async (req,res)=>{ 7 | const { name, email, password } = req.body; 8 | if (!name || !password || !email) { 9 | return res.status(400).json({ 10 | msg: 'fill all the credentials', 11 | }) 12 | } 13 | const emailExist = await User.findOne({email}); 14 | if(emailExist){ 15 | return res.status(400).json({ msg: 'email already exist' }); 16 | } 17 | 18 | 19 | // check if he is the first user , if he is the first user , then make him admin 20 | const isFirstAccount = (await User.countDocuments({})) === 0; 21 | 22 | 23 | // ternary operator 24 | const role = isFirstAccount ? 'admin' : 'user'; 25 | 26 | // create the user 27 | const user = await User.create({name,email,password,role}); 28 | 29 | 30 | // create the user token that contains the user id and name and role 31 | const userForToken = createTokenUser(user); 32 | 33 | // create the jwt token 34 | // let token = createJwt(userForToken); 35 | attachCookiesToResponse({res,user:userForToken}); 36 | 37 | res.status(200).json({ msg: 'user created',userForToken }); 38 | } 39 | let login = async (req,res)=>{ 40 | try { 41 | const { email, password } = req.body; 42 | if (!email || !password) { 43 | return res.status(400).json({ msg: 'fill all the credentials' }) 44 | } 45 | 46 | const user = await User.findOne({email}); 47 | if(!user){ 48 | return res.status(400).json({ msg: 'no user found' }) 49 | } 50 | if (!user.comparePassword(password)) { 51 | return res.status(400).json({ msg: 'incorrect password' }) 52 | } 53 | 54 | 55 | // create the user token that contains the user id and name and role 56 | const userForToken = createTokenUser(user); 57 | 58 | // create the jwt token 59 | // let token = createJwt(userForToken); 60 | 61 | // create the cookies for web , with cookies 62 | attachCookiesToResponse({res,user:userForToken}); 63 | res.status(200).json({ msg : "user logged in Successfully ", userForToken }) 64 | 65 | } catch (error) { 66 | console.log(error); 67 | return res.status(500).json({ msg: 'something went wrong',error }) 68 | 69 | } 70 | } 71 | 72 | const logout = async (req, res) => { 73 | try { 74 | // removing the cookie 75 | res.cookie('token', 'logout', { 76 | httpOnly: true, 77 | expires: new Date(Date.now() + 1000), 78 | }); 79 | res.status(StatusCodes.OK).json({ msg: 'user logged out!' }); 80 | } catch (error) { 81 | console.log(error); 82 | return res.status(500).json({ msg: 'something went wrong',error }) 83 | } 84 | }; 85 | 86 | module.exports = { 87 | login, 88 | register, 89 | logout 90 | } 91 | 92 | 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js E-Commerce API 2 | 3 | - This full E-Commerce API build using Express and MongoDb, and other Npm Packages listed below , for learning purposes. Here it contains all the required functionalities of a full-fledged E-commerce API like User registration, User Login, Reviews Add, Edit & Delete, Product Add, Edit, Delete, Add product feature image & Add product images, Order creation and etc..., 4 | 5 | ## Setup 6 | 7 | git clone https://github.com/1Farz1/Ecommerce-Api-NodeJs.git 8 | cd Ecommerce-Api-NodeJs 9 | npm install 10 | 11 | ### create the .env file and fill it with your own credentials 12 | 13 | ## Run The Service 14 | 15 | nodemon app.js 16 | 17 | ## API Endpoints 18 | 19 | ### Auth Routes 20 | 21 | - Create a new User (first used flagged as admin) 22 | - ```POST | /api/v1/auth/register``` 23 | - Login User 24 | - ```POST | /api/v1/auth/login``` 25 | 26 | ### User Routes 27 | 28 | - Get Users (only for admin) 29 | - ```GET | /api/v1/users``` 30 | - Get Single Users 31 | - ```GET | /api/v1/users/{id}``` 32 | - Update current User 33 | - ```PUT | /api/v1/users/me``` 34 | - Update Current User Password 35 | - ```PUT | /api/v1/users/me/updatepassword``` 36 | - Get current User signed In 37 | - ```GET | /api/v1/users/me``` 38 | - Get Users Count 39 | - ```GET | /api/v1/users/get/count``` 40 | 41 | ## Review Routes 42 | 43 | - Create Review 44 | - ```POST | /api/v1/reviews``` 45 | - Get Reviews 46 | - ```GET | /api/v1/reviews``` 47 | 48 | - Get Single Review 49 | - ```GET | /api/v1/reviews/{id}``` 50 | 51 | - Update Review 52 | - ```PUT | /api/v1/reviews/{id}``` 53 | 54 | - Delete Review 55 | - ```DELETE | /api/v1/reviews/{id}``` 56 | 57 | ## Product Routes 58 | 59 | - Create Product 60 | - ```POST | /api/v1/products``` 61 | - Get Products 62 | - ```GET | /api/v1/products``` 63 | - Get Single Product 64 | - ```GET | /api/v1/products/{id}``` 65 | - Upload Product Images 66 | - ```PUT | /api/v1/products/gallery-images/{id}``` 67 | - Update Product 68 | - ```PUT | /api/v1/products/{id}``` 69 | - Delete Product 70 | - ```DELETE | /api/v1/products/{id}``` 71 | 72 | ## Orders Routes 73 | 74 | - Create Order 75 | - ```POST | /api/v1/orders``` 76 | - Get Orders 77 | - ```GET | /api/v1/orders``` 78 | - Get Single Order 79 | - ```GET | /api/v1/orders/{id}``` 80 | - Get Total Order Count 81 | - ```GET | /api/v1/orders/get/count``` 82 | - Get Total Sales 83 | - ```GET | /api/v1/orders/get/totalsales``` 84 | - Get User Order 85 | - ```GET | /api/v1/orders/get/userorders/{userid}``` 86 | - Update Single Order 87 | - ```PUT | /api/v1/orders/{id}``` 88 | - Delete Single Order 89 | - ```DELETE | /api/v1/orders/{id}``` 90 | 91 | ## links 92 | - postman link : https://bold-star-484214.postman.co/workspace/Team-Workspace~c069bf4c-0db1-4e9a-875e-5dd4df65a669/collection/21855039-e1fd0f5e-943b-4ff6-a16b-34f62af44b65?action=share&creator=21855039 93 | 94 | ## Tech used 95 | 96 | - Nodejs 97 | - express 98 | - mongoDb 99 | - mongoose 100 | - dotenv 101 | - cookie-parser 102 | - json-web-token 103 | - validator 104 | - express-rate-limit 105 | - helmet 106 | - bcryptjs 107 | - xss-clean 108 | - morgna 109 | - cors 110 | - express-file-upload 111 | - Postman 112 | 113 | ## Features of Code 114 | 115 | - Maintainble and Scalable 116 | - following best Practises and Clean Code Concepts 117 | - Easy To Follow and Read 118 | - Follow The View-Controller-Repository Architecture 119 | - Feature First layer 120 | 121 | ## Author 122 | 123 | - Fares Bekkouche 124 | 125 | ## Contrubution 126 | 127 | - for any contrubution you re more then Welcomed 128 | 129 | ```Enjoy it While it Lasts``` 130 | -------------------------------------------------------------------------------- /controllers/order.js: -------------------------------------------------------------------------------- 1 | 2 | const Order = require('../models/order'); 3 | const Product = require('../models/product'); 4 | const checkPermissions = require('../utils/checkPermissions'); 5 | const fakeStripeAPI = require('../utils/fakeStripeApi'); 6 | 7 | const getAllOrders = async (req, res) => { 8 | try { 9 | const orders = await Order.find({}); 10 | res.status(200).json({ orders, count: orders.length }); 11 | } catch (error) { 12 | res.status(400).json({ error: error.message }); 13 | 14 | } 15 | }; 16 | 17 | let getSingleOrder = async (req, res) => { 18 | try { 19 | const { id } = req.params; 20 | const order= await Order.findById(id); 21 | if(!order){ 22 | return res.status(404).json({ 23 | success: false, 24 | error: `Order with id ${id} not found`, 25 | }); 26 | } 27 | checkPermissions(req.user,order.user); 28 | return res.status(200).json({ 29 | success: true, 30 | order, 31 | }); 32 | } catch (error) { 33 | return res.status(400).json({ 34 | success: false, 35 | error: error.message, 36 | }); 37 | } 38 | } 39 | 40 | let getCurrentUserOrders = async (req, res) => { 41 | const orders= await Order.find({user:req.user._id}); 42 | if(!orders){ 43 | return res.status(404).json({ 44 | success: false, 45 | error: 'Orders with this user not found', 46 | });} 47 | return res.status(200).json({orders,count:orders.length}); 48 | } 49 | 50 | 51 | 52 | 53 | // need to revise this 54 | const createOrder = async (req, res) => { 55 | const { items: cartItems, tax, shippingFee } = req.body; 56 | 57 | if (!cartItems || cartItems.length < 1) { 58 | return res.status(400).json({ msg: 'No items in cart' }); 59 | } 60 | if (!tax || !shippingFee) { 61 | return res.status(400).json({ msg: 'Please provide tax and shipping fee' }); 62 | } 63 | 64 | let orderItems = []; 65 | let subtotal = 0; 66 | 67 | for (const item of cartItems) { 68 | const dbProduct = await Product.findbyId(item.product); 69 | if (!dbProduct) { 70 | return res.status(400).json({ msg: `No product with id : ${item.product}` }); 71 | } 72 | const { name, price, image, _id } = dbProduct; 73 | const singleOrderItem = { 74 | amount: item.amount, 75 | name, 76 | price, 77 | image, 78 | product: _id, 79 | }; 80 | orderItems = [...orderItems, singleOrderItem]; 81 | // calculate subtotal 82 | 83 | // price of the items 84 | subtotal += item.amount * price; 85 | } 86 | 87 | // totol price including tax and shipping fee 88 | const total = tax + shippingFee + subtotal; 89 | 90 | 91 | // create the payment intent 92 | const paymentIntent = await fakeStripeAPI({ 93 | amount: total, 94 | currency: 'usd', 95 | }); 96 | 97 | 98 | // creating the order 99 | const order = await Order.create({ 100 | orderItems, 101 | total, 102 | subtotal, 103 | tax, 104 | shippingFee, 105 | clientSecret: paymentIntent.client_secret, 106 | user: req.user.userId, 107 | }); 108 | 109 | res 110 | .status(200) 111 | .json({ order, clientSecret: order.clientSecret }); 112 | }; 113 | 114 | let updateOrder = async (req, res) => { 115 | 116 | const { id } = req.params; 117 | const { paymentIntentId } = req.body; 118 | 119 | const order = await Order.findById(id); 120 | if (!order) { 121 | return res.status(400).send(`No order with id : ${id}`); 122 | } 123 | checkPermissions(req.user, order.user); 124 | order.paymentIntentId = paymentIntentId; 125 | order.status = 'paid'; 126 | await order.save(); 127 | 128 | return res.status(200).json({ order }); 129 | } 130 | 131 | 132 | 133 | module.exports = { 134 | getAllOrders, 135 | getSingleOrder, 136 | getCurrentUserOrders, 137 | createOrder, 138 | updateOrder, 139 | } -------------------------------------------------------------------------------- /controllers/product.js: -------------------------------------------------------------------------------- 1 | const Product = require('../models/product'); 2 | 3 | const path = require('path'); 4 | 5 | 6 | let createProduct = async (req,res)=>{ 7 | const { name, price, description, category, company, colors, featured, freeShipping, inventory } = req.body; 8 | 9 | let ProductToCreate = { 10 | name, 11 | price, 12 | description, 13 | category, 14 | company, 15 | colors, 16 | featured, 17 | freeShipping, 18 | inventory, 19 | user: req.user.userId, 20 | } 21 | try { 22 | const product = await Product.create(ProductToCreate); 23 | return res.status(201).json({ 24 | success: true, 25 | product, 26 | }); 27 | } 28 | catch (error) { 29 | return res.status(400).json({ 30 | success: false, 31 | error: error.message, 32 | }); 33 | } 34 | } 35 | 36 | 37 | let getAllProducts = async (req,res)=>{ 38 | try{ 39 | const products = await Product.find({}); 40 | return res.status(201).json({ 41 | success:true, 42 | products, 43 | count: products.length, 44 | }); 45 | 46 | } 47 | catch(error){ 48 | return res.status(400).json({ 49 | success: false, 50 | error: error.message, 51 | }); 52 | } 53 | } 54 | 55 | let uploadImage = async (req,res)=>{ 56 | if (!req.files) { 57 | return res.status(400).json({ 58 | success:false, 59 | message:"no file sent" 60 | }) 61 | } 62 | const productImage = req.files.image; 63 | 64 | if (!productImage.mimetype.startsWith('image')) { 65 | return res.status(400).json({ 66 | success:false, 67 | message:"upload an image please" 68 | }) 69 | } 70 | 71 | const maxSize = 1024 * 1024; 72 | 73 | if (productImage.size > maxSize) { 74 | return res.status(400).json({ 75 | success:false, 76 | message:"upload an image smaller then 1 mb" 77 | }) 78 | } 79 | 80 | const imagePath = path.join( 81 | __dirname, 82 | '../views/uploads/' + `${productImage.name}` 83 | ); 84 | await productImage.mv(imagePath); 85 | res.status(201).json({ image: `/uploads/${productImage.name}` }); 86 | } 87 | 88 | 89 | let getSingleProduct = async (req,res)=>{ 90 | try{ 91 | const product = await Product.findById(req.params.id).populate('reviews'); 92 | if(!product){ 93 | return res.status(404).json({ 94 | success: false, 95 | error: 'Product not found', 96 | }); 97 | } 98 | return res.status(201).json({ 99 | success: true, 100 | product, 101 | }); 102 | 103 | } 104 | catch(error){ 105 | return res.status(400).json({ 106 | success: false, 107 | error: error.message, 108 | }); 109 | } 110 | } 111 | 112 | 113 | let updateProduct = async (req,res)=>{ 114 | const {id} = req.params; 115 | try { 116 | let product = Product.findByIdAndUpdate(id, req.body, { 117 | new: true, 118 | runValidators: true, 119 | }); 120 | if(!product){ 121 | return res.status(404).json({ 122 | success: false, 123 | error: 'Product not found', 124 | }); 125 | } 126 | } catch (error) { 127 | return res.status(400).json({ 128 | success: false, 129 | error: error.message, 130 | }); 131 | } 132 | } 133 | 134 | let deleteProduct = async (req,res)=>{ 135 | const { id } = req.params; 136 | 137 | try { 138 | const product = await Product.findById(id); 139 | 140 | if (!product) { 141 | throw new CustomError.NotFoundError(`No product with id : ${id}`); 142 | } 143 | 144 | await product.remove(); 145 | res.status(200).json({ msg: 'Success! Product removed.' }); 146 | } catch (error) { 147 | return res.status(400).json({ 148 | success:false, 149 | error:error.message 150 | }) 151 | } 152 | } 153 | 154 | module.exports = { 155 | createProduct, 156 | getAllProducts, 157 | uploadImage, 158 | getSingleProduct, 159 | updateProduct, 160 | deleteProduct 161 | } 162 | -------------------------------------------------------------------------------- /controllers/review.js: -------------------------------------------------------------------------------- 1 | const Review = require("../models/review"); 2 | const Product = require("../models/product"); 3 | const checkPermissions = require("../utils/checkPermissions"); 4 | 5 | 6 | let createReview = async (req,res)=>{ 7 | const { rating, title, comment, product } = req.body; 8 | try { 9 | 10 | let productExist = Product.findById(product); 11 | if(!productExist){ 12 | return res.status(404).json({ 13 | success: false, 14 | error: `Product with id ${product} not found`, 15 | }); 16 | } 17 | let alreadyReviewed = await Review.findOne({user:req.user.userId,product}); 18 | if(alreadyReviewed){ 19 | return res.status(400).json({ msg: 'already reviewed' }); 20 | } 21 | 22 | 23 | if (!rating || !title || !comment || !product) { 24 | return res.status(400).json({ msg: 'fill all the credentials' }) 25 | } 26 | 27 | 28 | let reviewData = { 29 | rating, 30 | title, 31 | comment, 32 | product, 33 | user: req.user.userId, 34 | } 35 | const review = await Review.create(reviewData); 36 | return res.status(201).json({ 37 | success: true, 38 | review, 39 | }); 40 | } catch (error) { 41 | return res.status(400).json({ 42 | success: false, 43 | error: error.message, 44 | }); 45 | } 46 | 47 | } 48 | 49 | let updateReview = async (req,res)=>{ 50 | try { 51 | const { id } = req.params; 52 | const { rating, title, comment } = req.body; 53 | const review = await Review.findById(id); 54 | if(!review){ 55 | return res.status(404).json({ 56 | success: false, 57 | error: `Review with id ${id} not found`, 58 | }); 59 | } 60 | checkPermissions(req.user,review.user); 61 | review.rating = rating; 62 | review.title = title; 63 | review.comment = comment; 64 | await review.save(); 65 | res.status(200).json({ msg: 'Success! Review updated', review }); 66 | } catch (error) { 67 | return res.status(400).json({ 68 | success: false, 69 | error: error.message, 70 | }); 71 | } 72 | 73 | 74 | } 75 | 76 | const deleteReview = async (req, res) => { 77 | try { 78 | const { id } = req.params; 79 | 80 | const review = await Review.findById(id); 81 | 82 | if (!review) { 83 | return res.status(404).json({ 84 | success: false, 85 | error: 'Review not found', 86 | }); 87 | } 88 | checkPermissions(req.user, review.user); 89 | await review.remove(); 90 | res.status(200).json({ msg: 'Success! Review removed' }); 91 | } catch (error) { 92 | return res.status(400).json({ 93 | success: false, 94 | error: error.message, 95 | }); 96 | } 97 | }; 98 | 99 | 100 | 101 | 102 | let getAllReviews = async (req,res)=>{ 103 | try { 104 | const reviews = await Review.find({}).populate({ 105 | path: 'product', 106 | select: 'name company price', 107 | }); 108 | return res.status(200).json({ reviews, count: reviews.length }); 109 | 110 | } catch (error) { 111 | return res.status(400).json({ error: error.message }); 112 | } 113 | } 114 | 115 | let getSingleReview = async (req,res)=>{ 116 | try { 117 | const { id } = req.params; 118 | const review = await Review.findById(id); 119 | if(!review){ 120 | return res.status(404).json({ 121 | success: false, 122 | error: `Review with id ${id} not found`, 123 | }); 124 | } 125 | return res.status(200).json({ 126 | success: true, 127 | review, 128 | }); 129 | } catch (error) { 130 | return res.status(400).json({ 131 | success: false, 132 | error: error.message, 133 | }); 134 | } 135 | } 136 | 137 | const getSingleProductReviews = async (req, res) => { 138 | const { id } = req.params; 139 | const reviews = await Review.find({ product: id }); 140 | res.status(200).json({ reviews, count: reviews.length }); 141 | }; 142 | 143 | module.exports = { 144 | createReview, 145 | updateReview, 146 | deleteReview, 147 | getAllReviews, 148 | getSingleReview, 149 | getSingleProductReviews 150 | } 151 | --------------------------------------------------------------------------------