require('dotenv').config(); const express = require('express'); const mysql = require('mysql2/promise'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const cookieParser = require('cookie-parser'); const multer = require('multer'); const sharp = require('sharp'); const path = require('path'); const fs = require('fs'); const app = express(); const PORT = process.env.PORT || 3000; // ============================================================ // MIDDLEWARE // ============================================================ app.use(express.urlencoded({ extended: true })); app.use(express.json()); app.use(cookieParser()); app.use(express.static('public')); app.use('/uploads', express.static(path.join(__dirname, 'public/uploads'))); app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, 'views')); // ============================================================ // CREAR CARPETA UPLOADS // ============================================================ const uploadDir = path.join(__dirname, 'public/uploads'); if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } // ============================================================ // CONEXIÓN A LA BASE DE DATOS // ============================================================ const pool = mysql.createPool({ host: process.env.DB_HOST || '127.0.0.1', user: process.env.DB_USER || 'u708484156_cms', password: process.env.DB_PASSWORD || 'CMS2026!a', database: process.env.DB_NAME || 'u708484156_cms', waitForConnections: true, connectionLimit: 10, queueLimit: 0 }); // ============================================================ // CONFIGURACIÓN DE MULTER PARA SUBIDA DE IMÁGENES // ============================================================ const storage = multer.memoryStorage(); const upload = multer({ storage: storage, limits: { fileSize: 5 * 1024 * 1024 }, fileFilter: (req, file, cb) => { if (file.mimetype.startsWith('image/')) { cb(null, true); } else { cb(new Error('Solo se permiten imágenes')); } } }); // ============================================================ // FUNCIÓN PARA GUARDAR IMAGEN CON SHARP // ============================================================ async function saveImage(file, prefix) { const uniqueName = `${prefix}_${Date.now()}_${Math.random().toString(36).substring(7)}.webp`; const filePath = path.join(uploadDir, uniqueName); await sharp(file.buffer).webp({ quality: 80 }).toFile(filePath); return `/uploads/${uniqueName}`; } // ============================================================ // RUTA DE PRUEBA // ============================================================ app.get('/api/test', async (req, res) => { try { const [rows] = await pool.execute('SELECT 1 as connected'); res.json({ success: true, message: '✅ Conexión exitosa a la base de datos', data: rows }); } catch (error) { res.status(500).json({ success: false, message: '❌ Error de conexión: ' + error.message }); } }); // ============================================================ // RUTAS DEL SITIO PÚBLICO // ============================================================ app.get('/', async (req, res) => { try { const [config] = await pool.execute('SELECT * FROM header_config LIMIT 1'); const [links] = await pool.execute( 'SELECT * FROM header_links WHERE is_active = TRUE ORDER BY display_order ASC' ); const header = { config: config[0] || { site_name: 'Mi Sitio', site_description: 'Bienvenido a mi sitio web', header_bg_color: '#FFFFFF', header_text_color: '#1A1A1A', header_link_color: '#333333', header_link_hover_color: '#000000', logo_text: 'Mi Logo', logo_image: null }, links: links || [] }; res.render('site/index', { title: 'Inicio', header: header }); } catch (error) { console.error('Error en ruta /:', error); const header = { config: { site_name: 'Mi Sitio', site_description: 'Bienvenido a mi sitio web', header_bg_color: '#FFFFFF', header_text_color: '#1A1A1A', header_link_color: '#333333', header_link_hover_color: '#000000', logo_text: 'Mi Logo', logo_image: null }, links: [ { label: 'Inicio', url: '/' }, { label: 'Acerca de', url: '/acerca' }, { label: 'Contacto', url: '/contacto' } ] }; res.render('site/index', { title: 'Inicio', header: header }); } }); // ============================================================ // RUTAS DEL ADMINISTRADOR // ============================================================ // Login app.get('/admin', (req, res) => { res.render('admin/login', { error: null }); }); // Procesar login app.post('/admin/login', async (req, res) => { try { const { email, password } = req.body; const [rows] = await pool.execute('SELECT * FROM users WHERE email = ?', [email]); if (rows.length === 0) { return res.render('admin/login', { error: 'Email o contraseña incorrectos' }); } const validPassword = await bcrypt.compare(password, rows[0].password); if (!validPassword) { return res.render('admin/login', { error: 'Email o contraseña incorrectos' }); } const token = jwt.sign( { id: rows[0].id, email: rows[0].email, name: rows[0].name }, process.env.JWT_SECRET || 'mi-secreto', { expiresIn: '24h' } ); res.cookie('admin_token', token, { httpOnly: true, maxAge: 24 * 60 * 60 * 1000 }); res.redirect('/admin/dashboard'); } catch (error) { res.render('admin/login', { error: 'Error al iniciar sesión' }); } }); // Dashboard app.get('/admin/dashboard', async (req, res) => { try { const token = req.cookies.admin_token; if (!token) return res.redirect('/admin'); const decoded = jwt.verify(token, process.env.JWT_SECRET || 'mi-secreto'); const [config] = await pool.execute('SELECT * FROM header_config LIMIT 1'); const [links] = await pool.execute( 'SELECT * FROM header_links ORDER BY display_order ASC' ); const header = { config: config[0] || {}, links: links || [] }; res.render('admin/dashboard', { user: decoded, header: header }); } catch (error) { res.redirect('/admin'); } }); // Header app.get('/admin/header', async (req, res) => { try { const token = req.cookies.admin_token; if (!token) return res.redirect('/admin'); jwt.verify(token, process.env.JWT_SECRET || 'mi-secreto'); const [config] = await pool.execute('SELECT * FROM header_config LIMIT 1'); const [links] = await pool.execute( 'SELECT * FROM header_links ORDER BY display_order ASC' ); res.render('admin/header', { config: config[0] || {}, links: links || [], message: null, error: null }); } catch (error) { res.redirect('/admin'); } }); // Guardar configuración del header app.post('/admin/header/save', upload.single('logo_image'), async (req, res) => { try { const token = req.cookies.admin_token; if (!token) return res.redirect('/admin'); jwt.verify(token, process.env.JWT_SECRET || 'mi-secreto'); const { site_name, site_description, header_bg_color, header_text_color, header_link_color, header_link_hover_color, logo_text } = req.body; let logo_image = null; if (req.file) { logo_image = await saveImage(req.file, 'logo'); } const [current] = await pool.execute('SELECT * FROM header_config LIMIT 1'); if (current.length === 0) { await pool.execute( `INSERT INTO header_config (site_name, site_description, header_bg_color, header_text_color, header_link_color, header_link_hover_color, logo_text, logo_image) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [ site_name || 'Mi Sitio', site_description || '', header_bg_color || '#FFFFFF', header_text_color || '#1A1A1A', header_link_color || '#333333', header_link_hover_color || '#000000', logo_text || 'Mi Logo', logo_image ] ); } else { let query = `UPDATE header_config SET site_name = ?, site_description = ?, header_bg_color = ?, header_text_color = ?, header_link_color = ?, header_link_hover_color = ?, logo_text = ?`; const params = [ site_name || 'Mi Sitio', site_description || '', header_bg_color || '#FFFFFF', header_text_color || '#1A1A1A', header_link_color || '#333333', header_link_hover_color || '#000000', logo_text || 'Mi Logo' ]; if (logo_image) { query += `, logo_image = ?`; params.push(logo_image); } query += ` WHERE id = ?`; params.push(current[0].id); await pool.execute(query, params); } const [updatedConfig] = await pool.execute('SELECT * FROM header_config LIMIT 1'); const [links] = await pool.execute( 'SELECT * FROM header_links ORDER BY display_order ASC' ); res.render('admin/header', { config: updatedConfig[0] || {}, links: links || [], message: '✅ Configuración guardada correctamente', error: null }); } catch (error) { console.error(error); const [config] = await pool.execute('SELECT * FROM header_config LIMIT 1'); const [links] = await pool.execute( 'SELECT * FROM header_links ORDER BY display_order ASC' ); res.render('admin/header', { config: config[0] || {}, links: links || [], message: null, error: '❌ Error al guardar la configuración' }); } }); // Agregar enlace app.post('/admin/header/link/add', async (req, res) => { try { const token = req.cookies.admin_token; if (!token) return res.redirect('/admin'); jwt.verify(token, process.env.JWT_SECRET || 'mi-secreto'); const { label, url, display_order } = req.body; await pool.execute( 'INSERT INTO header_links (label, url, display_order, is_active) VALUES (?, ?, ?, TRUE)', [label, url, display_order || 0] ); res.redirect('/admin/header'); } catch (error) { res.redirect('/admin/header'); } }); // Eliminar enlace app.post('/admin/header/link/delete/:id', async (req, res) => { try { const token = req.cookies.admin_token; if (!token) return res.redirect('/admin'); jwt.verify(token, process.env.JWT_SECRET || 'mi-secreto'); await pool.execute('DELETE FROM header_links WHERE id = ?', [req.params.id]); res.redirect('/admin/header'); } catch (error) { res.redirect('/admin/header'); } }); // Logout app.get('/admin/logout', (req, res) => { res.clearCookie('admin_token'); res.redirect('/admin'); }); // ============================================================ // INICIAR SERVIDOR // ============================================================ app.listen(PORT, '0.0.0.0', () => { console.log(`✅ Servidor corriendo en puerto ${PORT}`); console.log(`📱 Sitio: https://wilmer.tutoflix.com`); console.log(`🔐 Admin: https://wilmer.tutoflix.com/admin`); });