const express = require('express'); const mysql = require('mysql2/promise'); const multer = require('multer'); const path = require('path'); const fs = require('fs'); require('dotenv').config(); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const JWT_SECRET = process.env.JWT_SECRET || 'supersecret_default_key_change_me_in_production'; const app = express(); const PORT = process.env.PORT || 3000; // CORS Middleware - Add this before other middleware app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); // Handle preflight requests if (req.method === 'OPTIONS') { res.sendStatus(200); } else { next(); } }); // Middleware app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Create uploads directory if it doesn't exist const uploadsDir = path.join(__dirname, 'uploads'); if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir, { recursive: true }); } // Serve static files from uploads directory app.use('/uploads', express.static('uploads')); // MySQL connection pool with optimized settings const pool = mysql.createPool({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, waitForConnections: true, connectionLimit: 10, queueLimit: 0, acquireTimeout: 60000, timeout: 60000, enableKeepAlive: true, keepAliveInitialDelay: 0, reconnect: true, idleTimeout: 300000, // 5 minutes maxIdle: 10, maxReuse: 1000, charset: 'utf8mb4' }); // Test database connection on startup async function testDatabaseConnection() { try { const connection = await pool.getConnection(); console.log('āœ… Database connected successfully'); await connection.execute('SELECT 1'); connection.release(); } catch (error) { console.error('āŒ Database connection failed:', error.message); process.exit(1); } } // Keep connections alive with periodic ping setInterval(async () => { try { const connection = await pool.getConnection(); await connection.execute('SELECT 1'); connection.release(); } catch (error) { console.error('Keep-alive ping failed:', error.message); } }, 240000); // Every 4 minutes // Multer configuration for file uploads const storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, 'uploads/'); }, filename: function (req, file, cb) { // Create timestamp-based filename const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const randomSuffix = Math.round(Math.random() * 1E9); const fileExtension = path.extname(file.originalname); const baseName = path.basename(file.originalname, fileExtension); // Format: originalname_timestamp_random.ext const filename = `${baseName}_${timestamp}_${randomSuffix}${fileExtension}`; cb(null, filename); } }); const upload = multer({ storage: storage, limits: { fileSize: 10 * 1024 * 1024 // 10MB limit }, fileFilter: function (req, file, cb) { // Accept images only if (!file.originalname.match(/\.(jpg|jpeg|png|gif|webp)$/)) { return cb(new Error('Only image files are allowed!'), false); } cb(null, true); } }); // =============================== // =============================== // AUTHENTICATION ROUTES // =============================== // POST login app.post('/api/auth/login', async (req, res) => { try { const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ success: false, message: 'Email and password are required' }); } const [rows] = await pool.execute('SELECT * FROM admin_users WHERE email = ?', [email]); if (rows.length === 0) { return res.status(401).json({ success: false, message: 'Invalid credentials' }); } const user = rows[0]; const isMatch = await bcrypt.compare(password, user.password_hash); if (!isMatch) { return res.status(401).json({ success: false, message: 'Invalid credentials' }); } const token = jwt.sign({ id: user.id, email: user.email, role: user.role }, JWT_SECRET, { expiresIn: '1d' }); res.json({ success: true, message: 'Login successful', data: { token, user: { id: user.id, email: user.email, role: user.role } } }); } catch (error) { console.error('Login error:', error); res.status(500).json({ success: false, message: 'Server error during login' }); } }); // =============================== // HEALTH CHECK & STATUS ROUTES // =============================== // Health check endpoint app.get('/api/health', async (req, res) => { try { const startTime = Date.now(); const connection = await pool.getConnection(); const [rows] = await connection.execute('SELECT 1 as health_check'); connection.release(); const dbResponseTime = Date.now() - startTime; res.json({ success: true, status: 'healthy', timestamp: new Date().toISOString(), database: { status: 'connected', responseTime: `${dbResponseTime}ms` }, uptime: process.uptime(), memory: process.memoryUsage() }); } catch (error) { res.status(500).json({ success: false, status: 'unhealthy', error: error.message, timestamp: new Date().toISOString() }); } }); // Database status endpoint app.get('/api/db-status', async (req, res) => { try { const connection = await pool.getConnection(); const [rows] = await connection.execute('SHOW STATUS LIKE "Threads_connected"'); connection.release(); res.json({ success: true, data: { activeConnections: rows[0]?.Value || 'Unknown', poolStatus: { totalConnections: pool.pool._allConnections.length, freeConnections: pool.pool._freeConnections.length, acquiringConnections: pool.pool._acquiringConnections.length } } }); } catch (error) { res.status(500).json({ success: false, message: 'Database status check failed', error: error.message }); } }); // =============================== // EVENTS CRUD ROUTES // =============================== // GET all events app.get('/api/events', async (req, res) => { try { const [rows] = await pool.execute('SELECT * FROM events ORDER BY year DESC, id DESC'); res.json({ success: true, data: rows }); } catch (error) { console.error('Error fetching events:', error); res.status(500).json({ success: false, message: 'Error fetching events', error: error.message }); } }); // GET single event by ID app.get('/api/events/:id', async (req, res) => { try { const [rows] = await pool.execute('SELECT * FROM events WHERE id = ?', [req.params.id]); if (rows.length === 0) { return res.status(404).json({ success: false, message: 'Event not found' }); } res.json({ success: true, data: rows[0] }); } catch (error) { console.error('Error fetching event:', error); res.status(500).json({ success: false, message: 'Error fetching event', error: error.message }); } }); // POST create new event app.post('/api/events', async (req, res) => { try { const { year, eventdate, eventtitle, eventimageurl, eventdescription } = req.body; if (!year || !eventtitle) { return res.status(400).json({ success: false, message: 'Year and event title are required' }); } const [result] = await pool.execute( 'INSERT INTO events (year, eventdate, eventtitle, eventimageurl, eventdescription) VALUES (?, ?, ?, ?, ?)', [year, eventdate, eventtitle, eventimageurl, eventdescription] ); res.status(201).json({ success: true, message: 'Event created successfully', data: { id: result.insertId } }); } catch (error) { console.error('Error creating event:', error); res.status(500).json({ success: false, message: 'Error creating event', error: error.message }); } }); // PUT update event app.put('/api/events/:id', async (req, res) => { try { const { year, eventdate, eventtitle, eventimageurl, eventdescription } = req.body; const eventId = req.params.id; if (!year || !eventtitle) { return res.status(400).json({ success: false, message: 'Year and event title are required' }); } const [result] = await pool.execute( 'UPDATE events SET year = ?, eventdate = ?, eventtitle = ?, eventimageurl = ?, eventdescription = ? WHERE id = ?', [year, eventdate, eventtitle, eventimageurl, eventdescription, eventId] ); if (result.affectedRows === 0) { return res.status(404).json({ success: false, message: 'Event not found' }); } res.json({ success: true, message: 'Event updated successfully' }); } catch (error) { console.error('Error updating event:', error); res.status(500).json({ success: false, message: 'Error updating event', error: error.message }); } }); // DELETE event app.delete('/api/events/:id', async (req, res) => { try { const [result] = await pool.execute('DELETE FROM events WHERE id = ?', [req.params.id]); if (result.affectedRows === 0) { return res.status(404).json({ success: false, message: 'Event not found' }); } res.json({ success: true, message: 'Event deleted successfully' }); } catch (error) { console.error('Error deleting event:', error); res.status(500).json({ success: false, message: 'Error deleting event', error: error.message }); } }); // =============================== // EVENT_IMAGES CRUD ROUTES // =============================== // GET all event images app.get('/api/event-images', async (req, res) => { try { const [rows] = await pool.execute(` SELECT ei.*, e.eventtitle FROM event_images ei LEFT JOIN events e ON ei.eventid = e.id ORDER BY ei.id DESC `); res.json({ success: true, data: rows }); } catch (error) { console.error('Error fetching event images:', error); res.status(500).json({ success: false, message: 'Error fetching event images', error: error.message }); } }); // GET event images by event ID app.get('/api/event-images/event/:eventId', async (req, res) => { try { const [rows] = await pool.execute('SELECT * FROM event_images WHERE eventid = ?', [req.params.eventId]); res.json({ success: true, data: rows }); } catch (error) { console.error('Error fetching event images:', error); res.status(500).json({ success: false, message: 'Error fetching event images', error: error.message }); } }); // GET single event image by ID app.get('/api/event-images/:id', async (req, res) => { try { const [rows] = await pool.execute('SELECT * FROM event_images WHERE id = ?', [req.params.id]); if (rows.length === 0) { return res.status(404).json({ success: false, message: 'Event image not found' }); } res.json({ success: true, data: rows[0] }); } catch (error) { console.error('Error fetching event image:', error); res.status(500).json({ success: false, message: 'Error fetching event image', error: error.message }); } }); // POST create new event image app.post('/api/event-images', async (req, res) => { try { const { eventid, imageurl } = req.body; if (!eventid || !imageurl) { return res.status(400).json({ success: false, message: 'Event ID and image URL are required' }); } // Check if event exists const [eventRows] = await pool.execute('SELECT id FROM events WHERE id = ?', [eventid]); if (eventRows.length === 0) { return res.status(400).json({ success: false, message: 'Event does not exist' }); } const [result] = await pool.execute( 'INSERT INTO event_images (eventid, imageurl) VALUES (?, ?)', [eventid, imageurl] ); res.status(201).json({ success: true, message: 'Event image created successfully', data: { id: result.insertId } }); } catch (error) { console.error('Error creating event image:', error); res.status(500).json({ success: false, message: 'Error creating event image', error: error.message }); } }); app.post('/api/event-images/bulk', async (req, res) => { try { const { eventid, imageurl } = req.body; // Input validation if ( !eventid || typeof eventid !== 'number' || isNaN(eventid) || !Array.isArray(imageurl) || imageurl.length === 0 || imageurl.some(url => typeof url !== 'string' || url.trim() === '') ) { return res.status(400).json({ success: false, message: 'Valid event ID and a non-empty array of image URLs (as strings) are required' }); } // Check if the event exists const [eventRows] = await pool.execute( 'SELECT id FROM events WHERE id = ?', [eventid] ); if (eventRows.length === 0) { return res.status(400).json({ success: false, message: 'Event does not exist' }); } // Prepare the SQL placeholders and values const placeholders = imageurl.map(() => '(?, ?)').join(', '); const values = imageurl.flatMap(url => [eventid, url]); // Debug logs (optional) // console.log('SQL:', `INSERT INTO event_images (eventid, imageurl) VALUES ${placeholders}`); // console.log('Values:', values); // Execute the bulk insert const [result] = await pool.execute( `INSERT INTO event_images (eventid, imageurl) VALUES ${placeholders}`, values ); res.status(201).json({ success: true, message: 'Event images inserted successfully', insertedCount: imageurl.length, data: { firstInsertId: result.insertId, affectedRows: result.affectedRows } }); } catch (error) { console.error('Error inserting event images in bulk:', error); res.status(500).json({ success: false, message: 'Error inserting event images', error: error.message }); } }); // PUT update event image app.put('/api/event-images/:id', async (req, res) => { try { const { eventid, imageurl } = req.body; const imageId = req.params.id; if (!eventid || !imageurl) { return res.status(400).json({ success: false, message: 'Event ID and image URL are required' }); } // Check if event exists const [eventRows] = await pool.execute('SELECT id FROM events WHERE id = ?', [eventid]); if (eventRows.length === 0) { return res.status(400).json({ success: false, message: 'Event does not exist' }); } const [result] = await pool.execute( 'UPDATE event_images SET eventid = ?, imageurl = ? WHERE id = ?', [eventid, imageurl, imageId] ); if (result.affectedRows === 0) { return res.status(404).json({ success: false, message: 'Event image not found' }); } res.json({ success: true, message: 'Event image updated successfully' }); } catch (error) { console.error('Error updating event image:', error); res.status(500).json({ success: false, message: 'Error updating event image', error: error.message }); } }); // DELETE event image app.delete('/api/event-images/:id', async (req, res) => { try { const [result] = await pool.execute('DELETE FROM event_images WHERE id = ?', [req.params.id]); if (result.affectedRows === 0) { return res.status(404).json({ success: false, message: 'Event image not found' }); } res.json({ success: true, message: 'Event image deleted successfully' }); } catch (error) { console.error('Error deleting event image:', error); res.status(500).json({ success: false, message: 'Error deleting event image', error: error.message }); } }); // =============================== // FILE UPLOAD ROUTES // =============================== // Upload single file app.post('/api/upload/single', upload.single('file'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ success: false, message: 'No file uploaded' }); } const filePath = `/uploads/${req.file.filename}`; const timestamp = new Date().toISOString(); res.json({ success: true, message: 'File uploaded successfully', data: { filename: req.file.filename, originalname: req.file.originalname, path: filePath, fullUrl: `${req.protocol}://${req.get('host')}${filePath}`, size: req.file.size, mimetype: req.file.mimetype, uploadedAt: timestamp } }); } catch (error) { console.error('Error uploading file:', error); res.status(500).json({ success: false, message: 'Error uploading file', error: error.message }); } }); // Upload multiple files app.post('/api/upload/multiple', upload.array('files', 10), async (req, res) => { try { if (!req.files || req.files.length === 0) { return res.status(400).json({ success: false, message: 'No files uploaded' }); } const timestamp = new Date().toISOString(); const uploadedFiles = req.files.map(file => ({ filename: file.filename, originalname: file.originalname, path: `/uploads/${file.filename}`, fullUrl: `${req.protocol}://${req.get('host')}/uploads/${file.filename}`, size: file.size, mimetype: file.mimetype, uploadedAt: timestamp })); res.json({ success: true, message: `${req.files.length} files uploaded successfully`, data: uploadedFiles, uploadedAt: timestamp }); } catch (error) { console.error('Error uploading files:', error); res.status(500).json({ success: false, message: 'Error uploading files', error: error.message }); } }); // Upload and save to event_images table app.post('/api/upload/event-images/:eventId', upload.array('files', 10), async (req, res) => { try { const eventId = req.params.eventId; if (!req.files || req.files.length === 0) { return res.status(400).json({ success: false, message: 'No files uploaded' }); } // Check if event exists const [eventRows] = await pool.execute('SELECT id FROM events WHERE id = ?', [eventId]); if (eventRows.length === 0) { return res.status(400).json({ success: false, message: 'Event does not exist' }); } const timestamp = new Date().toISOString(); const uploadedFiles = []; // Insert each uploaded file into event_images table for (const file of req.files) { const filePath = `/uploads/${file.filename}`; const [result] = await pool.execute( 'INSERT INTO event_images (eventid, imageurl) VALUES (?, ?)', [eventId, filePath] ); uploadedFiles.push({ id: result.insertId, filename: file.filename, originalname: file.originalname, path: filePath, fullUrl: `${req.protocol}://${req.get('host')}${filePath}`, size: file.size, mimetype: file.mimetype, uploadedAt: timestamp, eventId: parseInt(eventId) }); } res.json({ success: true, message: `${req.files.length} files uploaded and saved to database`, data: uploadedFiles, uploadedAt: timestamp }); } catch (error) { console.error('Error uploading and saving files:', error); res.status(500).json({ success: false, message: 'Error uploading and saving files', error: error.message }); } }); // Error handling middleware app.use((error, req, res, next) => { if (error instanceof multer.MulterError) { if (error.code === 'LIMIT_FILE_SIZE') { return res.status(400).json({ success: false, message: 'File too large. Maximum size is 10MB.' }); } } res.status(500).json({ success: false, message: error.message || 'Something went wrong!' }); }); // Start server app.listen(PORT, async () => { console.log(`šŸš€ Server is running on port ${PORT}`); console.log(`šŸ“ Uploads directory: ${uploadsDir}`); // Test database connection await testDatabaseConnection(); console.log('šŸŽ‰ Server is ready to accept requests'); }); // Graceful shutdown process.on('SIGINT', async () => { console.log('\nšŸ›‘ Shutting down gracefully...'); await pool.end(); process.exit(0); }); process.on('SIGTERM', async () => { console.log('\nšŸ›‘ Shutting down gracefully...'); await pool.end(); process.exit(0); }); module.exports = app;