Initial commit
This commit is contained in:
13
server/.env.example
Normal file
13
server/.env.example
Normal 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
8
server/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
.next/
|
||||
dist/
|
||||
build/
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
*.log
|
||||
77
server/app.js
Normal file
77
server/app.js
Normal 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
15
server/config/db.js
Normal 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;
|
||||
|
||||
128
server/controllers/authController.js
Normal file
128
server/controllers/authController.js
Normal 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
|
||||
};
|
||||
|
||||
44
server/controllers/bannerController.js
Normal file
44
server/controllers/bannerController.js
Normal 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 };
|
||||
|
||||
44
server/controllers/brandController.js
Normal file
44
server/controllers/brandController.js
Normal 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 };
|
||||
|
||||
150
server/controllers/cartController.js
Normal file
150
server/controllers/cartController.js
Normal 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 };
|
||||
|
||||
44
server/controllers/categoryController.js
Normal file
44
server/controllers/categoryController.js
Normal 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 };
|
||||
|
||||
34
server/controllers/contactController.js
Normal file
34
server/controllers/contactController.js
Normal 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 };
|
||||
|
||||
34
server/controllers/couponController.js
Normal file
34
server/controllers/couponController.js
Normal 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 };
|
||||
|
||||
24
server/controllers/emailTemplateController.js
Normal file
24
server/controllers/emailTemplateController.js
Normal 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 };
|
||||
|
||||
85
server/controllers/inventoryController.js
Normal file
85
server/controllers/inventoryController.js
Normal 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 };
|
||||
|
||||
212
server/controllers/orderController.js
Normal file
212
server/controllers/orderController.js
Normal 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
|
||||
};
|
||||
|
||||
235
server/controllers/productController.js
Normal file
235
server/controllers/productController.js
Normal 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
|
||||
};
|
||||
|
||||
50
server/controllers/reviewController.js
Normal file
50
server/controllers/reviewController.js
Normal 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 };
|
||||
|
||||
32
server/controllers/settingsController.js
Normal file
32
server/controllers/settingsController.js
Normal 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 };
|
||||
|
||||
33
server/controllers/socialMediaController.js
Normal file
33
server/controllers/socialMediaController.js
Normal 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 };
|
||||
|
||||
41
server/controllers/wishlistController.js
Normal file
41
server/controllers/wishlistController.js
Normal 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 };
|
||||
|
||||
43
server/middleware/authMiddleware.js
Normal file
43
server/middleware/authMiddleware.js
Normal 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 };
|
||||
|
||||
31
server/middleware/errorHandler.js
Normal file
31
server/middleware/errorHandler.js
Normal 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;
|
||||
|
||||
40
server/middleware/upload.js
Normal file
40
server/middleware/upload.js
Normal 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
16
server/models/Banner.js
Normal 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
15
server/models/Brand.js
Normal 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
20
server/models/Cart.js
Normal 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
16
server/models/Category.js
Normal 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);
|
||||
13
server/models/ContactMessage.js
Normal file
13
server/models/ContactMessage.js
Normal 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
17
server/models/Coupon.js
Normal 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);
|
||||
|
||||
11
server/models/EmailTemplate.js
Normal file
11
server/models/EmailTemplate.js
Normal 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);
|
||||
|
||||
19
server/models/InventoryHistory.js
Normal file
19
server/models/InventoryHistory.js
Normal 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
85
server/models/Order.js
Normal 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
105
server/models/Product.js
Normal 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
17
server/models/Review.js
Normal 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);
|
||||
|
||||
10
server/models/SocialMedia.js
Normal file
10
server/models/SocialMedia.js
Normal 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
47
server/models/User.js
Normal 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);
|
||||
34
server/models/WebsiteSettings.js
Normal file
34
server/models/WebsiteSettings.js
Normal 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);
|
||||
9
server/models/Wishlist.js
Normal file
9
server/models/Wishlist.js
Normal 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
1821
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
server/package.json
Normal file
31
server/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
|
||||
12
server/routes/authRoutes.js
Normal file
12
server/routes/authRoutes.js
Normal 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;
|
||||
|
||||
12
server/routes/bannerRoutes.js
Normal file
12
server/routes/bannerRoutes.js
Normal 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;
|
||||
|
||||
12
server/routes/brandRoutes.js
Normal file
12
server/routes/brandRoutes.js
Normal 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;
|
||||
|
||||
12
server/routes/cartRoutes.js
Normal file
12
server/routes/cartRoutes.js
Normal 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;
|
||||
|
||||
12
server/routes/categoryRoutes.js
Normal file
12
server/routes/categoryRoutes.js
Normal 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;
|
||||
|
||||
11
server/routes/contactRoutes.js
Normal file
11
server/routes/contactRoutes.js
Normal 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;
|
||||
|
||||
11
server/routes/couponRoutes.js
Normal file
11
server/routes/couponRoutes.js
Normal 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;
|
||||
|
||||
10
server/routes/emailTemplateRoutes.js
Normal file
10
server/routes/emailTemplateRoutes.js
Normal 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;
|
||||
|
||||
11
server/routes/inventoryRoutes.js
Normal file
11
server/routes/inventoryRoutes.js
Normal 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;
|
||||
|
||||
19
server/routes/orderRoutes.js
Normal file
19
server/routes/orderRoutes.js
Normal 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;
|
||||
|
||||
24
server/routes/productRoutes.js
Normal file
24
server/routes/productRoutes.js
Normal 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;
|
||||
|
||||
10
server/routes/reviewRoutes.js
Normal file
10
server/routes/reviewRoutes.js
Normal 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;
|
||||
|
||||
10
server/routes/settingsRoutes.js
Normal file
10
server/routes/settingsRoutes.js
Normal 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;
|
||||
|
||||
10
server/routes/socialMediaRoutes.js
Normal file
10
server/routes/socialMediaRoutes.js
Normal 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;
|
||||
|
||||
25
server/routes/uploadRoutes.js
Normal file
25
server/routes/uploadRoutes.js
Normal 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;
|
||||
10
server/routes/wishlistRoutes.js
Normal file
10
server/routes/wishlistRoutes.js
Normal 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
152
server/seed/seedData.js
Normal 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
430
server/seed/seeder.js
Normal 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
24
server/server.js
Normal 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}`);
|
||||
});
|
||||
|
||||
24
server/utils/apiResponse.js
Normal file
24
server/utils/apiResponse.js
Normal 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
|
||||
};
|
||||
|
||||
52
server/utils/emailSender.js
Normal file
52
server/utils/emailSender.js
Normal 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
12
server/utils/jwt.js
Normal 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 };
|
||||
|
||||
Reference in New Issue
Block a user