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 { authenticateToken, requireAdmin } = require('./middleware/auth'); const JWT_SECRET = process.env.JWT_SECRET || 'metatron-admin-secret-key'; process.env.JWT_SECRET = JWT_SECRET; const app = express(); const PORT = process.env.PORT; // CORS Middleware - Add this before other middleware app.use((req, res, next) => { const origin = req.headers.origin; if (origin) { res.header('Access-Control-Allow-Origin', origin); res.header('Access-Control-Allow-Credentials', 'true'); res.header('Vary', 'Origin'); } else { 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') { return res.sendStatus(200); } next(); }); app.options('*', (req, res) => { res.sendStatus(200); }); // 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' }); async function ensureAdminUsersTable() { try { await pool.execute(` CREATE TABLE IF NOT EXISTS admin_users ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), email VARCHAR(255) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, role VARCHAR(50) DEFAULT 'admin', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); const [columns] = await pool.execute('SHOW COLUMNS FROM admin_users'); const columnNames = columns.map(column => column.Field); if (!columnNames.includes('name')) { await pool.execute('ALTER TABLE admin_users ADD COLUMN name VARCHAR(255)'); } if (!columnNames.includes('email')) { await pool.execute('ALTER TABLE admin_users ADD COLUMN email VARCHAR(255) NOT NULL UNIQUE'); } if (!columnNames.includes('password_hash')) { await pool.execute('ALTER TABLE admin_users ADD COLUMN password_hash VARCHAR(255) NOT NULL'); } if (!columnNames.includes('role')) { await pool.execute("ALTER TABLE admin_users ADD COLUMN role VARCHAR(50) DEFAULT 'admin'"); } const [existingAdminRows] = await pool.execute('SELECT id FROM admin_users WHERE email = ?', ['admin@example.com']); if (existingAdminRows.length === 0) { const defaultHash = await bcrypt.hash('adminpassword', 10); await pool.execute( 'INSERT INTO admin_users (name, email, password_hash, role) VALUES (?, ?, ?, ?)', ['Admin', 'admin@example.com', defaultHash, 'admin'] ); console.log('āœ… Default admin user created'); } } catch (error) { console.error('āŒ Failed to ensure admin_users table:', error.message); throw error; } } async function ensureEventImagesSortOrderColumn() { try { const [columns] = await pool.execute("SHOW COLUMNS FROM event_images"); const columnNames = columns.map(col => col.Field); if (!columnNames.includes("sort_order")) { console.log("Adding sort_order column to event_images..."); await pool.execute(` ALTER TABLE event_images ADD COLUMN sort_order INT NOT NULL DEFAULT 0 `); await pool.execute(` UPDATE event_images SET sort_order = id `); console.log("sort_order column added successfully."); } else { console.log("sort_order column already exists."); } } catch (error) { console.error("Failed to ensure sort_order column:", error.message); throw error; } } // 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(); await ensureAdminUsersTable(); await ensureEventImagesSortOrderColumn(); } 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|svg)$/i)) { return cb(new Error('Only image files (jpg, jpeg, png, gif, webp, svg) are allowed!'), false); } cb(null, true); } }); // =============================== // =============================== // AUTHENTICATION ROUTES // =============================== // POST login app.post('/api/auth/login', async (req, res) => { try { const { email, password } = req.body; const normalizedEmail = typeof email === 'string' ? email.trim().toLowerCase() : ''; if (!normalizedEmail || !password) { return res.status(400).json({ success: false, message: 'Email and password are required' }); } const [rows] = await pool.execute('SELECT * FROM admin_users WHERE LOWER(email) = ?', [normalizedEmail]); 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 || normalizedEmail, role: user.role || 'admin' }, 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' }); } }); // POST register app.post('/api/auth/register', async (req, res) => { try { const { name, email, password } = req.body; if (!name || !email || !password) { return res.status(400).json({ success: false, message: 'Name, email, and password are required' }); } const trimmedName = name.trim(); const trimmedEmail = typeof email === 'string' ? email.trim().toLowerCase() : ''; if (!trimmedName || !trimmedEmail || !password) { return res.status(400).json({ success: false, message: 'Name, email, and password are required' }); } const [existingRows] = await pool.execute('SELECT id FROM admin_users WHERE LOWER(email) = ?', [trimmedEmail]); if (existingRows.length > 0) { return res.status(409).json({ success: false, message: 'Email already exists' }); } const hashedPassword = await bcrypt.hash(password, 10); const [columns] = await pool.execute('SHOW COLUMNS FROM admin_users'); const columnNames = columns.map(column => column.Field); const hasNameColumn = columnNames.includes('name'); const insertFields = hasNameColumn ? ['name', 'email', 'password_hash', 'role'] : ['email', 'password_hash', 'role']; const insertValues = hasNameColumn ? [trimmedName, trimmedEmail, hashedPassword, 'admin'] : [trimmedEmail, hashedPassword, 'admin']; const placeholders = insertFields.map(() => '?').join(', '); const [result] = await pool.execute( `INSERT INTO admin_users (${insertFields.join(', ')}) VALUES (${placeholders})`, insertValues ); const userId = result.insertId; const token = jwt.sign( { id: userId, email: trimmedEmail, role: 'admin' }, JWT_SECRET, { expiresIn: '1d' } ); res.json({ success: true, message: 'User registered successfully', data: { token, user: { id: userId, name: trimmedName, email: trimmedEmail, role: 'admin' } } }); } catch (error) { console.error('Registration error:', error); if (error && error.code === 'ER_NO_SUCH_TABLE') { try { await ensureAdminUsersTable(); return res.status(500).json({ success: false, message: 'Registration failed' }); } catch (retryError) { console.error('Retry registration setup failed:', retryError); } } res.status(500).json({ success: false, message: 'Registration failed' }); } }); app.get('/api/debug/tables', async (req, res) => { try { const [tables] = await pool.execute("SHOW TABLES"); res.json(tables); } catch (err) { console.error(err); res.status(500).json({ error: err.message }); } }); // =============================== // 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 = ? ORDER BY sort_order ASC, id ASC', [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 }); } }); // Bulk reorder images app.put('/api/event-images/reorder', async (req, res) => { try { console.log("Request body:", req.body); const { images } = req.body; // Array of { id, sort_order } console.log(`Reorder request received for ${images?.length} images`); if (!Array.isArray(images) || images.length === 0) { return res.status(400).json({ success: false, message: 'Images array is required for reordering' }); } // Using a transaction for bulk update const connection = await pool.getConnection(); try { await connection.beginTransaction(); for (const item of images) { await connection.execute( 'UPDATE event_images SET sort_order = ? WHERE id = ?', [item.sort_order, item.id] ); } await connection.commit(); res.json({ success: true, message: 'Images reordered successfully' }); } catch (err) { console.error('Transaction failed, rolling back:', err); await connection.rollback(); throw err; } finally { connection.release(); } } catch (error) { console.error("===== REORDER ERROR ====="); console.error(error); console.error("Message:", error.message); console.error("Code:", error.code); console.error("========================="); res.status(500).json({ success: false, message: error.message, code: error.code }); } }); 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', 50), 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', 50), 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!' }); }); app.get('/api/debug/tables', async (req, res) => { try { const [tables] = await pool.execute('SHOW TABLES'); res.json(tables); } catch (error) { console.error(error); res.status(500).json({ success: false, message: error.message }); } }); // 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;