Initial commit

This commit is contained in:
2026-09-16 17:36:46 +05:30
commit c9f429d123
117 changed files with 14145 additions and 0 deletions

13
server/.env.example Normal file
View File

@@ -0,0 +1,13 @@
PORT=5000
NODE_ENV=development
MONGODB_URI=mongodb://127.0.0.1:27017/guitar_ecommerce
JWT_SECRET=guitar_ecommerce_super_secret_jwt_key_2026
JWT_EXPIRE=30d
STRIPE_SECRET_KEY=sk_test_mock_stripe_key_guitar_ecommerce
CLIENT_URL=http://localhost:5173
EMAIL_HOST=smtp.mailtrap.io
EMAIL_PORT=2525
EMAIL_USER=mock_user
EMAIL_PASS=mock_pass
EMAIL_FROM=noreply@guitars.com

8
server/.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
node_modules/
.next/
dist/
build/
.env
.env.local
.env.*.local
*.log

77
server/app.js Normal file
View File

@@ -0,0 +1,77 @@
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const path = require('path');
const errorHandler = require('./middleware/errorHandler');
// Route Imports
const authRoutes = require('./routes/authRoutes');
const productRoutes = require('./routes/productRoutes');
const categoryRoutes = require('./routes/categoryRoutes');
const brandRoutes = require('./routes/brandRoutes');
const cartRoutes = require('./routes/cartRoutes');
const wishlistRoutes = require('./routes/wishlistRoutes');
const orderRoutes = require('./routes/orderRoutes');
const inventoryRoutes = require('./routes/inventoryRoutes');
const reviewRoutes = require('./routes/reviewRoutes');
const couponRoutes = require('./routes/couponRoutes');
const bannerRoutes = require('./routes/bannerRoutes');
const settingsRoutes = require('./routes/settingsRoutes');
const socialMediaRoutes = require('./routes/socialMediaRoutes');
const emailTemplateRoutes = require('./routes/emailTemplateRoutes');
const contactRoutes = require('./routes/contactRoutes');
const uploadRoutes = require('./routes/uploadRoutes');
const app = express();
// Security Middlewares
app.use(helmet({ crossOriginResourcePolicy: { policy: "cross-origin" } }));
app.use(cors({
origin: process.env.CLIENT_URL || '*',
credentials: true
}));
// Rate Limiter
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 300,
message: { success: false, message: 'Too many requests from this IP, please try again later.' }
});
app.use('/api', limiter);
// Body Parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Static Uploads Folder
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
// Root test route
app.get('/api/health', (req, res) => {
res.json({ success: true, message: 'Antigravity Guitars API is healthy', timestamp: new Date() });
});
// API Routes
app.use('/api/auth', authRoutes);
app.use('/api/products', productRoutes);
app.use('/api/categories', categoryRoutes);
app.use('/api/brands', brandRoutes);
app.use('/api/cart', cartRoutes);
app.use('/api/wishlist', wishlistRoutes);
app.use('/api/orders', orderRoutes);
app.use('/api/inventory', inventoryRoutes);
app.use('/api/reviews', reviewRoutes);
app.use('/api/coupons', couponRoutes);
app.use('/api/banners', bannerRoutes);
app.use('/api/settings', settingsRoutes);
app.use('/api/social-media', socialMediaRoutes);
app.use('/api/email-templates', emailTemplateRoutes);
app.use('/api/contact', contactRoutes);
app.use('/api/upload', uploadRoutes);
// Centralized Error Handling Middleware
app.use(errorHandler);
module.exports = app;

15
server/config/db.js Normal file
View File

@@ -0,0 +1,15 @@
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/guitar_ecommerce');
console.log(`[MongoDB Connected]: ${conn.connection.host}`);
} catch (error) {
console.error(`[MongoDB Connection Error]: ${error.message}`);
// Process can continue for seed script or fallback handling
process.exit(1);
}
};
module.exports = connectDB;

View File

@@ -0,0 +1,128 @@
const User = require('../models/User');
const { generateToken } = require('../utils/jwt');
const { successResponse, errorResponse } = require('../utils/apiResponse');
// @desc Register new user
// @route POST /api/auth/register
// @access Public
const registerUser = async (req, res, next) => {
try {
const { name, email, password, phone } = req.body;
const userExists = await User.findOne({ email: email.toLowerCase() });
if (userExists) {
return errorResponse(res, 'User already exists with this email', [], 400);
}
const user = await User.create({
name,
email,
password,
phone: phone || ''
});
const token = generateToken(user._id, user.role);
return successResponse(res, 'Registration successful', {
_id: user._id,
name: user.name,
email: user.email,
role: user.role,
token
}, 201);
} catch (error) {
next(error);
}
};
// @desc Login user & get token
// @route POST /api/auth/login
// @access Public
const loginUser = async (req, res, next) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return errorResponse(res, 'Please provide email and password', [], 400);
}
const user = await User.findOne({ email: email.toLowerCase() }).select('+password');
if (!user || !(await user.matchPassword(password))) {
return errorResponse(res, 'Invalid credentials', [], 401);
}
const token = generateToken(user._id, user.role);
return successResponse(res, 'Login successful', {
_id: user._id,
name: user.name,
email: user.email,
role: user.role,
avatar: user.avatar,
addresses: user.addresses,
token
});
} catch (error) {
next(error);
}
};
// @desc Get user profile
// @route GET /api/auth/profile
// @access Private
const getUserProfile = async (req, res, next) => {
try {
const user = await User.findById(req.user._id);
if (!user) {
return errorResponse(res, 'User not found', [], 404);
}
return successResponse(res, 'User profile retrieved', user);
} catch (error) {
next(error);
}
};
// @desc Update user profile & addresses
// @route PUT /api/auth/profile
// @access Private
const updateUserProfile = async (req, res, next) => {
try {
const user = await User.findById(req.user._id);
if (!user) {
return errorResponse(res, 'User not found', [], 404);
}
user.name = req.body.name || user.name;
user.phone = req.body.phone !== undefined ? req.body.phone : user.phone;
if (req.body.addresses) {
user.addresses = req.body.addresses;
}
if (req.body.password) {
user.password = req.body.password;
}
const updatedUser = await user.save();
const token = generateToken(updatedUser._id, updatedUser.role);
return successResponse(res, 'Profile updated successfully', {
_id: updatedUser._id,
name: updatedUser.name,
email: updatedUser.email,
role: updatedUser.role,
phone: updatedUser.phone,
addresses: updatedUser.addresses,
token
});
} catch (error) {
next(error);
}
};
module.exports = {
registerUser,
loginUser,
getUserProfile,
updateUserProfile
};

View File

@@ -0,0 +1,44 @@
const Banner = require('../models/Banner');
const { successResponse, errorResponse } = require('../utils/apiResponse');
const getBanners = async (req, res, next) => {
try {
const banners = await Banner.find({ isActive: true }).sort({ displayOrder: 1 });
return successResponse(res, 'Hero banners retrieved', banners);
} catch (error) {
next(error);
}
};
const createBanner = async (req, res, next) => {
try {
const banner = new Banner(req.body);
const saved = await banner.save();
return successResponse(res, 'Banner created', saved, 201);
} catch (error) {
next(error);
}
};
const updateBanner = async (req, res, next) => {
try {
const banner = await Banner.findByIdAndUpdate(req.params.id, req.body, { new: true });
if (!banner) return errorResponse(res, 'Banner not found', [], 404);
return successResponse(res, 'Banner updated', banner);
} catch (error) {
next(error);
}
};
const deleteBanner = async (req, res, next) => {
try {
const banner = await Banner.findByIdAndDelete(req.params.id);
if (!banner) return errorResponse(res, 'Banner not found', [], 404);
return successResponse(res, 'Banner deleted');
} catch (error) {
next(error);
}
};
module.exports = { getBanners, createBanner, updateBanner, deleteBanner };

View File

@@ -0,0 +1,44 @@
const Brand = require('../models/Brand');
const { successResponse, errorResponse } = require('../utils/apiResponse');
const getBrands = async (req, res, next) => {
try {
const brands = await Brand.find({ isActive: true }).sort({ name: 1 });
return successResponse(res, 'Brands retrieved', brands);
} catch (error) {
next(error);
}
};
const createBrand = async (req, res, next) => {
try {
const brand = new Brand(req.body);
const saved = await brand.save();
return successResponse(res, 'Brand created', saved, 201);
} catch (error) {
next(error);
}
};
const updateBrand = async (req, res, next) => {
try {
const brand = await Brand.findByIdAndUpdate(req.params.id, req.body, { new: true });
if (!brand) return errorResponse(res, 'Brand not found', [], 404);
return successResponse(res, 'Brand updated', brand);
} catch (error) {
next(error);
}
};
const deleteBrand = async (req, res, next) => {
try {
const brand = await Brand.findByIdAndDelete(req.params.id);
if (!brand) return errorResponse(res, 'Brand not found', [], 404);
return successResponse(res, 'Brand deleted');
} catch (error) {
next(error);
}
};
module.exports = { getBrands, createBrand, updateBrand, deleteBrand };

View File

@@ -0,0 +1,150 @@
const Cart = require('../models/Cart');
const Product = require('../models/Product');
const Coupon = require('../models/Coupon');
const { successResponse, errorResponse } = require('../utils/apiResponse');
const getCart = async (req, res, next) => {
try {
const userId = req.user ? req.user._id : null;
const sessionId = req.headers['x-session-id'] || null;
let cart = null;
if (userId) {
cart = await Cart.findOne({ user: userId }).populate('items.product');
} else if (sessionId) {
cart = await Cart.findOne({ sessionId }).populate('items.product');
}
if (!cart) {
cart = { items: [], couponCode: '', discountAmount: 0 };
}
return successResponse(res, 'Cart retrieved', cart);
} catch (error) {
next(error);
}
};
const addToCart = async (req, res, next) => {
try {
const { productId, quantity = 1, variantColor = '', variantFinish = '' } = req.body;
const userId = req.user ? req.user._id : null;
const sessionId = req.headers['x-session-id'] || 'guest-' + Date.now();
const product = await Product.findById(productId);
if (!product) {
return errorResponse(res, 'Product not found', [], 404);
}
// Inventory check
if (product.stock < quantity) {
return errorResponse(res, `Only ${product.stock} items remaining in stock`, [], 400);
}
let cart;
if (userId) {
cart = await Cart.findOne({ user: userId });
} else {
cart = await Cart.findOne({ sessionId });
}
const price = product.salePrice > 0 ? product.salePrice : product.price;
if (!cart) {
cart = new Cart({
user: userId,
sessionId,
items: [{ product: productId, quantity, variantColor, variantFinish, price }]
});
} else {
const existingItemIndex = cart.items.findIndex(
item => item.product.toString() === productId && item.variantColor === variantColor
);
if (existingItemIndex > -1) {
cart.items[existingItemIndex].quantity += Number(quantity);
} else {
cart.items.push({ product: productId, quantity, variantColor, variantFinish, price });
}
}
await cart.save();
const updatedCart = await Cart.findById(cart._id).populate('items.product');
return successResponse(res, 'Product added to cart', updatedCart);
} catch (error) {
next(error);
}
};
const updateCartItem = async (req, res, next) => {
try {
const { itemId, quantity } = req.body;
const userId = req.user ? req.user._id : null;
const sessionId = req.headers['x-session-id'];
const cart = userId
? await Cart.findOne({ user: userId })
: await Cart.findOne({ sessionId });
if (!cart) return errorResponse(res, 'Cart not found', [], 404);
const item = cart.items.id(itemId);
if (!item) return errorResponse(res, 'Item not found in cart', [], 404);
if (quantity <= 0) {
cart.items.pull(itemId);
} else {
item.quantity = quantity;
}
await cart.save();
const updatedCart = await Cart.findById(cart._id).populate('items.product');
return successResponse(res, 'Cart updated', updatedCart);
} catch (error) {
next(error);
}
};
const removeCartItem = async (req, res, next) => {
try {
const userId = req.user ? req.user._id : null;
const sessionId = req.headers['x-session-id'];
const cart = userId
? await Cart.findOne({ user: userId })
: await Cart.findOne({ sessionId });
if (cart) {
cart.items = cart.items.filter(item => item._id.toString() !== req.params.itemId);
await cart.save();
}
const updatedCart = cart ? await Cart.findById(cart._id).populate('items.product') : { items: [] };
return successResponse(res, 'Item removed from cart', updatedCart);
} catch (error) {
next(error);
}
};
const applyCoupon = async (req, res, next) => {
try {
const { code } = req.body;
const coupon = await Coupon.findOne({ code: code.toUpperCase(), isActive: true });
if (!coupon || coupon.endDate < new Date()) {
return errorResponse(res, 'Invalid or expired coupon code', [], 400);
}
return successResponse(res, 'Coupon applied successfully', {
code: coupon.code,
discountType: coupon.discountType,
discountValue: coupon.discountValue,
minPurchaseAmount: coupon.minPurchaseAmount
});
} catch (error) {
next(error);
}
};
module.exports = { getCart, addToCart, updateCartItem, removeCartItem, applyCoupon };

