new site
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user