Skip to content

Commit 831f609

Browse files
authored
Merge pull request #46 from deepesh224-ux/feature/wishlist-api
feat(wishlist): implement complete wishlist API with JWT authentication
2 parents 010ab81 + e4e2ff3 commit 831f609

8 files changed

Lines changed: 444 additions & 1 deletion

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,4 +140,6 @@ vite.config.ts.timestamp-*
140140

141141
# ...existing rules...
142142
src/playground-1.mongodb.js
143-
.env.example
143+
.env.example.DS_Store
144+
src/.DS_Store
145+
.DS_Store

src/app.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import passport from './config/passport.config.js';
44
import productRoutes from './routes/product.routes.js';
55
import cartRoutes from './routes/cart.routes.js';
66
import collectionRoutes from './routes/collection.routes.js';
7+
import wishlistRoutes from './routes/wishlist.routes.js';
8+
import testRoutes from './routes/test.routes.js';
79
import authRoutes from './routes/auth.routes.js';
810
import errorHandler from './middleware/error-handler.middleware.js';
911
import notFound from './middleware/notFound.middleware.js'
@@ -22,6 +24,8 @@ app.get('/',(req,res)=>{
2224
app.use('/api/products', productRoutes);
2325
app.use('/api/cart', cartRoutes);
2426
app.use('/api/collections', collectionRoutes);
27+
app.use('/api/wishlist', wishlistRoutes);
28+
app.use('/api/test', testRoutes);
2529
app.use('/auth', authRoutes);
2630

2731
// Middleware for not found 404
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import Wishlist from '../models/wishlist.model.js';
2+
import Product from '../models/product.model.js';
3+
import HttpException from '../utils/exceptions/http.exception.js';
4+
import mongoose from 'mongoose';
5+
6+
/**
7+
* GET /api/wishlist
8+
* Get user's wishlist with populated product details
9+
*/
10+
const getWishlist = async (req, res, next) => {
11+
try {
12+
const userId = req.user.id; // Assuming JWT middleware sets req.user
13+
14+
const wishlist = await Wishlist.findOne({ userId })
15+
.populate('products.productId', 'name price image rating reviewsCount category collection')
16+
.lean();
17+
18+
if (!wishlist) {
19+
return res.status(200).json({
20+
success: true,
21+
message: 'Wishlist is empty',
22+
data: {
23+
products: [],
24+
totalItems: 0,
25+
},
26+
});
27+
}
28+
29+
// Transform the data to match expected format
30+
const products = wishlist.products.map(item => ({
31+
...item.productId,
32+
addedAt: item.addedAt,
33+
}));
34+
35+
return res.status(200).json({
36+
success: true,
37+
message: 'Wishlist retrieved successfully',
38+
data: {
39+
products,
40+
totalItems: products.length,
41+
},
42+
});
43+
} catch (error) {
44+
console.error('Error fetching wishlist:', error);
45+
next(new HttpException(500, 'Internal server error while fetching wishlist'));
46+
}
47+
};
48+
49+
/**
50+
* POST /api/wishlist
51+
* Add product to user's wishlist
52+
*/
53+
const addToWishlist = async (req, res, next) => {
54+
try {
55+
const userId = req.user.id;
56+
const { productId } = req.body;
57+
58+
// Validate productId
59+
if (!productId) {
60+
return next(new HttpException(400, 'Product ID is required'));
61+
}
62+
63+
if (!mongoose.Types.ObjectId.isValid(productId)) {
64+
return next(new HttpException(400, 'Invalid product ID format'));
65+
}
66+
67+
// Check if product exists
68+
const product = await Product.findById(productId);
69+
if (!product) {
70+
return next(new HttpException(404, 'Product not found'));
71+
}
72+
73+
// Find or create wishlist
74+
let wishlist = await Wishlist.findOne({ userId });
75+
76+
if (!wishlist) {
77+
// Create new wishlist
78+
wishlist = new Wishlist({
79+
userId,
80+
products: [{ productId, addedAt: new Date() }],
81+
});
82+
} else {
83+
// Check if product already exists in wishlist
84+
const existingProduct = wishlist.products.find(
85+
item => item.productId.toString() === productId
86+
);
87+
88+
if (existingProduct) {
89+
return next(new HttpException(409, 'Product already in wishlist'));
90+
}
91+
92+
// Add product to existing wishlist
93+
wishlist.products.push({ productId, addedAt: new Date() });
94+
}
95+
96+
await wishlist.save();
97+
98+
// Populate and return updated wishlist
99+
const updatedWishlist = await Wishlist.findById(wishlist._id)
100+
.populate('products.productId', 'name price image rating reviewsCount category collection')
101+
.lean();
102+
103+
const products = updatedWishlist.products.map(item => ({
104+
...item.productId,
105+
addedAt: item.addedAt,
106+
}));
107+
108+
return res.status(201).json({
109+
success: true,
110+
message: 'Product added to wishlist successfully',
111+
data: {
112+
products,
113+
totalItems: products.length,
114+
},
115+
});
116+
} catch (error) {
117+
console.error('Error adding to wishlist:', error);
118+
next(new HttpException(500, 'Internal server error while adding to wishlist'));
119+
}
120+
};
121+
122+
/**
123+
* DELETE /api/wishlist/:productId
124+
* Remove product from user's wishlist
125+
*/
126+
const removeFromWishlist = async (req, res, next) => {
127+
try {
128+
const userId = req.user.id;
129+
const { productId } = req.params;
130+
131+
// Validate productId
132+
if (!mongoose.Types.ObjectId.isValid(productId)) {
133+
return next(new HttpException(400, 'Invalid product ID format'));
134+
}
135+
136+
// Find wishlist
137+
const wishlist = await Wishlist.findOne({ userId });
138+
139+
if (!wishlist) {
140+
return next(new HttpException(404, 'Wishlist not found'));
141+
}
142+
143+
// Check if product exists in wishlist
144+
const productIndex = wishlist.products.findIndex(
145+
item => item.productId.toString() === productId
146+
);
147+
148+
if (productIndex === -1) {
149+
return next(new HttpException(404, 'Product not found in wishlist'));
150+
}
151+
152+
// Remove product from wishlist
153+
wishlist.products.splice(productIndex, 1);
154+
await wishlist.save();
155+
156+
// If wishlist is empty, delete it
157+
if (wishlist.products.length === 0) {
158+
await Wishlist.findByIdAndDelete(wishlist._id);
159+
return res.status(200).json({
160+
success: true,
161+
message: 'Product removed from wishlist successfully',
162+
data: {
163+
products: [],
164+
totalItems: 0,
165+
},
166+
});
167+
}
168+
169+
// Populate and return updated wishlist
170+
const updatedWishlist = await Wishlist.findById(wishlist._id)
171+
.populate('products.productId', 'name price image rating reviewsCount category collection')
172+
.lean();
173+
174+
const products = updatedWishlist.products.map(item => ({
175+
...item.productId,
176+
addedAt: item.addedAt,
177+
}));
178+
179+
return res.status(200).json({
180+
success: true,
181+
message: 'Product removed from wishlist successfully',
182+
data: {
183+
products,
184+
totalItems: products.length,
185+
},
186+
});
187+
} catch (error) {
188+
console.error('Error removing from wishlist:', error);
189+
next(new HttpException(500, 'Internal server error while removing from wishlist'));
190+
}
191+
};
192+
193+
/**
194+
* DELETE /api/wishlist
195+
* Clear entire wishlist
196+
*/
197+
const clearWishlist = async (req, res, next) => {
198+
try {
199+
const userId = req.user.id;
200+
201+
const wishlist = await Wishlist.findOneAndDelete({ userId });
202+
203+
if (!wishlist) {
204+
return res.status(200).json({
205+
success: true,
206+
message: 'Wishlist is already empty',
207+
data: {
208+
products: [],
209+
totalItems: 0,
210+
},
211+
});
212+
}
213+
214+
return res.status(200).json({
215+
success: true,
216+
message: 'Wishlist cleared successfully',
217+
data: {
218+
products: [],
219+
totalItems: 0,
220+
},
221+
});
222+
} catch (error) {
223+
console.error('Error clearing wishlist:', error);
224+
next(new HttpException(500, 'Internal server error while clearing wishlist'));
225+
}
226+
};
227+
228+
export {
229+
getWishlist,
230+
addToWishlist,
231+
removeFromWishlist,
232+
clearWishlist,
233+
};

src/middleware/auth.middleware.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import jwt from 'jsonwebtoken';
2+
import HttpException from '../utils/exceptions/http.exception.js';
3+
4+
/**
5+
* JWT Authentication Middleware
6+
* Verifies JWT token and sets req.user
7+
*/
8+
const authenticateToken = (req, res, next) => {
9+
try {
10+
const authHeader = req.headers['authorization'];
11+
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
12+
13+
if (!token) {
14+
return next(new HttpException(401, 'Access token is required'));
15+
}
16+
17+
// For now, we'll use a simple secret. In production, use process.env.JWT_SECRET
18+
const secret = process.env.JWT_SECRET || 'your-secret-key';
19+
20+
jwt.verify(token, secret, (err, user) => {
21+
if (err) {
22+
return next(new HttpException(403, 'Invalid or expired token'));
23+
}
24+
25+
req.user = user;
26+
next();
27+
});
28+
} catch (error) {
29+
console.error('Auth middleware error:', error);
30+
next(new HttpException(500, 'Authentication error'));
31+
}
32+
};
33+
34+
/**
35+
* Optional Authentication Middleware
36+
* Sets req.user if token is present, but doesn't require it
37+
*/
38+
const optionalAuth = (req, res, next) => {
39+
try {
40+
const authHeader = req.headers['authorization'];
41+
const token = authHeader && authHeader.split(' ')[1];
42+
43+
if (!token) {
44+
req.user = null;
45+
return next();
46+
}
47+
48+
const secret = process.env.JWT_SECRET || 'your-secret-key';
49+
50+
jwt.verify(token, secret, (err, user) => {
51+
if (err) {
52+
req.user = null;
53+
} else {
54+
req.user = user;
55+
}
56+
next();
57+
});
58+
} catch (error) {
59+
console.error('Optional auth middleware error:', error);
60+
req.user = null;
61+
next();
62+
}
63+
};
64+
65+
export { authenticateToken, optionalAuth };

src/models/wishlist.model.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import mongoose from 'mongoose';
2+
3+
const wishlistSchema = new mongoose.Schema({
4+
userId: {
5+
type: mongoose.Schema.Types.ObjectId,
6+
ref: 'User',
7+
required: true,
8+
unique: true,
9+
},
10+
products: [{
11+
productId: {
12+
type: mongoose.Schema.Types.ObjectId,
13+
ref: 'Product',
14+
required: true,
15+
},
16+
addedAt: {
17+
type: Date,
18+
default: Date.now,
19+
},
20+
}],
21+
}, {
22+
timestamps: true,
23+
versionKey: false,
24+
});
25+
26+
// Index for efficient queries
27+
wishlistSchema.index({ userId: 1 });
28+
wishlistSchema.index({ 'products.productId': 1 });
29+
30+
// Prevent duplicate products in wishlist
31+
wishlistSchema.pre('save', function(next) {
32+
if (this.isModified('products')) {
33+
const productIds = this.products.map(item => item.productId.toString());
34+
const uniqueProductIds = [...new Set(productIds)];
35+
36+
if (productIds.length !== uniqueProductIds.length) {
37+
// Remove duplicates, keeping the first occurrence
38+
const seen = new Set();
39+
this.products = this.products.filter(item => {
40+
const id = item.productId.toString();
41+
if (seen.has(id)) {
42+
return false;
43+
}
44+
seen.add(id);
45+
return true;
46+
});
47+
}
48+
}
49+
next();
50+
});
51+
52+
const Wishlist = mongoose.model('Wishlist', wishlistSchema);
53+
54+
export default Wishlist;

0 commit comments

Comments
 (0)