View File

@@ -0,0 +1,44 @@
const Category = require('../models/Category');
const { successResponse, errorResponse } = require('../utils/apiResponse');
const getCategories = async (req, res, next) => {
try {
const categories = await Category.find({ isActive: true }).sort({ displayOrder: 1 });
return successResponse(res, 'Categories retrieved', categories);
} catch (error) {
next(error);
}
};
const createCategory = async (req, res, next) => {
try {
const category = new Category(req.body);
const saved = await category.save();
return successResponse(res, 'Category created', saved, 201);
} catch (error) {
next(error);
}
};
const updateCategory = async (req, res, next) => {
try {
const category = await Category.findByIdAndUpdate(req.params.id, req.body, { new: true });
if (!category) return errorResponse(res, 'Category not found', [], 404);
return successResponse(res, 'Category updated', category);
} catch (error) {
next(error);
}
};
const deleteCategory = async (req, res, next) => {
try {
const category = await Category.findByIdAndDelete(req.params.id);
if (!category) return errorResponse(res, 'Category not found', [], 404);
return successResponse(res, 'Category deleted');
} catch (error) {
next(error);
}
};
module.exports = { getCategories, createCategory, updateCategory, deleteCategory };

View File

@@ -0,0 +1,34 @@
const ContactMessage = require('../models/ContactMessage');
const { successResponse, errorResponse } = require('../utils/apiResponse');
const submitContactForm = async (req, res, next) => {
try {
const { name, email, phone, subject, message } = req.body;
const msg = await ContactMessage.create({ name, email, phone, subject, message });
return successResponse(res, 'Thank you! Your message has been received.', msg, 201);
} catch (error) {
next(error);
}
};
const getContactMessages = async (req, res, next) => {
try {
const messages = await ContactMessage.find({}).sort({ createdAt: -1 });
return successResponse(res, 'Contact messages retrieved', messages);
} catch (error) {
next(error);
}
};
const updateMessageStatus = async (req, res, next) => {
try {
const msg = await ContactMessage.findByIdAndUpdate(req.params.id, { status: req.body.status }, { new: true });
if (!msg) return errorResponse(res, 'Message not found', [], 404);
return successResponse(res, 'Message status updated', msg);
} catch (error) {
next(error);
}
};
module.exports = { submitContactForm, getContactMessages, updateMessageStatus };

View File

@@ -0,0 +1,34 @@
const Coupon = require('../models/Coupon');
const { successResponse, errorResponse } = require('../utils/apiResponse');
const getCoupons = async (req, res, next) => {
try {
const coupons = await Coupon.find({}).sort({ createdAt: -1 });
return successResponse(res, 'Coupons retrieved', coupons);
} catch (error) {
next(error);
}
};
const createCoupon = async (req, res, next) => {
try {
const coupon = new Coupon(req.body);
const saved = await coupon.save();
return successResponse(res, 'Coupon created', saved, 201);
} catch (error) {
next(error);
}
};
const deleteCoupon = async (req, res, next) => {
try {
const coupon = await Coupon.findByIdAndDelete(req.params.id);
if (!coupon) return errorResponse(res, 'Coupon not found', [], 404);
return successResponse(res, 'Coupon deleted');
} catch (error) {
next(error);
}
};
module.exports = { getCoupons, createCoupon, deleteCoupon };

View File

@@ -0,0 +1,24 @@
const EmailTemplate = require('../models/EmailTemplate');
const { successResponse, errorResponse } = require('../utils/apiResponse');
const getEmailTemplates = async (req, res, next) => {
try {
const templates = await EmailTemplate.find({});
return successResponse(res, 'Email templates retrieved', templates);
} catch (error) {
next(error);
}
};
const updateEmailTemplate = async (req, res, next) => {
try {
const template = await EmailTemplate.findByIdAndUpdate(req.params.id, req.body, { new: true });
if (!template) return errorResponse(res, 'Template not found', [], 404);
return successResponse(res, 'Email template updated', template);
} catch (error) {
next(error);
}
};
module.exports = { getEmailTemplates, updateEmailTemplate };

View File

@@ -0,0 +1,85 @@
const Product = require('../models/Product');
const InventoryHistory = require('../models/InventoryHistory');
const { successResponse, errorResponse } = require('../utils/apiResponse');
// @desc Get stock overview & alerts (Admin)
// @route GET /api/inventory/overview
// @access Private/Admin
const getStockOverview = async (req, res, next) => {
try {
const products = await Product.find({})
.select('name sku stock lowStockThreshold price category brand images')
.populate('category', 'name')
.populate('brand', 'name');
const lowStock = products.filter(p => p.stock > 0 && p.stock <= p.lowStockThreshold);
const outOfStock = products.filter(p => p.stock === 0);
return successResponse(res, 'Stock overview retrieved', {
totalProducts: products.length,
lowStockCount: lowStock.length,
outOfStockCount: outOfStock.length,
lowStock,
outOfStock,
products
});
} catch (error) {
next(error);
}
};
// @desc Adjust inventory stock level (Admin)
// @route POST /api/inventory/adjust
// @access Private/Admin
const adjustStock = async (req, res, next) => {
try {
const { productId, newStock, reason, changeType = 'Manual Adjustment' } = req.body;
const product = await Product.findById(productId);
if (!product) {
return errorResponse(res, 'Product not found', [], 404);
}
const previousStock = product.stock;
const quantityChanged = Number(newStock) - previousStock;
product.stock = Number(newStock);
await product.save();
// Log history
const history = await InventoryHistory.create({
product: product._id,
sku: product.sku,
changeType,
quantityChanged,
previousStock,
newStock: product.stock,
reason: reason || 'Manual admin inventory adjustment',
performedBy: req.user._id
});
return successResponse(res, 'Stock adjusted successfully', { product, history });
} catch (error) {
next(error);
}
};
// @desc Get complete inventory change audit log (Admin)
// @route GET /api/inventory/history
// @access Private/Admin
const getInventoryHistory = async (req, res, next) => {
try {
const history = await InventoryHistory.find({})
.populate('product', 'name images')
.populate('performedBy', 'name email')
.sort({ createdAt: -1 })
.limit(100);
return successResponse(res, 'Inventory history audit log retrieved', history);
} catch (error) {
next(error);
}
};
module.exports = { getStockOverview, adjustStock, getInventoryHistory };

View File

@@ -0,0 +1,212 @@
const Order = require('../models/Order');
const Product = require('../models/Product');
const InventoryHistory = require('../models/InventoryHistory');
const Cart = require('../models/Cart');
const { sendEmail } = require('../utils/emailSender');
const { successResponse, errorResponse } = require('../utils/apiResponse');
// @desc Create new order
// @route POST /api/orders
// @access Private
const createOrder = async (req, res, next) => {
try {
const {
orderItems,
shippingAddress,
paymentMethod,
itemsPrice,
taxPrice,
shippingPrice,
discountAmount,
totalAmount
} = req.body;
if (!orderItems || orderItems.length === 0) {
return errorResponse(res, 'No order items provided', [], 400);
}
// 1. Check stock and reduce inventory
for (const item of orderItems) {
const product = await Product.findById(item.product);
if (!product) {
return errorResponse(res, `Product ${item.name} not found`, [], 400);
}
if (product.stock < item.quantity) {
return errorResponse(res, `Insufficient stock for product ${product.name}`, [], 400);
}
// Record stock deduction
const oldStock = product.stock;
product.stock -= item.quantity;
product.salesCount += item.quantity;
await product.save();
await InventoryHistory.create({
product: product._id,
sku: product.sku,
changeType: 'Order Placed',
quantityChanged: -item.quantity,
previousStock: oldStock,
newStock: product.stock,
reason: `Order placed by user ${req.user.name}`,
performedBy: req.user._id
});
}
// 2. Generate unique order number
const orderNumber = `GTR-${Math.floor(100000 + Math.random() * 900000)}`;
// 3. Save Order
const order = new Order({
orderNumber,
user: req.user._id,
orderItems,
shippingAddress,
paymentMethod,
itemsPrice,
taxPrice,
shippingPrice,
discountAmount,
totalAmount,
isPaid: paymentMethod === 'Stripe', // Auto mark paid for simulated stripe
paidAt: paymentMethod === 'Stripe' ? Date.now() : null,
orderStatus: paymentMethod === 'Stripe' ? 'Payment Confirmed' : 'Pending',
statusHistory: [
{
status: paymentMethod === 'Stripe' ? 'Payment Confirmed' : 'Pending',
note: `Order created via ${paymentMethod}`
}
]
});
const createdOrder = await order.save();
// 4. Clear user cart
await Cart.findOneAndDelete({ user: req.user._id });
// 5. Send order confirmation email
sendEmail({
templateName: 'Order Confirmation',
to: req.user.email,
variables: {
customerName: req.user.name,
orderId: createdOrder.orderNumber,
orderTotal: `$${createdOrder.totalAmount.toFixed(2)}`
}
});
return successResponse(res, 'Order created successfully', createdOrder, 201);
} catch (error) {
next(error);
}
};
// @desc Get logged in user orders
// @route GET /api/orders/my-orders
// @access Private
const getMyOrders = async (req, res, next) => {
try {
const orders = await Order.find({ user: req.user._id }).sort({ createdAt: -1 });
return successResponse(res, 'User orders retrieved', orders);
} catch (error) {
next(error);
}
};
// @desc Get order by ID
// @route GET /api/orders/:id
// @access Private
const getOrderById = async (req, res, next) => {
try {
const order = await Order.findById(req.params.id).populate('user', 'name email phone');
if (!order) {
return errorResponse(res, 'Order not found', [], 404);
}
// Check authorization: customer can only view own order unless Admin
if (order.user._id.toString() !== req.user._id.toString() && req.user.role !== 'Admin') {
return errorResponse(res, 'Not authorized to view this order', [], 403);
}
return successResponse(res, 'Order retrieved', order);
} catch (error) {
next(error);
}
};
// @desc Get all orders (Admin)
// @route GET /api/orders
// @access Private/Admin
const getAllOrders = async (req, res, next) => {
try {
const { status } = req.query;
const filter = {};
if (status) filter.orderStatus = status;
const orders = await Order.find(filter)
.populate('user', 'name email')
.sort({ createdAt: -1 });
return successResponse(res, 'All orders retrieved', orders);
} catch (error) {
next(error);
}
};
// @desc Update order status & shipping info (Admin)
// @route PUT /api/orders/:id/status
// @access Private/Admin
const updateOrderStatus = async (req, res, next) => {
try {
const { orderStatus, trackingNumber, shippingProvider, note } = req.body;
const order = await Order.findById(req.params.id).populate('user', 'name email');
if (!order) {
return errorResponse(res, 'Order not found', [], 404);
}
if (orderStatus) {
order.orderStatus = orderStatus;
order.statusHistory.push({
status: orderStatus,
note: note || `Status updated to ${orderStatus}`
});
}
if (trackingNumber) order.trackingNumber = trackingNumber;
if (shippingProvider) order.shippingProvider = shippingProvider;
if (orderStatus === 'Delivered') {
order.isDelivered = true;
order.deliveredAt = Date.now();
}
const updatedOrder = await order.save();
// Trigger status change email
if (['Shipped', 'Delivered', 'Cancelled'].includes(orderStatus)) {
sendEmail({
templateName: orderStatus === 'Shipped' ? 'Order Shipped' : 'Order Confirmation',
to: order.user.email,
variables: {
customerName: order.user.name,
orderId: order.orderNumber,
trackingNumber: order.trackingNumber || 'N/A'
}
});
}
return successResponse(res, 'Order status updated', updatedOrder);
} catch (error) {
next(error);
}
};
module.exports = {
createOrder,
getMyOrders,
getOrderById,
getAllOrders,
updateOrderStatus
};

