diff --git a/.gitignore b/.gitignore index d6c9eb7..d444094 100644 --- a/.gitignore +++ b/.gitignore @@ -140,4 +140,6 @@ vite.config.ts.timestamp-* # ...existing rules... src/playground-1.mongodb.js -.env.example \ No newline at end of file +.env.example.DS_Store +src/.DS_Store +.DS_Store diff --git a/src/app.js b/src/app.js index 00e4e37..26b733c 100644 --- a/src/app.js +++ b/src/app.js @@ -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' @@ -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 diff --git a/src/controllers/wishlist.controller.js b/src/controllers/wishlist.controller.js new file mode 100644 index 0000000..68c9bdb --- /dev/null +++ b/src/controllers/wishlist.controller.js @@ -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, +}; diff --git a/src/middleware/auth.middleware.js b/src/middleware/auth.middleware.js new file mode 100644 index 0000000..c099378 --- /dev/null +++ b/src/middleware/auth.middleware.js @@ -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 }; diff --git a/src/models/wishlist.model.js b/src/models/wishlist.model.js new file mode 100644 index 0000000..c92aa8f --- /dev/null +++ b/src/models/wishlist.model.js @@ -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; diff --git a/src/routes/test.routes.js b/src/routes/test.routes.js new file mode 100644 index 0000000..437e0bf --- /dev/null +++ b/src/routes/test.routes.js @@ -0,0 +1,33 @@ +import express from 'express'; +import { generateTestToken } from '../utils/auth.util.js'; + +const router = express.Router(); + +/** + * GET /api/test/token + * Generate a test JWT token for development/testing + * This endpoint should be removed in production + */ +router.get('/token', (req, res) => { + try { + const token = generateTestToken(); + + res.status(200).json({ + success: true, + message: 'Test token generated successfully', + data: { + token, + expiresIn: '24h', + note: 'Use this token in Authorization header as: Bearer ', + }, + }); + } catch (error) { + console.error('Error generating test token:', error); + res.status(500).json({ + success: false, + message: 'Failed to generate test token', + }); + } +}); + +export default router; diff --git a/src/routes/wishlist.routes.js b/src/routes/wishlist.routes.js new file mode 100644 index 0000000..6db76c2 --- /dev/null +++ b/src/routes/wishlist.routes.js @@ -0,0 +1,27 @@ +import express from 'express'; +import { + getWishlist, + addToWishlist, + removeFromWishlist, + clearWishlist, +} from '../controllers/wishlist.controller.js'; +import { authenticateToken } from '../middleware/auth.middleware.js'; + +const router = express.Router(); + +// All wishlist routes require authentication +router.use(authenticateToken); + +// GET /api/wishlist - Get user's wishlist +router.get('/', getWishlist); + +// POST /api/wishlist - Add product to wishlist +router.post('/', addToWishlist); + +// DELETE /api/wishlist/:productId - Remove specific product from wishlist +router.delete('/:productId', removeFromWishlist); + +// DELETE /api/wishlist - Clear entire wishlist +router.delete('/', clearWishlist); + +export default router; diff --git a/src/utils/auth.util.js b/src/utils/auth.util.js new file mode 100644 index 0000000..d17e430 --- /dev/null +++ b/src/utils/auth.util.js @@ -0,0 +1,25 @@ +import jwt from 'jsonwebtoken'; + +/** + * Generate JWT token for testing purposes + * In production, this should be part of the login process + */ +export const generateTestToken = (userId = '507f1f77bcf86cd799439011') => { + const secret = process.env.JWT_SECRET || 'your-secret-key'; + + const payload = { + id: userId, + email: 'test@example.com', + role: 'user', + }; + + return jwt.sign(payload, secret, { expiresIn: '24h' }); +}; + +/** + * Verify JWT token + */ +export const verifyToken = (token) => { + const secret = process.env.JWT_SECRET || 'your-secret-key'; + return jwt.verify(token, secret); +};