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 = process.env.REDIS_URL || 'https://poetic-goshawk-11700.upstash.io'; const UPSTASH_TOKEN = process.env.REDIS_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/${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/${key}`, { method: 'POST', headers: { 'Authorization': `Bearer ${UPSTASH_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ value: value, ex: ttl }) }); return response.ok; } catch (error) { console.error('Cache set error:', error.message); return false; } } async function invalidateCache(pattern) { try { // Get all keys matching pattern const response = await fetch(`${UPSTASH_URL}/keys/${pattern}*`, { headers: { 'Authorization': `Bearer ${UPSTASH_TOKEN}` } }); if (response.ok) { const data = await response.json(); const keys = data.result || []; // Delete all matching keys for (const key of keys) { await fetch(`${UPSTASH_URL}/del/${key}`, { method: 'POST', headers: { 'Authorization': `Bearer ${UPSTASH_TOKEN}` } }); } } } catch (error) { console.error('Cache invalidation error:', error.message); } } // Middleware for Upstash caching async function upstashCacheMiddleware(req, res, next) { // Skip caching for POST, PUT, DELETE requests if (req.method !== 'GET') { return next(); } // Skip caching for admin or special URLs if (req.path.includes('/api/') || req.path.includes('/admin')) { return next(); } const cacheKey = getCacheKey(req.url); const ext = getFileExtension(req.path); // Try to get from cache const cached = await getFromCache(cacheKey); if (cached) { // Parse cached data try { const cachedData = JSON.parse(cached); // Set cached headers res.set(cachedData.headers); res.set('X-Cache', 'HIT'); res.set('X-Cache-Key', cacheKey); // Send cached content if (cachedData.type === 'file') { res.type(cachedData.contentType).send(Buffer.from(cachedData.content, 'base64')); } else { res.send(cachedData.content); } console.log(`Cache HIT: ${req.path}`); return; } catch (error) { console.error('Cache parse error:', error); } } // Cache MISS - continue with normal processing console.log(`Cache MISS: ${req.path}`); // Override res.send to cache the response const originalSend = res.send; res.send = function(data) { res.send = originalSend; // Cache the response if successful if (res.statusCode === 200) { const ttl = CACHE_TTL[ext] || CACHE_TTL.default; const cacheData = { content: data, headers: { 'Content-Type': res.get('Content-Type') || 'text/html', 'Cache-Control': `public, max-age=${ttl}` }, timestamp: Date.now() }; // Store in cache asynchronously setCache(cacheKey, JSON.stringify(cacheData), ttl).then(success => { if (success) { console.log(`Cached: ${req.path} for ${ttl}s`); } }); } res.set('X-Cache', 'MISS'); res.set('X-Cache-Key', cacheKey); return res.send(data); }; next(); } // HTTP Cache headers middleware (works alongside Upstash) 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(); }); // Apply Upstash caching middleware app.use(upstashCacheMiddleware); // Parse JSON bodies for cache API app.use(express.json()); // Cache management API endpoints app.post('/api/cache/invalidate', async (req, res) => { const { pattern } = req.body; if (!pattern) { return res.status(400).json({ error: 'Pattern required' }); } await invalidateCache(pattern); res.json({ success: true, message: `Cache invalidated for pattern: ${pattern}` }); }); 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 }); } }); // 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'); } } })); // Special handling for root route app.get('/', async (req, res) => { const indexPath = path.join(__dirname, 'index.html'); // Check cache first const cacheKey = getCacheKey('/'); const cached = await getFromCache(cacheKey); if (cached) { try { const cachedData = JSON.parse(cached); res.set(cachedData.headers); res.set('X-Cache', 'HIT'); res.send(cachedData.content); console.log('Homepage served from cache'); return; } catch (error) { console.error('Cache error:', error); } } // Read and cache the file fs.readFile(indexPath, 'utf8', async (err, content) => { if (err) { res.status(404).send('Page not found'); return; } // Cache the content const cacheData = { content: content, headers: { 'Content-Type': 'text/html', 'Cache-Control': 'public, max-age=3600' }, timestamp: Date.now() }; await setCache(cacheKey, JSON.stringify(cacheData), CACHE_TTL.html); res.set('X-Cache', 'MISS'); res.send(content); console.log('Homepage cached'); }); }); // Handle directory routes app.get('*', (req, res) => { const requestedPath = path.join(__dirname, req.path); if (fs.existsSync(requestedPath) && fs.statSync(requestedPath).isFile()) { res.sendFile(requestedPath); return; } if (fs.existsSync(requestedPath) && fs.statSync(requestedPath).isDirectory()) { const indexPath = path.join(requestedPath, 'index.html'); if (fs.existsSync(indexPath)) { res.sendFile(indexPath); } 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}`); });