View File

@@ -0,0 +1,235 @@
const Product = require('../models/Product');
const Category = require('../models/Category');
const Brand = require('../models/Brand');
const { successResponse, errorResponse } = require('../utils/apiResponse');
// @desc Get all products with filtering, search, sorting & pagination
// @route GET /api/products
// @access Public
const getProducts = async (req, res, next) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 12;
const skip = (page - 1) * limit;
const query = { isActive: true };
// Search query
if (req.query.search) {
const searchRegex = new RegExp(req.query.search, 'i');
query.$or = [
{ name: searchRegex },
{ description: searchRegex },
{ tags: searchRegex },
{ sku: searchRegex }
];
}
// Category filter
if (req.query.category) {
const cat = await Category.findOne({ slug: req.query.category });
if (cat) query.category = cat._id;
}
// Brand filter
if (req.query.brand) {
const brand = await Brand.findOne({ slug: req.query.brand });
if (brand) query.brand = brand._id;
}
// Price range filter
if (req.query.minPrice || req.query.maxPrice) {
query.price = {};
if (req.query.minPrice) query.price.$gte = Number(req.query.minPrice);
if (req.query.maxPrice) query.price.$lte = Number(req.query.maxPrice);
}
// Guitar specific filters
if (req.query.guitarType) {
query['specs.guitarType'] = req.query.guitarType;
}
if (req.query.bodyShape) {
query['specs.bodyShape'] = req.query.bodyShape;
}
if (req.query.handOrientation) {
query['specs.handOrientation'] = req.query.handOrientation;
}
if (req.query.numberOfStrings) {
query['specs.numberOfStrings'] = Number(req.query.numberOfStrings);
}
if (req.query.isFeatured) {
query.isFeatured = true;
}
if (req.query.isNewArrival) {
query.isNewArrival = true;
}
if (req.query.isBestSeller) {
query.isBestSeller = true;
}
// Sorting
let sortOption = { createdAt: -1 };
if (req.query.sort === 'price_asc') sortOption = { price: 1 };
if (req.query.sort === 'price_desc') sortOption = { price: -1 };
if (req.query.sort === 'rating') sortOption = { rating: -1 };
if (req.query.sort === 'bestselling') sortOption = { salesCount: -1 };
const total = await Product.countDocuments(query);
const products = await Product.find(query)
.populate('category', 'name slug')
.populate('brand', 'name slug logo')
.sort(sortOption)
.skip(skip)
.limit(limit);
return successResponse(res, 'Products retrieved successfully', {
products,
pagination: {
total,
page,
pages: Math.ceil(total / limit),
limit
}
});
} catch (error) {
next(error);
}
};
// @desc Guitar Finder Recommendation Tool
// @route POST /api/products/guitar-finder
// @access Public
const getGuitarRecommendations = async (req, res, next) => {
try {
const { skillLevel, acousticElectric, musicStyle, maxBudget, handOrientation } = req.body;
const query = { isActive: true };
if (acousticElectric && acousticElectric !== 'Any') {
query['specs.guitarType'] = acousticElectric;
}
if (maxBudget) {
query.price = { $lte: Number(maxBudget) };
}
if (handOrientation) {
query['specs.handOrientation'] = handOrientation;
}
const recommendations = await Product.find(query)
.populate('category', 'name slug')
.populate('brand', 'name slug logo')
.sort({ rating: -1, salesCount: -1 })
.limit(6);
return successResponse(res, 'Guitar recommendations generated', recommendations);
} catch (error) {
next(error);
}
};
// @desc Get single product by slug
// @route GET /api/products/:slug
// @access Public
const getProductBySlug = async (req, res, next) => {
try {
const product = await Product.findOne({ slug: req.params.slug })
.populate('category', 'name slug')
.populate('brand', 'name slug logo description');
if (!product) {
return errorResponse(res, 'Product not found', [], 404);
}
// Related products (same category)
const relatedProducts = await Product.find({
category: product.category._id,
_id: { $ne: product._id },
isActive: true
})
.limit(4)
.select('name slug price salePrice images rating brand');
return successResponse(res, 'Product retrieved successfully', {
product,
relatedProducts
});
} catch (error) {
next(error);
}
};
// @desc Compare products by array of IDs
// @route POST /api/products/compare
// @access Public
const compareProducts = async (req, res, next) => {
try {
const { productIds } = req.body;
if (!productIds || !Array.isArray(productIds)) {
return errorResponse(res, 'Please provide an array of product IDs', [], 400);
}
const products = await Product.find({ _id: { $in: productIds } })
.populate('brand', 'name logo')
.populate('category', 'name');
return successResponse(res, 'Comparison data retrieved', products);
} catch (error) {
next(error);
}
};
// @desc Create new product (Admin)
// @route POST /api/products
// @access Private/Admin
const createProduct = async (req, res, next) => {
try {
const product = new Product(req.body);
const createdProduct = await product.save();
return successResponse(res, 'Product created successfully', createdProduct, 201);
} catch (error) {
next(error);
}
};
// @desc Update product (Admin)
// @route PUT /api/products/:id
// @access Private/Admin
const updateProduct = async (req, res, next) => {
try {
const product = await Product.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
if (!product) {
return errorResponse(res, 'Product not found', [], 404);
}
return successResponse(res, 'Product updated successfully', product);
} catch (error) {
next(error);
}
};
// @desc Delete product (Admin)
// @route DELETE /api/products/:id
// @access Private/Admin
const deleteProduct = async (req, res, next) => {
try {
const product = await Product.findByIdAndDelete(req.params.id);
if (!product) {
return errorResponse(res, 'Product not found', [], 404);
}
return successResponse(res, 'Product deleted successfully');
} catch (error) {
next(error);
}
};
module.exports = {
getProducts,
getGuitarRecommendations,
getProductBySlug,
compareProducts,
createProduct,
updateProduct,
deleteProduct
};

View File

@@ -0,0 +1,50 @@
const Review = require('../models/Review');
const Product = require('../models/Product');
const { successResponse, errorResponse } = require('../utils/apiResponse');
const getProductReviews = async (req, res, next) => {
try {
const reviews = await Review.find({ product: req.params.productId, isApproved: true })
.populate('user', 'name avatar')
.sort({ createdAt: -1 });
return successResponse(res, 'Reviews retrieved', reviews);
} catch (error) {
next(error);
}
};
const createReview = async (req, res, next) => {
try {
const { productId, rating, title, comment } = req.body;
const existing = await Review.findOne({ user: req.user._id, product: productId });
if (existing) {
return errorResponse(res, 'You have already reviewed this product', [], 400);
}
const review = await Review.create({
user: req.user._id,
product: productId,
rating: Number(rating),
title,
comment
});
// Update Product average rating
const reviews = await Review.find({ product: productId, isApproved: true });
const avgRating = reviews.reduce((acc, item) => item.rating + acc, 0) / reviews.length;
await Product.findByIdAndUpdate(productId, {
rating: Number(avgRating.toFixed(1)),
numReviews: reviews.length
});
return successResponse(res, 'Review submitted successfully', review, 201);
} catch (error) {
next(error);
}
};
module.exports = { getProductReviews, createReview };

View File

@@ -0,0 +1,32 @@
const WebsiteSettings = require('../models/WebsiteSettings');
const { successResponse, errorResponse } = require('../utils/apiResponse');
const getSettings = async (req, res, next) => {
try {
let settings = await WebsiteSettings.findOne({});
if (!settings) {
settings = await WebsiteSettings.create({});
}
return successResponse(res, 'Website settings retrieved', settings);
} catch (error) {
next(error);
}
};
const updateSettings = async (req, res, next) => {
try {
let settings = await WebsiteSettings.findOne({});
if (!settings) {
settings = new WebsiteSettings(req.body);
} else {
Object.assign(settings, req.body);
}
await settings.save();
return successResponse(res, 'Website settings updated successfully', settings);
} catch (error) {
next(error);
}
};
module.exports = { getSettings, updateSettings };

View File

@@ -0,0 +1,33 @@
const SocialMedia = require('../models/SocialMedia');
const { successResponse, errorResponse } = require('../utils/apiResponse');
const getSocialMedia = async (req, res, next) => {
try {
const socialLinks = await SocialMedia.find({ isEnabled: true });
return successResponse(res, 'Social media links retrieved', socialLinks);
} catch (error) {
next(error);
}
};
const updateSocialMedia = async (req, res, next) => {
try {
const { links } = req.body; // Array of { platform, url, isEnabled }
if (Array.isArray(links)) {
for (const link of links) {
await SocialMedia.findOneAndUpdate(
{ platform: link.platform },
{ url: link.url, isEnabled: link.isEnabled },
{ upsert: true, new: true }
);
}
}
const updated = await SocialMedia.find({});
return successResponse(res, 'Social media accounts updated', updated);
} catch (error) {
next(error);
}
};
module.exports = { getSocialMedia, updateSocialMedia };

