K30 / server.js
Raí Santos
oi
98ace4c
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 7860;
// Detect Hugging Face environment and configure accordingly
const isHuggingFace = process.env.SPACE_ID || process.env.SPACE_AUTHOR_NAME ||
process.env.HF_SPACE || process.env.GRADIO_SERVER_NAME ||
(process.env.HOST && process.env.HOST.includes('hf.space'));
console.log(`🌍 Environment: ${isHuggingFace ? 'Hugging Face' : 'Local/Other'}`);
// Secure CSP configuration
const cspDirectives = {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com", "data:"],
scriptSrc: ["'self'", "'unsafe-inline'"], // unsafe-inline needed for inline event handlers
imgSrc: ["'self'", "https:", "data:", "blob:"],
frameSrc: ["'self'", "https://www.youtube-nocookie.com", "https://www.youtube.com"],
connectSrc: ["'self'"],
mediaSrc: ["'self'", "https:", "data:"],
objectSrc: ["'none'"],
childSrc: ["'self'", "https://www.youtube-nocookie.com"],
workerSrc: ["'self'", "blob:"],
manifestSrc: ["'self'"],
baseUri: ["'self'"],
formAction: ["'self'"],
upgradeInsecureRequests: isHuggingFace ? [] : null,
};
if (isHuggingFace) {
console.log('🔒 Hugging Face detected - Using balanced CSP for compatibility and security');
// Add Hugging Face specific domains
cspDirectives.frameSrc.push("https://*.hf.space", "https://*.huggingface.co");
cspDirectives.connectSrc.push("https://*.hf.space", "https://*.huggingface.co");
} else {
console.log('🔒 Local environment - Using strict CSP');
}
app.use(helmet({
contentSecurityPolicy: {
directives: cspDirectives,
reportOnly: false // Set to true for testing, false for enforcement
},
crossOriginEmbedderPolicy: false, // Disabled for YouTube embeds
crossOriginResourcePolicy: { policy: "cross-origin" },
referrerPolicy: { policy: "strict-origin-when-cross-origin" },
strictTransportSecurity: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
xContentTypeOptions: true,
xDnsPrefetchControl: { allow: false },
xDownloadOptions: true,
xFrameOptions: { action: isHuggingFace ? 'sameorigin' : 'deny' },
xPoweredBy: false,
xXssProtection: true
}));
// Configure CORS based on environment
// Whitelist of allowed origins
const allowedOrigins = [
'http://localhost:7860',
'http://localhost:3000',
'https://*.hf.space',
'https://*.huggingface.co'
];
const corsOptions = {
origin: function (origin, callback) {
// Allow requests with no origin (mobile apps, curl, etc.)
if (!origin) return callback(null, true);
if (isHuggingFace) {
// For Hugging Face, allow all hf.space and huggingface.co origins
if (origin.includes('hf.space') || origin.includes('huggingface.co')) {
return callback(null, true);
}
}
// Allow local network IPs (192.168.x.x, 10.x.x.x, etc.)
if (origin.match(/^http:\/\/(localhost|127\.0\.0\.1|192\.168\.\d+\.\d+|10\.\d+\.\d+\.\d+)(:\d+)?$/)) {
return callback(null, true);
}
// Check if origin is in whitelist
const isAllowed = allowedOrigins.some(allowed => {
if (allowed.includes('*')) {
const pattern = allowed.replace(/\*/g, '.*');
return new RegExp(pattern).test(origin);
}
return allowed === origin;
});
if (isAllowed) {
callback(null, true);
} else {
console.log(`Blocked origin: ${origin}`);
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
exposedHeaders: ['Content-Range', 'X-Content-Range'],
maxAge: 600 // Cache preflight requests for 10 minutes
};
app.use(cors(corsOptions));
// Additional security headers
app.use((req, res, next) => {
res.header('X-Content-Type-Options', 'nosniff');
res.header('X-Frame-Options', isHuggingFace ? 'SAMEORIGIN' : 'DENY');
res.header('Referrer-Policy', 'strict-origin-when-cross-origin');
next();
});
// ⚡ Compressão otimizada com Gzip/Brotli
app.use(compression({
// Comprimir todos os responses acima de 1KB
threshold: 1024,
// Nível de compressão (0-9, 6 é o padrão balanceado)
level: 6,
// Filtrar tipos de conteúdo que devem ser comprimidos
filter: (req, res) => {
if (req.headers['x-no-compression']) {
return false;
}
return compression.filter(req, res);
}
}));
app.use(express.json());
// 📦 Servir arquivos estáticos com cache agressivo
app.use(express.static(path.join(__dirname, 'public'), {
// Cache por 1 ano para assets com versão
maxAge: '1y',
// Habilitar ETags
etag: true,
// Habilitar Last-Modified
lastModified: true,
// Configurar cache específico por tipo de arquivo
setHeaders: (res, path, stat) => {
// Cache agressivo para assets versionados (JS, CSS com hash)
if (path.match(/\.(js|css)$/)) {
res.set('Cache-Control', 'public, max-age=31536000, immutable');
}
// Cache moderado para HTML (1 hora)
else if (path.endsWith('.html')) {
res.set('Cache-Control', 'public, max-age=3600, must-revalidate');
}
// Cache longo para imagens
else if (path.match(/\.(jpg|jpeg|png|gif|svg|webp|ico)$/)) {
res.set('Cache-Control', 'public, max-age=604800'); // 1 semana
}
// Cache para fontes
else if (path.match(/\.(woff|woff2|ttf|eot)$/)) {
res.set('Cache-Control', 'public, max-age=31536000'); // 1 ano
}
// Cache para manifest e service worker (1 dia)
else if (path.match(/\.(json|webmanifest)$/) || path.endsWith('sw.js')) {
res.set('Cache-Control', 'public, max-age=86400'); // 1 dia
}
}
}));
// Routes
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
app.get('/health', (req, res) => {
res.json({ status: 'OK', timestamp: new Date().toISOString() });
});
// API endpoints for progress tracking
app.post('/api/progress', (req, res) => {
// In a real app, this would save to a database
res.json({ success: true, message: 'Progress saved' });
});
app.get('/api/progress/:day', (req, res) => {
// In a real app, this would fetch from a database
res.json({ day: req.params.day, completed: [] });
});
const server = app.listen(PORT, '0.0.0.0', () => {
console.log(`🚀 30-Day Keto Planner running on port ${PORT}`);
console.log(`📱 Access at: http://localhost:${PORT}`);
console.log(`✅ Server status: RUNNING`);
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('📴 SIGTERM received, shutting down gracefully');
server.close(() => {
console.log('💤 Process terminated');
process.exit(0);
});
});
process.on('SIGINT', () => {
console.log('📴 SIGINT received, shutting down gracefully');
server.close(() => {
console.log('💤 Process terminated');
process.exit(0);
});
});
// Handle server errors
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`❌ Port ${PORT} is already in use`);
console.log('💡 For local development, try a different port or stop other services');
console.log('🚀 For Hugging Face deployment, port 7860 will work correctly');
} else {
console.error('❌ Server error:', err);
}
process.exit(1);
});