├── .gitignore ├── README.md ├── src ├── utils │ ├── authRoles.js │ ├── CustomError.js │ └── mailHelper.js ├── config │ ├── razorpay.config.js │ ├── s3.config.js │ ├── transporter.config.js │ └── index.js ├── routes │ ├── index.js │ ├── order.route.js │ ├── auth.route.js │ ├── collection.route.js │ └── coupon.route.js ├── models │ ├── coupon.schema.js │ ├── collection.schema.js │ ├── product.schema.js │ ├── order.schema.js │ └── user.schema.js ├── service │ ├── imageUpload.js │ └── asyncHandler.js ├── app.js ├── middlewares │ └── auth.middleware.js └── controllers │ ├── order.controller.js │ ├── coupon.controller.js │ ├── collection.controller.js │ ├── product.controller.js │ └── auth.controller.js ├── .env.example ├── index.js ├── package.json └── .devcontainer ├── devcontainer.json ├── docker-compose.yml └── Dockerfile /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # live-code-session 2 | A discussion over creating a backend for e-comm app 3 | -------------------------------------------------------------------------------- /src/utils/authRoles.js: -------------------------------------------------------------------------------- 1 | const AuthRoles = { 2 | ADMIN: "ADMIN", 3 | MODERATOR: "MODERATOR", 4 | USER: "USER" 5 | } 6 | 7 | export default AuthRoles -------------------------------------------------------------------------------- /src/utils/CustomError.js: -------------------------------------------------------------------------------- 1 | class CustomError extends Error { 2 | constructor(message, code){ 3 | super(message); 4 | this.code = code 5 | } 6 | } 7 | 8 | export default CustomError -------------------------------------------------------------------------------- /src/config/razorpay.config.js: -------------------------------------------------------------------------------- 1 | import Razorpay from "razorpay" 2 | import config from "./index.js" 3 | 4 | const razorpay = new Razorpay({ 5 | key_id: config.RAZORPAY_KEY_ID, 6 | key_secret: config.RAZORPAY_SECRET 7 | }) 8 | 9 | export default razorpay -------------------------------------------------------------------------------- /src/config/s3.config.js: -------------------------------------------------------------------------------- 1 | import aws from "aws-sdk" 2 | import config from "./index.js" 3 | 4 | const s3 = new aws.S3({ 5 | accessKeyId: config.S3_ACCESS_KEY, 6 | secretAccessKey: config.S3_SECRET_ACCESS_KEY, 7 | region: config.S3_REGION 8 | }) 9 | 10 | export default s3; -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import authRoutes from "./auth.route.js" 3 | import couponRoutes from "./coupon.route.js" 4 | import collectionRoutes from "./collection.route.js" 5 | 6 | const router = Router() 7 | router.use("/auth", authRoutes) 8 | router.use("/coupon", couponRoutes) 9 | router.use("/collection", collectionRoutes) 10 | 11 | 12 | 13 | export default router -------------------------------------------------------------------------------- /src/routes/order.route.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { generateOrder, generateRazorpayOrderId, getAllOrders, getMyOrders, updateOrderStatus } from "../controllers/order.controller.js"; 3 | import { isLoggedIn, authorize } from "../middlewares/auth.middleware"; 4 | import AuthRoles from "../utils/authRoles.js"; 5 | 6 | 7 | 8 | const router = Router() 9 | //TOodo: add all routes here 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /src/utils/mailHelper.js: -------------------------------------------------------------------------------- 1 | import config from "../config/index.js" 2 | 3 | import transporter from "../config/transporter.config.js" 4 | 5 | const mailHelper = async (option) => { 6 | 7 | const message = { 8 | from: config.SMTP_SENDER_EMAIL, 9 | to: option.email, 10 | subject: option.subject, 11 | text: option.message 12 | } 13 | 14 | await transporter.sendMail(message) 15 | } 16 | 17 | 18 | export default mailHelper -------------------------------------------------------------------------------- /src/models/coupon.schema.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose" 2 | 3 | const couponSchema = new mongoose.Schema({ 4 | code: { 5 | type: String, 6 | required: [true, "Please provide a coupon code"] 7 | }, 8 | discount: { 9 | type: Number, 10 | default: 0 11 | }, 12 | active: { 13 | type: Boolean, 14 | default: true 15 | } 16 | }, {timestamps: true}) 17 | 18 | 19 | export default mongoose.model("Coupon", couponSchema) -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT=5000 2 | MONGODB_URL=mongodb://localhost:27017/ecomm 3 | JWT_SECRET=yoursecret 4 | JWT_EXPIRY=7d 5 | 6 | S3_ACCESS_KEY=youraccesskey 7 | S3_SECRET_ACCESS_KEY=yoursecretaccesskey 8 | S3_BUCKET_NAME=yourbucketname 9 | S3_REGION=yourregion 10 | 11 | SMTP_MAIL_HOST=smtp.mailtrap.io 12 | SMTP_MAIL_PORT=2525 13 | SMTP_MAIL_USERNAME=yourusername 14 | SMTP_MAIL_PASSWORD=yourpassword 15 | SMTP_SENDER_EMAIL=some@mailtrap.com 16 | 17 | 18 | RAZORPAY_KEY_ID=yourkeyid 19 | RAZORPAY_SECRET=yoursecret 20 | -------------------------------------------------------------------------------- /src/config/transporter.config.js: -------------------------------------------------------------------------------- 1 | import nodemailer from "nodemailer" 2 | 3 | import config from "./index.js" 4 | 5 | const transporter = nodemailer.createTransport({ 6 | host: config.SMTP_MAIL_HOST, 7 | port: config.SMTP_MAIL_PORT, 8 | // secure: false, // true for 465, false for other ports 9 | auth: { 10 | user: config.SMTP_MAIL_USERNAME, // generated ethereal user 11 | pass: config.SMTP_MAIL_PASSWORD, // generated ethereal password 12 | }, 13 | }) 14 | 15 | 16 | export default transporter -------------------------------------------------------------------------------- /src/service/imageUpload.js: -------------------------------------------------------------------------------- 1 | import s3 from "../config/s3.config.js" 2 | 3 | export const s3FileUpload = async ({bucketName, key, body, contentType}) => { 4 | return await s3.upload({ 5 | Bucket: bucketName, 6 | Key: key, 7 | Body: body, 8 | ContentType: contentType 9 | }) 10 | .promise() 11 | } 12 | 13 | export const s3deleteFile = async ({bucketName, key}) => { 14 | return await s3.deleteObject({ 15 | Bucket: bucketName, 16 | Key: key, 17 | 18 | }) 19 | .promise() 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/service/asyncHandler.js: -------------------------------------------------------------------------------- 1 | 2 | const asyncHandler = (fn) => async (req, res, next) => { 3 | try { 4 | await fn(req, res, next) 5 | } catch (error) { 6 | res.status(error.code || 500).json({ 7 | success: false, 8 | message: error.message 9 | }) 10 | } 11 | } 12 | 13 | export default asyncHandler; 14 | 15 | // const asyncHandler = "Hitesh" 16 | // const asyncHandler = () => {} 17 | // const asyncHandler = (func) => {} 18 | // const asyncHandler = (func) => () => {} 19 | // const asyncHandler = (func) => async() => {} -------------------------------------------------------------------------------- /src/models/collection.schema.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const collectionSchema = new mongoose.Schema( 4 | { 5 | name: { 6 | type: String, 7 | required: ["true", "Please provide a collection name"], 8 | trim: true, 9 | maxLength : [ 10 | 120, 11 | "Collection name should not be more than 120 chars" 12 | ] 13 | } 14 | 15 | }, 16 | {timestamps: true} 17 | ); 18 | 19 | export default mongoose.model("Collection", collectionSchema) 20 | 21 | // collections -------------------------------------------------------------------------------- /src/routes/auth.route.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { getProfile, login, logout, signUp, forgotPassword, resetPassword } from "../controllers/auth.controller"; 3 | import { isLoggedIn } from "../middlewares/auth.middleware"; 4 | 5 | 6 | 7 | const router = Router() 8 | 9 | router.post("/signup", signUp) 10 | router.post("/login", login) 11 | router.get("/logout", logout) 12 | 13 | router.post("/password/forgot/", forgotPassword) 14 | router.post("/password/reset/:token", resetPassword) 15 | 16 | router.get("/profile", isLoggedIn, getProfile) 17 | 18 | 19 | export default router; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import app from "./src/app.js"; 3 | import config from "./src/config/index.js"; 4 | 5 | ( async () => { 6 | try { 7 | await mongoose.connect(config.MONGODB_URL) 8 | console.log("DB CONNECTED !"); 9 | 10 | app.on('error', (err) => { 11 | console.error("ERROR: ", err); 12 | throw err 13 | }) 14 | 15 | const onListening = () => { 16 | console.log(`Listening on port ${config.PORT}`); 17 | } 18 | 19 | app.listen(config.PORT, onListening) 20 | 21 | } catch (err) { 22 | console.error("ERROR: ", err) 23 | throw err 24 | } 25 | })() 26 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import express from "express" 2 | import cors from "cors" 3 | import cookieParser from "cookie-parser" 4 | import routes from "./routes/index.js" 5 | 6 | const app = express() 7 | 8 | app.use(express.json()) 9 | app.use(express.urlencoded({extended: true})) 10 | app.use(cors()) 11 | app.use(cookieParser()) 12 | 13 | app.use("/api/v1/", routes) 14 | 15 | app.get("/", (_req, res) => { 16 | res.send("Hello there hitesh - API") 17 | }) 18 | 19 | 20 | app.all("*", (_req, res) => { 21 | return res.status(404).json({ 22 | success: false, 23 | message: "Route not found" 24 | }) 25 | }) 26 | 27 | export default app; 28 | 29 | // http://localhost:4000/api/v1/auth/login 30 | // http://localhost:4000/api/v1/auth/logout -------------------------------------------------------------------------------- /src/routes/collection.route.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { createCollection, deleteCollection, getAllCollections, updateCollection } from "../controllers/collection.controller.js"; 3 | import { isLoggedIn, authorize } from "../middlewares/auth.middleware"; 4 | import AuthRoles from "../utils/authRoles.js"; 5 | 6 | 7 | 8 | 9 | const router = Router() 10 | 11 | router.post("/", isLoggedIn, authorize(AuthRoles.ADMIN), createCollection) 12 | router.put("/:id", isLoggedIn, authorize(AuthRoles.ADMIN), updateCollection) 13 | 14 | // delete a single collection 15 | router.delete("/:id", isLoggedIn, authorize(AuthRoles.ADMIN), deleteCollection) 16 | 17 | //get all collection 18 | router.get("/", getAllCollections) 19 | 20 | export default router; -------------------------------------------------------------------------------- /src/routes/coupon.route.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { createCoupon, deleteCoupon, getAllCoupons, updateCoupon } from "../controllers/coupon.controller.js"; 3 | import { isLoggedIn, authorize } from "../middlewares/auth.middleware"; 4 | import AuthRoles from "../utils/authRoles.js"; 5 | 6 | 7 | 8 | const router = Router() 9 | 10 | router.post("/", isLoggedIn, authorize(AuthRoles.ADMIN), createCoupon) 11 | 12 | router.delete("/:id", isLoggedIn, authorize(AuthRoles.ADMIN, AuthRoles.MODERATOR), deleteCoupon) 13 | 14 | router.put("/action/:id", isLoggedIn, authorize(AuthRoles.ADMIN, AuthRoles.MODERATOR), updateCoupon) 15 | 16 | router.get("/", isLoggedIn, authorize(AuthRoles.ADMIN, AuthRoles.MODERATOR), getAllCoupons) 17 | 18 | export default router; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "live-code-session", 3 | "version": "1.0.0", 4 | "description": "a backend for ecomm app", 5 | "type": "module", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "start": "node index.js" 10 | }, 11 | "keywords": [ 12 | "course" 13 | ], 14 | "author": "hitesh", 15 | "license": "ISC", 16 | "dependencies": { 17 | "aws-sdk": "^2.1359.0", 18 | "bcryptjs": "^2.4.3", 19 | "cookie-parser": "^1.4.6", 20 | "cors": "^2.8.5", 21 | "dotenv": "^16.0.3", 22 | "express": "^4.18.2", 23 | "formidable": "^2.1.1", 24 | "jsonwebtoken": "^9.0.0", 25 | "mongoose": "^7.0.3", 26 | "nodemailer": "^6.9.1", 27 | "razorpay": "^2.8.6" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv" 2 | 3 | dotenv.config() 4 | 5 | const config = { 6 | PORT: process.env.PORT || 5000, 7 | MONGODB_URL: process.env.MONGODB_URL || "mongodb://localhost:27017/ecomm", 8 | JWT_SECRET: process.env.JWT_SECRET || "yoursecret", 9 | JWT_EXPIRY: process.env.JWT_EXPIRY || "30d", 10 | S3_ACCESS_KEY: process.env.S3_ACCESS_KEY, 11 | S3_SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY, 12 | S3_BUCKET_NAME: process.env.S3_BUCKET_NAME, 13 | S3_REGION: process.env.S3_REGION, 14 | SMTP_MAIL_HOST: process.env.SMTP_MAIL_HOST, 15 | SMTP_MAIL_PORT: process.env.SMTP_MAIL_PORT, 16 | SMTP_MAIL_USERNAME: process.env.SMTP_MAIL_USERNAME, 17 | SMTP_MAIL_PASSWORD: process.env.SMTP_MAIL_PASSWORD, 18 | SMTP_SENDER_EMAIL: process.env.SMTP_SENDER_EMAIL, 19 | RAZORPAY_KEY_ID: process.env.RAZORPAY_KEY_ID, 20 | RAZORPAY_SECRET: process.env.RAZORPAY_SECRET, 21 | } 22 | 23 | export default config -------------------------------------------------------------------------------- /src/models/product.schema.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const productSchema = new mongoose.Schema({ 4 | name: { 5 | type: String, 6 | required: ["true", "please provide a product name"], 7 | trim: true, 8 | maxLength: [120, "product name should not be max than 120 chars"] 9 | }, 10 | price: { 11 | type: Number, 12 | required: ["true", "please provide a product price"], 13 | maxLength: [5, "product name should not be max than 5 chars"] 14 | }, 15 | description: { 16 | type: String 17 | }, 18 | photos: [ 19 | { 20 | secure_url: { 21 | type: String, 22 | required: true 23 | } 24 | } 25 | ], 26 | stock: { 27 | type: Number, 28 | default: 0 29 | }, 30 | sold: { 31 | type: Number, 32 | default: 0 33 | }, 34 | collectionId: { 35 | type: mongoose.Schema.Types.ObjectId, 36 | ref: "Collection" 37 | } 38 | }, {timestamps: true}) 39 | 40 | 41 | export default mongoose.model("Product", productSchema) -------------------------------------------------------------------------------- /src/models/order.schema.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose" 2 | 3 | const orderSchema = new mongoose.Schema({ 4 | product: { 5 | type: [ 6 | { 7 | productId: { 8 | type: mongoose.Schema.Types.ObjectId, 9 | ref: "Product" 10 | }, 11 | count: Number, 12 | price: Number 13 | } 14 | ], 15 | required: true 16 | }, 17 | user: { 18 | type: mongoose.Schema.Types.ObjectId, 19 | ref: "User", 20 | required: true 21 | }, 22 | address: { 23 | type: String, 24 | required: true 25 | }, 26 | phoneNumber: { 27 | type: Number, 28 | required: true 29 | }, 30 | amount: { 31 | type: Number, 32 | required: true 33 | }, 34 | coupon: String, 35 | transactionId: String, 36 | status: { 37 | type: String, 38 | enum: ["ORDERED", "SHIPPED", "DELIVERED", "CANCELLED"], 39 | //TODO: try better way 40 | default: "ORDERED" 41 | } 42 | }, {timestamps: true}) 43 | 44 | 45 | export default mongoose.model("Order", orderSchema) -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node-mongo 3 | { 4 | "name": "Node.js & Mongo DB", 5 | "dockerComposeFile": "docker-compose.yml", 6 | "service": "app", 7 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 8 | 9 | // Features to add to the dev container. More info: https://containers.dev/features. 10 | // "features": {}, 11 | 12 | // Configure tool-specific properties. 13 | "customizations": { 14 | // Configure properties specific to VS Code. 15 | "vscode": { 16 | // Add the IDs of extensions you want installed when the container is created. 17 | "extensions": [ 18 | "mongodb.mongodb-vscode" 19 | ] 20 | } 21 | } 22 | 23 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 24 | // "forwardPorts": [3000, 27017], 25 | 26 | // Use 'postCreateCommand' to run commands after the container is created. 27 | // "postCreateCommand": "yarn install", 28 | 29 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 30 | // "remoteUser": "root" 31 | } 32 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | app: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | volumes: 9 | - ../..:/workspaces:cached 10 | 11 | # Overrides default command so things don't shut down after the process ends. 12 | command: sleep infinity 13 | 14 | # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. 15 | network_mode: service:db 16 | 17 | # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. 18 | # (Adding the "ports" property to this file will not forward from a Codespace.) 19 | 20 | db: 21 | image: mongo:latest 22 | restart: unless-stopped 23 | volumes: 24 | - mongodb-data:/data/db 25 | 26 | # Uncomment to change startup options 27 | # environment: 28 | # MONGO_INITDB_ROOT_USERNAME: root 29 | # MONGO_INITDB_ROOT_PASSWORD: example 30 | # MONGO_INITDB_DATABASE: your-database-here 31 | 32 | # Add "forwardPorts": ["27017"] to **devcontainer.json** to forward MongoDB locally. 33 | # (Adding the "ports" property to this file will not forward from a Codespace.) 34 | 35 | volumes: 36 | mongodb-data: -------------------------------------------------------------------------------- /src/middlewares/auth.middleware.js: -------------------------------------------------------------------------------- 1 | import User from "../models/user.schema.js"; 2 | import JWT from "jsonwebtoken" 3 | import asyncHandler from "../service/asyncHandler.js"; 4 | import config from "../config.js"; 5 | import CustomError from "../utils/CustomError.js"; 6 | 7 | 8 | 9 | export const isLoggedIn = asyncHandler(async (req, res, next) => { 10 | let token; 11 | 12 | if (req.cookies.token || (req.headers.authorization && req.headers.authorization.startsWith("Bearer")) ) { 13 | token = req.cookies.token || req.headers.authorization.split(" ")[1] 14 | 15 | // token = "Bearer gbhnjm235r5hbnj" 16 | } 17 | 18 | if (!token) { 19 | throw new CustomError("Not authorized to access this resource", 401) 20 | } 21 | 22 | try { 23 | const decodedJwtPayload = JWT.verify(token, config.JWT_SECRET); 24 | 25 | req.user = await User.findById(decodedJwtPayload._id, "name email role") 26 | next() 27 | } catch (error) { 28 | throw new CustomError("Not authorized to access this resource", 401) 29 | } 30 | 31 | }) 32 | 33 | 34 | export const authorize = (...requiredRoles) => asyncHandler( async (req, res, next) => { 35 | if (!requiredRoles.includes(req.user.role)) { 36 | throw new CustomError("You are not authorized to access this resource") 37 | } 38 | next() 39 | }) -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/javascript-node:0-18 2 | 3 | # Install MongoDB command line tools - though mongo-database-tools not available on arm64 4 | ARG MONGO_TOOLS_VERSION=6.0 5 | RUN . /etc/os-release \ 6 | && curl -sSL "https://www.mongodb.org/static/pgp/server-${MONGO_TOOLS_VERSION}.asc" | gpg --dearmor > /usr/share/keyrings/mongodb-archive-keyring.gpg \ 7 | && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/mongodb-archive-keyring.gpg] http://repo.mongodb.org/apt/debian ${VERSION_CODENAME}/mongodb-org/${MONGO_TOOLS_VERSION} main" | tee /etc/apt/sources.list.d/mongodb-org-${MONGO_TOOLS_VERSION}.list \ 8 | && apt-get update && export DEBIAN_FRONTEND=noninteractive \ 9 | && apt-get install -y mongodb-mongosh \ 10 | && if [ "$(dpkg --print-architecture)" = "amd64" ]; then apt-get install -y mongodb-database-tools; fi \ 11 | && apt-get clean -y && rm -rf /var/lib/apt/lists/* 12 | 13 | # [Optional] Uncomment this section to install additional OS packages. 14 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 15 | # && apt-get -y install --no-install-recommends 16 | 17 | # [Optional] Uncomment if you want to install an additional version of node using nvm 18 | # ARG EXTRA_NODE_VERSION=10 19 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 20 | 21 | # [Optional] Uncomment if you want to install more global node modules 22 | # RUN su node -c "npm install -g " 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/models/user.schema.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import AuthRoles from "../utils/authRoles.js"; 3 | import bcrypt from "bcryptjs"; 4 | import JWT from "jsonwebtoken"; 5 | import config from "../config/index.js"; 6 | import crypto from "crypto" 7 | 8 | const userSchema = new mongoose.Schema({ 9 | name: { 10 | type: String, 11 | required: ["true", "Name is required"], 12 | maxLength: [50, "Name must be less than 50 chars"] 13 | }, 14 | email: { 15 | type: String, 16 | required: ["true", "Email is required"], 17 | }, 18 | password: { 19 | type: String, 20 | required: [true, "Password is required"], 21 | minLength: [8, "password must be at least 8 chars"], 22 | select: false 23 | }, 24 | role: { 25 | type: String, 26 | enum: Object.values(AuthRoles), 27 | default: AuthRoles.USER 28 | }, 29 | forgotPasswordToken: String, 30 | forgotPasswordExpiry: Date 31 | }, {timestamps: true}) 32 | 33 | //Encrypt the password before saving: HOOKS 34 | 35 | userSchema.pre("save", async function(next){ 36 | if (!this.isModified("password")) return next() 37 | this.password = await bcrypt.hash(this.password, 10) 38 | next() 39 | }) 40 | 41 | userSchema.methods = { 42 | //compare password 43 | comparePassword: async function(enteredPassword){ 44 | return await bcrypt.compare(enteredPassword, this.password) 45 | }, 46 | //generate JWT Token 47 | getJWTtoken: function(){ 48 | JWT.sign({_id: this._id, role: this.role}, config.JWT_SECRET, { 49 | expiresIn: config.JWT_EXPIRY 50 | }) 51 | }, 52 | //generate forgot password token 53 | 54 | generateForgotPasswordToken: function (){ 55 | const forgotToken = crypto.randomBytes(20).toString("hex") 56 | 57 | // just to encrypt the token generated by crypto 58 | this.forgotPasswordToken = crypto 59 | .createHash("sha256") 60 | .update(forgotToken) 61 | .digest("hex") 62 | 63 | //time for token to expire 64 | this.forgotPasswordExpiry = Date.now() + 20 * 60 * 1000 65 | 66 | return forgotToken 67 | } 68 | } 69 | 70 | 71 | export default mongoose.model("User", userSchema) -------------------------------------------------------------------------------- /src/controllers/order.controller.js: -------------------------------------------------------------------------------- 1 | import Product from "../models/product.schema.js"; 2 | import Coupon from "../models/coupon.schema.js"; 3 | import Order from "../models/order.schema.js"; 4 | import asyncHandler from "../service/asyncHandler.js"; 5 | import CustomError from "../utils/customError.js"; 6 | import razorpay from "../config/razorpay.config.js" 7 | 8 | export const generateRazorpayOrderId = asyncHandler(async (req, res) => { 9 | const {products, couponCode} = req.body 10 | 11 | if (!products || products.length === 0) { 12 | throw new CustomError("No product found", 400) 13 | } 14 | let totalAmount = 0 15 | let discountAmount = 0 16 | 17 | // TODO: DO product calculation based on DB calls 18 | 19 | let productPriceCalc = Promise.all( 20 | products.map(async (product) => { 21 | const {productId, count} = product; 22 | const productFromDB = await Product.findById(productId) 23 | if (!productFromDB) { 24 | throw new CustomError("No product found", 400) 25 | } 26 | if (productFromDB.stock < count) { 27 | return res.status(400).json({ 28 | error: "Product quantity not in stock" 29 | }) 30 | } 31 | totalAmount += productFromDB.price * count 32 | }) 33 | ) 34 | 35 | await productPriceCalc; 36 | 37 | //todo: check for coupon discount, if applicable 38 | 39 | const options = { 40 | amount: Math.round(totalAmount * 100), 41 | currency: "INR", 42 | receipt: `receipt_${new Date().getTime()}` 43 | } 44 | const order = await razorpay.orders.create(options) 45 | 46 | if (!order) { 47 | throw new CustomError("UNable to generate order", 400) 48 | } 49 | 50 | res.status(200).json({ 51 | success: true, 52 | message: "razorpay order id generated successfully", 53 | order 54 | }) 55 | }) 56 | 57 | // Todo: add order in database and update product stock 58 | 59 | export const generateOrder = asyncHandler(async(req, res) => { 60 | //add more fields below 61 | const {transactionId, products, coupon } = req.body 62 | }) 63 | 64 | //Todo: get only my orders 65 | export const getMyOrders = asyncHandler(async(req, res) => { 66 | // 67 | }) 68 | 69 | //Todo: get all my orders: Admin 70 | export const getAllOrders = asyncHandler(async(req, res) => { 71 | // 72 | }) 73 | 74 | //Todo: update order Status: Admin 75 | export const updateOrderStatus = asyncHandler(async(req, res) => { 76 | // 77 | }) -------------------------------------------------------------------------------- /src/controllers/coupon.controller.js: -------------------------------------------------------------------------------- 1 | import Coupon from "../models/coupon.schema.js"; 2 | import asyncHandler from "../service/asyncHandler.js"; 3 | import CustomError from "../utils/customError.js"; 4 | 5 | /********************************************************** 6 | * @CREATE_COUPON 7 | * @route https://localhost:5000/api/coupon 8 | * @description Controller used for creating a new coupon 9 | * @description Only admin and Moderator can create the coupon 10 | * @returns Coupon Object with success message "Coupon Created SuccessFully" 11 | *********************************************************/ 12 | 13 | export const createCoupon = asyncHandler(async (req, res) => { 14 | const {code, discount} = req.body 15 | 16 | if (!code || !discount) { 17 | throw new CustomError("Code and discount are required", 400) 18 | } 19 | 20 | // check id code already exists 21 | 22 | const coupon = await Coupon.create({ 23 | code, 24 | discount 25 | }) 26 | 27 | res.status(200).json({ 28 | success: true, 29 | message: "Coupon created successfully", 30 | coupon 31 | }) 32 | }) 33 | 34 | 35 | export const updateCoupon = asyncHandler(async (req, res) => { 36 | const {id: couponId} = req.params 37 | const {action} = req.body 38 | 39 | // action is boolean or not 40 | 41 | const coupon = await Coupon.findByIdAndUpdate( 42 | couponId, 43 | { 44 | active: action 45 | }, 46 | { 47 | new: true, 48 | runValidators: true 49 | } 50 | ) 51 | if (!coupon) { 52 | throw new CustomError("Coupon not found", 404) 53 | } 54 | 55 | res.status(200).json({ 56 | success: true, 57 | message: "Coupon updated", 58 | coupon 59 | }) 60 | }) 61 | 62 | export const deleteCoupon = asyncHandler(async(req, res) => { 63 | const {id: couponId} = req.params 64 | 65 | const coupon = await Coupon.findByIdAndDelete(couponId) 66 | 67 | if (!coupon) { 68 | throw new CustomError("Coupon not found", 404) 69 | } 70 | 71 | res.status(200).json({ 72 | success: true, 73 | message: "Coupon deleted", 74 | 75 | }) 76 | }) 77 | 78 | export const getAllCoupons = asyncHandler( async (req, res) => { 79 | const allCoupons = await Coupon.find(); 80 | 81 | if (!allCoupons) { 82 | throw new CustomError("No Coupons found", 400) 83 | 84 | } 85 | 86 | res.status(200).json({ 87 | success: true, 88 | allCoupons 89 | }) 90 | 91 | }) 92 | 93 | -------------------------------------------------------------------------------- /src/controllers/collection.controller.js: -------------------------------------------------------------------------------- 1 | import Collection from "../models/collection.schema.js" 2 | import asyncHandler from "../service/asyncHandler.js" 3 | import CustomError from "../utils/CustomError.js" 4 | 5 | 6 | /********************************************************** 7 | * @CREATE_COLLECTION 8 | * @route https://localhost:5000/api/collection/ 9 | * @description Controller used for creating a new collection 10 | * @description Only admin can create the collection 11 | *********************************************************/ 12 | 13 | export const createCollection = asyncHandler(async (req, res) => { 14 | const { name } = req.body; 15 | 16 | if (!name) { 17 | throw new CustomError("Collection name is required", 400); 18 | } 19 | 20 | const collection = await Collection.create({ 21 | name, 22 | }); 23 | 24 | res.status(200).json({ 25 | success: true, 26 | message: "Collection created Successfully", 27 | collection, 28 | }); 29 | }); 30 | 31 | /** 32 | * @UPDATE_COLLECTION 33 | * @route http://localhost:5000/api/collection/:collectionId 34 | * @description Controller for updating the collection details 35 | * @description Only admin can update the collection 36 | */ 37 | 38 | export const updateCollection = asyncHandler(async (req, res) => { 39 | const { name } = req.body; 40 | const { id: collectionId } = req.params; 41 | 42 | if (!name) { 43 | throw new CustomError("Collection name is required", 400); 44 | } 45 | 46 | let updatedCollection = await Collection.findByIdAndUpdate( 47 | collectionId, 48 | { 49 | name, 50 | }, 51 | { 52 | new: true, 53 | runValidators: true, 54 | } 55 | ); 56 | 57 | if (!updatedCollection) { 58 | throw new CustomError("Collection not found", 404); 59 | } 60 | 61 | res.status(200).json({ 62 | success: true, 63 | message: "Collection Updated Successfully", 64 | updatedCollection, 65 | }); 66 | }); 67 | 68 | /** 69 | * @DELETE_COLLECTION 70 | * @route http://localhost:5000/api/collection/:collectionId 71 | * @description Controller for deleting the collection 72 | * @description Only admin can delete the collection 73 | */ 74 | 75 | export const deleteCollection = asyncHandler(async (req, res) => { 76 | const { id: collectionId } = req.params; 77 | const collectionToDelete = await Collection.findById(collectionId); 78 | 79 | if (!collectionToDelete) { 80 | throw new CustomError("Collection not found", 404); 81 | } 82 | 83 | collectionToDelete.remove(); 84 | res.status(200).json({ 85 | success: true, 86 | message: "Collection has been deleted successfully", 87 | }); 88 | }); 89 | 90 | /** 91 | * @GET_ALL_COLLECTION 92 | * @route http://localhost:5000/api/collection/ 93 | * @description Controller for getting all collection list 94 | * @description Only admin can get collection list 95 | * @returns Collection Object with available collection in DB 96 | */ 97 | 98 | export const getAllCollections = asyncHandler(async (req, res) => { 99 | const collections = await Collection.find(); 100 | 101 | if (!collections) { 102 | throw new CustomError("No collection found", 404); 103 | } 104 | 105 | res.status(200).json({ 106 | success: true, 107 | collections, 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /src/controllers/product.controller.js: -------------------------------------------------------------------------------- 1 | import Product from "../models/product.schema.js" 2 | import formidable from "formidable" 3 | import { s3FileUpload, s3deleteFile} from "../service/imageUpload.js" 4 | import Mongoose from "mongoose" 5 | import asyncHandler from "../service/asyncHandler.js" 6 | import CustomError from "../utils/CustomError.js" 7 | import config from "../config/index.js" 8 | import fs from "fs" 9 | 10 | 11 | /********************************************************** 12 | * @ADD_PRODUCT 13 | * @route https://localhost:5000/api/product 14 | * @description Controller used for creating a new product 15 | * @description Only admin can create the coupon 16 | * @descriptio Uses AWS S3 Bucket for image upload 17 | * @returns Product Object 18 | *********************************************************/ 19 | 20 | 21 | export const addProduct = asyncHandler(async (req, res) => { 22 | const form = formidable({ multiples: true, keepExtensions: true }); 23 | 24 | form.parse(req, async function (err, fields, files){ 25 | if (err) { 26 | throw new CustomError(err.message || "Something went wrong", 500) 27 | } 28 | 29 | let productId = new Mongoose.Types.ObjectId().toHexString() 30 | 31 | console.log(fields, files); 32 | 33 | if ( 34 | !fields.name || 35 | !fields.price || 36 | !fields.description || 37 | !fields.collectionId 38 | ) { 39 | throw new CustomError("Please fill all the fields", 500) 40 | 41 | } 42 | 43 | 44 | 45 | let imgArrayResp = Promise.all( 46 | Object.keys(files).map( async (file, index) => { 47 | const element = file[fileKey] 48 | console.log(element); 49 | const data = fs.readFileSync(element.filepath) 50 | 51 | const upload = await s3FileUpload({ 52 | bucketName: config.S3_BUCKET_NAME, 53 | key: `products/${productId}/photo_${index + 1}.png`, 54 | body: data, 55 | contentType: element.mimetype 56 | }) 57 | 58 | // productId = 123abc456 59 | // 1: products/123abc456/photo_1.png 60 | // 2: products/123abc456/photo_2.png 61 | 62 | console.log(upload); 63 | return { 64 | secure_url : upload.Location 65 | } 66 | }) 67 | ) 68 | 69 | let imgArray = await imgArrayResp 70 | 71 | const product = await Product.create({ 72 | _id: productId, 73 | photos: imgArray, 74 | ...fields 75 | }) 76 | 77 | if (!product) { 78 | throw new CustomError("Product failed to be created in DB", 400) 79 | } 80 | res.status(200).json({ 81 | success: true, 82 | product, 83 | }) 84 | 85 | 86 | 87 | }) 88 | }) 89 | 90 | export const getAllProducts = asyncHandler(async (req, res) => { 91 | const products = await Product.find({}) 92 | 93 | if (!products) { 94 | throw new CustomError("No products found", 404) 95 | } 96 | 97 | res.status(200).json({ 98 | success: true, 99 | products 100 | }) 101 | }) 102 | 103 | export const getProductById = asyncHandler(async (req, res) => { 104 | const {id: productId} = req.params 105 | 106 | const product = await Product.findById(productId) 107 | 108 | if (!product) { 109 | throw new CustomError("No product found", 404) 110 | } 111 | 112 | res.status(200).json({ 113 | success: true, 114 | product 115 | }) 116 | }) 117 | 118 | export const getProductByCollectionId = asyncHandler(async(req, res) => { 119 | const {id: collectionId} = req.params 120 | 121 | const products = await Product.find({collectionId}) 122 | 123 | if (!products) { 124 | throw new CustomError("No products found", 404) 125 | } 126 | 127 | res.status(200).json({ 128 | success: true, 129 | products 130 | }) 131 | }) 132 | 133 | 134 | export const deleteProduct = asyncHandler(async(req, res) => { 135 | const {id: productId} = req.params 136 | 137 | const product = await Product.findById(productId) 138 | 139 | if (!product) { 140 | throw new CustomError("No product found", 404) 141 | 142 | } 143 | 144 | //resolve promise 145 | // loop through photos array => delete each photo 146 | //key : product._id 147 | 148 | const deletePhotos = Promise.all( 149 | product.photos.map(async( elem, index) => { 150 | await s3deleteFile({ 151 | bucketName: config.S3_BUCKET_NAME, 152 | key: `products/${product._id.toString()}/photo_${index + 1}.png` 153 | }) 154 | }) 155 | ) 156 | 157 | await deletePhotos; 158 | 159 | await product.remove() 160 | 161 | res.status(200).json({ 162 | success: true, 163 | message: "Product has been deleted successfully" 164 | }) 165 | }) -------------------------------------------------------------------------------- /src/controllers/auth.controller.js: -------------------------------------------------------------------------------- 1 | // signup a new user 2 | import User from "../models/user.schema.js" 3 | import asyncHandler from "../service/asyncHandler"; 4 | import CustomError from "../utils/CustomError"; 5 | import mailHelper from "../utils/mailHelper.js" 6 | 7 | export const cookieOptions = { 8 | expires: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), 9 | httpOnly: true 10 | } 11 | 12 | /****************************************************** 13 | * @SIGNUP 14 | * @route http://localhost:5000/api/auth/signup 15 | * @description User signUp Controller for creating new user 16 | * @returns User Object 17 | ******************************************************/ 18 | 19 | 20 | export const signUp = asyncHandler(async(req, res) => { 21 | //get data from user 22 | const {name, email, password } = req.body 23 | 24 | //validation 25 | if (!name || !email || !password) { 26 | throw new CustomError("PLease add all fields", 400) 27 | // throw new Error("Got an error") - We are using customError 28 | } 29 | 30 | //lets add this data to database 31 | 32 | //check if user already exists 33 | const existingUser = await User.findOne({email}) 34 | 35 | if (existingUser) { 36 | throw new CustomError("User already exists", 400) 37 | } 38 | 39 | const user = await User.create({ 40 | name, 41 | email, 42 | password 43 | }) 44 | 45 | const token = user.getJWTtoken() 46 | //safety 47 | user.password = undefined 48 | 49 | //store this token in user's cookie 50 | res.cookie("token", token, cookieOptions) 51 | 52 | // send back a response to user 53 | res.status(200).json({ 54 | success: true, 55 | token, 56 | user, 57 | }) 58 | 59 | 60 | }) 61 | 62 | /********************************************************* 63 | * @LOGIN 64 | * @route http://localhost:5000/api/auth/login 65 | * @description User Login Controller for signing in the user 66 | * @returns User Object 67 | *********************************************************/ 68 | 69 | export const login = asyncHandler(async (req, res) => { 70 | const {email, password} = req.body 71 | 72 | //validation 73 | if (!email || !password) { 74 | throw new CustomError("PLease fill all details", 400) 75 | } 76 | 77 | const user = User.findOne({email}).select("+password") 78 | 79 | if (!user) { 80 | throw new CustomError("Invalid credentials", 400) 81 | } 82 | 83 | const isPasswordMatched = await user.comparePassword(password) 84 | 85 | if (isPasswordMatched) { 86 | const token = user.getJWTtoken() 87 | user.password = undefined 88 | res.cookie("token", token, cookieOptions) 89 | return res.status(200).json({ 90 | success: true, 91 | token, 92 | user 93 | }) 94 | } 95 | 96 | throw new CustomError("Password is incorrect", 400) 97 | }) 98 | 99 | /********************************************************** 100 | * @LOGOUT 101 | * @route http://localhost:5000/api/auth/logout 102 | * @description User Logout Controller for logging out the user 103 | * @description Removes token from cookies 104 | * @returns Success Message with "Logged Out" 105 | **********************************************************/ 106 | 107 | export const logout = asyncHandler(async (req, res) => { 108 | res.cookie("token", null, { 109 | expires: new Date(Date.now()), 110 | httpOnly: true 111 | }) 112 | 113 | res.status(200).json({ 114 | success: true, 115 | message: 'Logged Out' 116 | }) 117 | }) 118 | 119 | 120 | /********************************************************** 121 | * @GET_PROFILE 122 | * @route http://localhost:5000/api/auth/profile 123 | * @description check token in cookies, if present then returns user details 124 | * @returns Logged In User Details 125 | **********************************************************/ 126 | 127 | export const getProfile = asyncHandler(async (req, res) => { 128 | 129 | const {user} = req 130 | 131 | if (!user) { 132 | throw new CustomError("User not found", 401) 133 | } 134 | 135 | res.status(200).json({ 136 | success: true, 137 | user 138 | }) 139 | }) 140 | 141 | 142 | export const forgotPassword = asyncHandler(async (req, res) => { 143 | const {email} = req.body 144 | //no email 145 | const user = await User.findOne({email}) 146 | 147 | if (!user) { 148 | throw new CustomError("User not found", 404) 149 | } 150 | 151 | const resetToken = user.generateForgotPasswordToken() 152 | 153 | await user.save({validateBeforeSave: false}) 154 | 155 | 156 | const resetUrl = `${req.protocol}://${req.get("host")}/api/v1/auth/password/reset/${resetToken}` 157 | 158 | const message = `Your password reset token is as follows \n\n ${resetUrl} \n\n if this was not requested by you, please ignore.` 159 | 160 | try { 161 | // const options = {} 162 | await mailHelper({ 163 | email: user.email, 164 | subject: "Password reset mail", 165 | message 166 | }) 167 | } catch (error) { 168 | user.forgotPasswordToken = undefined 169 | user.forgotPasswordExpiry = undefined 170 | 171 | await user.save({validateBeforeSave: false}) 172 | 173 | throw new CustomError(error.message || "Email could not be sent", 500) 174 | } 175 | 176 | }) 177 | 178 | 179 | export const resetPassword = asyncHandler(async (req, res) => { 180 | const {token: resetToken} = req.params 181 | const {password, confirmPassword} = req.body 182 | 183 | const resetPasswordToken = crypto 184 | .createHash("sha256") 185 | .update(resetToken) 186 | .digest("hex") 187 | 188 | const user = await User.findOne({ 189 | forgotPasswordToken: resetPasswordToken, 190 | forgotPasswordExpiry: { $gt : Date.now() } 191 | }) 192 | 193 | if (!user) { 194 | throw new CustomError( "password reset token in invalid or expired", 400) 195 | } 196 | 197 | if (password !== confirmPassword) { 198 | throw new CustomError("password does not match", 400) 199 | } 200 | 201 | user.password = password; 202 | user.forgotPasswordToken = undefined 203 | user.forgotPasswordExpiry = undefined 204 | 205 | await user.save() 206 | 207 | // optional 208 | 209 | const token = user.getJWTtoken() 210 | res.cookie("token", token, cookieOptions) 211 | 212 | res.status(200).json({ 213 | success: true, 214 | user, //your choice 215 | }) 216 | }) --------------------------------------------------------------------------------