Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 31 additions & 22 deletions BACKEND/controllers/product.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ export const getProducts = async (req, res, next) => {
if (maxPrice) filter.price.$lte = Number(maxPrice);
}
if (brand) {
// Case-insensitive brand search
filter.brand = { $regex: new RegExp(brand, 'i') };
}
if (minRating) {
Expand Down Expand Up @@ -158,6 +157,7 @@ export const createProduct = async (req, res, next) => {
}

let finalImageUrl = imageUrl || '';
let cloudinaryPublicId; // track uploaded image's public ID for potential cleanup

if (req.file) {
if (!cloudinaryConfigured()) {
Expand All @@ -166,7 +166,8 @@ export const createProduct = async (req, res, next) => {
try {
const result = await uploadToCloudinary(req.file.buffer);
finalImageUrl = result.secure_url;
} catch (error) {
cloudinaryPublicId = result.public_id; // save public ID for cleanup if needed
} catch (_error) {
return next(new AppError("Image upload failed", 500));
}
}
Expand Down Expand Up @@ -194,7 +195,15 @@ export const createProduct = async (req, res, next) => {
await invalidateProductCache();
res.status(201).json({ success: true, data: newProduct });
} catch (error) {
next(error);
// CLEANUP: delete uploaded Cloudinary image if save failed
if (cloudinaryPublicId) {
try {
await cloudinary.uploader.destroy(cloudinaryPublicId);
} catch (destroyError) {
console.error("Failed to delete Cloudinary image:", destroyError.message);
}
}
return next(error);
}
};

Expand Down Expand Up @@ -245,8 +254,7 @@ export const updateProduct = async (req, res, next) => {
try {
const result = await uploadToCloudinary(req.file.buffer);
updateData.image = result.secure_url;

} catch (error) {
} catch (_error) {
return next(new AppError("Image upload failed", 500));
}
}
Expand All @@ -256,13 +264,13 @@ export const updateProduct = async (req, res, next) => {
if (!updatedProduct) {
return next(new AppError("Product not found", 404));
}
if (req.file){
if (req.file) {
const oldPublicId = extractCloudinaryPublicId(existing.image);
if (oldPublicId) {
cloudinary.uploader.destroy(oldPublicId).catch((err) => {
console.warn("Old image cleanup failed:", err.message);
});
}
if (oldPublicId) {
cloudinary.uploader.destroy(oldPublicId).catch((err) => {
console.warn("Old image cleanup failed:", err.message);
});
}
}

await indexProduct(updatedProduct);
Expand Down Expand Up @@ -305,20 +313,19 @@ export const deleteProduct = async (req, res, next) => {
const { id } = req.params;

if (!mongoose.Types.ObjectId.isValid(id)) {
return res.status(404).json({ success: false, message: "Invalid Product Id" });
return next(new AppError("Invalid Product Id format", 404));
}

try {
const product = await Product.findByIdAndUpdate(id, { isDeleted: true }, { new: true });
if (!product) {
return res.status(404).json({ success: false, message: "Product not found" });
return next(new AppError("Product not found", 404));
}
await deleteProductFromIndex(id);
await invalidateProductCache();
res.status(200).json({ success: true, message: "Product deleted successfully" });
} catch (error) {
console.log("error in deleting product:", error.message);
res.status(500).json({ success: false, message: "Server Error" });
next(error);
}
};

Expand Down Expand Up @@ -371,7 +378,7 @@ export const getRelatedProducts = async (req, res) => {
const orConditions = [];
if (product.category) orConditions.push({ category: product.category });
if (product.brand) orConditions.push({ brand: product.brand });
if (targetTagsSet.size > 0) orConditions.push({ tags: { $in: [ ...targetTagsSet ] } });
if (targetTagsSet.size > 0) orConditions.push({ tags: { $in: [...targetTagsSet] } });

const query = {
_id: { $ne: product._id },
Expand All @@ -396,7 +403,7 @@ export const getRelatedProducts = async (req, res) => {

if (c.tags && c.tags.length > 0) {
for (const tag of c.tags) {
if (targetTags.has(tag.toLowerCase())) {
if (targetTagsSet.has(tag.toLowerCase())) {
score += 2;
}
}
Expand Down Expand Up @@ -441,11 +448,15 @@ export const getProductBundle = async (req, res) => {
.slice(0, 3);

const bundleTotal = [product, ...items.map(i => i.product)]
.reduce((sum, p) => sum + p.price, 0);
.reduce((sum, p) => sum + (Number(p?.price) || 0), 0);

const bundleDiscount = 0.1;
const bundlePrice = +(bundleTotal * (1 - bundleDiscount)).toFixed(2);
const savings = +(bundleTotal * bundleDiscount).toFixed(2);
const bundlePrice = bundleTotal > 0
? +(bundleTotal * (1 - bundleDiscount)).toFixed(2)
: 0;
const savings = bundleTotal > 0
? +(bundleTotal * bundleDiscount).toFixed(2)
: 0;

res.status(200).json({
success: true,
Expand Down Expand Up @@ -476,13 +487,11 @@ export const searchProducts = async (req, res, next) => {
}

try {
// Try Elasticsearch first
const esProducts = await searchProductsES(q);
if (esProducts) {
return res.status(200).json({ success: true, data: esProducts });
}

// Fallback to MongoDB regex search
const safeQuery = escapeRegex(q);
const regex = new RegExp(safeQuery, 'i');
const products = await Product.find({ name: regex, isDeleted: { $ne: true } });
Expand Down
6 changes: 1 addition & 5 deletions BACKEND/middleware/authMiddleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ const authMiddleware = async (req, res, next) => {

const token = authHeader.split(" ")[1];

// JWT specific errors alag handle honge
let decoded;
try {
decoded = jwt.verify(token, process.env.JWT_SECRET);
Expand All @@ -32,15 +31,13 @@ const authMiddleware = async (req, res, next) => {
});
}

// Validate decoded payload before DB lookup
if (!decoded?.id) {
return res.status(401).json({
success: false,
message: "Invalid token payload.",
});
}

// User existence verify karo DB se
const user = await User.findById(decoded.id).select("-password");

if (!user) {
Expand All @@ -59,7 +56,7 @@ const authMiddleware = async (req, res, next) => {
};

next();
} catch (error) {
} catch (_error) {
return res.status(500).json({
success: false,
message: "Internal server error during authentication.",
Expand All @@ -68,4 +65,3 @@ const authMiddleware = async (req, res, next) => {
};

export default authMiddleware;

18 changes: 7 additions & 11 deletions BACKEND/middleware/errorMiddleware.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// Custom Error Class (for different error types)
export class AppError extends Error {
constructor(message, statusCode) {
super(message);
Expand All @@ -7,45 +6,42 @@ export class AppError extends Error {
}
}

// ADD THIS - 404 Handler for unknown API routes
export const notFoundHandler = (req, res, next) => {
// Check if it's an API route (starts with /api)
if (req.path.startsWith('/api')) {
return res.status(404).json({
success: false,
message: `API route not found: ${req.method} ${req.path}`
});
}
// For non-API routes, pass to next middleware (like frontend)

next();
};

// Global Error Handler Middleware
export const errorHandler = (err, req, res, next) => {
// Default values

export const errorHandler = (err, req, res, _next) => {
let statusCode = err.statusCode || 500;
let message = err.message || "Internal Server Error";

// Handle Mongoose Validation Errors (e.g., min:0, required, etc.)

if (err.name === 'ValidationError') {
statusCode = 400;
message = Object.values(err.errors).map(e => e.message).join(', ');
}

// Handle Duplicate Key Error (MongoDB - when unique field repeats)

if (err.code === 11000) {
statusCode = 400;
const field = Object.keys(err.keyPattern)[0];
message = `Duplicate field value: ${field}. Please use another value.`;
}

// Handle Cast Error (Invalid ObjectId format)

if (err.name === 'CastError') {
statusCode = 400;
message = `Invalid ${err.path}: ${err.value}`;
}

// Send Response

res.status(statusCode).json({
success: false,
message: message,
Expand Down
27 changes: 10 additions & 17 deletions FRONTEND/src/components/ui/ProductCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ const ProductCard = ({ product }) => {
const [isInWishlist, setIsInWishlist] = useState(false);

const handleClose = () => {
setUpdatedProduct(product);
setImagePreview(product.image);
onClose();
setUpdatedProduct(product);
setImagePreview(product.image);
onClose();
};

const fileInputRef = useRef(null);
Expand All @@ -73,7 +73,6 @@ const ProductCard = ({ product }) => {

const isInCompare = compareList.some((p) => p._id === product._id);
const { addToCart } = useCart();
const { currency, rates } = useCurrencyStore();
const { addToWishlist, removeFromWishlist, checkInWishlist } = useWishlist();
const toast = useToast();

Expand All @@ -96,11 +95,8 @@ const ProductCard = ({ product }) => {

// Check wishlist status on mount
useEffect(() => {
const checkWishlist = async () => {
const inWishlist = await checkInWishlist(product._id);
setIsInWishlist(inWishlist);
};
checkWishlist();
const inWishlist = checkInWishlist(product._id);
setIsInWishlist(inWishlist);
}, [product._id, checkInWishlist]);

// Revoke blob URLs to avoid memory leaks
Expand Down Expand Up @@ -266,7 +262,7 @@ const ProductCard = ({ product }) => {

{/* Price */}
<Text fontWeight="bold" fontSize="xl" color={textColor} mb={4}>
{formatPrice(product.price, currency, rates)}
${product.price}
</Text>

{/* Tags */}
Expand Down Expand Up @@ -363,7 +359,7 @@ const ProductCard = ({ product }) => {
</Stack>
</Box>

{/* ── Delete Confirmation Dialog ── */}
{/* Delete Confirmation Dialog */}
<AlertDialog
isOpen={isDeleteOpen}
leastDestructiveRef={cancelRef}
Expand Down Expand Up @@ -397,7 +393,7 @@ const ProductCard = ({ product }) => {
</AlertDialogOverlay>
</AlertDialog>

{/* ── Edit / Update Modal ── */}
{/*Edit / Update Modal*/}
<Modal isOpen={isOpen} onClose={handleClose} size="xl" scrollBehavior="inside">
<ModalOverlay />
<ModalContent maxH="90vh">
Expand Down Expand Up @@ -620,10 +616,7 @@ const ProductCard = ({ product }) => {
>
Update
</Button>
<Button
variant="ghost"
onClick={handleClose}
>
<Button variant="ghost" onClick={handleClose}>
Cancel
</Button>
</ModalFooter>
Expand All @@ -633,4 +626,4 @@ const ProductCard = ({ product }) => {
);
};

export default ProductCard;
export default ProductCard;
14 changes: 9 additions & 5 deletions FRONTEND/src/components/ui/ProductReviews.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,15 @@ const RatingSummary = ({ reviews, filterStar, onFilterChange, distribution, aver

const rounded = averageRating ?? Math.round((reviews.reduce((sum, r) => sum + r.rating, 0) / totalCount) * 10) / 10;

const dist = distribution || [5, 4, 3, 2, 1].map((star) => ({
star,
count: reviews.filter((r) => r.rating === star).length,
}));

const dist = distribution
? [5, 4, 3, 2, 1].map((star) => ({
star,
count: distribution[star] ?? 0,
}))
: [5, 4, 3, 2, 1].map((star) => ({
star,
count: reviews.filter((r) => r.rating === star).length,
}));
return (
<Box
p={5}
Expand Down
Loading