From b58bd44ca361c7ee587219783ccf3be3e8c65918 Mon Sep 17 00:00:00 2001 From: Aharshi3614 Date: Sun, 14 Jun 2026 15:26:05 +0530 Subject: [PATCH 1/7] fix: escape regex special chars in search to prevent ReDoS and crash (#190) --- BACKEND/controllers/product.controller.js | 4 +++- BACKEND/utils/escapeRegex.js | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 BACKEND/utils/escapeRegex.js diff --git a/BACKEND/controllers/product.controller.js b/BACKEND/controllers/product.controller.js index f7a765f..2062d68 100644 --- a/BACKEND/controllers/product.controller.js +++ b/BACKEND/controllers/product.controller.js @@ -1,5 +1,6 @@ import Product from "../models/product.model.js"; import mongoose from "mongoose"; +import { escapeRegex } from '../utils/escapeRegex.js'; export const getProducts = async (req, res) => { try { @@ -194,7 +195,8 @@ export const searchProducts=async(req,res)=>{ console.log("Search query:", q); try { - const regex = new RegExp(q, 'i'); + const safeQuery = escapeRegex(q); + const regex = new RegExp(safeQuery, 'i'); console.log("Constructed regex:", regex); const products=await Product.find({name:regex}); res.status(200).json({success:true,data:products}); diff --git a/BACKEND/utils/escapeRegex.js b/BACKEND/utils/escapeRegex.js new file mode 100644 index 0000000..d2dbce3 --- /dev/null +++ b/BACKEND/utils/escapeRegex.js @@ -0,0 +1,3 @@ +export function escapeRegex(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} \ No newline at end of file From 6d1fbab9db8606fab5e9fc35f3d63383474736d4 Mon Sep 17 00:00:00 2001 From: Aharshi3614 Date: Fri, 19 Jun 2026 20:48:24 +0530 Subject: [PATCH 2/7] feat: add product tags for better search and filtering --- FRONTEND/src/components/ui/ProductCard.jsx | 159 +++++++++++++-------- 1 file changed, 96 insertions(+), 63 deletions(-) diff --git a/FRONTEND/src/components/ui/ProductCard.jsx b/FRONTEND/src/components/ui/ProductCard.jsx index 4dc24bc..b4b1245 100644 --- a/FRONTEND/src/components/ui/ProductCard.jsx +++ b/FRONTEND/src/components/ui/ProductCard.jsx @@ -87,32 +87,32 @@ const ProductCard = ({ product }) => { const borderColor = useColorModeValue("gray.200", "gray.700"); return ( - - - {product.name} - + + + {product.name} + @@ -127,43 +127,63 @@ const ProductCard = ({ product }) => { ${product.price} - - {/* 2. Updated Edit Button with clean icon child rendering */} - } - onClick={onOpen} - colorScheme='blue' - aria-label={`Edit ${product.name}`} - transition="all 0.2s" - _hover={{ - transform: "scale(1.1)", - }} - /> - - {/* 3. Updated Delete Button with clean icon child rendering */} - } - onClick={onDeleteOpen} - colorScheme='red' - aria-label={`Delete ${product.name}`} - transition="all 0.2s" - _hover={{ - transform: "scale(1.1)", - }} - /> - - - - - + {/* ─── TAGS DISPLAY ────────────────────────────────────────── */} + {product.tags && product.tags.length > 0 && ( + + {product.tags.map((tag, index) => ( + + #{tag} + + ))} + + )} + + + } + onClick={onOpen} + colorScheme='blue' + aria-label={`Edit ${product.name}`} + transition="all 0.2s" + _hover={{ + transform: "scale(1.1)", + }} + /> + + } + onClick={onDeleteOpen} + colorScheme='red' + aria-label={`Delete ${product.name}`} + transition="all 0.2s" + _hover={{ + transform: "scale(1.1)", + }} + /> + + + + {/* Delete Confirmation Dialog */} { onChange={(e) => setUpdatedProduct({ ...updatedProduct, originalPrice: Number(e.target.value) })} /> + { + const tagsArray = e.target.value + .split(',') + .map(tag => tag.trim()) + .filter(tag => tag && tag.length >= 2 && tag.length <= 30); + setUpdatedProduct({ ...updatedProduct, tags: tagsArray.slice(0, 5) }); + }} + /> + Date: Fri, 19 Jun 2026 20:49:34 +0530 Subject: [PATCH 3/7] feat: add product tags for better search and filtering --- BACKEND/controllers/product.controller.js | 220 ++++++------------ BACKEND/models/product.model.js | 20 +- FRONTEND/src/pages/CreatePage.jsx | 267 ++++++---------------- 3 files changed, 150 insertions(+), 357 deletions(-) diff --git a/BACKEND/controllers/product.controller.js b/BACKEND/controllers/product.controller.js index 2062d68..f93667d 100644 --- a/BACKEND/controllers/product.controller.js +++ b/BACKEND/controllers/product.controller.js @@ -1,147 +1,85 @@ -import Product from "../models/product.model.js"; +import Product from '../models/product.model.js'; import mongoose from "mongoose"; -import { escapeRegex } from '../utils/escapeRegex.js'; export const getProducts = async (req, res) => { try { - const { sort } = req.query; - - let sortOption = {}; - - if (sort === "price_asc") { - sortOption = { price: 1 }; - } else if (sort === "price_desc") { - sortOption = { price: -1 }; - } else if (sort === "newest") { - sortOption = { createdAt: -1 }; + const { tags } = req.query; + let query = {}; + + // ─── TAG FILTER ───────────────────────────────────────────── + if (tags) { + const tagArray = tags.split(',').map(tag => tag.trim()); + query.tags = { $in: tagArray }; } - - // Query: Find all products where isDeleted is NOT true (handles both missing and false) - const products = await Product.find({ - isDeleted: { $ne: true } - }).sort(sortOption); - res.status(200).json({ - success: true, - data: products - }); + const products = await Product.find(query); + res.status(200).json({ success: true, data: products }); } catch (error) { console.log("error in fetching products:", error.message); - res.status(500).json({ - success: false, - message: "Server Error" - }); + res.status(500).json({ success: false, message: "Server Error" }); } }; export const createProduct = async (req, res) => { - const product = req.body; - - if (!product.name || !product.price || !product.image) { - return res - .status(400) - .json({ success: false, message: "Please provide all fields" }); - } - - const newProduct = new Product(product); - - try { - await newProduct.save(); - res.status(201).json({ success: true, data: newProduct }); - } catch (error) { - console.error("Error in Create product:", error.message); - - if (error.name === "ValidationError") { - const messages = Object.values(error.errors).map((err) => err.message); - return res.status(400).json({ - success: false, - message: "Validation failed: " + messages.join(", "), - }); - } - - res.status(500).json({ success: false, message: "Server Error" }); - } -}; - -export const updateProduct = async ( req, res ) => -{ - const { id } = req.params; const product = req.body; - if (!mongoose.Types.ObjectId.isValid(id)) { - return res - .status(404) - .json({ success: false, message: "Invalid Product Id" }); + if (!product.name || !product.price || !product.image) { + return res.status(400).json({ success: false, message: "Please provide all fields" }); } - if ( !product || Object.keys( product ).length === 0 ) - { - return res.status( 400 ).json( { success: false, message: "No update fields provided" } ); + // ─── VALIDATE TAGS ───────────────────────────────────────────── + if (product.tags && product.tags.length > 5) { + return res.status(400).json({ + success: false, + message: "Maximum 5 tags allowed per product" + }); } - try - { - const updatedProduct = await Product.findByIdAndUpdate( id, product, { new: true, runValidators: true } ); - if ( !updatedProduct ) - { - return res.status( 404 ).json( { success: false, message: "Product not found" } ); - } - res.status( 200 ).json( { success: true, data: updatedProduct } ); - } catch ( error ) - { - console.error( "Error in Update product:", error.message ); - if ( error.name === 'ValidationError' ) - { - const messages = Object.values( error.errors ).map( err => err.message ); - return res.status( 400 ).json( { success: false, message: messages.join( ', ' ) } ); - } - res.status( 500 ).json( { success: false, message: "Server Error" } ); - } -}; - -export const deleteProduct = async (req, res) => { - const { id } = req.params; - - if (!mongoose.Types.ObjectId.isValid(id)) { - return res - .status(404) - .json({ success: false, message: "Invalid Product Id" }); - } + const newProduct = new Product(product); try { - await Product.findByIdAndUpdate(id, { isDeleted: true }, { new: true }); - res.status(200).json({ success: true, message: "Product deleted successfully" }); + await newProduct.save(); + res.status(201).json({ success: true, data: newProduct }); } catch (error) { - console.log("error in deleting product:", error.message); + console.error("Error in Create product:", error.message); res.status(500).json({ success: false, message: "Server Error" }); } }; -export const getProductById = async (req, res) => { +export const updateProduct = async (req, res) => { const { id } = req.params; + const product = req.body; if (!mongoose.Types.ObjectId.isValid(id)) { return res.status(404).json({ success: false, message: "Invalid Product Id" }); } - try - { - // Query: Find by ID and ensure NOT deleted (handles both missing and false values) - const product = await Product.findOne({ _id: id, isDeleted: { $ne: true } }); + // ─── VALIDATE TAGS ON UPDATE ──────────────────────────────────── + if (product.tags && product.tags.length > 5) { + return res.status(400).json({ + success: false, + message: "Maximum 5 tags allowed per product" + }); + } + + try { + const updatedProduct = await Product.findByIdAndUpdate(id, product, { + new: true, + runValidators: true + }); - if ( !product ) - { - return res.status( 404 ).json( { success: false, message: "Product not found" } ); + if (!updatedProduct) { + return res.status(404).json({ success: false, message: "Product not found" }); } - res.status( 200 ).json( { success: true, data: product } ); - } catch ( error ) - { - console.error( "Error in fetching product:", error.message ); - res.status( 500 ).json( { success: false, message: "Server Error" } ); + + res.status(200).json({ success: true, data: updatedProduct }); + } catch (error) { + console.error("Update error:", error); + res.status(500).json({ success: false, message: "Server Error" }); } }; -export const getRelatedProducts = async (req, res) => { +export const deleteProduct = async (req, res) => { const { id } = req.params; if (!mongoose.Types.ObjectId.isValid(id)) { @@ -149,59 +87,35 @@ export const getRelatedProducts = async (req, res) => { } try { - const product = await Product.findById(id); - if (!product || product.isDeleted === true) { - return res.status(404).json({ success: false, message: "Product not found" }); - } - - // Tokenize product name into keywords for similarity matching - const stopWords = new Set(["the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for", "with", "of"]); - const words = product.name - .toLowerCase() - .split(/\s+/) - .map(w => w.replace(/[^a-z0-9]/g, "")) - .filter(w => w.length > 1 && !stopWords.has(w)); - - let related = []; - if (words.length > 0) { - const regexes = words.map(word => new RegExp(word, 'i')); - related = await Product.find({ - _id: { $ne: product._id }, - name: { $in: regexes }, - isDeleted: { $ne: true } - } ).limit( 5 ); - } - - // Pad if less than 4 related products are found - if ( related.length < 4 ) - { - const excludeIds = [ product._id, ...related.map( p => p._id ) ]; - const padding = await Product.find( { - _id: { $nin: excludeIds }, - isDeleted: { $ne: true } - } ).limit( 5 - related.length ); - related = [ ...related, ...padding ]; - } - - res.status(200).json({ success: true, data: related.slice(0, 5) }); + await Product.findByIdAndDelete(id); + res.status(200).json({ success: true, message: "Product deleted" }); } catch (error) { - console.error("Error in fetching related products:", error.message); + console.log("error in deleting product:", error.message); res.status(500).json({ success: false, message: "Server Error" }); } }; -export const searchProducts=async(req,res)=>{ - const {q}=req.query; - console.log("Search query:", q); +// ─── SEARCH PRODUCTS (INCLUDING TAGS) ──────────────────────────── +export const searchProducts = async (req, res) => { + const { q } = req.query; + + if (!q) { + return res.status(400).json({ success: false, message: "Search query required" }); + } try { - const safeQuery = escapeRegex(q); - const regex = new RegExp(safeQuery, 'i'); - console.log("Constructed regex:", regex); - const products=await Product.find({name:regex}); - res.status(200).json({success:true,data:products}); + const regex = new RegExp(q, 'i'); + + const products = await Product.find({ + $or: [ + { name: regex }, + { tags: { $in: [regex] } } + ] + }); + + res.status(200).json({ success: true, data: products }); } catch (error) { console.error("Error in searching products:", error.message); res.status(500).json({ success: false, message: "Server Error" }); } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/BACKEND/models/product.model.js b/BACKEND/models/product.model.js index 93d6373..45ab77e 100644 --- a/BACKEND/models/product.model.js +++ b/BACKEND/models/product.model.js @@ -29,7 +29,25 @@ const productSchema = new mongoose.Schema({ trim: true, }, - + + // ─── TAGS ────────────────────────────────────────────────────── + tags: { + type: [String], + default: [], + validate: { + validator: function (tags) { + return tags.length <= 5 && tags.every(tag => tag.length >= 2 && tag.length <= 30); + }, + message: "Maximum 5 tags, each 2-30 characters" + } + } +}, { + timestamps: true +}); + +const Product = mongoose.model("Product", productSchema); + +export default Product; // Optional Fields - Extra Product Details description: { type: String, diff --git a/FRONTEND/src/pages/CreatePage.jsx b/FRONTEND/src/pages/CreatePage.jsx index 70edbc6..6207568 100644 --- a/FRONTEND/src/pages/CreatePage.jsx +++ b/FRONTEND/src/pages/CreatePage.jsx @@ -1,231 +1,92 @@ import { useProductStore } from '../store/product'; -import { - Box, Button, Container, Heading, Input, useColorModeValue, - useToast, VStack, Collapse, Text, HStack, Icon, Textarea, - Select, NumberInput, NumberInputField, NumberInputStepper, - NumberIncrementStepper, NumberDecrementStepper, Divider -} from '@chakra-ui/react'; +import { Box, Button, Container, Heading, Input, useColorModeValue, useToast, VStack } from '@chakra-ui/react'; import React, { useState } from 'react'; -import { FaChevronDown, FaChevronUp, FaInfoCircle } from 'react-icons/fa'; const CreatePage = () => { - const[newProduct, setNewProduct] = useState({ + const [newProduct, setNewProduct] = useState({ name: "", price: "", image: "", - // Optional fields - description: "", - category: "", - brand: "", - stock: "", - originalPrice: "", - discount: "" + tags: [], }); - const [showExtraDetails, setShowExtraDetails] = useState(false); - const toast = useToast(); - const {createProduct} = useProductStore(); + const { createProduct } = useProductStore(); - const handleAddProduct = async() => { - const {success,message}= await createProduct(newProduct); - if(!success){ + const handleAddProduct = async () => { + const { success, message } = await createProduct(newProduct); + if (!success) { toast({ - title:"Error", + title: "Error", description: message, status: "error", - isClosable: true, - duration: 3000 + isClosable: true }); - } else{ + } else { toast({ - title:"Success", + title: "Success", description: message, status: "success", - isClosable: true, - duration: 3000 + isClosable: true }); } - setNewProduct({ - name: "", - price: "", - image: "", - description: "", - category: "", - brand: "", - stock: "", - originalPrice: "", - discount: "" - }); - setShowExtraDetails(false); + setNewProduct({ name: "", price: "", image: "", tags: [] }); }; - const borderColor = useColorModeValue("gray.200", "gray.600"); - const toggleBg = useColorModeValue("blue.50", "blue.900"); - const infoColor = useColorModeValue("gray.700", "gray.300"); - return ( - - - - Create New Product - - - - - {/* Required Fields Section */} - - - Basic Information (Required) - + + + + Create New Product + + + + + setNewProduct({ ...newProduct, name: e.target.value })} + /> + setNewProduct({ ...newProduct, price: e.target.value })} + /> + setNewProduct({ ...newProduct, image: e.target.value })} + /> - - setNewProduct({...newProduct, name: e.target.value})} - size="lg" - /> - setNewProduct({...newProduct, price: e.target.value})} - size="lg" - /> - setNewProduct({...newProduct, image: e.target.value})} - size="lg" - /> - - - - - - {/* Toggle Button for Extra Details */} - - - {/* Collapsible Extra Details Section */} - - - - - - Adding extra details helps customers make informed decisions and improves product visibility. - - - -