├── .gitignore
├── LICENSE
├── README.md
├── backend
├── controllers
│ ├── analytics.controller.js
│ ├── auth.controller.js
│ ├── cart.controller.js
│ ├── coupon.controller.js
│ ├── payment.controller.js
│ └── product.controller.js
├── lib
│ ├── cloudinary.js
│ ├── db.js
│ ├── redis.js
│ └── stripe.js
├── middleware
│ └── auth.middleware.js
├── models
│ ├── coupon.model.js
│ ├── order.model.js
│ ├── product.model.js
│ └── user.model.js
├── routes
│ ├── analytics.route.js
│ ├── auth.route.js
│ ├── cart.route.js
│ ├── coupon.route.js
│ ├── payment.route.js
│ └── product.route.js
└── server.js
├── frontend
├── README.md
├── eslint.config.js
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
│ ├── bags.jpg
│ ├── glasses.png
│ ├── jackets.jpg
│ ├── jeans.jpg
│ ├── screenshot-for-readme.png
│ ├── shoes.jpg
│ ├── suits.jpg
│ ├── tshirts.jpg
│ └── vite.svg
├── src
│ ├── App.jsx
│ ├── components
│ │ ├── AnalyticsTab.jsx
│ │ ├── CartItem.jsx
│ │ ├── CategoryItem.jsx
│ │ ├── CreateProductForm.jsx
│ │ ├── FeaturedProducts.jsx
│ │ ├── GiftCouponCard.jsx
│ │ ├── LoadingSpinner.jsx
│ │ ├── Navbar.jsx
│ │ ├── OrderSummary.jsx
│ │ ├── PeopleAlsoBought.jsx
│ │ ├── ProductCard.jsx
│ │ └── ProductsList.jsx
│ ├── index.css
│ ├── lib
│ │ └── axios.js
│ ├── main.jsx
│ ├── pages
│ │ ├── AdminPage.jsx
│ │ ├── CartPage.jsx
│ │ ├── CategoryPage.jsx
│ │ ├── HomePage.jsx
│ │ ├── LoginPage.jsx
│ │ ├── PurchaseCancelPage.jsx
│ │ ├── PurchaseSuccessPage.jsx
│ │ └── SignUpPage.jsx
│ └── stores
│ │ ├── useCartStore.js
│ │ ├── useProductStore.js
│ │ └── useUserStore.js
├── tailwind.config.js
└── vite.config.js
├── package-lock.json
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | .env
16 |
17 | # Editor directories and files
18 | .vscode/*
19 | !.vscode/extensions.json
20 | .idea
21 | .DS_Store
22 | *.suo
23 | *.ntvs*
24 | *.njsproj
25 | *.sln
26 | *.sw?
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Burak
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
E-Commerce Store 🛒
2 |
3 | 
4 |
5 | [Video Tutorial on Youtube](https://youtu.be/sX57TLIPNx8)
6 |
7 | About This Course:
8 |
9 | - 🚀 Project Setup
10 | - 🗄️ MongoDB & Redis Integration
11 | - 💳 Stripe Payment Setup
12 | - 🔐 Robust Authentication System
13 | - 🔑 JWT with Refresh/Access Tokens
14 | - 📝 User Signup & Login
15 | - 🛒 E-Commerce Core
16 | - 📦 Product & Category Management
17 | - 🛍️ Shopping Cart Functionality
18 | - 💰 Checkout with Stripe
19 | - 🏷️ Coupon Code System
20 | - 👑 Admin Dashboard
21 | - 📊 Sales Analytics
22 | - 🎨 Design with Tailwind
23 | - 🛒 Cart & Checkout Process
24 | - 🔒 Security
25 | - 🛡️ Data Protection
26 | - 🚀Caching with Redis
27 | - ⌛ And a lot more...
28 |
29 | ### Setup .env file
30 |
31 | ```bash
32 | PORT=5000
33 | MONGO_URI=your_mongo_uri
34 |
35 | UPSTASH_REDIS_URL=your_redis_url
36 |
37 | ACCESS_TOKEN_SECRET=your_access_token_secret
38 | REFRESH_TOKEN_SECRET=your_refresh_token_secret
39 |
40 | CLOUDINARY_CLOUD_NAME=your_cloud_name
41 | CLOUDINARY_API_KEY=your_api_key
42 | CLOUDINARY_API_SECRET=your_api_secret
43 |
44 | STRIPE_SECRET_KEY=your_stripe_secret_key
45 | CLIENT_URL=http://localhost:5173
46 | NODE_ENV=development
47 | ```
48 |
49 | ### Run this app locally
50 |
51 | ```shell
52 | npm run build
53 | ```
54 |
55 | ### Start the app
56 |
57 | ```shell
58 | npm run start
59 | ```
60 |
--------------------------------------------------------------------------------
/backend/controllers/analytics.controller.js:
--------------------------------------------------------------------------------
1 | import Order from "../models/order.model.js";
2 | import Product from "../models/product.model.js";
3 | import User from "../models/user.model.js";
4 |
5 | export const getAnalyticsData = async () => {
6 | const totalUsers = await User.countDocuments();
7 | const totalProducts = await Product.countDocuments();
8 |
9 | const salesData = await Order.aggregate([
10 | {
11 | $group: {
12 | _id: null, // it groups all documents together,
13 | totalSales: { $sum: 1 },
14 | totalRevenue: { $sum: "$totalAmount" },
15 | },
16 | },
17 | ]);
18 |
19 | const { totalSales, totalRevenue } = salesData[0] || { totalSales: 0, totalRevenue: 0 };
20 |
21 | return {
22 | users: totalUsers,
23 | products: totalProducts,
24 | totalSales,
25 | totalRevenue,
26 | };
27 | };
28 |
29 | export const getDailySalesData = async (startDate, endDate) => {
30 | try {
31 | const dailySalesData = await Order.aggregate([
32 | {
33 | $match: {
34 | createdAt: {
35 | $gte: startDate,
36 | $lte: endDate,
37 | },
38 | },
39 | },
40 | {
41 | $group: {
42 | _id: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } },
43 | sales: { $sum: 1 },
44 | revenue: { $sum: "$totalAmount" },
45 | },
46 | },
47 | { $sort: { _id: 1 } },
48 | ]);
49 |
50 | // example of dailySalesData
51 | // [
52 | // {
53 | // _id: "2024-08-18",
54 | // sales: 12,
55 | // revenue: 1450.75
56 | // },
57 | // ]
58 |
59 | const dateArray = getDatesInRange(startDate, endDate);
60 | // console.log(dateArray) // ['2024-08-18', '2024-08-19', ... ]
61 |
62 | return dateArray.map((date) => {
63 | const foundData = dailySalesData.find((item) => item._id === date);
64 |
65 | return {
66 | date,
67 | sales: foundData?.sales || 0,
68 | revenue: foundData?.revenue || 0,
69 | };
70 | });
71 | } catch (error) {
72 | throw error;
73 | }
74 | };
75 |
76 | function getDatesInRange(startDate, endDate) {
77 | const dates = [];
78 | let currentDate = new Date(startDate);
79 |
80 | while (currentDate <= endDate) {
81 | dates.push(currentDate.toISOString().split("T")[0]);
82 | currentDate.setDate(currentDate.getDate() + 1);
83 | }
84 |
85 | return dates;
86 | }
87 |
--------------------------------------------------------------------------------
/backend/controllers/auth.controller.js:
--------------------------------------------------------------------------------
1 | import { redis } from "../lib/redis.js";
2 | import User from "../models/user.model.js";
3 | import jwt from "jsonwebtoken";
4 |
5 | const generateTokens = (userId) => {
6 | const accessToken = jwt.sign({ userId }, process.env.ACCESS_TOKEN_SECRET, {
7 | expiresIn: "15m",
8 | });
9 |
10 | const refreshToken = jwt.sign({ userId }, process.env.REFRESH_TOKEN_SECRET, {
11 | expiresIn: "7d",
12 | });
13 |
14 | return { accessToken, refreshToken };
15 | };
16 |
17 | const storeRefreshToken = async (userId, refreshToken) => {
18 | await redis.set(`refresh_token:${userId}`, refreshToken, "EX", 7 * 24 * 60 * 60); // 7days
19 | };
20 |
21 | const setCookies = (res, accessToken, refreshToken) => {
22 | res.cookie("accessToken", accessToken, {
23 | httpOnly: true, // prevent XSS attacks, cross site scripting attack
24 | secure: process.env.NODE_ENV === "production",
25 | sameSite: "strict", // prevents CSRF attack, cross-site request forgery attack
26 | maxAge: 15 * 60 * 1000, // 15 minutes
27 | });
28 | res.cookie("refreshToken", refreshToken, {
29 | httpOnly: true, // prevent XSS attacks, cross site scripting attack
30 | secure: process.env.NODE_ENV === "production",
31 | sameSite: "strict", // prevents CSRF attack, cross-site request forgery attack
32 | maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
33 | });
34 | };
35 |
36 | export const signup = async (req, res) => {
37 | const { email, password, name } = req.body;
38 | try {
39 | const userExists = await User.findOne({ email });
40 |
41 | if (userExists) {
42 | return res.status(400).json({ message: "User already exists" });
43 | }
44 | const user = await User.create({ name, email, password });
45 |
46 | // authenticate
47 | const { accessToken, refreshToken } = generateTokens(user._id);
48 | await storeRefreshToken(user._id, refreshToken);
49 |
50 | setCookies(res, accessToken, refreshToken);
51 |
52 | res.status(201).json({
53 | _id: user._id,
54 | name: user.name,
55 | email: user.email,
56 | role: user.role,
57 | });
58 | } catch (error) {
59 | console.log("Error in signup controller", error.message);
60 | res.status(500).json({ message: error.message });
61 | }
62 | };
63 |
64 | export const login = async (req, res) => {
65 | try {
66 | const { email, password } = req.body;
67 | const user = await User.findOne({ email });
68 |
69 | if (user && (await user.comparePassword(password))) {
70 | const { accessToken, refreshToken } = generateTokens(user._id);
71 | await storeRefreshToken(user._id, refreshToken);
72 | setCookies(res, accessToken, refreshToken);
73 |
74 | res.json({
75 | _id: user._id,
76 | name: user.name,
77 | email: user.email,
78 | role: user.role,
79 | });
80 | } else {
81 | res.status(400).json({ message: "Invalid email or password" });
82 | }
83 | } catch (error) {
84 | console.log("Error in login controller", error.message);
85 | res.status(500).json({ message: error.message });
86 | }
87 | };
88 |
89 | export const logout = async (req, res) => {
90 | try {
91 | const refreshToken = req.cookies.refreshToken;
92 | if (refreshToken) {
93 | const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
94 | await redis.del(`refresh_token:${decoded.userId}`);
95 | }
96 |
97 | res.clearCookie("accessToken");
98 | res.clearCookie("refreshToken");
99 | res.json({ message: "Logged out successfully" });
100 | } catch (error) {
101 | console.log("Error in logout controller", error.message);
102 | res.status(500).json({ message: "Server error", error: error.message });
103 | }
104 | };
105 |
106 | // this will refresh the access token
107 | export const refreshToken = async (req, res) => {
108 | try {
109 | const refreshToken = req.cookies.refreshToken;
110 |
111 | if (!refreshToken) {
112 | return res.status(401).json({ message: "No refresh token provided" });
113 | }
114 |
115 | const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
116 | const storedToken = await redis.get(`refresh_token:${decoded.userId}`);
117 |
118 | if (storedToken !== refreshToken) {
119 | return res.status(401).json({ message: "Invalid refresh token" });
120 | }
121 |
122 | const accessToken = jwt.sign({ userId: decoded.userId }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: "15m" });
123 |
124 | res.cookie("accessToken", accessToken, {
125 | httpOnly: true,
126 | secure: process.env.NODE_ENV === "production",
127 | sameSite: "strict",
128 | maxAge: 15 * 60 * 1000,
129 | });
130 |
131 | res.json({ message: "Token refreshed successfully" });
132 | } catch (error) {
133 | console.log("Error in refreshToken controller", error.message);
134 | res.status(500).json({ message: "Server error", error: error.message });
135 | }
136 | };
137 |
138 | export const getProfile = async (req, res) => {
139 | try {
140 | res.json(req.user);
141 | } catch (error) {
142 | res.status(500).json({ message: "Server error", error: error.message });
143 | }
144 | };
145 |
--------------------------------------------------------------------------------
/backend/controllers/cart.controller.js:
--------------------------------------------------------------------------------
1 | import Product from "../models/product.model.js";
2 |
3 | export const getCartProducts = async (req, res) => {
4 | try {
5 | const products = await Product.find({ _id: { $in: req.user.cartItems } });
6 |
7 | // add quantity for each product
8 | const cartItems = products.map((product) => {
9 | const item = req.user.cartItems.find((cartItem) => cartItem.id === product.id);
10 | return { ...product.toJSON(), quantity: item.quantity };
11 | });
12 |
13 | res.json(cartItems);
14 | } catch (error) {
15 | console.log("Error in getCartProducts controller", error.message);
16 | res.status(500).json({ message: "Server error", error: error.message });
17 | }
18 | };
19 |
20 | export const addToCart = async (req, res) => {
21 | try {
22 | const { productId } = req.body;
23 | const user = req.user;
24 |
25 | const existingItem = user.cartItems.find((item) => item.id === productId);
26 | if (existingItem) {
27 | existingItem.quantity += 1;
28 | } else {
29 | user.cartItems.push(productId);
30 | }
31 |
32 | await user.save();
33 | res.json(user.cartItems);
34 | } catch (error) {
35 | console.log("Error in addToCart controller", error.message);
36 | res.status(500).json({ message: "Server error", error: error.message });
37 | }
38 | };
39 |
40 | export const removeAllFromCart = async (req, res) => {
41 | try {
42 | const { productId } = req.body;
43 | const user = req.user;
44 | if (!productId) {
45 | user.cartItems = [];
46 | } else {
47 | user.cartItems = user.cartItems.filter((item) => item.id !== productId);
48 | }
49 | await user.save();
50 | res.json(user.cartItems);
51 | } catch (error) {
52 | res.status(500).json({ message: "Server error", error: error.message });
53 | }
54 | };
55 |
56 | export const updateQuantity = async (req, res) => {
57 | try {
58 | const { id: productId } = req.params;
59 | const { quantity } = req.body;
60 | const user = req.user;
61 | const existingItem = user.cartItems.find((item) => item.id === productId);
62 |
63 | if (existingItem) {
64 | if (quantity === 0) {
65 | user.cartItems = user.cartItems.filter((item) => item.id !== productId);
66 | await user.save();
67 | return res.json(user.cartItems);
68 | }
69 |
70 | existingItem.quantity = quantity;
71 | await user.save();
72 | res.json(user.cartItems);
73 | } else {
74 | res.status(404).json({ message: "Product not found" });
75 | }
76 | } catch (error) {
77 | console.log("Error in updateQuantity controller", error.message);
78 | res.status(500).json({ message: "Server error", error: error.message });
79 | }
80 | };
81 |
--------------------------------------------------------------------------------
/backend/controllers/coupon.controller.js:
--------------------------------------------------------------------------------
1 | import Coupon from "../models/coupon.model.js";
2 |
3 | export const getCoupon = async (req, res) => {
4 | try {
5 | const coupon = await Coupon.findOne({ userId: req.user._id, isActive: true });
6 | res.json(coupon || null);
7 | } catch (error) {
8 | console.log("Error in getCoupon controller", error.message);
9 | res.status(500).json({ message: "Server error", error: error.message });
10 | }
11 | };
12 |
13 | export const validateCoupon = async (req, res) => {
14 | try {
15 | const { code } = req.body;
16 | const coupon = await Coupon.findOne({ code: code, userId: req.user._id, isActive: true });
17 |
18 | if (!coupon) {
19 | return res.status(404).json({ message: "Coupon not found" });
20 | }
21 |
22 | if (coupon.expirationDate < new Date()) {
23 | coupon.isActive = false;
24 | await coupon.save();
25 | return res.status(404).json({ message: "Coupon expired" });
26 | }
27 |
28 | res.json({
29 | message: "Coupon is valid",
30 | code: coupon.code,
31 | discountPercentage: coupon.discountPercentage,
32 | });
33 | } catch (error) {
34 | console.log("Error in validateCoupon controller", error.message);
35 | res.status(500).json({ message: "Server error", error: error.message });
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/backend/controllers/payment.controller.js:
--------------------------------------------------------------------------------
1 | import Coupon from "../models/coupon.model.js";
2 | import Order from "../models/order.model.js";
3 | import { stripe } from "../lib/stripe.js";
4 |
5 | export const createCheckoutSession = async (req, res) => {
6 | try {
7 | const { products, couponCode } = req.body;
8 |
9 | if (!Array.isArray(products) || products.length === 0) {
10 | return res.status(400).json({ error: "Invalid or empty products array" });
11 | }
12 |
13 | let totalAmount = 0;
14 |
15 | const lineItems = products.map((product) => {
16 | const amount = Math.round(product.price * 100); // stripe wants u to send in the format of cents
17 | totalAmount += amount * product.quantity;
18 |
19 | return {
20 | price_data: {
21 | currency: "usd",
22 | product_data: {
23 | name: product.name,
24 | images: [product.image],
25 | },
26 | unit_amount: amount,
27 | },
28 | quantity: product.quantity || 1,
29 | };
30 | });
31 |
32 | let coupon = null;
33 | if (couponCode) {
34 | coupon = await Coupon.findOne({ code: couponCode, userId: req.user._id, isActive: true });
35 | if (coupon) {
36 | totalAmount -= Math.round((totalAmount * coupon.discountPercentage) / 100);
37 | }
38 | }
39 |
40 | const session = await stripe.checkout.sessions.create({
41 | payment_method_types: ["card"],
42 | line_items: lineItems,
43 | mode: "payment",
44 | success_url: `${process.env.CLIENT_URL}/purchase-success?session_id={CHECKOUT_SESSION_ID}`,
45 | cancel_url: `${process.env.CLIENT_URL}/purchase-cancel`,
46 | discounts: coupon
47 | ? [
48 | {
49 | coupon: await createStripeCoupon(coupon.discountPercentage),
50 | },
51 | ]
52 | : [],
53 | metadata: {
54 | userId: req.user._id.toString(),
55 | couponCode: couponCode || "",
56 | products: JSON.stringify(
57 | products.map((p) => ({
58 | id: p._id,
59 | quantity: p.quantity,
60 | price: p.price,
61 | }))
62 | ),
63 | },
64 | });
65 |
66 | if (totalAmount >= 20000) {
67 | await createNewCoupon(req.user._id);
68 | }
69 | res.status(200).json({ id: session.id, totalAmount: totalAmount / 100 });
70 | } catch (error) {
71 | console.error("Error processing checkout:", error);
72 | res.status(500).json({ message: "Error processing checkout", error: error.message });
73 | }
74 | };
75 |
76 | export const checkoutSuccess = async (req, res) => {
77 | try {
78 | const { sessionId } = req.body;
79 | const session = await stripe.checkout.sessions.retrieve(sessionId);
80 |
81 | if (session.payment_status === "paid") {
82 | if (session.metadata.couponCode) {
83 | await Coupon.findOneAndUpdate(
84 | {
85 | code: session.metadata.couponCode,
86 | userId: session.metadata.userId,
87 | },
88 | {
89 | isActive: false,
90 | }
91 | );
92 | }
93 |
94 | // create a new Order
95 | const products = JSON.parse(session.metadata.products);
96 | const newOrder = new Order({
97 | user: session.metadata.userId,
98 | products: products.map((product) => ({
99 | product: product.id,
100 | quantity: product.quantity,
101 | price: product.price,
102 | })),
103 | totalAmount: session.amount_total / 100, // convert from cents to dollars,
104 | stripeSessionId: sessionId,
105 | });
106 |
107 | await newOrder.save();
108 |
109 | res.status(200).json({
110 | success: true,
111 | message: "Payment successful, order created, and coupon deactivated if used.",
112 | orderId: newOrder._id,
113 | });
114 | }
115 | } catch (error) {
116 | console.error("Error processing successful checkout:", error);
117 | res.status(500).json({ message: "Error processing successful checkout", error: error.message });
118 | }
119 | };
120 |
121 | async function createStripeCoupon(discountPercentage) {
122 | const coupon = await stripe.coupons.create({
123 | percent_off: discountPercentage,
124 | duration: "once",
125 | });
126 |
127 | return coupon.id;
128 | }
129 |
130 | async function createNewCoupon(userId) {
131 | await Coupon.findOneAndDelete({ userId });
132 |
133 | const newCoupon = new Coupon({
134 | code: "GIFT" + Math.random().toString(36).substring(2, 8).toUpperCase(),
135 | discountPercentage: 10,
136 | expirationDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days from now
137 | userId: userId,
138 | });
139 |
140 | await newCoupon.save();
141 |
142 | return newCoupon;
143 | }
144 |
--------------------------------------------------------------------------------
/backend/controllers/product.controller.js:
--------------------------------------------------------------------------------
1 | import { redis } from "../lib/redis.js";
2 | import cloudinary from "../lib/cloudinary.js";
3 | import Product from "../models/product.model.js";
4 |
5 | export const getAllProducts = async (req, res) => {
6 | try {
7 | const products = await Product.find({}); // find all products
8 | res.json({ products });
9 | } catch (error) {
10 | console.log("Error in getAllProducts controller", error.message);
11 | res.status(500).json({ message: "Server error", error: error.message });
12 | }
13 | };
14 |
15 | export const getFeaturedProducts = async (req, res) => {
16 | try {
17 | let featuredProducts = await redis.get("featured_products");
18 | if (featuredProducts) {
19 | return res.json(JSON.parse(featuredProducts));
20 | }
21 |
22 | // if not in redis, fetch from mongodb
23 | // .lean() is gonna return a plain javascript object instead of a mongodb document
24 | // which is good for performance
25 | featuredProducts = await Product.find({ isFeatured: true }).lean();
26 |
27 | if (!featuredProducts) {
28 | return res.status(404).json({ message: "No featured products found" });
29 | }
30 |
31 | // store in redis for future quick access
32 |
33 | await redis.set("featured_products", JSON.stringify(featuredProducts));
34 |
35 | res.json(featuredProducts);
36 | } catch (error) {
37 | console.log("Error in getFeaturedProducts controller", error.message);
38 | res.status(500).json({ message: "Server error", error: error.message });
39 | }
40 | };
41 |
42 | export const createProduct = async (req, res) => {
43 | try {
44 | const { name, description, price, image, category } = req.body;
45 |
46 | let cloudinaryResponse = null;
47 |
48 | if (image) {
49 | cloudinaryResponse = await cloudinary.uploader.upload(image, { folder: "products" });
50 | }
51 |
52 | const product = await Product.create({
53 | name,
54 | description,
55 | price,
56 | image: cloudinaryResponse?.secure_url ? cloudinaryResponse.secure_url : "",
57 | category,
58 | });
59 |
60 | res.status(201).json(product);
61 | } catch (error) {
62 | console.log("Error in createProduct controller", error.message);
63 | res.status(500).json({ message: "Server error", error: error.message });
64 | }
65 | };
66 |
67 | export const deleteProduct = async (req, res) => {
68 | try {
69 | const product = await Product.findById(req.params.id);
70 |
71 | if (!product) {
72 | return res.status(404).json({ message: "Product not found" });
73 | }
74 |
75 | if (product.image) {
76 | const publicId = product.image.split("/").pop().split(".")[0];
77 | try {
78 | await cloudinary.uploader.destroy(`products/${publicId}`);
79 | console.log("deleted image from cloduinary");
80 | } catch (error) {
81 | console.log("error deleting image from cloduinary", error);
82 | }
83 | }
84 |
85 | await Product.findByIdAndDelete(req.params.id);
86 |
87 | res.json({ message: "Product deleted successfully" });
88 | } catch (error) {
89 | console.log("Error in deleteProduct controller", error.message);
90 | res.status(500).json({ message: "Server error", error: error.message });
91 | }
92 | };
93 |
94 | export const getRecommendedProducts = async (req, res) => {
95 | try {
96 | const products = await Product.aggregate([
97 | {
98 | $sample: { size: 4 },
99 | },
100 | {
101 | $project: {
102 | _id: 1,
103 | name: 1,
104 | description: 1,
105 | image: 1,
106 | price: 1,
107 | },
108 | },
109 | ]);
110 |
111 | res.json(products);
112 | } catch (error) {
113 | console.log("Error in getRecommendedProducts controller", error.message);
114 | res.status(500).json({ message: "Server error", error: error.message });
115 | }
116 | };
117 |
118 | export const getProductsByCategory = async (req, res) => {
119 | const { category } = req.params;
120 | try {
121 | const products = await Product.find({ category });
122 | res.json({ products });
123 | } catch (error) {
124 | console.log("Error in getProductsByCategory controller", error.message);
125 | res.status(500).json({ message: "Server error", error: error.message });
126 | }
127 | };
128 |
129 | export const toggleFeaturedProduct = async (req, res) => {
130 | try {
131 | const product = await Product.findById(req.params.id);
132 | if (product) {
133 | product.isFeatured = !product.isFeatured;
134 | const updatedProduct = await product.save();
135 | await updateFeaturedProductsCache();
136 | res.json(updatedProduct);
137 | } else {
138 | res.status(404).json({ message: "Product not found" });
139 | }
140 | } catch (error) {
141 | console.log("Error in toggleFeaturedProduct controller", error.message);
142 | res.status(500).json({ message: "Server error", error: error.message });
143 | }
144 | };
145 |
146 | async function updateFeaturedProductsCache() {
147 | try {
148 | // The lean() method is used to return plain JavaScript objects instead of full Mongoose documents. This can significantly improve performance
149 |
150 | const featuredProducts = await Product.find({ isFeatured: true }).lean();
151 | await redis.set("featured_products", JSON.stringify(featuredProducts));
152 | } catch (error) {
153 | console.log("error in update cache function");
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/backend/lib/cloudinary.js:
--------------------------------------------------------------------------------
1 | import { v2 as cloudinary } from "cloudinary";
2 | import dotenv from "dotenv";
3 |
4 | dotenv.config();
5 |
6 | cloudinary.config({
7 | cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
8 | api_key: process.env.CLOUDINARY_API_KEY,
9 | api_secret: process.env.CLOUDINARY_API_SECRET,
10 | });
11 |
12 | export default cloudinary;
13 |
--------------------------------------------------------------------------------
/backend/lib/db.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | export const connectDB = async () => {
4 | try {
5 | const conn = await mongoose.connect(process.env.MONGO_URI);
6 | console.log(`MongoDB connected: ${conn.connection.host}`);
7 | } catch (error) {
8 | console.log("Error connecting to MONGODB", error.message);
9 | process.exit(1);
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/backend/lib/redis.js:
--------------------------------------------------------------------------------
1 | import Redis from "ioredis";
2 | import dotenv from "dotenv";
3 |
4 | dotenv.config();
5 |
6 | export const redis = new Redis(process.env.UPSTASH_REDIS_URL);
7 |
--------------------------------------------------------------------------------
/backend/lib/stripe.js:
--------------------------------------------------------------------------------
1 | import Stripe from "stripe";
2 | import dotenv from "dotenv";
3 |
4 | dotenv.config();
5 |
6 | export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
7 |
--------------------------------------------------------------------------------
/backend/middleware/auth.middleware.js:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 | import User from "../models/user.model.js";
3 |
4 | export const protectRoute = async (req, res, next) => {
5 | try {
6 | const accessToken = req.cookies.accessToken;
7 |
8 | if (!accessToken) {
9 | return res.status(401).json({ message: "Unauthorized - No access token provided" });
10 | }
11 |
12 | try {
13 | const decoded = jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET);
14 | const user = await User.findById(decoded.userId).select("-password");
15 |
16 | if (!user) {
17 | return res.status(401).json({ message: "User not found" });
18 | }
19 |
20 | req.user = user;
21 |
22 | next();
23 | } catch (error) {
24 | if (error.name === "TokenExpiredError") {
25 | return res.status(401).json({ message: "Unauthorized - Access token expired" });
26 | }
27 | throw error;
28 | }
29 | } catch (error) {
30 | console.log("Error in protectRoute middleware", error.message);
31 | return res.status(401).json({ message: "Unauthorized - Invalid access token" });
32 | }
33 | };
34 |
35 | export const adminRoute = (req, res, next) => {
36 | if (req.user && req.user.role === "admin") {
37 | next();
38 | } else {
39 | return res.status(403).json({ message: "Access denied - Admin only" });
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/backend/models/coupon.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const couponSchema = new mongoose.Schema(
4 | {
5 | code: {
6 | type: String,
7 | required: true,
8 | unique: true,
9 | },
10 | discountPercentage: {
11 | type: Number,
12 | required: true,
13 | min: 0,
14 | max: 100,
15 | },
16 | expirationDate: {
17 | type: Date,
18 | required: true,
19 | },
20 | isActive: {
21 | type: Boolean,
22 | default: true,
23 | },
24 | userId: {
25 | type: mongoose.Schema.Types.ObjectId,
26 | ref: "User",
27 | required: true,
28 | unique: true,
29 | },
30 | },
31 | {
32 | timestamps: true,
33 | }
34 | );
35 |
36 | const Coupon = mongoose.model("Coupon", couponSchema);
37 |
38 | export default Coupon;
39 |
--------------------------------------------------------------------------------
/backend/models/order.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const orderSchema = new mongoose.Schema(
4 | {
5 | user: {
6 | type: mongoose.Schema.Types.ObjectId,
7 | ref: "User",
8 | required: true,
9 | },
10 | products: [
11 | {
12 | product: {
13 | type: mongoose.Schema.Types.ObjectId,
14 | ref: "Product",
15 | required: true,
16 | },
17 | quantity: {
18 | type: Number,
19 | required: true,
20 | min: 1,
21 | },
22 | price: {
23 | type: Number,
24 | required: true,
25 | min: 0,
26 | },
27 | },
28 | ],
29 | totalAmount: {
30 | type: Number,
31 | required: true,
32 | min: 0,
33 | },
34 | stripeSessionId: {
35 | type: String,
36 | unique: true,
37 | },
38 | },
39 | { timestamps: true }
40 | );
41 |
42 | const Order = mongoose.model("Order", orderSchema);
43 |
44 | export default Order;
45 |
--------------------------------------------------------------------------------
/backend/models/product.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const productSchema = new mongoose.Schema(
4 | {
5 | name: {
6 | type: String,
7 | required: true,
8 | },
9 | description: {
10 | type: String,
11 | required: true,
12 | },
13 | price: {
14 | type: Number,
15 | min: 0,
16 | required: true,
17 | },
18 | image: {
19 | type: String,
20 | required: [true, "Image is required"],
21 | },
22 | category: {
23 | type: String,
24 | required: true,
25 | },
26 | isFeatured: {
27 | type: Boolean,
28 | default: false,
29 | },
30 | },
31 | { timestamps: true }
32 | );
33 |
34 | const Product = mongoose.model("Product", productSchema);
35 |
36 | export default Product;
37 |
--------------------------------------------------------------------------------
/backend/models/user.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import bcrypt from "bcryptjs";
3 |
4 | const userSchema = new mongoose.Schema(
5 | {
6 | name: {
7 | type: String,
8 | required: [true, "Name is required"],
9 | },
10 | email: {
11 | type: String,
12 | required: [true, "Email is required"],
13 | unique: true,
14 | lowercase: true,
15 | trim: true,
16 | },
17 | password: {
18 | type: String,
19 | required: [true, "Password is required"],
20 | minlength: [6, "Password must be at least 6 characters long"],
21 | },
22 | cartItems: [
23 | {
24 | quantity: {
25 | type: Number,
26 | default: 1,
27 | },
28 | product: {
29 | type: mongoose.Schema.Types.ObjectId,
30 | ref: "Product",
31 | },
32 | },
33 | ],
34 | role: {
35 | type: String,
36 | enum: ["customer", "admin"],
37 | default: "customer",
38 | },
39 | },
40 | {
41 | timestamps: true,
42 | }
43 | );
44 |
45 | // Pre-save hook to hash password before saving to database
46 | userSchema.pre("save", async function (next) {
47 | if (!this.isModified("password")) return next();
48 |
49 | try {
50 | const salt = await bcrypt.genSalt(10);
51 | this.password = await bcrypt.hash(this.password, salt);
52 | next();
53 | } catch (error) {
54 | next(error);
55 | }
56 | });
57 |
58 | userSchema.methods.comparePassword = async function (password) {
59 | return bcrypt.compare(password, this.password);
60 | };
61 |
62 | const User = mongoose.model("User", userSchema);
63 |
64 | export default User;
65 |
--------------------------------------------------------------------------------
/backend/routes/analytics.route.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { adminRoute, protectRoute } from "../middleware/auth.middleware.js";
3 | import { getAnalyticsData, getDailySalesData } from "../controllers/analytics.controller.js";
4 |
5 | const router = express.Router();
6 |
7 | router.get("/", protectRoute, adminRoute, async (req, res) => {
8 | try {
9 | const analyticsData = await getAnalyticsData();
10 |
11 | const endDate = new Date();
12 | const startDate = new Date(endDate.getTime() - 7 * 24 * 60 * 60 * 1000);
13 |
14 | const dailySalesData = await getDailySalesData(startDate, endDate);
15 |
16 | res.json({
17 | analyticsData,
18 | dailySalesData,
19 | });
20 | } catch (error) {
21 | console.log("Error in analytics route", error.message);
22 | res.status(500).json({ message: "Server error", error: error.message });
23 | }
24 | });
25 |
26 | export default router;
27 |
--------------------------------------------------------------------------------
/backend/routes/auth.route.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { login, logout, signup, refreshToken, getProfile } from "../controllers/auth.controller.js";
3 | import { protectRoute } from "../middleware/auth.middleware.js";
4 |
5 | const router = express.Router();
6 |
7 | router.post("/signup", signup);
8 | router.post("/login", login);
9 | router.post("/logout", logout);
10 | router.post("/refresh-token", refreshToken);
11 | router.get("/profile", protectRoute, getProfile);
12 |
13 | export default router;
14 |
--------------------------------------------------------------------------------
/backend/routes/cart.route.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { addToCart, getCartProducts, removeAllFromCart, updateQuantity } from "../controllers/cart.controller.js";
3 | import { protectRoute } from "../middleware/auth.middleware.js";
4 |
5 | const router = express.Router();
6 |
7 | router.get("/", protectRoute, getCartProducts);
8 | router.post("/", protectRoute, addToCart);
9 | router.delete("/", protectRoute, removeAllFromCart);
10 | router.put("/:id", protectRoute, updateQuantity);
11 |
12 | export default router;
13 |
--------------------------------------------------------------------------------
/backend/routes/coupon.route.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { protectRoute } from "../middleware/auth.middleware.js";
3 | import { getCoupon, validateCoupon } from "../controllers/coupon.controller.js";
4 |
5 | const router = express.Router();
6 |
7 | router.get("/", protectRoute, getCoupon);
8 | router.post("/validate", protectRoute, validateCoupon);
9 |
10 | export default router;
11 |
--------------------------------------------------------------------------------
/backend/routes/payment.route.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { protectRoute } from "../middleware/auth.middleware.js";
3 | import { checkoutSuccess, createCheckoutSession } from "../controllers/payment.controller.js";
4 |
5 | const router = express.Router();
6 |
7 | router.post("/create-checkout-session", protectRoute, createCheckoutSession);
8 | router.post("/checkout-success", protectRoute, checkoutSuccess);
9 |
10 | export default router;
11 |
--------------------------------------------------------------------------------
/backend/routes/product.route.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import {
3 | createProduct,
4 | deleteProduct,
5 | getAllProducts,
6 | getFeaturedProducts,
7 | getProductsByCategory,
8 | getRecommendedProducts,
9 | toggleFeaturedProduct,
10 | } from "../controllers/product.controller.js";
11 | import { adminRoute, protectRoute } from "../middleware/auth.middleware.js";
12 |
13 | const router = express.Router();
14 |
15 | router.get("/", protectRoute, adminRoute, getAllProducts);
16 | router.get("/featured", getFeaturedProducts);
17 | router.get("/category/:category", getProductsByCategory);
18 | router.get("/recommendations", getRecommendedProducts);
19 | router.post("/", protectRoute, adminRoute, createProduct);
20 | router.patch("/:id", protectRoute, adminRoute, toggleFeaturedProduct);
21 | router.delete("/:id", protectRoute, adminRoute, deleteProduct);
22 |
23 | export default router;
24 |
--------------------------------------------------------------------------------
/backend/server.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import dotenv from "dotenv";
3 | import cookieParser from "cookie-parser";
4 | import path from "path";
5 |
6 | import authRoutes from "./routes/auth.route.js";
7 | import productRoutes from "./routes/product.route.js";
8 | import cartRoutes from "./routes/cart.route.js";
9 | import couponRoutes from "./routes/coupon.route.js";
10 | import paymentRoutes from "./routes/payment.route.js";
11 | import analyticsRoutes from "./routes/analytics.route.js";
12 |
13 | import { connectDB } from "./lib/db.js";
14 |
15 | dotenv.config();
16 |
17 | const app = express();
18 | const PORT = process.env.PORT || 5000;
19 |
20 | const __dirname = path.resolve();
21 |
22 | app.use(express.json({ limit: "10mb" })); // allows you to parse the body of the request
23 | app.use(cookieParser());
24 |
25 | app.use("/api/auth", authRoutes);
26 | app.use("/api/products", productRoutes);
27 | app.use("/api/cart", cartRoutes);
28 | app.use("/api/coupons", couponRoutes);
29 | app.use("/api/payments", paymentRoutes);
30 | app.use("/api/analytics", analyticsRoutes);
31 |
32 | if (process.env.NODE_ENV === "production") {
33 | app.use(express.static(path.join(__dirname, "/frontend/dist")));
34 |
35 | app.get("*", (req, res) => {
36 | res.sendFile(path.resolve(__dirname, "frontend", "dist", "index.html"));
37 | });
38 | }
39 |
40 | app.listen(PORT, () => {
41 | console.log("Server is running on http://localhost:" + PORT);
42 | connectDB();
43 | });
44 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # React + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
--------------------------------------------------------------------------------
/frontend/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from "@eslint/js";
2 | import globals from "globals";
3 | import react from "eslint-plugin-react";
4 | import reactHooks from "eslint-plugin-react-hooks";
5 | import reactRefresh from "eslint-plugin-react-refresh";
6 |
7 | export default [
8 | { ignores: ["dist"] },
9 | {
10 | files: ["**/*.{js,jsx}"],
11 | languageOptions: {
12 | ecmaVersion: 2020,
13 | globals: globals.browser,
14 | parserOptions: {
15 | ecmaVersion: "latest",
16 | ecmaFeatures: { jsx: true },
17 | sourceType: "module",
18 | },
19 | },
20 | settings: { react: { version: "18.3" } },
21 | plugins: {
22 | react,
23 | "react-hooks": reactHooks,
24 | "react-refresh": reactRefresh,
25 | },
26 | rules: {
27 | ...js.configs.recommended.rules,
28 | ...react.configs.recommended.rules,
29 | ...react.configs["jsx-runtime"].rules,
30 | ...reactHooks.configs.recommended.rules,
31 | "react/jsx-no-target-blank": "off",
32 | "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
33 | "react/prop-types": "off",
34 | },
35 | },
36 | ];
37 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "react": "^18.3.1",
14 | "react-dom": "^18.3.1",
15 | "lucide-react": "^0.435.0",
16 | "react-confetti": "^6.1.0",
17 | "react-hot-toast": "^2.4.1",
18 | "react-router-dom": "^6.26.1",
19 | "recharts": "^2.12.7",
20 | "framer-motion": "^11.3.30",
21 | "zustand": "^4.5.5",
22 | "@stripe/stripe-js": "^4.3.0",
23 | "axios": "^1.7.5"
24 | },
25 | "devDependencies": {
26 | "@eslint/js": "^9.9.0",
27 | "@types/react": "^18.3.3",
28 | "@types/react-dom": "^18.3.0",
29 | "@vitejs/plugin-react": "^4.3.1",
30 | "autoprefixer": "^10.4.20",
31 | "eslint": "^9.9.0",
32 | "eslint-plugin-react": "^7.35.0",
33 | "eslint-plugin-react-hooks": "^5.1.0-rc.0",
34 | "eslint-plugin-react-refresh": "^0.4.9",
35 | "globals": "^15.9.0",
36 | "postcss": "^8.4.41",
37 | "tailwindcss": "^3.4.10",
38 | "vite": "^5.4.1"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/frontend/public/bags.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/mern-ecommerce/3cccaac88b233c678cd539bf0bb538ae28c2e0ad/frontend/public/bags.jpg
--------------------------------------------------------------------------------
/frontend/public/glasses.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/mern-ecommerce/3cccaac88b233c678cd539bf0bb538ae28c2e0ad/frontend/public/glasses.png
--------------------------------------------------------------------------------
/frontend/public/jackets.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/mern-ecommerce/3cccaac88b233c678cd539bf0bb538ae28c2e0ad/frontend/public/jackets.jpg
--------------------------------------------------------------------------------
/frontend/public/jeans.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/mern-ecommerce/3cccaac88b233c678cd539bf0bb538ae28c2e0ad/frontend/public/jeans.jpg
--------------------------------------------------------------------------------
/frontend/public/screenshot-for-readme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/mern-ecommerce/3cccaac88b233c678cd539bf0bb538ae28c2e0ad/frontend/public/screenshot-for-readme.png
--------------------------------------------------------------------------------
/frontend/public/shoes.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/mern-ecommerce/3cccaac88b233c678cd539bf0bb538ae28c2e0ad/frontend/public/shoes.jpg
--------------------------------------------------------------------------------
/frontend/public/suits.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/mern-ecommerce/3cccaac88b233c678cd539bf0bb538ae28c2e0ad/frontend/public/suits.jpg
--------------------------------------------------------------------------------
/frontend/public/tshirts.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/mern-ecommerce/3cccaac88b233c678cd539bf0bb538ae28c2e0ad/frontend/public/tshirts.jpg
--------------------------------------------------------------------------------
/frontend/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { Navigate, Route, Routes } from "react-router-dom";
2 |
3 | import HomePage from "./pages/HomePage";
4 | import SignUpPage from "./pages/SignUpPage";
5 | import LoginPage from "./pages/LoginPage";
6 | import AdminPage from "./pages/AdminPage";
7 | import CategoryPage from "./pages/CategoryPage";
8 |
9 | import Navbar from "./components/Navbar";
10 | import { Toaster } from "react-hot-toast";
11 | import { useUserStore } from "./stores/useUserStore";
12 | import { useEffect } from "react";
13 | import LoadingSpinner from "./components/LoadingSpinner";
14 | import CartPage from "./pages/CartPage";
15 | import { useCartStore } from "./stores/useCartStore";
16 | import PurchaseSuccessPage from "./pages/PurchaseSuccessPage";
17 | import PurchaseCancelPage from "./pages/PurchaseCancelPage";
18 |
19 | function App() {
20 | const { user, checkAuth, checkingAuth } = useUserStore();
21 | const { getCartItems } = useCartStore();
22 | useEffect(() => {
23 | checkAuth();
24 | }, [checkAuth]);
25 |
26 | useEffect(() => {
27 | if (!user) return;
28 |
29 | getCartItems();
30 | }, [getCartItems, user]);
31 |
32 | if (checkingAuth) return ;
33 |
34 | return (
35 |
36 | {/* Background gradient */}
37 |
42 |
43 |
44 |
45 |
46 | } />
47 | : } />
48 | : } />
49 | : }
52 | />
53 | } />
54 | : } />
55 | : }
58 | />
59 | : } />
60 |
61 |
62 |
63 |
64 | );
65 | }
66 |
67 | export default App;
68 |
--------------------------------------------------------------------------------
/frontend/src/components/AnalyticsTab.jsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion";
2 | import { useEffect, useState } from "react";
3 | import axios from "../lib/axios";
4 | import { Users, Package, ShoppingCart, DollarSign } from "lucide-react";
5 | import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts";
6 |
7 | const AnalyticsTab = () => {
8 | const [analyticsData, setAnalyticsData] = useState({
9 | users: 0,
10 | products: 0,
11 | totalSales: 0,
12 | totalRevenue: 0,
13 | });
14 | const [isLoading, setIsLoading] = useState(true);
15 | const [dailySalesData, setDailySalesData] = useState([]);
16 |
17 | useEffect(() => {
18 | const fetchAnalyticsData = async () => {
19 | try {
20 | const response = await axios.get("/analytics");
21 | setAnalyticsData(response.data.analyticsData);
22 | setDailySalesData(response.data.dailySalesData);
23 | } catch (error) {
24 | console.error("Error fetching analytics data:", error);
25 | } finally {
26 | setIsLoading(false);
27 | }
28 | };
29 |
30 | fetchAnalyticsData();
31 | }, []);
32 |
33 | if (isLoading) {
34 | return Loading...
;
35 | }
36 |
37 | return (
38 |
39 |
65 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
87 |
95 |
96 |
97 |
98 |
99 | );
100 | };
101 | export default AnalyticsTab;
102 |
103 | const AnalyticsCard = ({ title, value, icon: Icon, color }) => (
104 |
110 |
111 |
112 |
{title}
113 |
{value}
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 | );
122 |
--------------------------------------------------------------------------------
/frontend/src/components/CartItem.jsx:
--------------------------------------------------------------------------------
1 | import { Minus, Plus, Trash } from "lucide-react";
2 | import { useCartStore } from "../stores/useCartStore";
3 |
4 | const CartItem = ({ item }) => {
5 | const { removeFromCart, updateQuantity } = useCartStore();
6 |
7 | return (
8 |
9 |
10 |
11 |

12 |
13 |
14 |
15 |
16 |
17 |
25 |
{item.quantity}
26 |
34 |
35 |
36 |
39 |
40 |
41 |
42 |
43 | {item.name}
44 |
45 |
{item.description}
46 |
47 |
48 |
55 |
56 |
57 |
58 |
59 | );
60 | };
61 | export default CartItem;
62 |
--------------------------------------------------------------------------------
/frontend/src/components/CategoryItem.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | const CategoryItem = ({ category }) => {
4 | return (
5 |
6 |
7 |
8 |
9 |

15 |
16 |
{category.name}
17 |
Explore {category.name}
18 |
19 |
20 |
21 |
22 | );
23 | };
24 |
25 | export default CategoryItem;
26 |
--------------------------------------------------------------------------------
/frontend/src/components/CreateProductForm.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { motion } from "framer-motion";
3 | import { PlusCircle, Upload, Loader } from "lucide-react";
4 | import { useProductStore } from "../stores/useProductStore";
5 |
6 | const categories = ["jeans", "t-shirts", "shoes", "glasses", "jackets", "suits", "bags"];
7 |
8 | const CreateProductForm = () => {
9 | const [newProduct, setNewProduct] = useState({
10 | name: "",
11 | description: "",
12 | price: "",
13 | category: "",
14 | image: "",
15 | });
16 |
17 | const { createProduct, loading } = useProductStore();
18 |
19 | const handleSubmit = async (e) => {
20 | e.preventDefault();
21 | try {
22 | await createProduct(newProduct);
23 | setNewProduct({ name: "", description: "", price: "", category: "", image: "" });
24 | } catch {
25 | console.log("error creating a product");
26 | }
27 | };
28 |
29 | const handleImageChange = (e) => {
30 | const file = e.target.files[0];
31 | if (file) {
32 | const reader = new FileReader();
33 |
34 | reader.onloadend = () => {
35 | setNewProduct({ ...newProduct, image: reader.result });
36 | };
37 |
38 | reader.readAsDataURL(file); // base64
39 | }
40 | };
41 |
42 | return (
43 |
49 | Create New Product
50 |
51 |
159 |
160 | );
161 | };
162 | export default CreateProductForm;
163 |
--------------------------------------------------------------------------------
/frontend/src/components/FeaturedProducts.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { ShoppingCart, ChevronLeft, ChevronRight } from "lucide-react";
3 | import { useCartStore } from "../stores/useCartStore";
4 |
5 | const FeaturedProducts = ({ featuredProducts }) => {
6 | const [currentIndex, setCurrentIndex] = useState(0);
7 | const [itemsPerPage, setItemsPerPage] = useState(4);
8 |
9 | const { addToCart } = useCartStore();
10 |
11 | useEffect(() => {
12 | const handleResize = () => {
13 | if (window.innerWidth < 640) setItemsPerPage(1);
14 | else if (window.innerWidth < 1024) setItemsPerPage(2);
15 | else if (window.innerWidth < 1280) setItemsPerPage(3);
16 | else setItemsPerPage(4);
17 | };
18 |
19 | handleResize();
20 | window.addEventListener("resize", handleResize);
21 | return () => window.removeEventListener("resize", handleResize);
22 | }, []);
23 |
24 | const nextSlide = () => {
25 | setCurrentIndex((prevIndex) => prevIndex + itemsPerPage);
26 | };
27 |
28 | const prevSlide = () => {
29 | setCurrentIndex((prevIndex) => prevIndex - itemsPerPage);
30 | };
31 |
32 | const isStartDisabled = currentIndex === 0;
33 | const isEndDisabled = currentIndex >= featuredProducts.length - itemsPerPage;
34 |
35 | return (
36 |
37 |
38 |
Featured
39 |
40 |
41 |
45 | {featuredProducts?.map((product) => (
46 |
47 |
48 |
49 |

54 |
55 |
56 |
{product.name}
57 |
58 | ${product.price.toFixed(2)}
59 |
60 |
68 |
69 |
70 |
71 | ))}
72 |
73 |
74 |
83 |
84 |
93 |
94 |
95 |
96 | );
97 | };
98 | export default FeaturedProducts;
99 |
--------------------------------------------------------------------------------
/frontend/src/components/GiftCouponCard.jsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion";
2 | import { useEffect, useState } from "react";
3 | import { useCartStore } from "../stores/useCartStore";
4 |
5 | const GiftCouponCard = () => {
6 | const [userInputCode, setUserInputCode] = useState("");
7 | const { coupon, isCouponApplied, applyCoupon, getMyCoupon, removeCoupon } = useCartStore();
8 |
9 | useEffect(() => {
10 | getMyCoupon();
11 | }, [getMyCoupon]);
12 |
13 | useEffect(() => {
14 | if (coupon) setUserInputCode(coupon.code);
15 | }, [coupon]);
16 |
17 | const handleApplyCoupon = () => {
18 | if (!userInputCode) return;
19 | applyCoupon(userInputCode);
20 | };
21 |
22 | const handleRemoveCoupon = async () => {
23 | await removeCoupon();
24 | setUserInputCode("");
25 | };
26 |
27 | return (
28 |
34 |
35 |
36 |
39 | setUserInputCode(e.target.value)}
48 | required
49 | />
50 |
51 |
52 |
59 | Apply Code
60 |
61 |
62 | {isCouponApplied && coupon && (
63 |
64 |
Applied Coupon
65 |
66 |
67 | {coupon.code} - {coupon.discountPercentage}% off
68 |
69 |
70 |
79 | Remove Coupon
80 |
81 |
82 | )}
83 |
84 | {coupon && (
85 |
86 |
Your Available Coupon:
87 |
88 | {coupon.code} - {coupon.discountPercentage}% off
89 |
90 |
91 | )}
92 |
93 | );
94 | };
95 | export default GiftCouponCard;
96 |
--------------------------------------------------------------------------------
/frontend/src/components/LoadingSpinner.jsx:
--------------------------------------------------------------------------------
1 | const LoadingSpinner = () => {
2 | return (
3 |
10 | );
11 | };
12 |
13 | export default LoadingSpinner;
14 |
--------------------------------------------------------------------------------
/frontend/src/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import { ShoppingCart, UserPlus, LogIn, LogOut, Lock } from "lucide-react";
2 | import { Link } from "react-router-dom";
3 | import { useUserStore } from "../stores/useUserStore";
4 | import { useCartStore } from "../stores/useCartStore";
5 |
6 | const Navbar = () => {
7 | const { user, logout } = useUserStore();
8 | const isAdmin = user?.role === "admin";
9 | const { cart } = useCartStore();
10 |
11 | return (
12 |
89 | );
90 | };
91 | export default Navbar;
92 |
--------------------------------------------------------------------------------
/frontend/src/components/OrderSummary.jsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion";
2 | import { useCartStore } from "../stores/useCartStore";
3 | import { Link } from "react-router-dom";
4 | import { MoveRight } from "lucide-react";
5 | import { loadStripe } from "@stripe/stripe-js";
6 | import axios from "../lib/axios";
7 |
8 | const stripePromise = loadStripe(
9 | "pk_test_51KZYccCoOZF2UhtOwdXQl3vcizup20zqKqT9hVUIsVzsdBrhqbUI2fE0ZdEVLdZfeHjeyFXtqaNsyCJCmZWnjNZa00PzMAjlcL"
10 | );
11 |
12 | const OrderSummary = () => {
13 | const { total, subtotal, coupon, isCouponApplied, cart } = useCartStore();
14 |
15 | const savings = subtotal - total;
16 | const formattedSubtotal = subtotal.toFixed(2);
17 | const formattedTotal = total.toFixed(2);
18 | const formattedSavings = savings.toFixed(2);
19 |
20 | const handlePayment = async () => {
21 | const stripe = await stripePromise;
22 | const res = await axios.post("/payments/create-checkout-session", {
23 | products: cart,
24 | couponCode: coupon ? coupon.code : null,
25 | });
26 |
27 | const session = res.data;
28 | const result = await stripe.redirectToCheckout({
29 | sessionId: session.id,
30 | });
31 |
32 | if (result.error) {
33 | console.error("Error:", result.error);
34 | }
35 | };
36 |
37 | return (
38 |
44 | Order summary
45 |
46 |
47 |
48 |
49 | - Original price
50 | - ${formattedSubtotal}
51 |
52 |
53 | {savings > 0 && (
54 |
55 | - Savings
56 | - -${formattedSavings}
57 |
58 | )}
59 |
60 | {coupon && isCouponApplied && (
61 |
62 | - Coupon ({coupon.code})
63 | - -{coupon.discountPercentage}%
64 |
65 | )}
66 |
67 | - Total
68 | - ${formattedTotal}
69 |
70 |
71 |
72 |
78 | Proceed to Checkout
79 |
80 |
81 |
82 | or
83 |
87 | Continue Shopping
88 |
89 |
90 |
91 |
92 |
93 | );
94 | };
95 | export default OrderSummary;
96 |
--------------------------------------------------------------------------------
/frontend/src/components/PeopleAlsoBought.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import ProductCard from "./ProductCard";
3 | import axios from "../lib/axios";
4 | import toast from "react-hot-toast";
5 | import LoadingSpinner from "./LoadingSpinner";
6 |
7 | const PeopleAlsoBought = () => {
8 | const [recommendations, setRecommendations] = useState([]);
9 | const [isLoading, setIsLoading] = useState(true);
10 |
11 | useEffect(() => {
12 | const fetchRecommendations = async () => {
13 | try {
14 | const res = await axios.get("/products/recommendations");
15 | setRecommendations(res.data);
16 | } catch (error) {
17 | toast.error(error.response.data.message || "An error occurred while fetching recommendations");
18 | } finally {
19 | setIsLoading(false);
20 | }
21 | };
22 |
23 | fetchRecommendations();
24 | }, []);
25 |
26 | if (isLoading) return ;
27 |
28 | return (
29 |
30 |
People also bought
31 |
32 | {recommendations.map((product) => (
33 |
34 | ))}
35 |
36 |
37 | );
38 | };
39 | export default PeopleAlsoBought;
40 |
--------------------------------------------------------------------------------
/frontend/src/components/ProductCard.jsx:
--------------------------------------------------------------------------------
1 | import toast from "react-hot-toast";
2 | import { ShoppingCart } from "lucide-react";
3 | import { useUserStore } from "../stores/useUserStore";
4 | import { useCartStore } from "../stores/useCartStore";
5 |
6 | const ProductCard = ({ product }) => {
7 | const { user } = useUserStore();
8 | const { addToCart } = useCartStore();
9 | const handleAddToCart = () => {
10 | if (!user) {
11 | toast.error("Please login to add products to cart", { id: "login" });
12 | return;
13 | } else {
14 | // add to cart
15 | addToCart(product);
16 | }
17 | };
18 |
19 | return (
20 |
21 |
22 |

23 |
24 |
25 |
26 |
27 |
{product.name}
28 |
29 |
30 | ${product.price}
31 |
32 |
33 |
41 |
42 |
43 | );
44 | };
45 | export default ProductCard;
46 |
--------------------------------------------------------------------------------
/frontend/src/components/ProductsList.jsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion";
2 | import { Trash, Star } from "lucide-react";
3 | import { useProductStore } from "../stores/useProductStore";
4 |
5 | const ProductsList = () => {
6 | const { deleteProduct, toggleFeaturedProduct, products } = useProductStore();
7 |
8 | console.log("products", products);
9 |
10 | return (
11 |
17 |
18 |
19 |
20 |
24 | Product
25 | |
26 |
30 | Price
31 | |
32 |
36 | Category
37 | |
38 |
39 |
43 | Featured
44 | |
45 |
49 | Actions
50 | |
51 |
52 |
53 |
54 |
55 | {products?.map((product) => (
56 |
57 |
58 |
59 |
60 | 
65 |
66 |
69 |
70 | |
71 |
72 | ${product.price.toFixed(2)}
73 | |
74 |
75 | {product.category}
76 | |
77 |
78 |
86 | |
87 |
88 |
94 | |
95 |
96 | ))}
97 |
98 |
99 |
100 | );
101 | };
102 | export default ProductsList;
103 |
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/frontend/src/lib/axios.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const axiosInstance = axios.create({
4 | baseURL: import.meta.mode === "development" ? "http://localhost:5000/api" : "/api",
5 | withCredentials: true, // send cookies to the server
6 | });
7 |
8 | export default axiosInstance;
9 |
--------------------------------------------------------------------------------
/frontend/src/main.jsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from "react";
2 | import { createRoot } from "react-dom/client";
3 | import App from "./App.jsx";
4 | import "./index.css";
5 |
6 | import { BrowserRouter } from "react-router-dom";
7 |
8 | createRoot(document.getElementById("root")).render(
9 |
10 |
11 |
12 |
13 |
14 | );
15 |
--------------------------------------------------------------------------------
/frontend/src/pages/AdminPage.jsx:
--------------------------------------------------------------------------------
1 | import { BarChart, PlusCircle, ShoppingBasket } from "lucide-react";
2 | import { useEffect, useState } from "react";
3 | import { motion } from "framer-motion";
4 |
5 | import AnalyticsTab from "../components/AnalyticsTab";
6 | import CreateProductForm from "../components/CreateProductForm";
7 | import ProductsList from "../components/ProductsList";
8 | import { useProductStore } from "../stores/useProductStore";
9 |
10 | const tabs = [
11 | { id: "create", label: "Create Product", icon: PlusCircle },
12 | { id: "products", label: "Products", icon: ShoppingBasket },
13 | { id: "analytics", label: "Analytics", icon: BarChart },
14 | ];
15 |
16 | const AdminPage = () => {
17 | const [activeTab, setActiveTab] = useState("create");
18 | const { fetchAllProducts } = useProductStore();
19 |
20 | useEffect(() => {
21 | fetchAllProducts();
22 | }, [fetchAllProducts]);
23 |
24 | return (
25 |
26 |
27 |
33 | Admin Dashboard
34 |
35 |
36 |
37 | {tabs.map((tab) => (
38 |
50 | ))}
51 |
52 | {activeTab === "create" &&
}
53 | {activeTab === "products" &&
}
54 | {activeTab === "analytics" &&
}
55 |
56 |
57 | );
58 | };
59 | export default AdminPage;
60 |
--------------------------------------------------------------------------------
/frontend/src/pages/CartPage.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 | import { useCartStore } from "../stores/useCartStore";
3 | import { motion } from "framer-motion";
4 | import { ShoppingCart } from "lucide-react";
5 | import CartItem from "../components/CartItem";
6 | import PeopleAlsoBought from "../components/PeopleAlsoBought";
7 | import OrderSummary from "../components/OrderSummary";
8 | import GiftCouponCard from "../components/GiftCouponCard";
9 |
10 | const CartPage = () => {
11 | const { cart } = useCartStore();
12 |
13 | return (
14 |
15 |
16 |
17 |
23 | {cart.length === 0 ? (
24 |
25 | ) : (
26 |
27 | {cart.map((item) => (
28 |
29 | ))}
30 |
31 | )}
32 | {cart.length > 0 && }
33 |
34 |
35 | {cart.length > 0 && (
36 |
42 |
43 |
44 |
45 | )}
46 |
47 |
48 |
49 | );
50 | };
51 | export default CartPage;
52 |
53 | const EmptyCartUI = () => (
54 |
60 |
61 | Your cart is empty
62 | Looks like you {"haven't"} added anything to your cart yet.
63 |
67 | Start Shopping
68 |
69 |
70 | );
71 |
--------------------------------------------------------------------------------
/frontend/src/pages/CategoryPage.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useProductStore } from "../stores/useProductStore";
3 | import { useParams } from "react-router-dom";
4 | import { motion } from "framer-motion";
5 | import ProductCard from "../components/ProductCard";
6 |
7 | const CategoryPage = () => {
8 | const { fetchProductsByCategory, products } = useProductStore();
9 |
10 | const { category } = useParams();
11 |
12 | useEffect(() => {
13 | fetchProductsByCategory(category);
14 | }, [fetchProductsByCategory, category]);
15 |
16 | console.log("products:", products);
17 | return (
18 |
19 |
20 |
26 | {category.charAt(0).toUpperCase() + category.slice(1)}
27 |
28 |
29 |
35 | {products?.length === 0 && (
36 |
37 | No products found
38 |
39 | )}
40 |
41 | {products?.map((product) => (
42 |
43 | ))}
44 |
45 |
46 |
47 | );
48 | };
49 | export default CategoryPage;
50 |
--------------------------------------------------------------------------------
/frontend/src/pages/HomePage.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import CategoryItem from "../components/CategoryItem";
3 | import { useProductStore } from "../stores/useProductStore";
4 | import FeaturedProducts from "../components/FeaturedProducts";
5 |
6 | const categories = [
7 | { href: "/jeans", name: "Jeans", imageUrl: "/jeans.jpg" },
8 | { href: "/t-shirts", name: "T-shirts", imageUrl: "/tshirts.jpg" },
9 | { href: "/shoes", name: "Shoes", imageUrl: "/shoes.jpg" },
10 | { href: "/glasses", name: "Glasses", imageUrl: "/glasses.png" },
11 | { href: "/jackets", name: "Jackets", imageUrl: "/jackets.jpg" },
12 | { href: "/suits", name: "Suits", imageUrl: "/suits.jpg" },
13 | { href: "/bags", name: "Bags", imageUrl: "/bags.jpg" },
14 | ];
15 |
16 | const HomePage = () => {
17 | const { fetchFeaturedProducts, products, isLoading } = useProductStore();
18 |
19 | useEffect(() => {
20 | fetchFeaturedProducts();
21 | }, [fetchFeaturedProducts]);
22 |
23 | return (
24 |
25 |
26 |
27 | Explore Our Categories
28 |
29 |
30 | Discover the latest trends in eco-friendly fashion
31 |
32 |
33 |
34 | {categories.map((category) => (
35 |
36 | ))}
37 |
38 |
39 | {!isLoading && products.length > 0 &&
}
40 |
41 |
42 | );
43 | };
44 | export default HomePage;
45 |
--------------------------------------------------------------------------------
/frontend/src/pages/LoginPage.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { motion } from "framer-motion";
3 | import { Link } from "react-router-dom";
4 | import { LogIn, Mail, Lock, ArrowRight, Loader } from "lucide-react";
5 | import { useUserStore } from "../stores/useUserStore";
6 |
7 | const LoginPage = () => {
8 | const [email, setEmail] = useState("");
9 | const [password, setPassword] = useState("");
10 |
11 | const { login, loading } = useUserStore();
12 |
13 | const handleSubmit = (e) => {
14 | e.preventDefault();
15 | console.log(email, password);
16 | login(email, password);
17 | };
18 |
19 | return (
20 |
21 |
27 | Create your account
28 |
29 |
30 |
36 |
37 |
103 |
104 |
105 | Not a member?{" "}
106 |
107 | Sign up now
108 |
109 |
110 |
111 |
112 |
113 | );
114 | };
115 | export default LoginPage;
116 |
--------------------------------------------------------------------------------
/frontend/src/pages/PurchaseCancelPage.jsx:
--------------------------------------------------------------------------------
1 | import { XCircle, ArrowLeft } from "lucide-react";
2 | import { motion } from "framer-motion";
3 | import { Link } from "react-router-dom";
4 |
5 | const PurchaseCancelPage = () => {
6 | return (
7 |
8 |
14 |
15 |
16 |
17 |
18 |
Purchase Cancelled
19 |
20 | Your order has been cancelled. No charges have been made.
21 |
22 |
23 |
24 | If you encountered any issues during the checkout process, please don't hesitate to
25 | contact our support team.
26 |
27 |
28 |
29 |
33 |
34 | Return to Shop
35 |
36 |
37 |
38 |
39 |
40 | );
41 | };
42 |
43 | export default PurchaseCancelPage;
44 |
--------------------------------------------------------------------------------
/frontend/src/pages/PurchaseSuccessPage.jsx:
--------------------------------------------------------------------------------
1 | import { ArrowRight, CheckCircle, HandHeart } from "lucide-react";
2 | import { useEffect, useState } from "react";
3 | import { Link } from "react-router-dom";
4 | import { useCartStore } from "../stores/useCartStore";
5 | import axios from "../lib/axios";
6 | import Confetti from "react-confetti";
7 |
8 | const PurchaseSuccessPage = () => {
9 | const [isProcessing, setIsProcessing] = useState(true);
10 | const { clearCart } = useCartStore();
11 | const [error, setError] = useState(null);
12 |
13 | useEffect(() => {
14 | const handleCheckoutSuccess = async (sessionId) => {
15 | try {
16 | await axios.post("/payments/checkout-success", {
17 | sessionId,
18 | });
19 | clearCart();
20 | } catch (error) {
21 | console.log(error);
22 | } finally {
23 | setIsProcessing(false);
24 | }
25 | };
26 |
27 | const sessionId = new URLSearchParams(window.location.search).get("session_id");
28 | if (sessionId) {
29 | handleCheckoutSuccess(sessionId);
30 | } else {
31 | setIsProcessing(false);
32 | setError("No session ID found in the URL");
33 | }
34 | }, [clearCart]);
35 |
36 | if (isProcessing) return "Processing...";
37 |
38 | if (error) return `Error: ${error}`;
39 |
40 | return (
41 |
42 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | Purchase Successful!
58 |
59 |
60 |
61 | Thank you for your order. {"We're"} processing it now.
62 |
63 |
64 | Check your email for order details and updates.
65 |
66 |
67 |
68 | Order number
69 | #12345
70 |
71 |
72 | Estimated delivery
73 | 3-5 business days
74 |
75 |
76 |
77 |
78 |
85 |
90 | Continue Shopping
91 |
92 |
93 |
94 |
95 |
96 |
97 | );
98 | };
99 | export default PurchaseSuccessPage;
100 |
--------------------------------------------------------------------------------
/frontend/src/pages/SignUpPage.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Link } from "react-router-dom";
3 | import { UserPlus, Mail, Lock, User, ArrowRight, Loader } from "lucide-react";
4 | import { motion } from "framer-motion";
5 | import { useUserStore } from "../stores/useUserStore";
6 |
7 | const SignUpPage = () => {
8 | const [formData, setFormData] = useState({
9 | name: "",
10 | email: "",
11 | password: "",
12 | confirmPassword: "",
13 | });
14 |
15 | const { signup, loading } = useUserStore();
16 |
17 | const handleSubmit = (e) => {
18 | e.preventDefault();
19 | signup(formData);
20 | };
21 |
22 | return (
23 |
24 |
30 | Create your account
31 |
32 |
33 |
39 |
40 |
148 |
149 |
150 | Already have an account?{" "}
151 |
152 | Login here
153 |
154 |
155 |
156 |
157 |
158 | );
159 | };
160 | export default SignUpPage;
161 |
--------------------------------------------------------------------------------
/frontend/src/stores/useCartStore.js:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import axios from "../lib/axios";
3 | import { toast } from "react-hot-toast";
4 |
5 | export const useCartStore = create((set, get) => ({
6 | cart: [],
7 | coupon: null,
8 | total: 0,
9 | subtotal: 0,
10 | isCouponApplied: false,
11 |
12 | getMyCoupon: async () => {
13 | try {
14 | const response = await axios.get("/coupons");
15 | set({ coupon: response.data });
16 | } catch (error) {
17 | console.error("Error fetching coupon:", error);
18 | }
19 | },
20 | applyCoupon: async (code) => {
21 | try {
22 | const response = await axios.post("/coupons/validate", { code });
23 | set({ coupon: response.data, isCouponApplied: true });
24 | get().calculateTotals();
25 | toast.success("Coupon applied successfully");
26 | } catch (error) {
27 | toast.error(error.response?.data?.message || "Failed to apply coupon");
28 | }
29 | },
30 | removeCoupon: () => {
31 | set({ coupon: null, isCouponApplied: false });
32 | get().calculateTotals();
33 | toast.success("Coupon removed");
34 | },
35 |
36 | getCartItems: async () => {
37 | try {
38 | const res = await axios.get("/cart");
39 | set({ cart: res.data });
40 | get().calculateTotals();
41 | } catch (error) {
42 | set({ cart: [] });
43 | toast.error(error.response.data.message || "An error occurred");
44 | }
45 | },
46 | clearCart: async () => {
47 | set({ cart: [], coupon: null, total: 0, subtotal: 0 });
48 | },
49 | addToCart: async (product) => {
50 | try {
51 | await axios.post("/cart", { productId: product._id });
52 | toast.success("Product added to cart");
53 |
54 | set((prevState) => {
55 | const existingItem = prevState.cart.find((item) => item._id === product._id);
56 | const newCart = existingItem
57 | ? prevState.cart.map((item) =>
58 | item._id === product._id ? { ...item, quantity: item.quantity + 1 } : item
59 | )
60 | : [...prevState.cart, { ...product, quantity: 1 }];
61 | return { cart: newCart };
62 | });
63 | get().calculateTotals();
64 | } catch (error) {
65 | toast.error(error.response.data.message || "An error occurred");
66 | }
67 | },
68 | removeFromCart: async (productId) => {
69 | await axios.delete(`/cart`, { data: { productId } });
70 | set((prevState) => ({ cart: prevState.cart.filter((item) => item._id !== productId) }));
71 | get().calculateTotals();
72 | },
73 | updateQuantity: async (productId, quantity) => {
74 | if (quantity === 0) {
75 | get().removeFromCart(productId);
76 | return;
77 | }
78 |
79 | await axios.put(`/cart/${productId}`, { quantity });
80 | set((prevState) => ({
81 | cart: prevState.cart.map((item) => (item._id === productId ? { ...item, quantity } : item)),
82 | }));
83 | get().calculateTotals();
84 | },
85 | calculateTotals: () => {
86 | const { cart, coupon } = get();
87 | const subtotal = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
88 | let total = subtotal;
89 |
90 | if (coupon) {
91 | const discount = subtotal * (coupon.discountPercentage / 100);
92 | total = subtotal - discount;
93 | }
94 |
95 | set({ subtotal, total });
96 | },
97 | }));
98 |
--------------------------------------------------------------------------------
/frontend/src/stores/useProductStore.js:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import toast from "react-hot-toast";
3 | import axios from "../lib/axios";
4 |
5 | export const useProductStore = create((set) => ({
6 | products: [],
7 | loading: false,
8 |
9 | setProducts: (products) => set({ products }),
10 | createProduct: async (productData) => {
11 | set({ loading: true });
12 | try {
13 | const res = await axios.post("/products", productData);
14 | set((prevState) => ({
15 | products: [...prevState.products, res.data],
16 | loading: false,
17 | }));
18 | } catch (error) {
19 | toast.error(error.response.data.error);
20 | set({ loading: false });
21 | }
22 | },
23 | fetchAllProducts: async () => {
24 | set({ loading: true });
25 | try {
26 | const response = await axios.get("/products");
27 | set({ products: response.data.products, loading: false });
28 | } catch (error) {
29 | set({ error: "Failed to fetch products", loading: false });
30 | toast.error(error.response.data.error || "Failed to fetch products");
31 | }
32 | },
33 | fetchProductsByCategory: async (category) => {
34 | set({ loading: true });
35 | try {
36 | const response = await axios.get(`/products/category/${category}`);
37 | set({ products: response.data.products, loading: false });
38 | } catch (error) {
39 | set({ error: "Failed to fetch products", loading: false });
40 | toast.error(error.response.data.error || "Failed to fetch products");
41 | }
42 | },
43 | deleteProduct: async (productId) => {
44 | set({ loading: true });
45 | try {
46 | await axios.delete(`/products/${productId}`);
47 | set((prevProducts) => ({
48 | products: prevProducts.products.filter((product) => product._id !== productId),
49 | loading: false,
50 | }));
51 | } catch (error) {
52 | set({ loading: false });
53 | toast.error(error.response.data.error || "Failed to delete product");
54 | }
55 | },
56 | toggleFeaturedProduct: async (productId) => {
57 | set({ loading: true });
58 | try {
59 | const response = await axios.patch(`/products/${productId}`);
60 | // this will update the isFeatured prop of the product
61 | set((prevProducts) => ({
62 | products: prevProducts.products.map((product) =>
63 | product._id === productId ? { ...product, isFeatured: response.data.isFeatured } : product
64 | ),
65 | loading: false,
66 | }));
67 | } catch (error) {
68 | set({ loading: false });
69 | toast.error(error.response.data.error || "Failed to update product");
70 | }
71 | },
72 | fetchFeaturedProducts: async () => {
73 | set({ loading: true });
74 | try {
75 | const response = await axios.get("/products/featured");
76 | set({ products: response.data, loading: false });
77 | } catch (error) {
78 | set({ error: "Failed to fetch products", loading: false });
79 | console.log("Error fetching featured products:", error);
80 | }
81 | },
82 | }));
83 |
--------------------------------------------------------------------------------
/frontend/src/stores/useUserStore.js:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import axios from "../lib/axios";
3 | import { toast } from "react-hot-toast";
4 |
5 | export const useUserStore = create((set, get) => ({
6 | user: null,
7 | loading: false,
8 | checkingAuth: true,
9 |
10 | signup: async ({ name, email, password, confirmPassword }) => {
11 | set({ loading: true });
12 |
13 | if (password !== confirmPassword) {
14 | set({ loading: false });
15 | return toast.error("Passwords do not match");
16 | }
17 |
18 | try {
19 | const res = await axios.post("/auth/signup", { name, email, password });
20 | set({ user: res.data, loading: false });
21 | } catch (error) {
22 | set({ loading: false });
23 | toast.error(error.response.data.message || "An error occurred");
24 | }
25 | },
26 | login: async (email, password) => {
27 | set({ loading: true });
28 |
29 | try {
30 | const res = await axios.post("/auth/login", { email, password });
31 |
32 | set({ user: res.data, loading: false });
33 | } catch (error) {
34 | set({ loading: false });
35 | toast.error(error.response.data.message || "An error occurred");
36 | }
37 | },
38 |
39 | logout: async () => {
40 | try {
41 | await axios.post("/auth/logout");
42 | set({ user: null });
43 | } catch (error) {
44 | toast.error(error.response?.data?.message || "An error occurred during logout");
45 | }
46 | },
47 |
48 | checkAuth: async () => {
49 | set({ checkingAuth: true });
50 | try {
51 | const response = await axios.get("/auth/profile");
52 | set({ user: response.data, checkingAuth: false });
53 | } catch (error) {
54 | console.log(error.message);
55 | set({ checkingAuth: false, user: null });
56 | }
57 | },
58 |
59 | refreshToken: async () => {
60 | // Prevent multiple simultaneous refresh attempts
61 | if (get().checkingAuth) return;
62 |
63 | set({ checkingAuth: true });
64 | try {
65 | const response = await axios.post("/auth/refresh-token");
66 | set({ checkingAuth: false });
67 | return response.data;
68 | } catch (error) {
69 | set({ user: null, checkingAuth: false });
70 | throw error;
71 | }
72 | },
73 | }));
74 |
75 | // TODO: Implement the axios interceptors for refreshing access token
76 |
77 | // Axios interceptor for token refresh
78 | let refreshPromise = null;
79 |
80 | axios.interceptors.response.use(
81 | (response) => response,
82 | async (error) => {
83 | const originalRequest = error.config;
84 | if (error.response?.status === 401 && !originalRequest._retry) {
85 | originalRequest._retry = true;
86 |
87 | try {
88 | // If a refresh is already in progress, wait for it to complete
89 | if (refreshPromise) {
90 | await refreshPromise;
91 | return axios(originalRequest);
92 | }
93 |
94 | // Start a new refresh process
95 | refreshPromise = useUserStore.getState().refreshToken();
96 | await refreshPromise;
97 | refreshPromise = null;
98 |
99 | return axios(originalRequest);
100 | } catch (refreshError) {
101 | // If refresh fails, redirect to login or handle as needed
102 | useUserStore.getState().logout();
103 | return Promise.reject(refreshError);
104 | }
105 | }
106 | return Promise.reject(error);
107 | }
108 | );
109 |
--------------------------------------------------------------------------------
/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | };
9 |
--------------------------------------------------------------------------------
/frontend/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | server: {
8 | proxy: {
9 | "/api": {
10 | target: "http://localhost:5000",
11 | },
12 | },
13 | },
14 | });
15 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "e-commerce-store",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "e-commerce-store",
9 | "version": "1.0.0",
10 | "license": "ISC",
11 | "dependencies": {
12 | "bcryptjs": "^2.4.3",
13 | "cloudinary": "^2.4.0",
14 | "cookie-parser": "^1.4.6",
15 | "dotenv": "^16.4.5",
16 | "express": "^4.19.2",
17 | "ioredis": "^5.4.1",
18 | "jsonwebtoken": "^9.0.2",
19 | "mongoose": "^8.5.3",
20 | "stripe": "^16.8.0"
21 | },
22 | "devDependencies": {
23 | "nodemon": "^3.1.4"
24 | }
25 | },
26 | "node_modules/@ioredis/commands": {
27 | "version": "1.2.0",
28 | "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
29 | "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="
30 | },
31 | "node_modules/@mongodb-js/saslprep": {
32 | "version": "1.1.8",
33 | "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.8.tgz",
34 | "integrity": "sha512-qKwC/M/nNNaKUBMQ0nuzm47b7ZYWQHN3pcXq4IIcoSBc2hOIrflAxJduIvvqmhoz3gR2TacTAs8vlsCVPkiEdQ==",
35 | "dependencies": {
36 | "sparse-bitfield": "^3.0.3"
37 | }
38 | },
39 | "node_modules/@types/node": {
40 | "version": "22.5.0",
41 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz",
42 | "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==",
43 | "dependencies": {
44 | "undici-types": "~6.19.2"
45 | }
46 | },
47 | "node_modules/@types/webidl-conversions": {
48 | "version": "7.0.3",
49 | "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
50 | "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="
51 | },
52 | "node_modules/@types/whatwg-url": {
53 | "version": "11.0.5",
54 | "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
55 | "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
56 | "dependencies": {
57 | "@types/webidl-conversions": "*"
58 | }
59 | },
60 | "node_modules/accepts": {
61 | "version": "1.3.8",
62 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
63 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
64 | "dependencies": {
65 | "mime-types": "~2.1.34",
66 | "negotiator": "0.6.3"
67 | },
68 | "engines": {
69 | "node": ">= 0.6"
70 | }
71 | },
72 | "node_modules/anymatch": {
73 | "version": "3.1.3",
74 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
75 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
76 | "dev": true,
77 | "dependencies": {
78 | "normalize-path": "^3.0.0",
79 | "picomatch": "^2.0.4"
80 | },
81 | "engines": {
82 | "node": ">= 8"
83 | }
84 | },
85 | "node_modules/array-flatten": {
86 | "version": "1.1.1",
87 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
88 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
89 | },
90 | "node_modules/balanced-match": {
91 | "version": "1.0.2",
92 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
93 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
94 | "dev": true
95 | },
96 | "node_modules/bcryptjs": {
97 | "version": "2.4.3",
98 | "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
99 | "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="
100 | },
101 | "node_modules/binary-extensions": {
102 | "version": "2.3.0",
103 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
104 | "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
105 | "dev": true,
106 | "engines": {
107 | "node": ">=8"
108 | },
109 | "funding": {
110 | "url": "https://github.com/sponsors/sindresorhus"
111 | }
112 | },
113 | "node_modules/body-parser": {
114 | "version": "1.20.2",
115 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
116 | "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
117 | "dependencies": {
118 | "bytes": "3.1.2",
119 | "content-type": "~1.0.5",
120 | "debug": "2.6.9",
121 | "depd": "2.0.0",
122 | "destroy": "1.2.0",
123 | "http-errors": "2.0.0",
124 | "iconv-lite": "0.4.24",
125 | "on-finished": "2.4.1",
126 | "qs": "6.11.0",
127 | "raw-body": "2.5.2",
128 | "type-is": "~1.6.18",
129 | "unpipe": "1.0.0"
130 | },
131 | "engines": {
132 | "node": ">= 0.8",
133 | "npm": "1.2.8000 || >= 1.4.16"
134 | }
135 | },
136 | "node_modules/brace-expansion": {
137 | "version": "1.1.11",
138 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
139 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
140 | "dev": true,
141 | "dependencies": {
142 | "balanced-match": "^1.0.0",
143 | "concat-map": "0.0.1"
144 | }
145 | },
146 | "node_modules/braces": {
147 | "version": "3.0.3",
148 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
149 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
150 | "dev": true,
151 | "dependencies": {
152 | "fill-range": "^7.1.1"
153 | },
154 | "engines": {
155 | "node": ">=8"
156 | }
157 | },
158 | "node_modules/bson": {
159 | "version": "6.8.0",
160 | "resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz",
161 | "integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==",
162 | "engines": {
163 | "node": ">=16.20.1"
164 | }
165 | },
166 | "node_modules/buffer-equal-constant-time": {
167 | "version": "1.0.1",
168 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
169 | "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
170 | },
171 | "node_modules/bytes": {
172 | "version": "3.1.2",
173 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
174 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
175 | "engines": {
176 | "node": ">= 0.8"
177 | }
178 | },
179 | "node_modules/call-bind": {
180 | "version": "1.0.7",
181 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
182 | "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
183 | "dependencies": {
184 | "es-define-property": "^1.0.0",
185 | "es-errors": "^1.3.0",
186 | "function-bind": "^1.1.2",
187 | "get-intrinsic": "^1.2.4",
188 | "set-function-length": "^1.2.1"
189 | },
190 | "engines": {
191 | "node": ">= 0.4"
192 | },
193 | "funding": {
194 | "url": "https://github.com/sponsors/ljharb"
195 | }
196 | },
197 | "node_modules/chokidar": {
198 | "version": "3.6.0",
199 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
200 | "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
201 | "dev": true,
202 | "dependencies": {
203 | "anymatch": "~3.1.2",
204 | "braces": "~3.0.2",
205 | "glob-parent": "~5.1.2",
206 | "is-binary-path": "~2.1.0",
207 | "is-glob": "~4.0.1",
208 | "normalize-path": "~3.0.0",
209 | "readdirp": "~3.6.0"
210 | },
211 | "engines": {
212 | "node": ">= 8.10.0"
213 | },
214 | "funding": {
215 | "url": "https://paulmillr.com/funding/"
216 | },
217 | "optionalDependencies": {
218 | "fsevents": "~2.3.2"
219 | }
220 | },
221 | "node_modules/cloudinary": {
222 | "version": "2.4.0",
223 | "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.4.0.tgz",
224 | "integrity": "sha512-5HA9VffeaR3MKiHpRo9A5SWgZFPxzlEDep0O4KzL3TIDi1hmQC9gjA4dHpVmdeFC0ZD1Xr5fGsWRKVDK9Ay9PQ==",
225 | "dependencies": {
226 | "lodash": "^4.17.21",
227 | "q": "^1.5.1"
228 | },
229 | "engines": {
230 | "node": ">=9"
231 | }
232 | },
233 | "node_modules/cluster-key-slot": {
234 | "version": "1.1.2",
235 | "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
236 | "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
237 | "engines": {
238 | "node": ">=0.10.0"
239 | }
240 | },
241 | "node_modules/concat-map": {
242 | "version": "0.0.1",
243 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
244 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
245 | "dev": true
246 | },
247 | "node_modules/content-disposition": {
248 | "version": "0.5.4",
249 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
250 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
251 | "dependencies": {
252 | "safe-buffer": "5.2.1"
253 | },
254 | "engines": {
255 | "node": ">= 0.6"
256 | }
257 | },
258 | "node_modules/content-type": {
259 | "version": "1.0.5",
260 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
261 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
262 | "engines": {
263 | "node": ">= 0.6"
264 | }
265 | },
266 | "node_modules/cookie": {
267 | "version": "0.4.1",
268 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
269 | "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
270 | "engines": {
271 | "node": ">= 0.6"
272 | }
273 | },
274 | "node_modules/cookie-parser": {
275 | "version": "1.4.6",
276 | "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
277 | "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
278 | "dependencies": {
279 | "cookie": "0.4.1",
280 | "cookie-signature": "1.0.6"
281 | },
282 | "engines": {
283 | "node": ">= 0.8.0"
284 | }
285 | },
286 | "node_modules/cookie-signature": {
287 | "version": "1.0.6",
288 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
289 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
290 | },
291 | "node_modules/debug": {
292 | "version": "2.6.9",
293 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
294 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
295 | "dependencies": {
296 | "ms": "2.0.0"
297 | }
298 | },
299 | "node_modules/define-data-property": {
300 | "version": "1.1.4",
301 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
302 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
303 | "dependencies": {
304 | "es-define-property": "^1.0.0",
305 | "es-errors": "^1.3.0",
306 | "gopd": "^1.0.1"
307 | },
308 | "engines": {
309 | "node": ">= 0.4"
310 | },
311 | "funding": {
312 | "url": "https://github.com/sponsors/ljharb"
313 | }
314 | },
315 | "node_modules/denque": {
316 | "version": "2.1.0",
317 | "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
318 | "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
319 | "engines": {
320 | "node": ">=0.10"
321 | }
322 | },
323 | "node_modules/depd": {
324 | "version": "2.0.0",
325 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
326 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
327 | "engines": {
328 | "node": ">= 0.8"
329 | }
330 | },
331 | "node_modules/destroy": {
332 | "version": "1.2.0",
333 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
334 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
335 | "engines": {
336 | "node": ">= 0.8",
337 | "npm": "1.2.8000 || >= 1.4.16"
338 | }
339 | },
340 | "node_modules/dotenv": {
341 | "version": "16.4.5",
342 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
343 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
344 | "engines": {
345 | "node": ">=12"
346 | },
347 | "funding": {
348 | "url": "https://dotenvx.com"
349 | }
350 | },
351 | "node_modules/ecdsa-sig-formatter": {
352 | "version": "1.0.11",
353 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
354 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
355 | "dependencies": {
356 | "safe-buffer": "^5.0.1"
357 | }
358 | },
359 | "node_modules/ee-first": {
360 | "version": "1.1.1",
361 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
362 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
363 | },
364 | "node_modules/encodeurl": {
365 | "version": "1.0.2",
366 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
367 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
368 | "engines": {
369 | "node": ">= 0.8"
370 | }
371 | },
372 | "node_modules/es-define-property": {
373 | "version": "1.0.0",
374 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
375 | "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
376 | "dependencies": {
377 | "get-intrinsic": "^1.2.4"
378 | },
379 | "engines": {
380 | "node": ">= 0.4"
381 | }
382 | },
383 | "node_modules/es-errors": {
384 | "version": "1.3.0",
385 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
386 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
387 | "engines": {
388 | "node": ">= 0.4"
389 | }
390 | },
391 | "node_modules/escape-html": {
392 | "version": "1.0.3",
393 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
394 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
395 | },
396 | "node_modules/etag": {
397 | "version": "1.8.1",
398 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
399 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
400 | "engines": {
401 | "node": ">= 0.6"
402 | }
403 | },
404 | "node_modules/express": {
405 | "version": "4.19.2",
406 | "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
407 | "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
408 | "dependencies": {
409 | "accepts": "~1.3.8",
410 | "array-flatten": "1.1.1",
411 | "body-parser": "1.20.2",
412 | "content-disposition": "0.5.4",
413 | "content-type": "~1.0.4",
414 | "cookie": "0.6.0",
415 | "cookie-signature": "1.0.6",
416 | "debug": "2.6.9",
417 | "depd": "2.0.0",
418 | "encodeurl": "~1.0.2",
419 | "escape-html": "~1.0.3",
420 | "etag": "~1.8.1",
421 | "finalhandler": "1.2.0",
422 | "fresh": "0.5.2",
423 | "http-errors": "2.0.0",
424 | "merge-descriptors": "1.0.1",
425 | "methods": "~1.1.2",
426 | "on-finished": "2.4.1",
427 | "parseurl": "~1.3.3",
428 | "path-to-regexp": "0.1.7",
429 | "proxy-addr": "~2.0.7",
430 | "qs": "6.11.0",
431 | "range-parser": "~1.2.1",
432 | "safe-buffer": "5.2.1",
433 | "send": "0.18.0",
434 | "serve-static": "1.15.0",
435 | "setprototypeof": "1.2.0",
436 | "statuses": "2.0.1",
437 | "type-is": "~1.6.18",
438 | "utils-merge": "1.0.1",
439 | "vary": "~1.1.2"
440 | },
441 | "engines": {
442 | "node": ">= 0.10.0"
443 | }
444 | },
445 | "node_modules/express/node_modules/cookie": {
446 | "version": "0.6.0",
447 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
448 | "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
449 | "engines": {
450 | "node": ">= 0.6"
451 | }
452 | },
453 | "node_modules/fill-range": {
454 | "version": "7.1.1",
455 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
456 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
457 | "dev": true,
458 | "dependencies": {
459 | "to-regex-range": "^5.0.1"
460 | },
461 | "engines": {
462 | "node": ">=8"
463 | }
464 | },
465 | "node_modules/finalhandler": {
466 | "version": "1.2.0",
467 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
468 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
469 | "dependencies": {
470 | "debug": "2.6.9",
471 | "encodeurl": "~1.0.2",
472 | "escape-html": "~1.0.3",
473 | "on-finished": "2.4.1",
474 | "parseurl": "~1.3.3",
475 | "statuses": "2.0.1",
476 | "unpipe": "~1.0.0"
477 | },
478 | "engines": {
479 | "node": ">= 0.8"
480 | }
481 | },
482 | "node_modules/forwarded": {
483 | "version": "0.2.0",
484 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
485 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
486 | "engines": {
487 | "node": ">= 0.6"
488 | }
489 | },
490 | "node_modules/fresh": {
491 | "version": "0.5.2",
492 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
493 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
494 | "engines": {
495 | "node": ">= 0.6"
496 | }
497 | },
498 | "node_modules/fsevents": {
499 | "version": "2.3.3",
500 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
501 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
502 | "dev": true,
503 | "hasInstallScript": true,
504 | "optional": true,
505 | "os": [
506 | "darwin"
507 | ],
508 | "engines": {
509 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
510 | }
511 | },
512 | "node_modules/function-bind": {
513 | "version": "1.1.2",
514 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
515 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
516 | "funding": {
517 | "url": "https://github.com/sponsors/ljharb"
518 | }
519 | },
520 | "node_modules/get-intrinsic": {
521 | "version": "1.2.4",
522 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
523 | "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
524 | "dependencies": {
525 | "es-errors": "^1.3.0",
526 | "function-bind": "^1.1.2",
527 | "has-proto": "^1.0.1",
528 | "has-symbols": "^1.0.3",
529 | "hasown": "^2.0.0"
530 | },
531 | "engines": {
532 | "node": ">= 0.4"
533 | },
534 | "funding": {
535 | "url": "https://github.com/sponsors/ljharb"
536 | }
537 | },
538 | "node_modules/glob-parent": {
539 | "version": "5.1.2",
540 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
541 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
542 | "dev": true,
543 | "dependencies": {
544 | "is-glob": "^4.0.1"
545 | },
546 | "engines": {
547 | "node": ">= 6"
548 | }
549 | },
550 | "node_modules/gopd": {
551 | "version": "1.0.1",
552 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
553 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
554 | "dependencies": {
555 | "get-intrinsic": "^1.1.3"
556 | },
557 | "funding": {
558 | "url": "https://github.com/sponsors/ljharb"
559 | }
560 | },
561 | "node_modules/has-flag": {
562 | "version": "3.0.0",
563 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
564 | "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
565 | "dev": true,
566 | "engines": {
567 | "node": ">=4"
568 | }
569 | },
570 | "node_modules/has-property-descriptors": {
571 | "version": "1.0.2",
572 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
573 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
574 | "dependencies": {
575 | "es-define-property": "^1.0.0"
576 | },
577 | "funding": {
578 | "url": "https://github.com/sponsors/ljharb"
579 | }
580 | },
581 | "node_modules/has-proto": {
582 | "version": "1.0.3",
583 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
584 | "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
585 | "engines": {
586 | "node": ">= 0.4"
587 | },
588 | "funding": {
589 | "url": "https://github.com/sponsors/ljharb"
590 | }
591 | },
592 | "node_modules/has-symbols": {
593 | "version": "1.0.3",
594 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
595 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
596 | "engines": {
597 | "node": ">= 0.4"
598 | },
599 | "funding": {
600 | "url": "https://github.com/sponsors/ljharb"
601 | }
602 | },
603 | "node_modules/hasown": {
604 | "version": "2.0.2",
605 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
606 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
607 | "dependencies": {
608 | "function-bind": "^1.1.2"
609 | },
610 | "engines": {
611 | "node": ">= 0.4"
612 | }
613 | },
614 | "node_modules/http-errors": {
615 | "version": "2.0.0",
616 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
617 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
618 | "dependencies": {
619 | "depd": "2.0.0",
620 | "inherits": "2.0.4",
621 | "setprototypeof": "1.2.0",
622 | "statuses": "2.0.1",
623 | "toidentifier": "1.0.1"
624 | },
625 | "engines": {
626 | "node": ">= 0.8"
627 | }
628 | },
629 | "node_modules/iconv-lite": {
630 | "version": "0.4.24",
631 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
632 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
633 | "dependencies": {
634 | "safer-buffer": ">= 2.1.2 < 3"
635 | },
636 | "engines": {
637 | "node": ">=0.10.0"
638 | }
639 | },
640 | "node_modules/ignore-by-default": {
641 | "version": "1.0.1",
642 | "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
643 | "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
644 | "dev": true
645 | },
646 | "node_modules/inherits": {
647 | "version": "2.0.4",
648 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
649 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
650 | },
651 | "node_modules/ioredis": {
652 | "version": "5.4.1",
653 | "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz",
654 | "integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==",
655 | "dependencies": {
656 | "@ioredis/commands": "^1.1.1",
657 | "cluster-key-slot": "^1.1.0",
658 | "debug": "^4.3.4",
659 | "denque": "^2.1.0",
660 | "lodash.defaults": "^4.2.0",
661 | "lodash.isarguments": "^3.1.0",
662 | "redis-errors": "^1.2.0",
663 | "redis-parser": "^3.0.0",
664 | "standard-as-callback": "^2.1.0"
665 | },
666 | "engines": {
667 | "node": ">=12.22.0"
668 | },
669 | "funding": {
670 | "type": "opencollective",
671 | "url": "https://opencollective.com/ioredis"
672 | }
673 | },
674 | "node_modules/ioredis/node_modules/debug": {
675 | "version": "4.3.6",
676 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
677 | "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
678 | "dependencies": {
679 | "ms": "2.1.2"
680 | },
681 | "engines": {
682 | "node": ">=6.0"
683 | },
684 | "peerDependenciesMeta": {
685 | "supports-color": {
686 | "optional": true
687 | }
688 | }
689 | },
690 | "node_modules/ioredis/node_modules/ms": {
691 | "version": "2.1.2",
692 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
693 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
694 | },
695 | "node_modules/ipaddr.js": {
696 | "version": "1.9.1",
697 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
698 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
699 | "engines": {
700 | "node": ">= 0.10"
701 | }
702 | },
703 | "node_modules/is-binary-path": {
704 | "version": "2.1.0",
705 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
706 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
707 | "dev": true,
708 | "dependencies": {
709 | "binary-extensions": "^2.0.0"
710 | },
711 | "engines": {
712 | "node": ">=8"
713 | }
714 | },
715 | "node_modules/is-extglob": {
716 | "version": "2.1.1",
717 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
718 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
719 | "dev": true,
720 | "engines": {
721 | "node": ">=0.10.0"
722 | }
723 | },
724 | "node_modules/is-glob": {
725 | "version": "4.0.3",
726 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
727 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
728 | "dev": true,
729 | "dependencies": {
730 | "is-extglob": "^2.1.1"
731 | },
732 | "engines": {
733 | "node": ">=0.10.0"
734 | }
735 | },
736 | "node_modules/is-number": {
737 | "version": "7.0.0",
738 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
739 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
740 | "dev": true,
741 | "engines": {
742 | "node": ">=0.12.0"
743 | }
744 | },
745 | "node_modules/jsonwebtoken": {
746 | "version": "9.0.2",
747 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
748 | "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
749 | "dependencies": {
750 | "jws": "^3.2.2",
751 | "lodash.includes": "^4.3.0",
752 | "lodash.isboolean": "^3.0.3",
753 | "lodash.isinteger": "^4.0.4",
754 | "lodash.isnumber": "^3.0.3",
755 | "lodash.isplainobject": "^4.0.6",
756 | "lodash.isstring": "^4.0.1",
757 | "lodash.once": "^4.0.0",
758 | "ms": "^2.1.1",
759 | "semver": "^7.5.4"
760 | },
761 | "engines": {
762 | "node": ">=12",
763 | "npm": ">=6"
764 | }
765 | },
766 | "node_modules/jsonwebtoken/node_modules/ms": {
767 | "version": "2.1.3",
768 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
769 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
770 | },
771 | "node_modules/jwa": {
772 | "version": "1.4.1",
773 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
774 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
775 | "dependencies": {
776 | "buffer-equal-constant-time": "1.0.1",
777 | "ecdsa-sig-formatter": "1.0.11",
778 | "safe-buffer": "^5.0.1"
779 | }
780 | },
781 | "node_modules/jws": {
782 | "version": "3.2.2",
783 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
784 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
785 | "dependencies": {
786 | "jwa": "^1.4.1",
787 | "safe-buffer": "^5.0.1"
788 | }
789 | },
790 | "node_modules/kareem": {
791 | "version": "2.6.3",
792 | "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
793 | "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==",
794 | "engines": {
795 | "node": ">=12.0.0"
796 | }
797 | },
798 | "node_modules/lodash": {
799 | "version": "4.17.21",
800 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
801 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
802 | },
803 | "node_modules/lodash.defaults": {
804 | "version": "4.2.0",
805 | "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
806 | "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
807 | },
808 | "node_modules/lodash.includes": {
809 | "version": "4.3.0",
810 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
811 | "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
812 | },
813 | "node_modules/lodash.isarguments": {
814 | "version": "3.1.0",
815 | "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
816 | "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="
817 | },
818 | "node_modules/lodash.isboolean": {
819 | "version": "3.0.3",
820 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
821 | "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
822 | },
823 | "node_modules/lodash.isinteger": {
824 | "version": "4.0.4",
825 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
826 | "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
827 | },
828 | "node_modules/lodash.isnumber": {
829 | "version": "3.0.3",
830 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
831 | "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
832 | },
833 | "node_modules/lodash.isplainobject": {
834 | "version": "4.0.6",
835 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
836 | "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
837 | },
838 | "node_modules/lodash.isstring": {
839 | "version": "4.0.1",
840 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
841 | "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
842 | },
843 | "node_modules/lodash.once": {
844 | "version": "4.1.1",
845 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
846 | "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
847 | },
848 | "node_modules/media-typer": {
849 | "version": "0.3.0",
850 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
851 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
852 | "engines": {
853 | "node": ">= 0.6"
854 | }
855 | },
856 | "node_modules/memory-pager": {
857 | "version": "1.5.0",
858 | "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
859 | "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
860 | },
861 | "node_modules/merge-descriptors": {
862 | "version": "1.0.1",
863 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
864 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
865 | },
866 | "node_modules/methods": {
867 | "version": "1.1.2",
868 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
869 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
870 | "engines": {
871 | "node": ">= 0.6"
872 | }
873 | },
874 | "node_modules/mime": {
875 | "version": "1.6.0",
876 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
877 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
878 | "bin": {
879 | "mime": "cli.js"
880 | },
881 | "engines": {
882 | "node": ">=4"
883 | }
884 | },
885 | "node_modules/mime-db": {
886 | "version": "1.52.0",
887 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
888 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
889 | "engines": {
890 | "node": ">= 0.6"
891 | }
892 | },
893 | "node_modules/mime-types": {
894 | "version": "2.1.35",
895 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
896 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
897 | "dependencies": {
898 | "mime-db": "1.52.0"
899 | },
900 | "engines": {
901 | "node": ">= 0.6"
902 | }
903 | },
904 | "node_modules/minimatch": {
905 | "version": "3.1.2",
906 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
907 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
908 | "dev": true,
909 | "dependencies": {
910 | "brace-expansion": "^1.1.7"
911 | },
912 | "engines": {
913 | "node": "*"
914 | }
915 | },
916 | "node_modules/mongodb": {
917 | "version": "6.7.0",
918 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.7.0.tgz",
919 | "integrity": "sha512-TMKyHdtMcO0fYBNORiYdmM25ijsHs+Njs963r4Tro4OQZzqYigAzYQouwWRg4OIaiLRUEGUh/1UAcH5lxdSLIA==",
920 | "dependencies": {
921 | "@mongodb-js/saslprep": "^1.1.5",
922 | "bson": "^6.7.0",
923 | "mongodb-connection-string-url": "^3.0.0"
924 | },
925 | "engines": {
926 | "node": ">=16.20.1"
927 | },
928 | "peerDependencies": {
929 | "@aws-sdk/credential-providers": "^3.188.0",
930 | "@mongodb-js/zstd": "^1.1.0",
931 | "gcp-metadata": "^5.2.0",
932 | "kerberos": "^2.0.1",
933 | "mongodb-client-encryption": ">=6.0.0 <7",
934 | "snappy": "^7.2.2",
935 | "socks": "^2.7.1"
936 | },
937 | "peerDependenciesMeta": {
938 | "@aws-sdk/credential-providers": {
939 | "optional": true
940 | },
941 | "@mongodb-js/zstd": {
942 | "optional": true
943 | },
944 | "gcp-metadata": {
945 | "optional": true
946 | },
947 | "kerberos": {
948 | "optional": true
949 | },
950 | "mongodb-client-encryption": {
951 | "optional": true
952 | },
953 | "snappy": {
954 | "optional": true
955 | },
956 | "socks": {
957 | "optional": true
958 | }
959 | }
960 | },
961 | "node_modules/mongodb-connection-string-url": {
962 | "version": "3.0.1",
963 | "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz",
964 | "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==",
965 | "dependencies": {
966 | "@types/whatwg-url": "^11.0.2",
967 | "whatwg-url": "^13.0.0"
968 | }
969 | },
970 | "node_modules/mongoose": {
971 | "version": "8.5.3",
972 | "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.5.3.tgz",
973 | "integrity": "sha512-OubSDbsAclDFGHjV82MsKyIGQWFc42Ot1l+0dhRS6U9xODM7rm/ES/WpOQd8Ds9j0Mx8QzxZtrSCnBh6o9wUqw==",
974 | "dependencies": {
975 | "bson": "^6.7.0",
976 | "kareem": "2.6.3",
977 | "mongodb": "6.7.0",
978 | "mpath": "0.9.0",
979 | "mquery": "5.0.0",
980 | "ms": "2.1.3",
981 | "sift": "17.1.3"
982 | },
983 | "engines": {
984 | "node": ">=16.20.1"
985 | },
986 | "funding": {
987 | "type": "opencollective",
988 | "url": "https://opencollective.com/mongoose"
989 | }
990 | },
991 | "node_modules/mongoose/node_modules/ms": {
992 | "version": "2.1.3",
993 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
994 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
995 | },
996 | "node_modules/mpath": {
997 | "version": "0.9.0",
998 | "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
999 | "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
1000 | "engines": {
1001 | "node": ">=4.0.0"
1002 | }
1003 | },
1004 | "node_modules/mquery": {
1005 | "version": "5.0.0",
1006 | "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz",
1007 | "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==",
1008 | "dependencies": {
1009 | "debug": "4.x"
1010 | },
1011 | "engines": {
1012 | "node": ">=14.0.0"
1013 | }
1014 | },
1015 | "node_modules/mquery/node_modules/debug": {
1016 | "version": "4.3.6",
1017 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
1018 | "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
1019 | "dependencies": {
1020 | "ms": "2.1.2"
1021 | },
1022 | "engines": {
1023 | "node": ">=6.0"
1024 | },
1025 | "peerDependenciesMeta": {
1026 | "supports-color": {
1027 | "optional": true
1028 | }
1029 | }
1030 | },
1031 | "node_modules/mquery/node_modules/ms": {
1032 | "version": "2.1.2",
1033 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
1034 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
1035 | },
1036 | "node_modules/ms": {
1037 | "version": "2.0.0",
1038 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1039 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
1040 | },
1041 | "node_modules/negotiator": {
1042 | "version": "0.6.3",
1043 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
1044 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
1045 | "engines": {
1046 | "node": ">= 0.6"
1047 | }
1048 | },
1049 | "node_modules/nodemon": {
1050 | "version": "3.1.4",
1051 | "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz",
1052 | "integrity": "sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==",
1053 | "dev": true,
1054 | "dependencies": {
1055 | "chokidar": "^3.5.2",
1056 | "debug": "^4",
1057 | "ignore-by-default": "^1.0.1",
1058 | "minimatch": "^3.1.2",
1059 | "pstree.remy": "^1.1.8",
1060 | "semver": "^7.5.3",
1061 | "simple-update-notifier": "^2.0.0",
1062 | "supports-color": "^5.5.0",
1063 | "touch": "^3.1.0",
1064 | "undefsafe": "^2.0.5"
1065 | },
1066 | "bin": {
1067 | "nodemon": "bin/nodemon.js"
1068 | },
1069 | "engines": {
1070 | "node": ">=10"
1071 | },
1072 | "funding": {
1073 | "type": "opencollective",
1074 | "url": "https://opencollective.com/nodemon"
1075 | }
1076 | },
1077 | "node_modules/nodemon/node_modules/debug": {
1078 | "version": "4.3.6",
1079 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
1080 | "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
1081 | "dev": true,
1082 | "dependencies": {
1083 | "ms": "2.1.2"
1084 | },
1085 | "engines": {
1086 | "node": ">=6.0"
1087 | },
1088 | "peerDependenciesMeta": {
1089 | "supports-color": {
1090 | "optional": true
1091 | }
1092 | }
1093 | },
1094 | "node_modules/nodemon/node_modules/ms": {
1095 | "version": "2.1.2",
1096 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
1097 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
1098 | "dev": true
1099 | },
1100 | "node_modules/normalize-path": {
1101 | "version": "3.0.0",
1102 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
1103 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
1104 | "dev": true,
1105 | "engines": {
1106 | "node": ">=0.10.0"
1107 | }
1108 | },
1109 | "node_modules/object-inspect": {
1110 | "version": "1.13.2",
1111 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
1112 | "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
1113 | "engines": {
1114 | "node": ">= 0.4"
1115 | },
1116 | "funding": {
1117 | "url": "https://github.com/sponsors/ljharb"
1118 | }
1119 | },
1120 | "node_modules/on-finished": {
1121 | "version": "2.4.1",
1122 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1123 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1124 | "dependencies": {
1125 | "ee-first": "1.1.1"
1126 | },
1127 | "engines": {
1128 | "node": ">= 0.8"
1129 | }
1130 | },
1131 | "node_modules/parseurl": {
1132 | "version": "1.3.3",
1133 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1134 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1135 | "engines": {
1136 | "node": ">= 0.8"
1137 | }
1138 | },
1139 | "node_modules/path-to-regexp": {
1140 | "version": "0.1.7",
1141 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
1142 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
1143 | },
1144 | "node_modules/picomatch": {
1145 | "version": "2.3.1",
1146 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
1147 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
1148 | "dev": true,
1149 | "engines": {
1150 | "node": ">=8.6"
1151 | },
1152 | "funding": {
1153 | "url": "https://github.com/sponsors/jonschlinkert"
1154 | }
1155 | },
1156 | "node_modules/proxy-addr": {
1157 | "version": "2.0.7",
1158 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1159 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1160 | "dependencies": {
1161 | "forwarded": "0.2.0",
1162 | "ipaddr.js": "1.9.1"
1163 | },
1164 | "engines": {
1165 | "node": ">= 0.10"
1166 | }
1167 | },
1168 | "node_modules/pstree.remy": {
1169 | "version": "1.1.8",
1170 | "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
1171 | "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
1172 | "dev": true
1173 | },
1174 | "node_modules/punycode": {
1175 | "version": "2.3.1",
1176 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
1177 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
1178 | "engines": {
1179 | "node": ">=6"
1180 | }
1181 | },
1182 | "node_modules/q": {
1183 | "version": "1.5.1",
1184 | "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
1185 | "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==",
1186 | "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)",
1187 | "engines": {
1188 | "node": ">=0.6.0",
1189 | "teleport": ">=0.2.0"
1190 | }
1191 | },
1192 | "node_modules/qs": {
1193 | "version": "6.11.0",
1194 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
1195 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
1196 | "dependencies": {
1197 | "side-channel": "^1.0.4"
1198 | },
1199 | "engines": {
1200 | "node": ">=0.6"
1201 | },
1202 | "funding": {
1203 | "url": "https://github.com/sponsors/ljharb"
1204 | }
1205 | },
1206 | "node_modules/range-parser": {
1207 | "version": "1.2.1",
1208 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1209 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1210 | "engines": {
1211 | "node": ">= 0.6"
1212 | }
1213 | },
1214 | "node_modules/raw-body": {
1215 | "version": "2.5.2",
1216 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
1217 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
1218 | "dependencies": {
1219 | "bytes": "3.1.2",
1220 | "http-errors": "2.0.0",
1221 | "iconv-lite": "0.4.24",
1222 | "unpipe": "1.0.0"
1223 | },
1224 | "engines": {
1225 | "node": ">= 0.8"
1226 | }
1227 | },
1228 | "node_modules/readdirp": {
1229 | "version": "3.6.0",
1230 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1231 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1232 | "dev": true,
1233 | "dependencies": {
1234 | "picomatch": "^2.2.1"
1235 | },
1236 | "engines": {
1237 | "node": ">=8.10.0"
1238 | }
1239 | },
1240 | "node_modules/redis-errors": {
1241 | "version": "1.2.0",
1242 | "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
1243 | "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
1244 | "engines": {
1245 | "node": ">=4"
1246 | }
1247 | },
1248 | "node_modules/redis-parser": {
1249 | "version": "3.0.0",
1250 | "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
1251 | "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
1252 | "dependencies": {
1253 | "redis-errors": "^1.0.0"
1254 | },
1255 | "engines": {
1256 | "node": ">=4"
1257 | }
1258 | },
1259 | "node_modules/safe-buffer": {
1260 | "version": "5.2.1",
1261 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1262 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1263 | "funding": [
1264 | {
1265 | "type": "github",
1266 | "url": "https://github.com/sponsors/feross"
1267 | },
1268 | {
1269 | "type": "patreon",
1270 | "url": "https://www.patreon.com/feross"
1271 | },
1272 | {
1273 | "type": "consulting",
1274 | "url": "https://feross.org/support"
1275 | }
1276 | ]
1277 | },
1278 | "node_modules/safer-buffer": {
1279 | "version": "2.1.2",
1280 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1281 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
1282 | },
1283 | "node_modules/semver": {
1284 | "version": "7.6.3",
1285 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
1286 | "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
1287 | "bin": {
1288 | "semver": "bin/semver.js"
1289 | },
1290 | "engines": {
1291 | "node": ">=10"
1292 | }
1293 | },
1294 | "node_modules/send": {
1295 | "version": "0.18.0",
1296 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
1297 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
1298 | "dependencies": {
1299 | "debug": "2.6.9",
1300 | "depd": "2.0.0",
1301 | "destroy": "1.2.0",
1302 | "encodeurl": "~1.0.2",
1303 | "escape-html": "~1.0.3",
1304 | "etag": "~1.8.1",
1305 | "fresh": "0.5.2",
1306 | "http-errors": "2.0.0",
1307 | "mime": "1.6.0",
1308 | "ms": "2.1.3",
1309 | "on-finished": "2.4.1",
1310 | "range-parser": "~1.2.1",
1311 | "statuses": "2.0.1"
1312 | },
1313 | "engines": {
1314 | "node": ">= 0.8.0"
1315 | }
1316 | },
1317 | "node_modules/send/node_modules/ms": {
1318 | "version": "2.1.3",
1319 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1320 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
1321 | },
1322 | "node_modules/serve-static": {
1323 | "version": "1.15.0",
1324 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
1325 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
1326 | "dependencies": {
1327 | "encodeurl": "~1.0.2",
1328 | "escape-html": "~1.0.3",
1329 | "parseurl": "~1.3.3",
1330 | "send": "0.18.0"
1331 | },
1332 | "engines": {
1333 | "node": ">= 0.8.0"
1334 | }
1335 | },
1336 | "node_modules/set-function-length": {
1337 | "version": "1.2.2",
1338 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
1339 | "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
1340 | "dependencies": {
1341 | "define-data-property": "^1.1.4",
1342 | "es-errors": "^1.3.0",
1343 | "function-bind": "^1.1.2",
1344 | "get-intrinsic": "^1.2.4",
1345 | "gopd": "^1.0.1",
1346 | "has-property-descriptors": "^1.0.2"
1347 | },
1348 | "engines": {
1349 | "node": ">= 0.4"
1350 | }
1351 | },
1352 | "node_modules/setprototypeof": {
1353 | "version": "1.2.0",
1354 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1355 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
1356 | },
1357 | "node_modules/side-channel": {
1358 | "version": "1.0.6",
1359 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
1360 | "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
1361 | "dependencies": {
1362 | "call-bind": "^1.0.7",
1363 | "es-errors": "^1.3.0",
1364 | "get-intrinsic": "^1.2.4",
1365 | "object-inspect": "^1.13.1"
1366 | },
1367 | "engines": {
1368 | "node": ">= 0.4"
1369 | },
1370 | "funding": {
1371 | "url": "https://github.com/sponsors/ljharb"
1372 | }
1373 | },
1374 | "node_modules/sift": {
1375 | "version": "17.1.3",
1376 | "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz",
1377 | "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ=="
1378 | },
1379 | "node_modules/simple-update-notifier": {
1380 | "version": "2.0.0",
1381 | "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
1382 | "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
1383 | "dev": true,
1384 | "dependencies": {
1385 | "semver": "^7.5.3"
1386 | },
1387 | "engines": {
1388 | "node": ">=10"
1389 | }
1390 | },
1391 | "node_modules/sparse-bitfield": {
1392 | "version": "3.0.3",
1393 | "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
1394 | "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
1395 | "dependencies": {
1396 | "memory-pager": "^1.0.2"
1397 | }
1398 | },
1399 | "node_modules/standard-as-callback": {
1400 | "version": "2.1.0",
1401 | "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
1402 | "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="
1403 | },
1404 | "node_modules/statuses": {
1405 | "version": "2.0.1",
1406 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
1407 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
1408 | "engines": {
1409 | "node": ">= 0.8"
1410 | }
1411 | },
1412 | "node_modules/stripe": {
1413 | "version": "16.8.0",
1414 | "resolved": "https://registry.npmjs.org/stripe/-/stripe-16.8.0.tgz",
1415 | "integrity": "sha512-6rOIcGOkxcc29jvhEyOYmpPFilekOBV+7vpemAoIAfbtCRW1yxzdDGM0/0vyekHglLL+wqGpP5ldrhO3dJ2JEQ==",
1416 | "dependencies": {
1417 | "@types/node": ">=8.1.0",
1418 | "qs": "^6.11.0"
1419 | },
1420 | "engines": {
1421 | "node": ">=12.*"
1422 | }
1423 | },
1424 | "node_modules/supports-color": {
1425 | "version": "5.5.0",
1426 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
1427 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
1428 | "dev": true,
1429 | "dependencies": {
1430 | "has-flag": "^3.0.0"
1431 | },
1432 | "engines": {
1433 | "node": ">=4"
1434 | }
1435 | },
1436 | "node_modules/to-regex-range": {
1437 | "version": "5.0.1",
1438 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
1439 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1440 | "dev": true,
1441 | "dependencies": {
1442 | "is-number": "^7.0.0"
1443 | },
1444 | "engines": {
1445 | "node": ">=8.0"
1446 | }
1447 | },
1448 | "node_modules/toidentifier": {
1449 | "version": "1.0.1",
1450 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1451 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1452 | "engines": {
1453 | "node": ">=0.6"
1454 | }
1455 | },
1456 | "node_modules/touch": {
1457 | "version": "3.1.1",
1458 | "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
1459 | "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
1460 | "dev": true,
1461 | "bin": {
1462 | "nodetouch": "bin/nodetouch.js"
1463 | }
1464 | },
1465 | "node_modules/tr46": {
1466 | "version": "4.1.1",
1467 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz",
1468 | "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==",
1469 | "dependencies": {
1470 | "punycode": "^2.3.0"
1471 | },
1472 | "engines": {
1473 | "node": ">=14"
1474 | }
1475 | },
1476 | "node_modules/type-is": {
1477 | "version": "1.6.18",
1478 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
1479 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1480 | "dependencies": {
1481 | "media-typer": "0.3.0",
1482 | "mime-types": "~2.1.24"
1483 | },
1484 | "engines": {
1485 | "node": ">= 0.6"
1486 | }
1487 | },
1488 | "node_modules/undefsafe": {
1489 | "version": "2.0.5",
1490 | "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
1491 | "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
1492 | "dev": true
1493 | },
1494 | "node_modules/undici-types": {
1495 | "version": "6.19.8",
1496 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
1497 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
1498 | },
1499 | "node_modules/unpipe": {
1500 | "version": "1.0.0",
1501 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1502 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1503 | "engines": {
1504 | "node": ">= 0.8"
1505 | }
1506 | },
1507 | "node_modules/utils-merge": {
1508 | "version": "1.0.1",
1509 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
1510 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
1511 | "engines": {
1512 | "node": ">= 0.4.0"
1513 | }
1514 | },
1515 | "node_modules/vary": {
1516 | "version": "1.1.2",
1517 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1518 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1519 | "engines": {
1520 | "node": ">= 0.8"
1521 | }
1522 | },
1523 | "node_modules/webidl-conversions": {
1524 | "version": "7.0.0",
1525 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
1526 | "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
1527 | "engines": {
1528 | "node": ">=12"
1529 | }
1530 | },
1531 | "node_modules/whatwg-url": {
1532 | "version": "13.0.0",
1533 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz",
1534 | "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==",
1535 | "dependencies": {
1536 | "tr46": "^4.1.1",
1537 | "webidl-conversions": "^7.0.0"
1538 | },
1539 | "engines": {
1540 | "node": ">=16"
1541 | }
1542 | }
1543 | }
1544 | }
1545 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "e-commerce-store",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "backend/server.js",
6 | "scripts": {
7 | "dev": "nodemon backend/server.js",
8 | "start": "node backend/server.js",
9 | "build": "npm install && npm install --prefix frontend && npm run build --prefix frontend"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "type": "module",
14 | "license": "ISC",
15 | "dependencies": {
16 | "bcryptjs": "^2.4.3",
17 | "cloudinary": "^2.4.0",
18 | "cookie-parser": "^1.4.6",
19 | "dotenv": "^16.4.5",
20 | "express": "^4.19.2",
21 | "ioredis": "^5.4.1",
22 | "jsonwebtoken": "^9.0.2",
23 | "mongoose": "^8.5.3",
24 | "stripe": "^16.8.0"
25 | },
26 | "devDependencies": {
27 | "nodemon": "^3.1.4"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------