View File

@@ -0,0 +1,41 @@
const Wishlist = require('../models/Wishlist');
const { successResponse, errorResponse } = require('../utils/apiResponse');
const getWishlist = async (req, res, next) => {
try {
let wishlist = await Wishlist.findOne({ user: req.user._id }).populate('products');
if (!wishlist) {
wishlist = await Wishlist.create({ user: req.user._id, products: [] });
}
return successResponse(res, 'Wishlist retrieved', wishlist);
} catch (error) {
next(error);
}
};
const toggleWishlist = async (req, res, next) => {
try {
const { productId } = req.body;
let wishlist = await Wishlist.findOne({ user: req.user._id });
if (!wishlist) {
wishlist = new Wishlist({ user: req.user._id, products: [productId] });
} else {
const exists = wishlist.products.includes(productId);
if (exists) {
wishlist.products.pull(productId);
} else {
wishlist.products.push(productId);
}
}
await wishlist.save();
const updated = await Wishlist.findById(wishlist._id).populate('products');
return successResponse(res, 'Wishlist updated', updated);
} catch (error) {
next(error);
}
};
module.exports = { getWishlist, toggleWishlist };

View File

@@ -0,0 +1,43 @@
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const { errorResponse } = require('../utils/apiResponse');
const protect = async (req, res, next) => {
let token;
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}
if (!token) {
return errorResponse(res, 'Not authorized to access this route', [], 401);
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'guitar_ecommerce_super_secret_jwt_key_2026');
req.user = await User.findById(decoded.id).select('-password');
if (!req.user) {
return errorResponse(res, 'User account no longer exists', [], 401);
}
next();
} catch (error) {
return errorResponse(res, 'Not authorized, token failed or expired', [], 401);
}
};
const authorize = (...roles) => {
return (req, res, next) => {
if (!req.user || !roles.includes(req.user.role)) {
return errorResponse(
res,
`User role '${req.user ? req.user.role : 'Guest'}' is not authorized to access this resource`,
[],
403
);
}
next();
};
};
module.exports = { protect, authorize };

View File

@@ -0,0 +1,31 @@
const { errorResponse } = require('../utils/apiResponse');
const errorHandler = (err, req, res, next) => {
let statusCode = err.statusCode || res.statusCode === 200 ? 500 : res.statusCode;
let message = err.message || 'Server Error';
// Mongoose bad ObjectId
if (err.name === 'CastError') {
message = `Resource not found with id ${err.value}`;
statusCode = 404;
}
// Mongoose duplicate key
if (err.code === 11000) {
const field = Object.keys(err.keyValue)[0];
message = `Duplicate field value entered for ${field}. Please use another value.`;
statusCode = 400;
}
// Mongoose validation error
if (err.name === 'ValidationError') {
message = Object.values(err.errors).map(val => val.message).join(', ');
statusCode = 400;
}
console.error('[Error Handler]:', err);
return errorResponse(res, message, err.errors || [], statusCode);
};
module.exports = errorHandler;

View File

@@ -0,0 +1,40 @@
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const uploadDir = path.join(__dirname, '../uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
const storage = multer.diskStorage({
destination(req, file, cb) {
cb(null, uploadDir);
},
filename(req, file, cb) {
cb(null, `${file.fieldname}-${Date.now()}${path.extname(file.originalname)}`);
}
});
function checkFileType(file, cb) {
const filetypes = /jpg|jpeg|png|webp|svg|mp4|webm/;
const extname = filetypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = filetypes.test(file.mimetype);
if (extname && mimetype) {
return cb(null, true);
} else {
cb(new Error('Images and video media only!'));
}
}
const upload = multer({
storage,
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
fileFilter: function (req, file, cb) {
checkFileType(file, cb);
}
});
module.exports = upload;

16
server/models/Banner.js Normal file
View File

@@ -0,0 +1,16 @@
const mongoose = require('mongoose');
const bannerSchema = new mongoose.Schema({
title: { type: String, required: true },
subtitle: { type: String, default: '' },
desktopImage: { type: String, required: true },
mobileImage: { type: String, default: '' },
buttonText: { type: String, default: 'Shop Now' },
buttonLink: { type: String, default: '/shop' },
overlayOpacity: { type: Number, default: 0.4 },
displayOrder: { type: Number, default: 0 },
isActive: { type: Boolean, default: true }
}, { timestamps: true });
module.exports = mongoose.model('Banner', bannerSchema);

15
server/models/Brand.js Normal file
View File

@@ -0,0 +1,15 @@
const mongoose = require('mongoose');
const brandSchema = new mongoose.Schema({
name: { type: String, required: true, trim: true, unique: true },
slug: { type: String, required: true, unique: true, lowercase: true },
logo: { type: String, default: '' },
description: { type: String, default: '' },
website: { type: String, default: '' },
metaTitle: { type: String, default: '' },
metaDescription: { type: String, default: '' },
isActive: { type: Boolean, default: true }
}, { timestamps: true });
module.exports = mongoose.model('Brand', brandSchema);

20
server/models/Cart.js Normal file
View File

@@ -0,0 +1,20 @@
const mongoose = require('mongoose');
const cartItemSchema = new mongoose.Schema({
product: { type: mongoose.Schema.Types.ObjectId, ref: 'Product', required: true },
variantColor: { type: String, default: '' },
variantFinish: { type: String, default: '' },
quantity: { type: Number, required: true, default: 1, min: 1 },
price: { type: Number, required: true }
});
const cartSchema = new mongoose.Schema({
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', default: null },
sessionId: { type: String, default: null },
items: [cartItemSchema],
couponCode: { type: String, default: '' },
discountAmount: { type: Number, default: 0 }
}, { timestamps: true });
module.exports = mongoose.model('Cart', cartSchema);

16
server/models/Category.js Normal file
View File

@@ -0,0 +1,16 @@
const mongoose = require('mongoose');
const categorySchema = new mongoose.Schema({
name: { type: String, required: true, trim: true, unique: true },
slug: { type: String, required: true, unique: true, lowercase: true },
description: { type: String, default: '' },
image: { type: String, default: 'https://images.unsplash.com/photo-1510915361894-db8b60106cb1?w=600' },
homeImage: { type: String, default: '' },
fallbackImage: { type: String, default: 'https://images.unsplash.com/photo-1510915361894-db8b60106cb1?w=600' },
parentCategory: { type: mongoose.Schema.Types.ObjectId, ref: 'Category', default: null },
displayOrder: { type: Number, default: 0 },
isFeaturedHome: { type: Boolean, default: true },
isActive: { type: Boolean, default: true }
}, { timestamps: true });
module.exports = mongoose.model('Category', categorySchema);

View File

@@ -0,0 +1,13 @@
const mongoose = require('mongoose');
const contactMessageSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true },
phone: { type: String, default: '' },
subject: { type: String, default: 'General Inquiry' },
message: { type: String, required: true },
status: { type: String, enum: ['Pending', 'Read', 'Resolved'], default: 'Pending' }
}, { timestamps: true });
module.exports = mongoose.model('ContactMessage', contactMessageSchema);

17
server/models/Coupon.js Normal file
View File

@@ -0,0 +1,17 @@
const mongoose = require('mongoose');
const couponSchema = new mongoose.Schema({
code: { type: String, required: true, unique: true, uppercase: true, trim: true },
discountType: { type: String, enum: ['percentage', 'fixed'], default: 'percentage' },
discountValue: { type: Number, required: true },
minPurchaseAmount: { type: Number, default: 0 },
maxDiscountAmount: { type: Number, default: 0 },
startDate: { type: Date, default: Date.now },
endDate: { type: Date, required: true },
usageLimit: { type: Number, default: 100 },
usedCount: { type: Number, default: 0 },
isActive: { type: Boolean, default: true }
}, { timestamps: true });
module.exports = mongoose.model('Coupon', couponSchema);

View File

@@ -0,0 +1,11 @@
const mongoose = require('mongoose');
const emailTemplateSchema = new mongoose.Schema({
name: { type: String, required: true, unique: true }, // Welcome, Order Confirmation, Order Shipped, Password Reset, Low Stock, Contact Form
subject: { type: String, required: true },
bodyHtml: { type: String, required: true },
placeholders: [{ type: String }] // e.g. ['{{customerName}}', '{{orderId}}', '{{orderTotal}}']
}, { timestamps: true });
module.exports = mongoose.model('EmailTemplate', emailTemplateSchema);

View File

@@ -0,0 +1,19 @@
const mongoose = require('mongoose');
const inventoryHistorySchema = new mongoose.Schema({
product: { type: mongoose.Schema.Types.ObjectId, ref: 'Product', required: true },
sku: { type: String, required: true },
changeType: {
type: String,
enum: ['Order Placed', 'Order Cancelled', 'Manual Adjustment', 'Restock', 'Returned Product'],
required: true
},
quantityChanged: { type: Number, required: true }, // positive for addition, negative for deduction
previousStock: { type: Number, required: true },
newStock: { type: Number, required: true },
reason: { type: String, default: '' },
performedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User', default: null }
}, { timestamps: true });
module.exports = mongoose.model('InventoryHistory', inventoryHistorySchema);

85
server/models/Order.js Normal file
View File

@@ -0,0 +1,85 @@
const mongoose = require('mongoose');
const orderItemSchema = new mongoose.Schema({
product: { type: mongoose.Schema.Types.ObjectId, ref: 'Product', required: true },
name: { type: String, required: true },
image: { type: String, required: true },
price: { type: Number, required: true },
quantity: { type: Number, required: true },
variantColor: { type: String, default: '' },
variantFinish: { type: String, default: '' },
sku: { type: String, default: '' }
});
const statusHistorySchema = new mongoose.Schema({
status: { type: String, required: true },
note: { type: String, default: '' },
updatedAt: { type: Date, default: Date.now }
});
const orderSchema = new mongoose.Schema({
orderNumber: { type: String, required: true, unique: true },
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
orderItems: [orderItemSchema],
// Indian Shipping Address Schema
shippingAddress: {
fullName: { type: String, default: '' },
mobileNumber: { type: String, required: true },
addressLine1: { type: String, required: true },
addressLine2: { type: String, default: '' },
landmark: { type: String, default: '' },
city: { type: String, required: true },
district: { type: String, default: '' },
state: { type: String, required: true },
postalCode: { type: String, required: true }, // 6-digit PIN code
country: { type: String, required: true, default: 'India' }
},
// Payment Configuration
paymentMethod: {
type: String,
required: true,
enum: ['Razorpay', 'UPI', 'Stripe', 'Credit/Debit Card', 'Net Banking', 'Cash on Delivery']
},
paymentResult: {
id: { type: String },
status: { type: String },
update_time: { type: String },
email_address: { type: String }
},
// Pricing & Indian GST breakdown
itemsPrice: { type: Number, required: true, default: 0 },
gstPercentage: { type: Number, default: 18 },
cgst: { type: Number, default: 0 },
sgst: { type: Number, default: 0 },
igst: { type: Number, default: 0 },
taxPrice: { type: Number, required: true, default: 0 },
shippingPrice: { type: Number, required: true, default: 0 },
discountAmount: { type: Number, default: 0 },
totalAmount: { type: Number, required: true, default: 0 },
// Order Status Lifecycle
orderStatus: {
type: String,
enum: ['Pending', 'Payment Pending', 'Payment Confirmed', 'Processing', 'Packed', 'Shipped', 'Out for Delivery', 'Delivered', 'Cancelled', 'Returned', 'Refunded'],
default: 'Pending'
},
statusHistory: [statusHistorySchema],
// Indian Courier Logistics
courierProvider: { type: String, default: 'Delhivery' }, // Delhivery, BlueDart, Bluedart, FedEx, Xpressbees
awbNumber: { type: String, default: '' },
trackingNumber: { type: String, default: '' },
trackingUrl: { type: String, default: '' },
estimatedDeliveryDate: { type: Date },
isPaid: { type: Boolean, default: false },
paidAt: { type: Date },
isDelivered: { type: Boolean, default: false },
deliveredAt: { type: Date },
notes: { type: String, default: '' }
}, { timestamps: true });
module.exports = mongoose.model('Order', orderSchema);

