const express = require('express'); const path = require('path'); const fs = require('fs'); const crypto = require('crypto'); const app = express(); const PORT = process.env.PORT || 8888; // Upstash Redis Configuration const UPSTASH_URL = 'https://poetic-goshawk-11700.upstash.io'; const UPSTASH_TOKEN = 'AS20AAIjcDE5YTVjY2VhZDJjOWY0ZDQyOTMzMTlkODFjNjkzZWZmOXAxMA'; // Cache TTL configurations (in seconds) const CACHE_TTL = { html: 3600, // 1 hour for HTML pages css: 86400, // 1 day for CSS js: 86400, // 1 day for JS images: 604800, // 1 week for images api: 1800, // 30 minutes for API responses default: 3600 // 1 hour default }; // Helper function to get cache key function getCacheKey(url) { return `page_${crypto.createHash('md5').update(url).digest('hex')}`; } // Helper function to get file extension function getFileExtension(filePath) { return path.extname(filePath).toLowerCase().slice(1) || 'html'; } // Upstash cache operations async function getFromCache(key) { try { const response = await fetch(`${UPSTASH_URL}/get/${encodeURIComponent(key)}`, { headers: { 'Authorization': `Bearer ${UPSTASH_TOKEN}` } }); if (response.ok) { const data = await response.json(); return data.result; } } catch (error) { console.error('Cache get error:', error.message); } return null; } async function setCache(key, value, ttl = CACHE_TTL.default) { try { const response = await fetch(`${UPSTASH_URL}/set/${encodeURIComponent(key)}/EX/${ttl}`, { method: 'POST', headers: { 'Authorization': `Bearer ${UPSTASH_TOKEN}`, 'Content-Type': 'text/plain' }, body: value }); return response.ok; } catch (error) { console.error('Cache set error:', error.message); return false; } } // Parse JSON bodies for cache API app.use(express.json()); // HTTP Cache headers middleware app.use((req, res, next) => { const ext = path.extname(req.path).toLowerCase(); // Set browser cache headers based on file type if (['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico'].includes(ext)) { res.setHeader('Cache-Control', 'public, max-age=604800, immutable'); // 1 week } else if (['.css', '.js'].includes(ext) && !req.path.includes('animations.js')) { res.setHeader('Cache-Control', 'public, max-age=86400, must-revalidate'); // 1 day } else if (ext === '.html' || req.path === '/') { res.setHeader('Cache-Control', 'public, max-age=3600, must-revalidate'); // 1 hour } else if (['.woff', '.woff2', '.ttf', '.eot'].includes(ext)) { res.setHeader('Cache-Control', 'public, max-age=2592000, immutable'); // 30 days } // Security headers res.setHeader('X-Frame-Options', 'SAMEORIGIN'); res.setHeader('X-XSS-Protection', '1; mode=block'); res.setHeader('X-Content-Type-Options', 'nosniff'); next(); }); // Cache management API endpoint for stats app.get('/api/cache/stats', async (req, res) => { try { const response = await fetch(`${UPSTASH_URL}/dbsize`, { headers: { 'Authorization': `Bearer ${UPSTASH_TOKEN}` } }); if (response.ok) { const data = await response.json(); res.json({ success: true, stats: { keys: data.result || 0, connected: true, backend: 'Upstash Redis' } }); } else { res.json({ success: false, error: 'Failed to get stats' }); } } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // Middleware for Upstash caching - simplified version async function upstashCacheMiddleware(req, res, next) { // Skip caching for non-GET requests if (req.method !== 'GET') { return next(); } // Skip caching for API endpoints if (req.path.startsWith('/api/')) { return next(); } const cacheKey = getCacheKey(req.url); const ext = getFileExtension(req.path); // Try to get from cache const cached = await getFromCache(cacheKey); if (cached) { // Set cache hit header res.set('X-Cache', 'HIT'); res.set('X-Cache-Key', cacheKey); res.set('Content-Type', ext === 'html' ? 'text/html' : `text/${ext}`); res.send(cached); console.log(`Cache HIT: ${req.path}`); return; } // Cache MISS - continue with normal processing console.log(`Cache MISS: ${req.path}`); res.set('X-Cache', 'MISS'); res.set('X-Cache-Key', cacheKey); next(); } // Apply Upstash caching middleware for HTML pages only app.get('/', upstashCacheMiddleware, async (req, res) => { const indexPath = path.join(__dirname, 'index.html'); fs.readFile(indexPath, 'utf8', async (err, content) => { if (err) { res.status(404).send('Page not found'); return; } // Cache the content if it was a miss if (res.get('X-Cache') === 'MISS') { const cacheKey = getCacheKey(req.url); await setCache(cacheKey, content, CACHE_TTL.html); console.log('Homepage cached'); } res.send(content); }); }); // Serve static files with caching app.use(express.static(__dirname, { etag: true, lastModified: true, setHeaders: (res, filePath) => { if (filePath.endsWith('.html')) { res.setHeader('X-Powered-By', 'Odoo Expertos with Upstash Cache'); } } })); // Handle directory routes app.get('*', async (req, res) => { const requestedPath = path.join(__dirname, req.path); // Check cache for HTML pages if (req.path.endsWith('/') || req.path.endsWith('.html')) { const cacheKey = getCacheKey(req.url); const cached = await getFromCache(cacheKey); if (cached) { res.set('X-Cache', 'HIT'); res.set('Content-Type', 'text/html'); res.send(cached); console.log(`Cache HIT: ${req.path}`); return; } } if (fs.existsSync(requestedPath) && fs.statSync(requestedPath).isFile()) { const content = fs.readFileSync(requestedPath, 'utf8'); // Cache HTML files if (requestedPath.endsWith('.html')) { const cacheKey = getCacheKey(req.url); await setCache(cacheKey, content, CACHE_TTL.html); res.set('X-Cache', 'MISS'); console.log(`Cached: ${req.path}`); } res.sendFile(requestedPath); return; } if (fs.existsSync(requestedPath) && fs.statSync(requestedPath).isDirectory()) { const indexPath = path.join(requestedPath, 'index.html'); if (fs.existsSync(indexPath)) { const content = fs.readFileSync(indexPath, 'utf8'); // Cache the directory index const cacheKey = getCacheKey(req.url); await setCache(cacheKey, content, CACHE_TTL.html); res.set('X-Cache', 'MISS'); console.log(`Cached directory index: ${req.path}`); res.send(content); } else { res.status(404).send('Directory index not found'); } } else { const notFoundPath = path.join(__dirname, '404.html'); if (fs.existsSync(notFoundPath)) { res.status(404).sendFile(notFoundPath); } else { res.status(404).send('Page not found'); } } }); app.listen(PORT, () => { console.log(` ╔════════════════════════════════════════════════════════════╗ ║ ║ ║ 🚀 Odoo Expertos with Upstash Redis Cache ║ ║ ║ ║ Local URL: http://localhost:${PORT} ║ ║ ║ ║ ✅ Upstash Redis caching active ║ ║ ✅ Global page caching enabled ║ ║ ✅ HTTP cache headers configured ║ ║ ✅ Performance optimizations active ║ ║ ║ ║ Cache Stats: http://localhost:${PORT}/api/cache/stats ║ ║ ║ ║ Press Ctrl+C to stop the server ║ ║ ║ ╚════════════════════════════════════════════════════════════╝ `); // Test Upstash connection getFromCache('test').then(() => { console.log('✅ Upstash Redis connected successfully'); }).catch(error => { console.error('⚠️ Upstash connection error:', error.message); }); // Open browser automatically const opener = require('opener'); opener(`http://localhost:${PORT}`); });