UNPKG

ajinkya-mhetre-mern

Version:

A MERN starter with frontend and backend folders

1,295 lines (1,111 loc) 32 kB
// ===== PROJECT STRUCTURE ===== /* farm-produce-backend/ ├── src/ │ ├── config/ │ │ ├── database.js │ │ └── cloudinary.js │ ├── controllers/ │ │ ├── authController.js │ │ ├── adminController.js │ │ ├── farmerController.js │ │ └── customerController.js │ ├── middleware/ │ │ ├── auth.js │ │ ├── roleCheck.js │ │ └── upload.js │ ├── models/ │ │ ├── User.js │ │ ├── Product.js │ │ └── Order.js │ ├── routes/ │ │ ├── auth.js │ │ ├── admin.js │ │ ├── farmer.js │ │ └── customer.js │ ├── utils/ │ │ └── helpers.js │ └── app.js ├── uploads/ ├── .env ├── .gitignore ├── package.json └── server.js */ // ===== package.json ===== { "name": "farm-produce-backend", "version": "1.0.0", "description": "Backend for Farm Produce Platform", "main": "server.js", "scripts": { "start": "node server.js", "dev": "nodemon server.js" }, "dependencies": { "express": "^4.18.2", "mongoose": "^7.5.0", "bcryptjs": "^2.4.3", "jsonwebtoken": "^9.0.2", "multer": "^1.4.5-lts.1", "cors": "^2.8.5", "dotenv": "^16.3.1", "helmet": "^7.0.0", "express-rate-limit": "^6.10.0", "cloudinary": "^1.40.0", "socket.io": "^4.7.2" }, "devDependencies": { "nodemon": "^3.0.1" } } // ===== .env ===== /* NODE_ENV=development PORT=5000 MONGODB_URI=mongodb://localhost:27017/farm-produce JWT_SECRET=your_jwt_secret_key_here JWT_EXPIRE=30d CLOUDINARY_CLOUD_NAME=your_cloud_name CLOUDINARY_API_KEY=your_api_key CLOUDINARY_API_SECRET=your_api_secret */ // ===== server.js ===== const express = require('express'); const http = require('http'); const socketIo = require('socket.io'); const dotenv = require('dotenv'); const connectDB = require('./src/config/database'); const app = require('./src/app'); dotenv.config(); // Connect to database connectDB(); const server = http.createServer(app); const io = socketIo(server, { cors: { origin: process.env.CLIENT_URL || "http://localhost:3000", methods: ["GET", "POST"] } }); // Socket.IO for real-time order tracking io.on('connection', (socket) => { console.log('User connected:', socket.id); socket.on('join-room', (userId) => { socket.join(userId); }); socket.on('disconnect', () => { console.log('User disconnected:', socket.id); }); }); // Make io accessible to routes app.set('socketio', io); const PORT = process.env.PORT || 5000; server.listen(PORT, () => { console.log(`Server running on port ${PORT}`); }); // ===== src/config/database.js ===== const mongoose = require('mongoose'); const connectDB = async () => { try { const conn = await mongoose.connect(process.env.MONGODB_URI); console.log(`MongoDB Connected: ${conn.connection.host}`); } catch (error) { console.error('Database connection error:', error); process.exit(1); } }; module.exports = connectDB; // ===== src/config/cloudinary.js ===== const cloudinary = require('cloudinary').v2; cloudinary.config({ cloud_name: process.env.CLOUDINARY_CLOUD_NAME, api_key: process.env.CLOUDINARY_API_KEY, api_secret: process.env.CLOUDINARY_API_SECRET, }); module.exports = cloudinary; // ===== src/models/User.js ===== const mongoose = require('mongoose'); const bcrypt = require('bcryptjs'); const userSchema = new mongoose.Schema({ name: { type: String, required: [true, 'Name is required'], trim: true }, email: { type: String, required: [true, 'Email is required'], unique: true, lowercase: true }, password: { type: String, required: [true, 'Password is required'], minlength: 6 }, phone: { type: String, required: [true, 'Phone number is required'] }, role: { type: String, enum: ['farmer', 'customer', 'admin'], required: true }, isVerified: { type: Boolean, default: function() { return this.role === 'customer' || this.role === 'admin'; } }, address: { street: String, city: String, state: String, zipCode: String }, farmDetails: { farmName: String, farmSize: String, farmLocation: String, certifications: [String] } }, { timestamps: true }); userSchema.pre('save', async function(next) { if (!this.isModified('password')) return next(); this.password = await bcrypt.hash(this.password, 12); next(); }); userSchema.methods.comparePassword = async function(candidatePassword) { return await bcrypt.compare(candidatePassword, this.password); }; module.exports = mongoose.model('User', userSchema); // ===== src/models/Product.js ===== const mongoose = require('mongoose'); const productSchema = new mongoose.Schema({ name: { type: String, required: [true, 'Product name is required'], trim: true }, description: { type: String, required: [true, 'Description is required'] }, category: { type: String, enum: ['fruit', 'vegetable'], required: [true, 'Category is required'] }, price: { type: Number, required: [true, 'Price is required'], min: 0 }, unit: { type: String, enum: ['kg', 'gram', 'piece', 'dozen'], default: 'kg' }, availableQuantity: { type: Number, required: [true, 'Available quantity is required'], min: 0 }, images: [{ url: String, public_id: String }], farmer: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, isActive: { type: Boolean, default: true }, harvestDate: { type: Date }, expiryDate: { type: Date } }, { timestamps: true }); productSchema.index({ farmer: 1, category: 1 }); productSchema.index({ name: 'text', description: 'text' }); module.exports = mongoose.model('Product', productSchema); // ===== src/models/Order.js ===== const mongoose = require('mongoose'); const orderItemSchema = new mongoose.Schema({ product: { type: mongoose.Schema.Types.ObjectId, ref: 'Product', required: true }, quantity: { type: Number, required: true, min: 1 }, price: { type: Number, required: true }, farmer: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true } }); const orderSchema = new mongoose.Schema({ customer: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, items: [orderItemSchema], totalAmount: { type: Number, required: true }, status: { type: String, enum: ['placed', 'processing', 'dispatched', 'delivered', 'cancelled'], default: 'placed' }, shippingAddress: { street: { type: String, required: true }, city: { type: String, required: true }, state: { type: String, required: true }, zipCode: { type: String, required: true } }, paymentStatus: { type: String, enum: ['pending', 'paid', 'failed'], default: 'pending' }, trackingNumber: String, statusHistory: [{ status: String, timestamp: { type: Date, default: Date.now }, note: String }] }, { timestamps: true }); orderSchema.index({ customer: 1, status: 1 }); orderSchema.index({ 'items.farmer': 1 }); module.exports = mongoose.model('Order', orderSchema); // ===== src/middleware/auth.js ===== const jwt = require('jsonwebtoken'); const User = require('../models/User'); const auth = async (req, res, next) => { try { const token = req.header('Authorization')?.replace('Bearer ', ''); if (!token) { return res.status(401).json({ message: 'Access denied. No token provided.' }); } const decoded = jwt.verify(token, process.env.JWT_SECRET); const user = await User.findById(decoded.id).select('-password'); if (!user) { return res.status(401).json({ message: 'Invalid token.' }); } req.user = user; next(); } catch (error) { res.status(401).json({ message: 'Invalid token.' }); } }; module.exports = auth; // ===== src/middleware/roleCheck.js ===== const checkRole = (...roles) => { return (req, res, next) => { if (!roles.includes(req.user.role)) { return res.status(403).json({ message: 'Access denied. Insufficient permissions.' }); } next(); }; }; const checkVerified = (req, res, next) => { if (req.user.role === 'farmer' && !req.user.isVerified) { return res.status(403).json({ message: 'Account not verified. Please wait for admin approval.' }); } next(); }; module.exports = { checkRole, checkVerified }; // ===== src/middleware/upload.js ===== const multer = require('multer'); const path = require('path'); const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, 'uploads/'); }, filename: (req, file, cb) => { cb(null, `${Date.now()}-${Math.round(Math.random() * 1E9)}${path.extname(file.originalname)}`); } }); const fileFilter = (req, file, cb) => { if (file.mimetype.startsWith('image/')) { cb(null, true); } else { cb(new Error('Only image files are allowed!'), false); } }; const upload = multer({ storage, fileFilter, limits: { fileSize: 5 * 1024 * 1024 } // 5MB limit }); module.exports = upload; // ===== src/controllers/authController.js ===== const jwt = require('jsonwebtoken'); const User = require('../models/User'); const generateToken = (id) => { return jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: process.env.JWT_EXPIRE }); }; const register = async (req, res) => { try { const { name, email, password, phone, role, address, farmDetails } = req.body; const existingUser = await User.findOne({ email }); if (existingUser) { return res.status(400).json({ message: 'User already exists' }); } const userData = { name, email, password, phone, role, address }; if (role === 'farmer' && farmDetails) { userData.farmDetails = farmDetails; } const user = await User.create(userData); const token = generateToken(user._id); res.status(201).json({ success: true, message: role === 'farmer' ? 'Registration successful. Account pending verification.' : 'Registration successful', token, user: { id: user._id, name: user.name, email: user.email, role: user.role, isVerified: user.isVerified } }); } catch (error) { res.status(500).json({ message: error.message }); } }; const login = async (req, res) => { try { const { email, password } = req.body; const user = await User.findOne({ email }); if (!user || !(await user.comparePassword(password))) { return res.status(401).json({ message: 'Invalid credentials' }); } const token = generateToken(user._id); res.json({ success: true, message: 'Login successful', token, user: { id: user._id, name: user.name, email: user.email, role: user.role, isVerified: user.isVerified } }); } catch (error) { res.status(500).json({ message: error.message }); } }; const getProfile = async (req, res) => { try { res.json({ success: true, user: req.user }); } catch (error) { res.status(500).json({ message: error.message }); } }; module.exports = { register, login, getProfile }; // ===== src/controllers/adminController.js ===== const User = require('../models/User'); const Product = require('../models/Product'); const Order = require('../models/Order'); const getPendingFarmers = async (req, res) => { try { const farmers = await User.find({ role: 'farmer', isVerified: false }).select('-password'); res.json({ success: true, farmers }); } catch (error) { res.status(500).json({ message: error.message }); } }; const verifyFarmer = async (req, res) => { try { const { farmerId } = req.params; const { action } = req.body; // 'approve' or 'reject' const farmer = await User.findById(farmerId); if (!farmer || farmer.role !== 'farmer') { return res.status(404).json({ message: 'Farmer not found' }); } if (action === 'approve') { farmer.isVerified = true; await farmer.save(); res.json({ success: true, message: 'Farmer verified successfully' }); } else if (action === 'reject') { await User.findByIdAndDelete(farmerId); res.json({ success: true, message: 'Farmer application rejected' }); } else { res.status(400).json({ message: 'Invalid action' }); } } catch (error) { res.status(500).json({ message: error.message }); } }; const getDashboardStats = async (req, res) => { try { const totalUsers = await User.countDocuments(); const totalFarmers = await User.countDocuments({ role: 'farmer', isVerified: true }); const totalCustomers = await User.countDocuments({ role: 'customer' }); const pendingFarmers = await User.countDocuments({ role: 'farmer', isVerified: false }); const totalProducts = await Product.countDocuments(); const totalOrders = await Order.countDocuments(); res.json({ success: true, stats: { totalUsers, totalFarmers, totalCustomers, pendingFarmers, totalProducts, totalOrders } }); } catch (error) { res.status(500).json({ message: error.message }); } }; const getAllUsers = async (req, res) => { try { const users = await User.find().select('-password'); res.json({ success: true, users }); } catch (error) { res.status(500).json({ message: error.message }); } }; module.exports = { getPendingFarmers, verifyFarmer, getDashboardStats, getAllUsers }; // ===== src/controllers/farmerController.js ===== const Product = require('../models/Product'); const Order = require('../models/Order'); const cloudinary = require('../config/cloudinary'); const fs = require('fs'); const addProduct = async (req, res) => { try { const { name, description, category, price, unit, availableQuantity, harvestDate, expiryDate } = req.body; const productData = { name, description, category, price: parseFloat(price), unit, availableQuantity: parseInt(availableQuantity), farmer: req.user._id, harvestDate: harvestDate ? new Date(harvestDate) : undefined, expiryDate: expiryDate ? new Date(expiryDate) : undefined }; // Handle image uploads if (req.files && req.files.length > 0) { const imageUploadPromises = req.files.map(async (file) => { const result = await cloudinary.uploader.upload(file.path, { folder: 'farm-products', transformation: [ { width: 800, height: 600, crop: 'fill' } ] }); // Delete local file after upload fs.unlinkSync(file.path); return { url: result.secure_url, public_id: result.public_id }; }); productData.images = await Promise.all(imageUploadPromises); } const product = await Product.create(productData); await product.populate('farmer', 'name email'); res.status(201).json({ success: true, message: 'Product added successfully', product }); } catch (error) { // Clean up uploaded files if error occurs if (req.files) { req.files.forEach(file => { if (fs.existsSync(file.path)) { fs.unlinkSync(file.path); } }); } res.status(500).json({ message: error.message }); } }; const getMyProducts = async (req, res) => { try { const products = await Product.find({ farmer: req.user._id }) .populate('farmer', 'name email') .sort({ createdAt: -1 }); res.json({ success: true, products }); } catch (error) { res.status(500).json({ message: error.message }); } }; const updateProduct = async (req, res) => { try { const { productId } = req.params; const updates = req.body; const product = await Product.findOne({ _id: productId, farmer: req.user._id }); if (!product) { return res.status(404).json({ message: 'Product not found' }); } Object.keys(updates).forEach(key => { if (updates[key] !== undefined) { product[key] = updates[key]; } }); await product.save(); await product.populate('farmer', 'name email'); res.json({ success: true, message: 'Product updated successfully', product }); } catch (error) { res.status(500).json({ message: error.message }); } }; const deleteProduct = async (req, res) => { try { const { productId } = req.params; const product = await Product.findOne({ _id: productId, farmer: req.user._id }); if (!product) { return res.status(404).json({ message: 'Product not found' }); } // Delete images from cloudinary if (product.images && product.images.length > 0) { const deletePromises = product.images.map(image => cloudinary.uploader.destroy(image.public_id) ); await Promise.all(deletePromises); } await Product.findByIdAndDelete(productId); res.json({ success: true, message: 'Product deleted successfully' }); } catch (error) { res.status(500).json({ message: error.message }); } }; const getMyOrders = async (req, res) => { try { const orders = await Order.find({ 'items.farmer': req.user._id }) .populate('customer', 'name email phone') .populate('items.product', 'name images') .sort({ createdAt: -1 }); res.json({ success: true, orders }); } catch (error) { res.status(500).json({ message: error.message }); } }; const updateOrderStatus = async (req, res) => { try { const { orderId } = req.params; const { status, note } = req.body; const order = await Order.findOne({ _id: orderId, 'items.farmer': req.user._id }); if (!order) { return res.status(404).json({ message: 'Order not found' }); } order.status = status; order.statusHistory.push({ status, note: note || `Order ${status}`, timestamp: new Date() }); await order.save(); // Emit real-time update const io = req.app.get('socketio'); io.to(order.customer.toString()).emit('orderStatusUpdate', { orderId: order._id, status: order.status, timestamp: new Date() }); res.json({ success: true, message: 'Order status updated successfully', order }); } catch (error) { res.status(500).json({ message: error.message }); } }; const getDashboardStats = async (req, res) => { try { const totalProducts = await Product.countDocuments({ farmer: req.user._id }); const activeProducts = await Product.countDocuments({ farmer: req.user._id, isActive: true }); const totalOrders = await Order.countDocuments({ 'items.farmer': req.user._id }); const pendingOrders = await Order.countDocuments({ 'items.farmer': req.user._id, status: { $in: ['placed', 'processing'] } }); res.json({ success: true, stats: { totalProducts, activeProducts, totalOrders, pendingOrders } }); } catch (error) { res.status(500).json({ message: error.message }); } }; module.exports = { addProduct, getMyProducts, updateProduct, deleteProduct, getMyOrders, updateOrderStatus, getDashboardStats }; // ===== src/controllers/customerController.js ===== const Product = require('../models/Product'); const Order = require('../models/Order'); const User = require('../models/User'); const getAllProducts = async (req, res) => { try { const { category, search, page = 1, limit = 12, sortBy = 'createdAt' } = req.query; const query = { isActive: true, availableQuantity: { $gt: 0 } }; if (category) { query.category = category; } if (search) { query.$text = { $search: search }; } const skip = (page - 1) * limit; const products = await Product.find(query) .populate('farmer', 'name farmDetails.farmName') .sort({ [sortBy]: -1 }) .limit(limit * 1) .skip(skip); const total = await Product.countDocuments(query); res.json({ success: true, products, pagination: { page: parseInt(page), pages: Math.ceil(total / limit), total } }); } catch (error) { res.status(500).json({ message: error.message }); } }; const getProduct = async (req, res) => { try { const { productId } = req.params; const product = await Product.findById(productId) .populate('farmer', 'name email phone farmDetails'); if (!product) { return res.status(404).json({ message: 'Product not found' }); } res.json({ success: true, product }); } catch (error) { res.status(500).json({ message: error.message }); } }; const placeOrder = async (req, res) => { try { const { items, shippingAddress } = req.body; let totalAmount = 0; const orderItems = []; // Validate and calculate order for (const item of items) { const product = await Product.findById(item.productId); if (!product) { return res.status(404).json({ message: `Product ${item.productId} not found` }); } if (product.availableQuantity < item.quantity) { return res.status(400).json({ message: `Insufficient stock for ${product.name}. Available: ${product.availableQuantity}` }); } const itemTotal = product.price * item.quantity; totalAmount += itemTotal; orderItems.push({ product: product._id, quantity: item.quantity, price: product.price, farmer: product.farmer }); // Update product quantity product.availableQuantity -= item.quantity; await product.save(); } const order = await Order.create({ customer: req.user._id, items: orderItems, totalAmount, shippingAddress, statusHistory: [{ status: 'placed', timestamp: new Date(), note: 'Order placed successfully' }] }); await order.populate([ { path: 'customer', select: 'name email' }, { path: 'items.product', select: 'name images' }, { path: 'items.farmer', select: 'name' } ]); res.status(201).json({ success: true, message: 'Order placed successfully', order }); } catch (error) { res.status(500).json({ message: error.message }); } }; const getMyOrders = async (req, res) => { try { const orders = await Order.find({ customer: req.user._id }) .populate('items.product', 'name images') .populate('items.farmer', 'name') .sort({ createdAt: -1 }); res.json({ success: true, orders }); } catch (error) { res.status(500).json({ message: error.message }); } }; const getOrder = async (req, res) => { try { const { orderId } = req.params; const order = await Order.findOne({ _id: orderId, customer: req.user._id }) .populate('items.product', 'name images') .populate('items.farmer', 'name phone'); if (!order) { return res.status(404).json({ message: 'Order not found' }); } res.json({ success: true, order }); } catch (error) { res.status(500).json({ message: error.message }); } }; const cancelOrder = async (req, res) => { try { const { orderId } = req.params; const order = await Order.findOne({ _id: orderId, customer: req.user._id }).populate('items.product'); if (!order) { return res.status(404).json({ message: 'Order not found' }); } if (order.status !== 'placed') { return res.status(400).json({ message: 'Order cannot be cancelled at this stage' }); } // Restore product quantities for (const item of order.items) { await Product.findByIdAndUpdate(item.product._id, { $inc: { availableQuantity: item.quantity } }); } order.status = 'cancelled'; order.statusHistory.push({ status: 'cancelled', timestamp: new Date(), note: 'Order cancelled by customer' }); await order.save(); res.json({ success: true, message: 'Order cancelled successfully' }); } catch (error) { res.status(500).json({ message: error.message }); } }; module.exports = { getAllProducts, getProduct, placeOrder, getMyOrders, getOrder, cancelOrder }; // ===== src/routes/auth.js ===== const express = require('express'); const router = express.Router(); const { register, login, getProfile } = require('../controllers/authController'); const auth = require('../middleware/auth'); router.post('/register', register); router.post('/login', login); router.get('/profile', auth, getProfile); module.exports = router; // ===== src/routes/admin.js ===== const express = require('express'); const router = express.Router(); const { getPendingFarmers, verifyFarmer, getDashboardStats, getAllUsers } = require('../controllers/adminController'); const auth = require('../middleware/auth'); const { checkRole } = require('../middleware/roleCheck'); // Apply middleware to all admin routes router.use(auth); router.use(checkRole('admin')); router.get('/dashboard-stats', getDashboardStats); router.get('/pending-farmers', getPendingFarmers); router.put('/verify-farmer/:farmerId', verifyFarmer); router.get('/users', getAllUsers); module.exports = router; // ===== src/routes/farmer.js ===== const express = require('express'); const router = express.Router(); const { addProduct, getMyProducts, updateProduct, deleteProduct, getMyOrders, updateOrderStatus, getDashboardStats } = require('../controllers/farmerController'); const auth = require('../middleware/auth'); const { checkRole, checkVerified } = require('../middleware/roleCheck'); const upload = require('../middleware/upload'); // Apply middleware to all farmer routes router.use(auth); router.use(checkRole('farmer')); router.use(checkVerified); // Dashboard router.get('/dashboard-stats', getDashboardStats); // Product management router.post('/products', upload.array('images', 5), addProduct); router.get('/products', getMyProducts); router.put('/products/:productId', updateProduct); router.delete('/products/:productId', deleteProduct); // Order management router.get('/orders', getMyOrders); router.put('/orders/:orderId/status', updateOrderStatus); module.exports = router; // ===== src/routes/customer.js ===== const express = require('express'); const router = express.Router(); const { getAllProducts, getProduct, placeOrder, getMyOrders, getOrder, cancelOrder } = require('../controllers/customerController'); const auth = require('../middleware/auth'); const { checkRole } = require('../middleware/roleCheck'); // Public routes (no auth required) router.get('/products', getAllProducts); router.get('/products/:productId', getProduct); // Protected customer routes router.use(auth); router.use(checkRole('customer')); router.post('/orders', placeOrder); router.get('/orders', getMyOrders); router.get('/orders/:orderId', getOrder); router.put('/orders/:orderId/cancel', cancelOrder); module.exports = router; // ===== src/utils/helpers.js ===== const generateTrackingNumber = () => { const timestamp = Date.now().toString(36).toUpperCase(); const random = Math.random().toString(36).substr(2, 5).toUpperCase(); return `FP${timestamp}${random}`; }; const formatPrice = (price) => { return new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR' }).format(price); }; const calculateDeliveryDate = (days = 3) => { const date = new Date(); date.setDate(date.getDate() + days); return date; }; module.exports = { generateTrackingNumber, formatPrice, calculateDeliveryDate }; // ===== src/app.js ===== const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const rateLimit = require('express-rate-limit'); const path = require('path'); // Import routes const authRoutes = require('./routes/auth'); const adminRoutes = require('./routes/admin'); const farmerRoutes = require('./routes/farmer'); const customerRoutes = require('./routes/customer'); const app = express(); // Security middleware app.use(helmet()); app.use(cors({ origin: process.env.CLIENT_URL || "http://localhost:3000", credentials: true })); // Rate limiting const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100 // limit each IP to 100 requests per windowMs }); app.use('/api/', limiter); // Body parsing middleware app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); // Static files app.use('/uploads', express.static(path.join(__dirname, '../uploads'))); // API routes app.use('/api/auth', authRoutes); app.use('/api/admin', adminRoutes); app.use('/api/farmer', farmerRoutes); app.use('/api/customer', customerRoutes); // Health check endpoint app.get('/api/health', (req, res) => { res.json({ status: 'OK', timestamp: new Date().toISOString(), uptime: process.uptime() }); }); // 404 handler app.use('*', (req, res) => { res.status(404).json({ message: 'Route not found' }); }); // Global error handler app.use((error, req, res, next) => { console.error('Error:', error); if (error.name === 'ValidationError') { const errors = Object.values(error.errors).map(err => err.message); return res.status(400).json({ message: 'Validation Error', errors }); } if (error.code === 11000) { const field = Object.keys(error.keyValue)[0]; return res.status(400).json({ message: `${field} already exists` }); } res.status(500).json({ message: process.env.NODE_ENV === 'production' ? 'Internal server error' : error.message }); }); module.exports = app; // ===== .gitignore ===== /* node_modules/ uploads/ .env .env.local .env.production .DS_Store *.log dist/ build/ coverage/ .nyc_output/ */