105
server/models/Product.js Normal file
View File

@@ -0,0 +1,105 @@
const mongoose = require('mongoose');
const variantSchema = new mongoose.Schema({
color: { type: String, required: true },
colorHex: { type: String, default: '#000000' },
finish: { type: String, default: 'Gloss' },
sku: { type: String, default: '' },
price: { type: Number, required: true },
salePrice: { type: Number, default: 0 },
stock: { type: Number, required: true, default: 0 },
image: { type: String, default: '' }
});
const customSpecSchema = new mongoose.Schema({
key: { type: String, required: true },
value: { type: String, required: true }
});
const guitarSpecSchema = new mongoose.Schema({
guitarType: { type: String, default: 'Electric' }, // Electric, Acoustic, Bass, Classical, Accessories
bodyShape: { type: String, default: 'Stratocaster' },
bodyMaterial: { type: String, default: 'Alder' },
topMaterial: { type: String, default: '' },
backMaterial: { type: String, default: '' },
sideMaterial: { type: String, default: '' },
neckMaterial: { type: String, default: 'Maple' },
fingerboardMaterial: { type: String, default: 'Rosewood' },
scaleLength: { type: String, default: '25.5"' },
numberOfStrings: { type: Number, default: 6 },
frets: { type: Number, default: 22 },
pickupConfiguration: { type: String, default: 'SSS' }, // SSS, HSS, HH, Single, Acoustic Pickup
pickupType: { type: String, default: 'Passive Single-Coil' },
bridgeType: { type: String, default: 'Tremolo' },
tuningMachines: { type: String, default: 'Standard Die-Cast' },
handOrientation: { type: String, enum: ['Right-Handed', 'Left-Handed'], default: 'Right-Handed' },
color: { type: String, default: '' },
finish: { type: String, default: 'Gloss Polyurethane' },
includesCase: { type: Boolean, default: false },
customSpecs: [customSpecSchema]
});
const productSchema = new mongoose.Schema({
name: { type: String, required: true, trim: true },
slug: { type: String, required: true, unique: true, lowercase: true },
sku: { type: String, required: true, unique: true },
brand: { type: mongoose.Schema.Types.ObjectId, ref: 'Brand', required: true },
category: { type: mongoose.Schema.Types.ObjectId, ref: 'Category', required: true },
description: { type: String, required: true },
shortDescription: { type: String, default: '' },
// Pricing & Discounts
price: { type: Number, required: true, min: 0 },
salePrice: { type: Number, default: 0, min: 0 },
discountType: { type: String, enum: ['percentage', 'fixed'], default: 'percentage' },
discountValue: { type: Number, default: 0, min: 0 },
costPrice: { type: Number, default: 0 },
// Inventory
stock: { type: Number, required: true, default: 10, min: 0 },
lowStockThreshold: { type: Number, default: 3 },
// Media
images: [{ type: String }],
primaryImage: { type: String, default: '' },
videoUrl: { type: String, default: '' },
audioDemoUrl: { type: String, default: '' },
// Specs & Variants
specs: guitarSpecSchema,
variants: [variantSchema],
tags: [{ type: String }],
weightKg: { type: Number, default: 4.5 },
dimensionsCm: { type: String, default: '105 x 38 x 10' },
// Rating & Sales Metrics
rating: { type: Number, default: 4.8 },
numReviews: { type: Number, default: 0 },
salesCount: { type: Number, default: 0 },
// Badges & Flags
isFeatured: { type: Boolean, default: false },
isNewArrival: { type: Boolean, default: false },
isBestSeller: { type: Boolean, default: false },
isActive: { type: Boolean, default: true },
// SEO
metaTitle: { type: String, default: '' },
metaDescription: { type: String, default: '' }
}, { timestamps: true });
// Auto-generate SKU before validation if missing
productSchema.pre('validate', function(next) {
if (!this.sku) {
const randomNum = Math.floor(10000 + Math.random() * 90000);
this.sku = `GTR-2026-${randomNum}`;
}
if (this.images && this.images.length > 0 && !this.primaryImage) {
this.primaryImage = this.images[0];
}
next();
});
productSchema.index({ name: 'text', description: 'text', tags: 'text' });
module.exports = mongoose.model('Product', productSchema);

17
server/models/Review.js Normal file
View File

@@ -0,0 +1,17 @@
const mongoose = require('mongoose');
const reviewSchema = new mongoose.Schema({
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
product: { type: mongoose.Schema.Types.ObjectId, ref: 'Product', required: true },
rating: { type: Number, required: true, min: 1, max: 5 },
title: { type: String, required: true, trim: true },
comment: { type: String, required: true },
isVerifiedPurchase: { type: Boolean, default: true },
isApproved: { type: Boolean, default: true }
}, { timestamps: true });
// Prevent multiple reviews per product by same user
reviewSchema.index({ user: 1, product: 1 }, { unique: true });
module.exports = mongoose.model('Review', reviewSchema);

View File

@@ -0,0 +1,10 @@
const mongoose = require('mongoose');
const socialMediaSchema = new mongoose.Schema({
platform: { type: String, required: true, unique: true }, // instagram, facebook, youtube, tiktok, x, linkedin
url: { type: String, required: true },
isEnabled: { type: Boolean, default: true }
}, { timestamps: true });
module.exports = mongoose.model('SocialMedia', socialMediaSchema);

47
server/models/User.js Normal file
View File

@@ -0,0 +1,47 @@
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const addressSchema = new mongoose.Schema({
fullName: { type: String, default: '' },
mobileNumber: { type: String, default: '' },
addressLine1: { type: String, default: '' },
addressLine2: { type: String, default: '' },
landmark: { type: String, default: '' },
street: { type: String, default: '' },
city: { type: String, required: true },
district: { type: String, default: '' },
state: { type: String, required: true },
postalCode: { type: String, required: true },
country: { type: String, required: true, default: 'India' },
isDefault: { type: Boolean, default: false }
});
const userSchema = new mongoose.Schema({
name: { type: String, required: [true, 'Please provide your name'], trim: true },
email: { type: String, required: [true, 'Please provide your email'], unique: true, lowercase: true, trim: true },
password: { type: String, required: [true, 'Please provide a password'], minlength: 6, select: false },
role: {
type: String,
enum: ['Customer', 'Admin', 'Staff', 'Warehouse Manager', 'Order Manager'],
default: 'Customer'
},
phone: { type: String, default: '' },
avatar: { type: String, default: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300' },
addresses: [addressSchema],
isVerified: { type: Boolean, default: true },
resetPasswordToken: String,
resetPasswordExpire: Date
}, { timestamps: true });
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
userSchema.methods.matchPassword = async function(enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
module.exports = mongoose.model('User', userSchema);

View File

@@ -0,0 +1,34 @@
const mongoose = require('mongoose');
const websiteSettingsSchema = new mongoose.Schema({
siteName: { type: String, default: 'Antigravity Guitars India' },
tagline: { type: String, default: 'Crafted Sound. Pure Perfection.' },
contactEmail: { type: String, default: 'support@antigravityguitars.in' },
phone: { type: String, default: '+91 98765 43210' },
businessName: { type: String, default: 'Antigravity Music Private Limited' },
gstin: { type: String, default: '29ABCDE1234F1Z5' },
addressLine1: { type: String, default: '100 Brigade Road, Ashok Nagar' },
addressLine2: { type: String, default: 'Suite 400, Music Square' },
city: { type: String, default: 'Bengaluru' },
district: { type: String, default: 'Bengaluru Urban' },
state: { type: String, default: 'Karnataka' },
pincode: { type: String, default: '560025' },
country: { type: String, default: 'India' },
currencySymbol: { type: String, default: '₹' },
currencyCode: { type: String, default: 'INR' },
timezone: { type: String, default: 'Asia/Kolkata' },
logoPrimary: { type: String, default: '/assets/logo-primary.svg' },
logoDark: { type: String, default: '/assets/logo-dark.svg' },
logoLight: { type: String, default: '/assets/logo-light.svg' },
logoMobile: { type: String, default: '/assets/logo-mobile.svg' },
favicon: { type: String, default: '/favicon.ico' },
freeShippingThreshold: { type: Number, default: 5000 }, // ₹5,000 threshold
codEnabled: { type: Boolean, default: true },
maxCodAmount: { type: Number, default: 50000 }, // ₹50,000 max COD
aboutUsText: { type: String, default: 'We are India’s premier music destination providing hand-crafted acoustic, electric, and custom shop guitars.' },
privacyPolicy: { type: String, default: 'Your privacy is paramount to us under Indian IT Act regulations.' },
termsConditions: { type: String, default: 'All sales governed by Antigravity Guitars warranty policies.' },
returnPolicy: { type: String, default: '10-day hassle-free returns on all standard model guitars in original condition.' }
}, { timestamps: true });
module.exports = mongoose.model('WebsiteSettings', websiteSettingsSchema);

View File

@@ -0,0 +1,9 @@
const mongoose = require('mongoose');
const wishlistSchema = new mongoose.Schema({
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, unique: true },
products: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Product' }]
}, { timestamps: true });
module.exports = mongoose.model('Wishlist', wishlistSchema);

1821
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

31
server/package.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "guitar-ecommerce-server",
"version": "1.0.0",
"description": "Backend API for Modern Guitar Ecommerce Application",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"seed": "node seed/seeder.js"
},
"keywords": ["mern", "ecommerce", "guitar", "express", "mongodb"],
"author": "Antigravity",
"license": "ISC",
"dependencies": {
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-rate-limit": "^7.2.0",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.3.1",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.13",
"stripe": "^15.3.0"
},
"devDependencies": {
"nodemon": "^3.1.0"
}
}

View File

@@ -0,0 +1,12 @@
const express = require('express');
const router = express.Router();
const { registerUser, loginUser, getUserProfile, updateUserProfile } = require('../controllers/authController');
const { protect } = require('../middleware/authMiddleware');
router.post('/register', registerUser);
router.post('/login', loginUser);
router.get('/profile', protect, getUserProfile);
router.put('/profile', protect, updateUserProfile);
module.exports = router;

