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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,6 @@ vite.config.ts.timestamp-*

# ...existing rules...
src/playground-1.mongodb.js
.env.example
.env.example.DS_Store
src/.DS_Store
.DS_Store
4 changes: 4 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import passport from './config/passport.config.js';
import productRoutes from './routes/product.routes.js';
import cartRoutes from './routes/cart.routes.js';
import collectionRoutes from './routes/collection.routes.js';
import wishlistRoutes from './routes/wishlist.routes.js';
import testRoutes from './routes/test.routes.js';
import authRoutes from './routes/auth.routes.js';
import errorHandler from './middleware/error-handler.middleware.js';
import notFound from './middleware/notFound.middleware.js'
Expand All @@ -22,6 +24,8 @@ app.get('/',(req,res)=>{
app.use('/api/products', productRoutes);
app.use('/api/cart', cartRoutes);
app.use('/api/collections', collectionRoutes);
app.use('/api/wishlist', wishlistRoutes);
app.use('/api/test', testRoutes);
app.use('/auth', authRoutes);

// Middleware for not found 404
Expand Down
233 changes: 233 additions & 0 deletions src/controllers/wishlist.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import Wishlist from '../models/wishlist.model.js';
import Product from '../models/product.model.js';
import HttpException from '../utils/exceptions/http.exception.js';
import mongoose from 'mongoose';

/**
* GET /api/wishlist
* Get user's wishlist with populated product details
*/
const getWishlist = async (req, res, next) => {
try {
const userId = req.user.id; // Assuming JWT middleware sets req.user

const wishlist = await Wishlist.findOne({ userId })
.populate('products.productId', 'name price image rating reviewsCount category collection')
.lean();

if (!wishlist) {
return res.status(200).json({
success: true,
message: 'Wishlist is empty',
data: {
products: [],
totalItems: 0,
},
});
}

// Transform the data to match expected format
const products = wishlist.products.map(item => ({
...item.productId,
addedAt: item.addedAt,
}));

return res.status(200).json({
success: true,
message: 'Wishlist retrieved successfully',
data: {
products,
totalItems: products.length,
},
});
} catch (error) {
console.error('Error fetching wishlist:', error);
next(new HttpException(500, 'Internal server error while fetching wishlist'));
}
};

/**
* POST /api/wishlist
* Add product to user's wishlist
*/
const addToWishlist = async (req, res, next) => {
try {
const userId = req.user.id;
const { productId } = req.body;

// Validate productId
if (!productId) {
return next(new HttpException(400, 'Product ID is required'));
}

if (!mongoose.Types.ObjectId.isValid(productId)) {
return next(new HttpException(400, 'Invalid product ID format'));
}

// Check if product exists
const product = await Product.findById(productId);
if (!product) {
return next(new HttpException(404, 'Product not found'));
}

// Find or create wishlist
let wishlist = await Wishlist.findOne({ userId });

if (!wishlist) {
// Create new wishlist
wishlist = new Wishlist({
userId,
products: [{ productId, addedAt: new Date() }],
});
} else {
// Check if product already exists in wishlist
const existingProduct = wishlist.products.find(
item => item.productId.toString() === productId
);

if (existingProduct) {
return next(new HttpException(409, 'Product already in wishlist'));
}

// Add product to existing wishlist
wishlist.products.push({ productId, addedAt: new Date() });
}

await wishlist.save();

// Populate and return updated wishlist
const updatedWishlist = await Wishlist.findById(wishlist._id)
.populate('products.productId', 'name price image rating reviewsCount category collection')
.lean();

const products = updatedWishlist.products.map(item => ({
...item.productId,
addedAt: item.addedAt,
}));

return res.status(201).json({
success: true,
message: 'Product added to wishlist successfully',
data: {
products,
totalItems: products.length,
},
});
} catch (error) {
console.error('Error adding to wishlist:', error);
next(new HttpException(500, 'Internal server error while adding to wishlist'));
}
};

/**
* DELETE /api/wishlist/:productId
* Remove product from user's wishlist
*/
const removeFromWishlist = async (req, res, next) => {
try {
const userId = req.user.id;
const { productId } = req.params;

// Validate productId
if (!mongoose.Types.ObjectId.isValid(productId)) {
return next(new HttpException(400, 'Invalid product ID format'));
}

// Find wishlist
const wishlist = await Wishlist.findOne({ userId });

if (!wishlist) {
return next(new HttpException(404, 'Wishlist not found'));
}

// Check if product exists in wishlist
const productIndex = wishlist.products.findIndex(
item => item.productId.toString() === productId
);

if (productIndex === -1) {
return next(new HttpException(404, 'Product not found in wishlist'));
}

// Remove product from wishlist
wishlist.products.splice(productIndex, 1);
await wishlist.save();

// If wishlist is empty, delete it
if (wishlist.products.length === 0) {
await Wishlist.findByIdAndDelete(wishlist._id);
return res.status(200).json({
success: true,
message: 'Product removed from wishlist successfully',
data: {
products: [],
totalItems: 0,
},
});
}

// Populate and return updated wishlist
const updatedWishlist = await Wishlist.findById(wishlist._id)
.populate('products.productId', 'name price image rating reviewsCount category collection')
.lean();

const products = updatedWishlist.products.map(item => ({
...item.productId,
addedAt: item.addedAt,
}));

return res.status(200).json({
success: true,
message: 'Product removed from wishlist successfully',
data: {
products,
totalItems: products.length,
},
});
} catch (error) {
console.error('Error removing from wishlist:', error);
next(new HttpException(500, 'Internal server error while removing from wishlist'));
}
};

/**
* DELETE /api/wishlist
* Clear entire wishlist
*/
const clearWishlist = async (req, res, next) => {
try {
const userId = req.user.id;

const wishlist = await Wishlist.findOneAndDelete({ userId });

if (!wishlist) {
return res.status(200).json({
success: true,
message: 'Wishlist is already empty',
data: {
products: [],
totalItems: 0,
},
});
}

return res.status(200).json({
success: true,
message: 'Wishlist cleared successfully',
data: {
products: [],
totalItems: 0,
},
});
} catch (error) {
console.error('Error clearing wishlist:', error);
next(new HttpException(500, 'Internal server error while clearing wishlist'));
}
};

export {
getWishlist,
addToWishlist,
removeFromWishlist,
clearWishlist,
};
65 changes: 65 additions & 0 deletions src/middleware/auth.middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import jwt from 'jsonwebtoken';
import HttpException from '../utils/exceptions/http.exception.js';

/**
* JWT Authentication Middleware
* Verifies JWT token and sets req.user
*/
const authenticateToken = (req, res, next) => {
try {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN

if (!token) {
return next(new HttpException(401, 'Access token is required'));
}

// For now, we'll use a simple secret. In production, use process.env.JWT_SECRET
const secret = process.env.JWT_SECRET || 'your-secret-key';

jwt.verify(token, secret, (err, user) => {
if (err) {
return next(new HttpException(403, 'Invalid or expired token'));
}

req.user = user;
next();
});
} catch (error) {
console.error('Auth middleware error:', error);
next(new HttpException(500, 'Authentication error'));
}
};

/**
* Optional Authentication Middleware
* Sets req.user if token is present, but doesn't require it
*/
const optionalAuth = (req, res, next) => {
try {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];

if (!token) {
req.user = null;
return next();
}

const secret = process.env.JWT_SECRET || 'your-secret-key';

jwt.verify(token, secret, (err, user) => {
if (err) {
req.user = null;
} else {
req.user = user;
}
next();
});
} catch (error) {
console.error('Optional auth middleware error:', error);
req.user = null;
next();
}
};

export { authenticateToken, optionalAuth };
54 changes: 54 additions & 0 deletions src/models/wishlist.model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import mongoose from 'mongoose';

const wishlistSchema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
unique: true,
},
products: [{
productId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Product',
required: true,
},
addedAt: {
type: Date,
default: Date.now,
},
}],
}, {
timestamps: true,
versionKey: false,
});

// Index for efficient queries
wishlistSchema.index({ userId: 1 });
wishlistSchema.index({ 'products.productId': 1 });

// Prevent duplicate products in wishlist
wishlistSchema.pre('save', function(next) {
if (this.isModified('products')) {
const productIds = this.products.map(item => item.productId.toString());
const uniqueProductIds = [...new Set(productIds)];

if (productIds.length !== uniqueProductIds.length) {
// Remove duplicates, keeping the first occurrence
const seen = new Set();
this.products = this.products.filter(item => {
const id = item.productId.toString();
if (seen.has(id)) {
return false;
}
seen.add(id);
return true;
});
}
}
next();
});

const Wishlist = mongoose.model('Wishlist', wishlistSchema);

export default Wishlist;
Loading