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; } } async function ensureUpcomingEventsTable() { try { await pool.execute(` CREATE TABLE IF NOT EXISTS upcoming_events ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255), eventtitle VARCHAR(255), slug VARCHAR(255), date VARCHAR(255), eventdate VARCHAR(255), time VARCHAR(255), location VARCHAR(255), image TEXT, eventimageurl TEXT, link TEXT, btn_text VARCHAR(255) DEFAULT 'Details Coming Soon', btnText VARCHAR(255) DEFAULT 'Details Coming Soon', description TEXT, eventdescription TEXT, admission VARCHAR(255), sort_order INT DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); const [columns] = await pool.execute("SHOW COLUMNS FROM upcoming_events"); const colNames = columns.map(col => col.Field); if (colNames.includes('year')) { try { await pool.execute("ALTER TABLE upcoming_events MODIFY COLUMN year INT NULL DEFAULT 2026"); } catch (err) { console.warn('Could not modify year column:', err.message); } } const columnsToAdd = [ { name: 'title', type: 'VARCHAR(255)' }, { name: 'date', type: 'VARCHAR(255)' }, { name: 'image', type: 'TEXT' }, { name: 'description', type: 'TEXT' }, { name: 'btn_text', type: "VARCHAR(255) DEFAULT 'Details Coming Soon'" }, { name: 'admission', type: 'VARCHAR(255)' }, { name: 'sort_order', type: 'INT DEFAULT 0' } ]; for (const col of columnsToAdd) { if (!colNames.includes(col.name)) { await pool.execute(`ALTER TABLE upcoming_events ADD COLUMN ${col.name} ${col.type}`); } } console.log('✅ Verified upcoming_events table'); const [existing] = await pool.execute( 'SELECT id FROM upcoming_events WHERE title = ? OR eventtitle = ?', ['KW Multicultural Festival', 'KW Multicultural Festival'] ); if (existing.length === 0) { const hasYearCol = colNames.includes('year'); if (hasYearCol) { await pool.execute( `INSERT INTO upcoming_events (year, title, eventtitle, slug, date, eventdate, time, location, image, eventimageurl, link, btn_text, btnText, description, eventdescription) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ 2026, 'KW Multicultural Festival', 'KW Multicultural Festival', 'kw-multicultural-festival-2026', 'Jun 20, 2026 and Jun 21, 2026', 'Jun 20, 2026 and Jun 21, 2026', 'Details will be announced', 'Victoria Park, Kitchener', '/assets/img/event/upcoming-event/multicultural-festival.webp', '/assets/img/event/upcoming-event/multicultural-festival.webp', '/upcoming-event/kw-multicultural-festival-2026', 'Details Coming Soon', 'Details Coming Soon', 'KW Multicultural Festival will take place on Jun 20-21, 2026 at Victoria Park, Kitchener. More details will be updated soon.', 'KW Multicultural Festival will take place on Jun 20-21, 2026 at Victoria Park, Kitchener. More details will be updated soon.' ] ); } else { await pool.execute( `INSERT INTO upcoming_events (title, eventtitle, slug, date, eventdate, time, location, image, eventimageurl, link, btn_text, btnText, description, eventdescription) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ 'KW Multicultural Festival', 'KW Multicultural Festival', 'kw-multicultural-festival-2026', 'Jun 20, 2026 and Jun 21, 2026', 'Jun 20, 2026 and Jun 21, 2026', 'Details will be announced', 'Victoria Park, Kitchener', '/assets/img/event/upcoming-event/multicultural-festival.webp', '/assets/img/event/upcoming-event/multicultural-festival.webp', '/upcoming-event/kw-multicultural-festival-2026', 'Details Coming Soon', 'Details Coming Soon', 'KW Multicultural Festival will take place on Jun 20-21, 2026 at Victoria Park, Kitchener. More details will be updated soon.', 'KW Multicultural Festival will take place on Jun 20-21, 2026 at Victoria Park, Kitchener. More details will be updated soon.' ] ); } console.log('✅ Seeded initial upcoming event: KW Multicultural Festival'); } } catch (error) { console.error('❌ Failed to ensure upcoming_events table:', 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(); await ensureUpcomingEventsTable(); } 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 }); } }); // =============================== // UPCOMING EVENTS CRUD ROUTES // =============================== // Helper function to slugify string function slugify(text) { if (!text) return ''; return text .toString() .toLowerCase() .trim() .replace(/\s+/g, '-') // Replace spaces with - .replace(/[^\w\-]+/g, '') // Remove all non-word chars .replace(/\-\-+/g, '-'); // Replace multiple - with single - } // GET all upcoming events app.get('/api/upcoming-events', async (req, res) => { try { const [rows] = await pool.execute('SELECT * FROM upcoming_events ORDER BY sort_order ASC, id DESC'); res.json({ success: true, data: rows }); } catch (error) { console.error('Error fetching upcoming events:', error); res.status(500).json({ success: false, message: 'Error fetching upcoming events', error: error.message }); } }); // GET single upcoming event by ID app.get('/api/upcoming-events/:id', async (req, res) => { try { const [rows] = await pool.execute('SELECT * FROM upcoming_events WHERE id = ?', [req.params.id]); if (rows.length === 0) { return res.status(404).json({ success: false, message: 'Upcoming event not found' }); } res.json({ success: true, data: rows[0] }); } catch (error) { console.error('Error fetching upcoming event:', error); res.status(500).json({ success: false, message: 'Error fetching upcoming event', error: error.message }); } }); // GET single upcoming event by slug or ID app.get('/api/upcoming-events/slug/:slug', async (req, res) => { try { const rawSlug = req.params.slug ? req.params.slug.trim() : ''; const cleanedSlug = slugify(rawSlug); // 1. First priority: exact slug or ID match let [rows] = await pool.execute( 'SELECT * FROM upcoming_events WHERE slug = ? OR slug = ? OR id = ?', [rawSlug, cleanedSlug, rawSlug] ); // 2. Second priority: title slug match fallback if (rows.length === 0 && cleanedSlug) { [rows] = await pool.execute( 'SELECT * FROM upcoming_events WHERE LOWER(REPLACE(title, " ", "-")) = ? OR LOWER(REPLACE(eventtitle, " ", "-")) = ?', [cleanedSlug, cleanedSlug] ); } if (rows.length === 0) { return res.status(404).json({ success: false, message: 'Upcoming event not found' }); } res.json({ success: true, data: rows[0] }); } catch (error) { console.error('Error fetching upcoming event by slug:', error); res.status(500).json({ success: false, message: 'Error fetching upcoming event', error: error.message }); } }); // POST create new upcoming event app.post('/api/upcoming-events', async (req, res) => { try { const { title, eventtitle, slug, date, eventdate, time, location, venue, image, image_url, eventimageurl, link, btn_text, btnText, description, desc, eventdescription, admission } = req.body; const inputTitle = title || eventtitle; if (!inputTitle || String(inputTitle).trim() === '') { return res.status(400).json({ success: false, message: 'Event title is required' }); } const finalTitle = String(inputTitle).trim(); const generatedSlug = slugify(finalTitle); const finalSlug = (slug && String(slug).trim()) ? slugify(String(slug).trim()) : generatedSlug; const finalLocation = location || venue || ''; const finalImage = image || image_url || eventimageurl || ''; const finalBtnText = btn_text || btnText || 'Details Coming Soon'; const finalDesc = description || desc || eventdescription || ''; const finalLink = (link && String(link).trim() && !String(link).startsWith('/upcoming-event/')) ? String(link).trim() : `/upcoming-event/${finalSlug}`; const finalDate = date || eventdate || ''; const finalTime = time || ''; const finalAdmission = admission || null; const [result] = await pool.execute( `INSERT INTO upcoming_events (title, eventtitle, slug, date, eventdate, time, location, image, eventimageurl, link, btn_text, btnText, description, eventdescription, admission) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [finalTitle, finalTitle, finalSlug, finalDate, finalDate, finalTime, finalLocation, finalImage, finalImage, finalLink, finalBtnText, finalBtnText, finalDesc, finalDesc, finalAdmission] ); res.status(201).json({ success: true, message: 'Upcoming event created successfully', data: { id: result.insertId, title: finalTitle, slug: finalSlug, link: finalLink } }); } catch (error) { console.error('Error creating upcoming event:', error); res.status(500).json({ success: false, message: 'Error creating upcoming event', error: error.message }); } }); // PUT update upcoming event (supports full or partial updates) app.put('/api/upcoming-events/:id', async (req, res) => { try { const eventId = req.params.id; // Fetch existing record const [rows] = await pool.execute('SELECT * FROM upcoming_events WHERE id = ?', [eventId]); if (rows.length === 0) { return res.status(404).json({ success: false, message: 'Upcoming event not found' }); } const existing = rows[0]; const { title, eventtitle, slug, date, eventdate, time, location, venue, image, image_url, eventimageurl, link, btn_text, btnText, description, desc, eventdescription, admission, sort_order } = req.body || {}; const inputTitle = title || eventtitle || existing.title || existing.eventtitle; if (!inputTitle || String(inputTitle).trim() === '') { return res.status(400).json({ success: false, message: 'Event title is required' }); } const finalTitle = String(inputTitle).trim(); const generatedSlug = slugify(finalTitle); let finalSlug = generatedSlug; if (slug && String(slug).trim()) { const userSlug = slugify(String(slug).trim()); const oldTitleSlug = slugify(existing.title || existing.eventtitle || ''); if (userSlug !== oldTitleSlug) { finalSlug = userSlug; } } const finalLocation = location !== undefined ? location : (venue !== undefined ? venue : (existing.location || '')); const finalImage = image !== undefined ? image : (image_url !== undefined ? image_url : (eventimageurl !== undefined ? eventimageurl : (existing.image || existing.eventimageurl || ''))); const finalBtnText = btn_text !== undefined ? btn_text : (btnText !== undefined ? btnText : (existing.btn_text || existing.btnText || 'Details Coming Soon')); const finalDesc = description !== undefined ? description : (desc !== undefined ? desc : (eventdescription !== undefined ? eventdescription : (existing.description || existing.eventdescription || ''))); const finalLink = (link && String(link).trim() && !String(link).startsWith('/upcoming-event/')) ? String(link).trim() : `/upcoming-event/${finalSlug}`; const finalDate = date !== undefined ? date : (eventdate !== undefined ? eventdate : (existing.date || existing.eventdate || '')); const finalTime = time !== undefined ? time : (existing.time || ''); const finalAdmission = admission !== undefined ? admission : existing.admission; const finalSortOrder = sort_order !== undefined ? Number(sort_order) : (existing.sort_order || 0); await pool.execute( `UPDATE upcoming_events SET title = ?, eventtitle = ?, slug = ?, date = ?, eventdate = ?, time = ?, location = ?, image = ?, eventimageurl = ?, link = ?, btn_text = ?, btnText = ?, description = ?, eventdescription = ?, admission = ?, sort_order = ? WHERE id = ?`, [finalTitle, finalTitle, finalSlug, finalDate, finalDate, finalTime, finalLocation, finalImage, finalImage, finalLink, finalBtnText, finalBtnText, finalDesc, finalDesc, finalAdmission, finalSortOrder, eventId] ); res.json({ success: true, message: 'Upcoming event updated successfully' }); } catch (error) { console.error('Error updating upcoming event:', error); res.status(500).json({ success: false, message: 'Error updating upcoming event', error: error.message }); } }); // DELETE upcoming event app.delete('/api/upcoming-events/:id', async (req, res) => { try { const [result] = await pool.execute('DELETE FROM upcoming_events WHERE id = ?', [req.params.id]); if (result.affectedRows === 0) { return res.status(404).json({ success: false, message: 'Upcoming event not found' }); } res.json({ success: true, message: 'Upcoming event deleted successfully' }); } catch (error) { console.error('Error deleting upcoming event:', error); res.status(500).json({ success: false, message: 'Error deleting upcoming 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;