View File

@@ -0,0 +1,12 @@
const express = require('express');
const router = express.Router();
const { getBanners, createBanner, updateBanner, deleteBanner } = require('../controllers/bannerController');
const { protect, authorize } = require('../middleware/authMiddleware');
router.get('/', getBanners);
router.post('/', protect, authorize('Admin'), createBanner);
router.put('/:id', protect, authorize('Admin'), updateBanner);
router.delete('/:id', protect, authorize('Admin'), deleteBanner);
module.exports = router;

View File

@@ -0,0 +1,12 @@
const express = require('express');
const router = express.Router();
const { getBrands, createBrand, updateBrand, deleteBrand } = require('../controllers/brandController');
const { protect, authorize } = require('../middleware/authMiddleware');
router.get('/', getBrands);
router.post('/', protect, authorize('Admin'), createBrand);
router.put('/:id', protect, authorize('Admin'), updateBrand);
router.delete('/:id', protect, authorize('Admin'), deleteBrand);
module.exports = router;

View File

@@ -0,0 +1,12 @@
const express = require('express');
const router = express.Router();
const { getCart, addToCart, updateCartItem, removeCartItem, applyCoupon } = require('../controllers/cartController');
router.get('/', getCart);
router.post('/add', addToCart);
router.put('/update', updateCartItem);
router.delete('/item/:itemId', removeCartItem);
router.post('/coupon', applyCoupon);
module.exports = router;

View File

@@ -0,0 +1,12 @@
const express = require('express');
const router = express.Router();
const { getCategories, createCategory, updateCategory, deleteCategory } = require('../controllers/categoryController');
const { protect, authorize } = require('../middleware/authMiddleware');
router.get('/', getCategories);
router.post('/', protect, authorize('Admin'), createCategory);
router.put('/:id', protect, authorize('Admin'), updateCategory);
router.delete('/:id', protect, authorize('Admin'), deleteCategory);
module.exports = router;

View File

@@ -0,0 +1,11 @@
const express = require('express');
const router = express.Router();
const { submitContactForm, getContactMessages, updateMessageStatus } = require('../controllers/contactController');
const { protect, authorize } = require('../middleware/authMiddleware');
router.post('/', submitContactForm);
router.get('/', protect, authorize('Admin'), getContactMessages);
router.put('/:id/status', protect, authorize('Admin'), updateMessageStatus);
module.exports = router;

View File

@@ -0,0 +1,11 @@
const express = require('express');
const router = express.Router();
const { getCoupons, createCoupon, deleteCoupon } = require('../controllers/couponController');
const { protect, authorize } = require('../middleware/authMiddleware');
router.get('/', protect, authorize('Admin'), getCoupons);
router.post('/', protect, authorize('Admin'), createCoupon);
router.delete('/:id', protect, authorize('Admin'), deleteCoupon);
module.exports = router;

View File

@@ -0,0 +1,10 @@
const express = require('express');
const router = express.Router();
const { getEmailTemplates, updateEmailTemplate } = require('../controllers/emailTemplateController');
const { protect, authorize } = require('../middleware/authMiddleware');
router.get('/', protect, authorize('Admin'), getEmailTemplates);
router.put('/:id', protect, authorize('Admin'), updateEmailTemplate);
module.exports = router;

View File

@@ -0,0 +1,11 @@
const express = require('express');
const router = express.Router();
const { getStockOverview, adjustStock, getInventoryHistory } = require('../controllers/inventoryController');
const { protect, authorize } = require('../middleware/authMiddleware');
router.get('/overview', protect, authorize('Admin'), getStockOverview);
router.post('/adjust', protect, authorize('Admin'), adjustStock);
router.get('/history', protect, authorize('Admin'), getInventoryHistory);
module.exports = router;

View File

@@ -0,0 +1,19 @@
const express = require('express');
const router = express.Router();
const {
createOrder,
getMyOrders,
getOrderById,
getAllOrders,
updateOrderStatus
} = require('../controllers/orderController');
const { protect, authorize } = require('../middleware/authMiddleware');
router.post('/', protect, createOrder);
router.get('/my-orders', protect, getMyOrders);
router.get('/admin/all', protect, authorize('Admin'), getAllOrders);
router.get('/:id', protect, getOrderById);
router.put('/:id/status', protect, authorize('Admin'), updateOrderStatus);
module.exports = router;

View File

@@ -0,0 +1,24 @@
const express = require('express');
const router = express.Router();
const {
getProducts,
getGuitarRecommendations,
getProductBySlug,
compareProducts,
createProduct,
updateProduct,
deleteProduct
} = require('../controllers/productController');
const { protect, authorize } = require('../middleware/authMiddleware');
router.get('/', getProducts);
router.post('/guitar-finder', getGuitarRecommendations);
router.post('/compare', compareProducts);
router.get('/:slug', getProductBySlug);
router.post('/', protect, authorize('Admin'), createProduct);
router.put('/:id', protect, authorize('Admin'), updateProduct);
router.delete('/:id', protect, authorize('Admin'), deleteProduct);
module.exports = router;

View File

@@ -0,0 +1,10 @@
const express = require('express');
const router = express.Router();
const { getProductReviews, createReview } = require('../controllers/reviewController');
const { protect } = require('../middleware/authMiddleware');
router.get('/product/:productId', getProductReviews);
router.post('/', protect, createReview);
module.exports = router;

View File

@@ -0,0 +1,10 @@
const express = require('express');
const router = express.Router();
const { getSettings, updateSettings } = require('../controllers/settingsController');
const { protect, authorize } = require('../middleware/authMiddleware');
router.get('/', getSettings);
router.put('/', protect, authorize('Admin'), updateSettings);
module.exports = router;

View File

@@ -0,0 +1,10 @@
const express = require('express');
const router = express.Router();
const { getSocialMedia, updateSocialMedia } = require('../controllers/socialMediaController');
const { protect, authorize } = require('../middleware/authMiddleware');
router.get('/', getSocialMedia);
router.put('/', protect, authorize('Admin'), updateSocialMedia);
module.exports = router;

View File

@@ -0,0 +1,25 @@
const express = require('express');
const router = express.Router();
const upload = require('../middleware/upload');
const { protect, authorize } = require('../middleware/authMiddleware');
const { successResponse, errorResponse } = require('../utils/apiResponse');
// Single image upload
router.post('/', protect, authorize('Admin'), upload.single('media'), (req, res) => {
if (!req.file) {
return errorResponse(res, 'Please upload a file', [], 400);
}
const fileUrl = `/uploads/${req.file.filename}`;
return successResponse(res, 'File uploaded successfully', { url: fileUrl });
});
// Multiple product images upload
router.post('/multiple', protect, authorize('Admin'), upload.array('images', 10), (req, res) => {
if (!req.files || req.files.length === 0) {
return errorResponse(res, 'Please upload at least one image file', [], 400);
}
const urls = req.files.map(file => `/uploads/${file.filename}`);
return successResponse(res, 'Images uploaded successfully', { urls });
});
module.exports = router;

View File

@@ -0,0 +1,10 @@
const express = require('express');
const router = express.Router();
const { getWishlist, toggleWishlist } = require('../controllers/wishlistController');
const { protect } = require('../middleware/authMiddleware');
router.get('/', protect, getWishlist);
router.post('/toggle', protect, toggleWishlist);
module.exports = router;

152
server/seed/seedData.js Normal file
View File

@@ -0,0 +1,152 @@
const seedUsers = [
{
name: 'Admin User',
email: 'admin@guitars.com',
password: 'Admin@123',
role: 'Admin',
phone: '+91 98765 43210',
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300'
},
{
name: 'Rahul Sharma',
email: 'john@example.com',
password: 'Customer@123',
role: 'Customer',
phone: '+91 98123 45678',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=300',
addresses: [
{
fullName: 'Rahul Sharma',
mobileNumber: '9812345678',
addressLine1: 'Flat 402, Green Glen Layout',
addressLine2: 'Outer Ring Road, Bellandur',
landmark: 'Near Sakra World Hospital',
city: 'Bengaluru',
district: 'Bengaluru Urban',
state: 'Karnataka',
postalCode: '560103',
country: 'India'
}
]
}
];
const seedCategories = [
{
name: 'Electric Guitars',
slug: 'electric-guitars',
description: 'Iconic solid-body and semi-hollow electric guitars for rock, blues, jazz, and metal.',
image: 'https://images.unsplash.com/photo-1564186763535-ebb21ef5277f?w=600',
displayOrder: 1,
isFeaturedHome: true
},
{
name: 'Acoustic Guitars',
slug: 'acoustic-guitars',
description: 'Rich sounding steel-string acoustic and acoustic-electric guitars.',
image: 'https://images.unsplash.com/photo-1525201548942-d8732f6617a0?w=600',
displayOrder: 2,
isFeaturedHome: true
},
{
name: 'Bass Guitars',
slug: 'bass-guitars',
description: '4-string, 5-string, and fretless bass guitars delivering deep low-end punch.',
image: 'https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?w=600',
displayOrder: 3,
isFeaturedHome: true
},
{
name: 'Amplifiers & Effects',
slug: 'amplifiers',
description: 'Tube, modeling amplifiers and stompboxes for studio and stage performance.',
image: 'https://images.unsplash.com/photo-1598488035139-bdbb2231ce04?w=600',
displayOrder: 4,
isFeaturedHome: true
}
];
const seedBrands = [
{ name: 'Fender', slug: 'fender', logo: 'https://images.unsplash.com/photo-1564186763535-ebb21ef5277f?w=200', description: 'Legendary manufacturer of Stratocaster and Telecaster guitars.' },
{ name: 'Gibson', slug: 'gibson', logo: 'https://images.unsplash.com/photo-1550291652-6ea9114a47b1?w=200', description: 'Historic maker of Les Paul, SG, and ES-335 electric guitars.' },
{ name: 'Ibanez', slug: 'ibanez', logo: 'https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?w=200', description: 'High-performance fast neck guitars favored by modern virtuosos.' },
{ name: 'Martin', slug: 'martin', logo: 'https://images.unsplash.com/photo-1525201548942-d8732f6617a0?w=200', description: 'The gold standard of dreadnought acoustic guitars since 1833.' },
{ name: 'Taylor', slug: 'taylor', logo: 'https://images.unsplash.com/photo-1510915361894-db8b60106cb1?w=200', description: 'Innovative high-end acoustic guitars with articulate response.' },
{ name: 'Yamaha', slug: 'yamaha', logo: 'https://images.unsplash.com/photo-1510915361894-db8b60106cb1?w=200', description: 'Precision Japanese engineering and exceptional acoustic resonance.' },
{ name: 'PRS Guitars', slug: 'prs', logo: 'https://images.unsplash.com/photo-1516924962500-2b4b3b99ea02?w=200', description: 'Flawless craftsmanship, carved tops, and versatile humbucking tone.' },
{ name: 'Boss', slug: 'boss', logo: 'https://images.unsplash.com/photo-1598488035139-bdbb2231ce04?w=200', description: 'World leader in guitar effects stompboxes and modeling amplifiers.' }
];
const seedBanners = [
{
title: 'Master Your Craft with Iconic Tone',
subtitle: 'Explore the 2026 Custom Shop Acoustic & Electric Guitar Collection in India',
desktopImage: 'https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?w=1600',
mobileImage: 'https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?w=800',
buttonText: 'Shop Guitars',
buttonLink: '/shop',
displayOrder: 1,
isActive: true
},
{
title: 'Unrivaled American & Japanese Craftsmanship',
subtitle: 'Limited Edition Les Paul & Stratocaster Releases Now In Stock',
desktopImage: 'https://images.unsplash.com/photo-1564186763535-ebb21ef5277f?w=1600',
mobileImage: 'https://images.unsplash.com/photo-1564186763535-ebb21ef5277f?w=800',
buttonText: 'Explore New Arrivals',
buttonLink: '/shop?isNewArrival=true',
displayOrder: 2,
isActive: true
}
];
const seedCoupons = [
{
code: 'WELCOME10',
discountType: 'percentage',
discountValue: 10,
minPurchaseAmount: 5000,
endDate: new Date('2028-12-31'),
isActive: true
},
{
code: 'GUITARHERO',
discountType: 'fixed',
discountValue: 5000,
minPurchaseAmount: 40000,
endDate: new Date('2028-12-31'),
isActive: true
}
];
const seedSocialMedia = [
{ platform: 'instagram', url: 'https://instagram.com', isEnabled: true },
{ platform: 'facebook', url: 'https://facebook.com', isEnabled: true },
{ platform: 'youtube', url: 'https://youtube.com', isEnabled: true },
{ platform: 'x', url: 'https://x.com', isEnabled: true }
];
const seedEmailTemplates = [
{
name: 'Order Confirmation',
subject: 'Order Confirmation #{{orderId}} - Antigravity Guitars India',
bodyHtml: `<h2>Thank you for your order, {{customerName}}!</h2><p>We are preparing your order <strong>#{{orderId}}</strong> for shipment. Total: <strong>₹{{orderTotal}}</strong>.</p>`,
placeholders: ['{{customerName}}', '{{orderId}}', '{{orderTotal}}']
},
{
name: 'Order Shipped',
subject: 'Your Guitar Order #{{orderId}} Has Shipped via Delhivery!',
bodyHtml: `<h2>Great news, {{customerName}}!</h2><p>Your order <strong>#{{orderId}}</strong> is on its way. Courier AWB Tracking Number: <strong>{{trackingNumber}}</strong>.</p>`,
placeholders: ['{{customerName}}', '{{orderId}}', '{{trackingNumber}}']
}
];
module.exports = {
seedUsers,
seedCategories,
seedBrands,
seedBanners,
seedCoupons,
seedSocialMedia,
seedEmailTemplates
};

