E-Commerce-Module/backend/src/index.js

416 lines
No EOL
14 KiB
JavaScript

const express = require('express');
const multer = require('multer');
const path = require('path');
const cors = require('cors');
const morgan = require('morgan');
const config = require('./config');
const { query, pool } = require('./db')
const authMiddleware = require('./middleware/auth');
const adminAuthMiddleware = require('./middleware/adminAuth');
const settingsAdminRoutes = require('./routes/settingsAdmin');
const SystemSettings = require('./models/SystemSettings');
const fs = require('fs');
// services
const notificationService = require('./services/notificationService');
const siteMapService = require('./services/sitemapService');
const emailService = require('./services/emailService');
const storageService = require('./services/storageService');
// routes
const stripePaymentRoutes = require('./routes/stripePayment');
const productRoutes = require('./routes/products');
const authRoutes = require('./routes/auth');
const cartRoutes = require('./routes/cart');
const productAdminRoutes = require('./routes/productAdmin');
const categoryAdminRoutes = require('./routes/categoryAdmin');
const usersAdminRoutes = require('./routes/userAdmin');
const ordersAdminRoutes = require('./routes/orderAdmin');
const couponsAdminRoutes = require('./routes/couponAdmin');
const userOrdersRoutes = require('./routes/userOrders');
const shippingRoutes = require('./routes/shipping');
const blogRoutes = require('./routes/blog');
const blogAdminRoutes = require('./routes/blogAdmin');
const blogCommentsAdminRoutes = require('./routes/blogCommentsAdmin');
const productReviewsRoutes = require('./routes/productReviews');
const productReviewsAdminRoutes = require('./routes/productReviewsAdmin');
const emailTemplatesAdminRoutes = require('./routes/emailTemplatesAdmin');
const publicSettingsRoutes = require('./routes/publicSettings');
const mailingListRoutes = require('./routes/mailingListAdmin');
const emailCampaignListRoutes = require('./routes/emailCampaignsAdmin');
const subscribersAdminRoutes = require('./routes/subscribersAdmin');
const subscribersRoutes = require('./routes/subscribers');
const emailTrackingRoutes = require('./routes/emailTracking');
// Create Express app
const app = express();
const port = config.port || 4000;
// Ensure uploads directories exist
const uploadsDir = path.join(__dirname, '../public/uploads');
const productImagesDir = path.join(uploadsDir, 'products');
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}
if (!fs.existsSync(productImagesDir)) {
fs.mkdirSync(productImagesDir, { recursive: true });
}
const blogImagesDir = path.join(uploadsDir, 'blog');
if (!fs.existsSync(blogImagesDir)) {
fs.mkdirSync(blogImagesDir, { recursive: true });
}
// Configure storage
// const storage = multer.diskStorage({
// destination: (req, file, cb) => {
// // Determine destination based on upload type
// if (req.originalUrl.includes('/product')) {
// cb(null, productImagesDir);
// } else {
// cb(null, uploadsDir);
// }
// },
// filename: (req, file, cb) => {
// // Create unique filename with original extension
// const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
// const fileExt = path.extname(file.originalname);
// const safeName = path.basename(file.originalname, fileExt)
// .toLowerCase()
// .replace(/[^a-z0-9]/g, '-');
// cb(null, `${safeName}-${uniqueSuffix}${fileExt}`);
// }
// });
// // File filter to only allow images
// const fileFilter = (req, file, cb) => {
// // Accept only image files
// if (file.mimetype.startsWith('image/')) {
// cb(null, true);
// } else {
// cb(new Error('Only image files are allowed!'), false);
// }
// };
// Create the multer instance
// const upload = multer({
// storage,
// fileFilter,
// limits: {
// fileSize: 5 * 1024 * 1024 // 5MB limit
// }
// });
const upload = storageService.getUploadMiddleware();
pool.connect()
.then(async () => {
console.log('Connected to PostgreSQL database');
// Load settings from database
try {
const settings = await SystemSettings.getAllSettings(pool, query);
if (settings) {
// Update config with database settings
config.updateFromDatabase(settings);
console.log('Loaded settings from database');
}
} catch (error) {
console.error('Error loading settings from database:', error);
}
})
.catch(err => console.error('Database connection error:', err))
.finally((() => {
setupRepeatedChecks(pool, query);
}));
async function setupRepeatedChecks(p, q){
let timeInterval = 60 * 1000;
let siteGeneratorInterval = 60 * 1000;
if (config.environment === 'prod') {
console.log('Setting up scheduled tasks for production environment, 10 mins, 60 mins');
timeInterval = 10 * 60 * 1000;
siteGeneratorInterval = 60 * 60 * 1000;
}else {
console.log('Setting up scheduled tasks for development environment, 2 mins, 10 mins');
timeInterval = 2 * 60 * 1000;
siteGeneratorInterval = 10 * 60 * 1000;
}
// Process stock notifications every X minutes
setInterval(async () => {
try {
console.log('Processing low stock notifications...');
const processedCount = await notificationService.processLowStockNotifications(p, q);
console.log(`Processed ${processedCount} low stock notifications`);
} catch (error) {
console.error('Error processing low stock notifications:', error);
}
}, timeInterval);
console.log('Processing sitemap...');
siteMapService.generateSitemap(p, q);
console.log(`Sitemap generated`);
setInterval(async () => {
try {
console.log('Processing sitemap...');
await siteMapService.generateSitemap(p, q);
console.log(`Sitemap generated`);
} catch (error) {
console.error('Error processing low stock notifications:', error);
}
}, timeInterval);
}
// Handle SSL proxy headers
app.use((req, res, next) => {
// Trust X-Forwarded-Proto header from Cloudflare
if (req.headers['x-forwarded-proto'] === 'https') {
req.secure = true;
}
next();
});
app.set('trust proxy', true);
// Middleware
app.use(cors({
origin: '*',
credentials: true
}));
app.use('/api/payment', stripePaymentRoutes(pool, query, authMiddleware(pool, query)));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use(morgan('dev'));
// Serve static files - serve the entire public directory
app.use(express.static(path.join(__dirname, '../public')));
// More specific static file serving for images
app.use('/images', express.static(path.join(__dirname, '../public/uploads')));
app.use('/uploads', express.static(path.join(__dirname, '../public/uploads')));
app.use('/api/uploads', express.static(path.join(__dirname, '../public/uploads')));
app.use('/api/images', express.static(path.join(__dirname, '../public/uploads')));
app.use('/uploads/blog', express.static(path.join(__dirname, '../public/uploads/blog')));
app.use('/api/uploads/blog', express.static(path.join(__dirname, '../public/uploads/blog')));
if (!fs.existsSync(path.join(__dirname, '../public/uploads'))) {
fs.mkdirSync(path.join(__dirname, '../public/uploads'), { recursive: true });
}
if (!fs.existsSync(path.join(__dirname, '../public/uploads/products'))) {
fs.mkdirSync(path.join(__dirname, '../public/uploads/products'), { recursive: true });
}
// For direct access to product images
app.use('/products/images', express.static(path.join(__dirname, '../public/uploads/products')));
app.use('/api/products/images', express.static(path.join(__dirname, '../public/uploads/products')));
// Health check route
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', message: 'API is running' });
});
app.use('/api/settings', publicSettingsRoutes(pool, query));
// Upload endpoints
// Public upload endpoint (basic)
app.post('/api/image/upload', upload.single('image'), (req, res) => {
console.log('/api/image/upload');
if (!req.file) {
return res.status(400).json({
error: true,
message: 'No image file provided'
});
}
const imagePath = req.file.path ? `/uploads/${req.file.filename}` : req.file.location;
res.json({
success: true,
imagePath: storageService.getImageUrl(imagePath)
});
// res.json({
// success: true,
// imagePath: `/uploads/${req.file.filename}`
// });
});
app.get('/api/public-file/:filename', (req, res) => {
const { filename } = req.params;
// Prevent path traversal attacks
if (filename.includes('..') || filename.includes('/')) {
return res.status(400).json({
error: true,
message: 'Invalid filename'
});
}
// Serve files from public uploads folder
res.sendFile(path.join(__dirname, '../public/uploads', filename));
});
app.use('/api/product-reviews', productReviewsRoutes(pool, query, authMiddleware(pool, query)));
app.use('/api/admin/product-reviews', productReviewsAdminRoutes(pool, query, adminAuthMiddleware(pool, query)));
app.use('/api/admin/users', usersAdminRoutes(pool, query, adminAuthMiddleware(pool, query)));
app.use('/api/admin/coupons', couponsAdminRoutes(pool, query, adminAuthMiddleware(pool, query)));
app.use('/api/admin/subscribers', subscribersAdminRoutes(pool, query, adminAuthMiddleware(pool, query)));
app.use('/api/admin/orders', ordersAdminRoutes(pool, query, adminAuthMiddleware(pool, query)));
app.use('/api/admin/blog', blogAdminRoutes(pool, query, adminAuthMiddleware(pool, query)));
app.use('/api/admin/blog-comments', blogCommentsAdminRoutes(pool, query, adminAuthMiddleware(pool, query)));
app.use('/api/admin/email-templates', emailTemplatesAdminRoutes(pool, query, adminAuthMiddleware(pool, query)));
app.use('/api/admin/mailing-lists', mailingListRoutes(pool, query, adminAuthMiddleware(pool, query)));
app.use('/api/admin/email-campaigns', emailCampaignListRoutes(pool, query, adminAuthMiddleware(pool, query)));
// Admin-only product image upload
app.post('/api/image/product', adminAuthMiddleware(pool, query), upload.single('image'), (req, res) => {
console.log('/api/image/product', req.file);
if (!req.file) {
return res.status(400).json({
error: true,
message: 'No image file provided'
});
}
// Get the relative path to the image
const imagePath = req.file.path ?
`/uploads/products/${req.file.filename}` :
req.file.location;
res.json({
success: true,
imagePath: storageService.getImageUrl(imagePath),
filename: req.file.filename || path.basename(req.file.location)
});
// const imagePath = `/uploads/products/${req.file.filename}`;
// res.json({
// success: true,
// imagePath,
// filename: req.file.filename
// });
});
// Upload multiple product images (admin only)
app.post('/api/image/products', adminAuthMiddleware(pool, query), upload.array('images', 10), (req, res) => {
console.log('/api/image/products', req.files);
if (!req.files || req.files.length === 0) {
return res.status(400).json({
error: true,
message: 'No image files provided'
});
}
// Get the relative paths to the images
const imagePaths = req.files.map(file => {
const imagePath = file.path ?
`/uploads/products/${file.filename}` :
file.location;
return {
imagePath: storageService.getImageUrl(imagePath),
filename: file.filename || path.basename(file.location)
};
});
res.json({
success: true,
images: imagePaths
});
// const imagePaths = req.files.map(file => ({
// imagePath: `/uploads/products/${file.filename}`,
// filename: file.filename
// }));
// res.json({
// success: true,
// images: imagePaths
// });
});
// Delete product image (admin only)
app.delete('/api/image/product/:filename', adminAuthMiddleware(pool, query), (req, res) => {
try {
const { filename } = req.params;
// Prevent path traversal attacks
if (filename.includes('..') || filename.includes('/')) {
return res.status(400).json({
error: true,
message: 'Invalid filename'
});
}
if (config.site.deployment === 'cloud' && config.site.awsS3Bucket) {
// Implementation for S3 deletion would go here
// For now, we'll just log and continue
console.log('S3 file deletion not implemented yet');
} else {
// Delete from local filesystem
const filePath = path.join(__dirname, '../public/uploads/products', filename);
// Check if file exists
if (!fs.existsSync(filePath)) {
return res.status(404).json({
error: true,
message: 'Image not found'
});
}
// Delete the file
fs.unlinkSync(filePath);
}
res.json({
success: true,
message: 'Image deleted successfully'
});
} catch (error) {
console.error('Error deleting image:', error);
res.status(500).json({
error: true,
message: 'Failed to delete image'
});
}
});
// Use routes
app.use('/api/admin/settings', settingsAdminRoutes(pool, query, adminAuthMiddleware(pool, query)));
app.use('/api/products', productRoutes(pool, query));
app.use('/api/subscribers', subscribersRoutes(pool, query));
app.use('/api/email', emailTrackingRoutes(pool, query));
app.use('/api/auth', authRoutes(pool, query));
app.use('/api/user/orders', userOrdersRoutes(pool, query, authMiddleware(pool, query)));
app.use('/api/blog', blogRoutes(pool, query, authMiddleware(pool, query)));
app.use('/api/cart', cartRoutes(pool, query, authMiddleware(pool, query)));
app.use('/api/admin/products', productAdminRoutes(pool, query, adminAuthMiddleware(pool, query)));
app.use('/api/admin/categories', categoryAdminRoutes(pool, query, adminAuthMiddleware(pool, query)));
app.use('/api/shipping', shippingRoutes(pool, query, authMiddleware(pool, query)));
// Error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
error: true,
message: err.message || 'An unexpected error occurred'
});
});
// Start server
app.listen(port, () => {
console.log(`Server running on port ${port} in ${config.environment} environment`);
console.log(`Deployment mode: ${config.site.deployment || 'self-hosted'}`);
});
module.exports = app;