430
server/seed/seeder.js Normal file
View File

@@ -0,0 +1,430 @@
const mongoose = require('mongoose');
const dotenv = require('dotenv');
dotenv.config();
const User = require('../models/User');
const Product = require('../models/Product');
const Category = require('../models/Category');
const Brand = require('../models/Brand');
const Banner = require('../models/Banner');
const Coupon = require('../models/Coupon');
const WebsiteSettings = require('../models/WebsiteSettings');
const SocialMedia = require('../models/SocialMedia');
const EmailTemplate = require('../models/EmailTemplate');
const Review = require('../models/Review');
const {
seedUsers,
seedCategories,
seedBrands,
seedBanners,
seedCoupons,
seedSocialMedia,
seedEmailTemplates
} = require('./seedData');
const seedDB = async () => {
try {
await mongoose.connect(process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/guitar_ecommerce');
console.log('[SEEDER]: Connected to MongoDB');
// Clear existing collections
await User.deleteMany();
await Product.deleteMany();
await Category.deleteMany();
await Brand.deleteMany();
await Banner.deleteMany();
await Coupon.deleteMany();
await WebsiteSettings.deleteMany();
await SocialMedia.deleteMany();
await EmailTemplate.deleteMany();
await Review.deleteMany();
console.log('[SEEDER]: Cleaned old collections');
// 1. Insert Users
const users = await User.create(seedUsers);
const adminUser = users.find(u => u.role === 'Admin');
const customerUser = users.find(u => u.role === 'Customer');
// 2. Insert Categories
const categories = await Category.create(seedCategories);
const catMap = {};
categories.forEach(c => { catMap[c.slug] = c._id; });
// 3. Insert Brands
const brands = await Brand.create(seedBrands);
const brandMap = {};
brands.forEach(b => { brandMap[b.slug] = b._id; });
// 4. Seed Products with INR Prices (₹) and Full Specs
const seedProducts = [
{
name: 'Fender Player II Stratocaster Electric Guitar',
slug: 'fender-player-ii-stratocaster-electric-guitar',
sku: 'GTR-2026-10001',
brand: brandMap['fender'],
category: catMap['electric-guitars'],
price: 74999,
salePrice: 69999,
discountType: 'fixed',
discountValue: 5000,
stock: 15,
description: 'The Fender Player II Stratocaster radiates authentic Fender style and modern playability. Featuring an Alder body, Maple neck with Modern C profile, Rosewood fingerboard, and Alnico V Single-Coil pickups for pristine bell-like highs.',
shortDescription: 'Modern C Maple Neck, Alnico V Pickups, Rosewood Fingerboard',
images: [
'https://images.unsplash.com/photo-1564186763535-ebb21ef5277f?w=800',
'https://images.unsplash.com/photo-1550291652-6ea9114a47b1?w=800'
],
specs: {
guitarType: 'Electric',
bodyShape: 'Stratocaster',
bodyMaterial: 'Alder',
topMaterial: 'None',
neckMaterial: 'Maple',
fingerboardMaterial: 'Rosewood',
scaleLength: '25.5"',
numberOfStrings: 6,
frets: 22,
pickupConfiguration: 'SSS',
pickupType: 'Player Series Alnico 5 Single-Coil',
bridgeType: '2-Point Synchronized Tremolo with Bent Steel Saddles',
tuningMachines: 'Standard Cast/Sealed',
handOrientation: 'Right-Handed',
color: '3-Color Sunburst',
finish: 'Gloss Polyester',
includesCase: false
},
variants: [
{ color: '3-Color Sunburst', colorHex: '#8B4513', finish: 'Gloss', sku: 'GTR-2026-10001-SB', price: 74999, salePrice: 69999, stock: 8 },
{ color: 'Polar White', colorHex: '#FFFFFF', finish: 'Gloss', sku: 'GTR-2026-10001-WH', price: 74999, salePrice: 69999, stock: 7 }
],
rating: 4.9,
numReviews: 24,
isFeatured: true,
isNewArrival: true,
isBestSeller: true
},
{
name: 'Gibson Les Paul Standard 60s Bourbon Burst',
slug: 'gibson-les-paul-standard-60s-bourbon-burst',
sku: 'GTR-2026-10002',
brand: brandMap['gibson'],
category: catMap['electric-guitars'],
price: 249999,
salePrice: 229999,
discountType: 'percentage',
discountValue: 8,
stock: 6,
description: 'The Les Paul Standard 60s has a solid mahogany body with an AA figured maple top and a slim taper 60s-style mahogany neck. Equipped with Burstbucker 61R and 61T humbuckers with Alnico 5 magnets for rich sustain.',
shortDescription: 'AA Maple Top, Mahogany Body, Burstbucker 61 Pickups',
images: [
'https://images.unsplash.com/photo-1550291652-6ea9114a47b1?w=800',
'https://images.unsplash.com/photo-1564186763535-ebb21ef5277f?w=800'
],
specs: {
guitarType: 'Electric',
bodyShape: 'Les Paul Singlecut',
bodyMaterial: 'Mahogany',
topMaterial: 'AA Figured Maple',
neckMaterial: 'Mahogany',
fingerboardMaterial: 'Indian Rosewood',
scaleLength: '24.75"',
numberOfStrings: 6,
frets: 22,
pickupConfiguration: 'HH',
pickupType: 'Burstbucker 61R / 61T Humbuckers',
bridgeType: 'ABR-1 Tune-O-Matic with Aluminum Stopbar',
tuningMachines: 'Grover Rotomatics with Kidney Buttons',
handOrientation: 'Right-Handed',
color: 'Bourbon Burst',
finish: 'Gloss Nitrocellulose Lacquer',
includesCase: true
},
rating: 5.0,
numReviews: 42,
isFeatured: true,
isBestSeller: true
},
{
name: 'PRS Custom 24 SE Bonnie Pink',
slug: 'prs-custom-24-se-bonnie-pink',
sku: 'GTR-2026-10003',
brand: brandMap['prs'],
category: catMap['electric-guitars'],
price: 84999,
salePrice: 79999,
stock: 12,
description: 'The SE Custom 24 brings the original PRS design platform to high-grade instruments. Features a maple top with flame maple veneer, mahogany back, wide thin maple neck, 24 frets, 85/15 "S" humbuckers.',
shortDescription: 'Flame Maple Veneer, 85/15 "S" Humbuckers, Bird Inlays',
images: ['https://images.unsplash.com/photo-1516924962500-2b4b3b99ea02?w=800'],
specs: {
guitarType: 'Electric',
bodyShape: 'Double Cutaway',
bodyMaterial: 'Mahogany',
topMaterial: 'Flame Maple Veneer',
neckMaterial: 'Maple',
fingerboardMaterial: 'Rosewood',
scaleLength: '25"',
numberOfStrings: 6,
frets: 24,
pickupConfiguration: 'HH',
pickupType: 'PRS 85/15 "S" Humbuckers',
bridgeType: 'PRS Patented Tremolo, Molded',
tuningMachines: 'PRS Designed Tuners',
handOrientation: 'Right-Handed',
color: 'Bonnie Pink',
finish: 'Gloss Polyurethane',
includesCase: true
},
rating: 4.8,
numReviews: 18,
isFeatured: true
},
{
name: 'Ibanez Genesis Collection RG550 Desert Sun Yellow',
slug: 'ibanez-genesis-collection-rg550-desert-sun-yellow',
sku: 'GTR-2026-10004',
brand: brandMap['ibanez'],
category: catMap['electric-guitars'],
price: 94999,
salePrice: 89999,
stock: 8,
description: 'Made in Japan, the RG550 features a Basswood body, 5-piece Maple/Walnut Wizard neck, Edge double-locking tremolo system, and V7/S1/V8 pickup combo for lightning-fast solos.',
shortDescription: 'Made in Japan, Wizard 5-pc Neck, Edge Tremolo',
images: ['https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?w=800'],
specs: {
guitarType: 'Electric',
bodyShape: 'Superstrat / RG',
bodyMaterial: 'Basswood',
neckMaterial: '5-pc Maple/Walnut',
fingerboardMaterial: 'Maple',
scaleLength: '25.5"',
numberOfStrings: 6,
frets: 24,
pickupConfiguration: 'HSH',
pickupType: 'Ibanez V7 / S1 / V8 Passives',
bridgeType: 'Edge Double-Locking Tremolo',
tuningMachines: 'Gotoh Machine Heads',
handOrientation: 'Right-Handed',
color: 'Desert Sun Yellow',
finish: 'Gloss',
includesCase: false
},
rating: 4.9,
numReviews: 15,
isNewArrival: true
},
// ACOUSTIC GUITARS
{
name: 'Martin D-28 Standard Dreadnought Acoustic Guitar',
slug: 'martin-d-28-standard-dreadnought-acoustic-guitar',
sku: 'GTR-2026-10005',
brand: brandMap['martin'],
category: catMap['acoustic-guitars'],
price: 289999,
salePrice: 269999,
stock: 5,
description: 'The Martin D-28 is the dreadnought by which all acoustic guitars are judged. Crafted with Solid Sitka Spruce top, East Indian Rosewood back and sides, giving massive projection.',
shortDescription: 'Solid Sitka Spruce Top, East Indian Rosewood Back & Sides',
images: [
'https://images.unsplash.com/photo-1525201548942-d8732f6617a0?w=800',
'https://images.unsplash.com/photo-1510915361894-db8b60106cb1?w=800'
],
specs: {
guitarType: 'Acoustic',
bodyShape: 'Dreadnought',
bodyMaterial: 'East Indian Rosewood',
topMaterial: 'Solid Sitka Spruce',
neckMaterial: 'Select Hardwood',
fingerboardMaterial: 'Ebony',
scaleLength: '25.4"',
numberOfStrings: 6,
frets: 20,
pickupConfiguration: 'None',
bridgeType: 'Ebony Modern Belly',
tuningMachines: 'Nickel Open Gear',
handOrientation: 'Right-Handed',
color: 'Natural Spruce',
finish: 'Gloss Natural',
includesCase: true
},
rating: 5.0,
numReviews: 31,
isFeatured: true,
isBestSeller: true
},
{
name: 'Taylor 214ce Grand Auditorium Acoustic-Electric',
slug: 'taylor-214ce-grand-auditorium-acoustic-electric-guitar',
sku: 'GTR-2026-10006',
brand: brandMap['taylor'],
category: catMap['acoustic-guitars'],
price: 109999,
salePrice: 99999,
stock: 9,
description: 'Combines a solid Torrefied Sitka Spruce top with layered Rosewood back and sides for rich acoustic balance. Includes Taylor ES2 electronics.',
shortDescription: 'Torrefied Spruce Top, ES2 Electronics, Venetian Cutaway',
images: ['https://images.unsplash.com/photo-1510915361894-db8b60106cb1?w=800'],
specs: {
guitarType: 'Acoustic',
bodyShape: 'Grand Auditorium Cutaway',
bodyMaterial: 'Layered Rosewood',
topMaterial: 'Solid Sitka Spruce',
neckMaterial: 'Neo-Tropical Mahogany',
fingerboardMaterial: 'West African Ebony',
scaleLength: '25.5"',
numberOfStrings: 6,
frets: 20,
pickupConfiguration: 'Taylor Expression System 2 (ES2)',
bridgeType: 'Ebony',
tuningMachines: 'Taylor Chrome Tuners',
handOrientation: 'Right-Handed',
color: 'Natural',
finish: 'Matte Finish',
includesCase: true
},
rating: 4.8,
numReviews: 19,
isNewArrival: true
},
{
name: 'Yamaha FG800 Solid Top Dreadnought Acoustic',
slug: 'yamaha-fg800-solid-top-dreadnought-acoustic-guitar',
sku: 'GTR-2026-10007',
brand: brandMap['yamaha'],
category: catMap['acoustic-guitars'],
price: 19999,
salePrice: 17999,
stock: 25,
description: 'Yamaha’s flagship entry-level acoustic with solid Sitka spruce top, nato back and sides, scalloped bracing for enhanced low-end.',
shortDescription: 'Solid Sitka Spruce Top, Scalloped Bracing',
images: ['https://images.unsplash.com/photo-1510915361894-db8b60106cb1?w=800'],
specs: {
guitarType: 'Acoustic',
bodyShape: 'Dreadnought',
bodyMaterial: 'Nato / Okoume',
topMaterial: 'Solid Sitka Spruce',
neckMaterial: 'Nato',
fingerboardMaterial: 'Rosewood',
scaleLength: '25"',
numberOfStrings: 6,
frets: 20,
pickupConfiguration: 'None',
bridgeType: 'Rosewood',
tuningMachines: 'Die-Cast Chrome',
handOrientation: 'Right-Handed',
color: 'Natural',
finish: 'Gloss',
includesCase: false
},
rating: 4.9,
numReviews: 67,
isBestSeller: true
},
// BASS GUITARS
{
name: 'Fender Player II Precision Bass Olympic White',
slug: 'fender-player-ii-precision-bass-olympic-white',
sku: 'GTR-2026-10008',
brand: brandMap['fender'],
category: catMap['bass-guitars'],
price: 79999,
salePrice: 74999,
stock: 7,
description: 'Features a punchy Player Series Split Single-Coil pickup, Modern C maple neck, 9.5"-radius rosewood fingerboard for definitive Fender low-end.',
shortDescription: 'Split Single-Coil Pickup, Modern C Neck, Long Scale',
images: ['https://images.unsplash.com/photo-1564186763535-ebb21ef5277f?w=800'],
specs: {
guitarType: 'Bass',
bodyShape: 'Precision Bass',
bodyMaterial: 'Alder',
topMaterial: 'None',
neckMaterial: 'Maple',
fingerboardMaterial: 'Rosewood',
scaleLength: '34" Long Scale',
numberOfStrings: 4,
frets: 20,
pickupConfiguration: 'Split Single-Coil',
pickupType: 'Player Series Alnico 5 Split-Coil',
bridgeType: '4-Saddle Standard',
tuningMachines: 'Standard Open-Gear',
handOrientation: 'Right-Handed',
color: 'Olympic White',
finish: 'Gloss Polyester',
includesCase: false
},
rating: 4.9,
numReviews: 11,
isNewArrival: true
},
// AMPLIFIERS & PEDALS
{
name: 'Boss Katana-100 MkII 100-watt 1x12 Combo Amp',
slug: 'boss-katana-100-mkii-combo-amplifier',
sku: 'GTR-2026-10009',
brand: brandMap['boss'],
category: catMap['amplifiers'],
price: 36999,
salePrice: 33999,
stock: 20,
description: 'Five primary amp characters with five variation options, plus 60 customizable Boss effects accessible via BOSS Tone Studio software.',
shortDescription: '100W Output, 12" Custom Speaker, 60 BOSS Effects',
images: ['https://images.unsplash.com/photo-1598488035139-bdbb2231ce04?w=800'],
specs: {
guitarType: 'Accessories',
bodyShape: 'Combo Amp',
finish: 'Black Tolex',
includesCase: false
},
rating: 4.9,
numReviews: 54,
isBestSeller: true
}
];
const insertedProducts = await Product.create(seedProducts);
console.log(`[SEEDER]: Inserted ${insertedProducts.length} realistic INR products with SKUs & specs`);
// 5. Insert Banners, Coupons, SocialMedia, EmailTemplates, WebsiteSettings
await Banner.create(seedBanners);
await Coupon.create(seedCoupons);
await SocialMedia.create(seedSocialMedia);
await EmailTemplate.create(seedEmailTemplates);
await WebsiteSettings.create({});
// 6. Demo Reviews
await Review.create([
{
user: customerUser._id,
product: insertedProducts[0]._id,
rating: 5,
title: 'Sensational playability!',
comment: 'The Player II Stratocaster neck feels like butter. Clean tones ring out beautifully.'
},
{
user: customerUser._id,
product: insertedProducts[1]._id,
rating: 5,
title: 'Dream guitar achieved in India!',
comment: 'The Les Paul Standard 60s in Bourbon Burst has unbeatable sustain. Fast delivery in Bengaluru!'
}
]);
console.log('====================================================');
console.log('[SEED COMPLETE]: Database fully populated!');
console.log('Admin Email: admin@guitars.com | Password: Admin@123');
console.log('Customer Email: john@example.com | Password: Customer@123');
console.log('====================================================');
process.exit(0);
} catch (error) {
console.error('[SEED ERROR]:', error);
process.exit(1);
}
};
seedDB();

24
server/server.js Normal file
View File

@@ -0,0 +1,24 @@
const dotenv = require('dotenv');
dotenv.config();
const app = require('./app');
const connectDB = require('./config/db');
const PORT = process.env.PORT || 5000;
// Connect to MongoDB
connectDB();
const server = app.listen(PORT, () => {
console.log(`====================================================`);
console.log(`[GUITAR ECOMMERCE SERVER RUNNING]`);
console.log(`Port: ${PORT}`);
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
console.log(`====================================================`);
});
// Handle unhandled promise rejections
process.on('unhandledRejection', (err) => {
console.error(`Unhandled Rejection Error: ${err.message}`);
});

View File

@@ -0,0 +1,24 @@
/**
* Standardized API Response helper functions
*/
const successResponse = (res, message = 'Success', data = {}, statusCode = 200) => {
return res.status(statusCode).json({
success: true,
message,
data
});
};
const errorResponse = (res, message = 'Internal Server Error', errors = [], statusCode = 500) => {
return res.status(statusCode).json({
success: false,
message,
errors: Array.isArray(errors) ? errors : [errors]
});
};
module.exports = {
successResponse,
errorResponse
};

View File

@@ -0,0 +1,52 @@
const nodemailer = require('nodemailer');
const EmailTemplate = require('../models/EmailTemplate');
const sendEmail = async ({ templateName, to, variables = {} }) => {
try {
const template = await EmailTemplate.findOne({ name: templateName });
let subject = variables.subject || 'Antigravity Guitars Update';
let htmlContent = `<p>Thank you for contacting Antigravity Guitars!</p>`;
if (template) {
subject = template.subject;
htmlContent = template.bodyHtml;
// Replace placeholders e.g. {{customerName}} -> variables.customerName
Object.keys(variables).forEach((key) => {
const regex = new RegExp(`{{${key}}}`, 'g');
subject = subject.replace(regex, variables[key] || '');
htmlContent = htmlContent.replace(regex, variables[key] || '');
});
}
const transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST || 'smtp.mailtrap.io',
port: process.env.EMAIL_PORT || 2525,
auth: {
user: process.env.EMAIL_USER || '',
pass: process.env.EMAIL_PASS || ''
}
});
const mailOptions = {
from: process.env.EMAIL_FROM || '"Antigravity Guitars" <noreply@guitars.com>',
to,
subject,
html: htmlContent
};
if (process.env.NODE_ENV !== 'test' && process.env.EMAIL_USER) {
await transporter.sendMail(mailOptions);
console.log(`[Email Sent]: Template "${templateName}" to ${to}`);
} else {
console.log(`[Email Logged (Mock)] To: ${to} | Subject: ${subject}`);
}
return true;
} catch (error) {
console.error(`[Email Sender Exception]: ${error.message}`);
return false;
}
};
module.exports = { sendEmail };

12
server/utils/jwt.js Normal file
View File

@@ -0,0 +1,12 @@
const jwt = require('jsonwebtoken');
const generateToken = (userId, role) => {
return jwt.sign(
{ id: userId, role },
process.env.JWT_SECRET || 'guitar_ecommerce_super_secret_jwt_key_2026',
{ expiresIn: process.env.JWT_EXPIRE || '30d' }
);
};
module.exports = { generateToken };