|
|
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; |
|
|
|
|
|
|
|
|
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'}`); |
|
|
|
|
|
|
|
|
const cspDirectives = { |
|
|
defaultSrc: ["'self'"], |
|
|
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"], |
|
|
fontSrc: ["'self'", "https://fonts.gstatic.com", "data:"], |
|
|
scriptSrc: ["'self'", "'unsafe-inline'"], |
|
|
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'); |
|
|
|
|
|
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 |
|
|
}, |
|
|
crossOriginEmbedderPolicy: false, |
|
|
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 |
|
|
})); |
|
|
|
|
|
|
|
|
|
|
|
const allowedOrigins = [ |
|
|
'http://localhost:7860', |
|
|
'http://localhost:3000', |
|
|
'https://*.hf.space', |
|
|
'https://*.huggingface.co' |
|
|
]; |
|
|
|
|
|
const corsOptions = { |
|
|
origin: function (origin, callback) { |
|
|
|
|
|
if (!origin) return callback(null, true); |
|
|
|
|
|
if (isHuggingFace) { |
|
|
|
|
|
if (origin.includes('hf.space') || origin.includes('huggingface.co')) { |
|
|
return callback(null, true); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (origin.match(/^http:\/\/(localhost|127\.0\.0\.1|192\.168\.\d+\.\d+|10\.\d+\.\d+\.\d+)(:\d+)?$/)) { |
|
|
return callback(null, true); |
|
|
} |
|
|
|
|
|
|
|
|
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 |
|
|
}; |
|
|
|
|
|
app.use(cors(corsOptions)); |
|
|
|
|
|
|
|
|
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(); |
|
|
}); |
|
|
|
|
|
|
|
|
app.use(compression({ |
|
|
|
|
|
threshold: 1024, |
|
|
|
|
|
level: 6, |
|
|
|
|
|
filter: (req, res) => { |
|
|
if (req.headers['x-no-compression']) { |
|
|
return false; |
|
|
} |
|
|
return compression.filter(req, res); |
|
|
} |
|
|
})); |
|
|
|
|
|
app.use(express.json()); |
|
|
|
|
|
|
|
|
app.use(express.static(path.join(__dirname, 'public'), { |
|
|
|
|
|
maxAge: '1y', |
|
|
|
|
|
etag: true, |
|
|
|
|
|
lastModified: true, |
|
|
|
|
|
setHeaders: (res, path, stat) => { |
|
|
|
|
|
if (path.match(/\.(js|css)$/)) { |
|
|
res.set('Cache-Control', 'public, max-age=31536000, immutable'); |
|
|
} |
|
|
|
|
|
else if (path.endsWith('.html')) { |
|
|
res.set('Cache-Control', 'public, max-age=3600, must-revalidate'); |
|
|
} |
|
|
|
|
|
else if (path.match(/\.(jpg|jpeg|png|gif|svg|webp|ico)$/)) { |
|
|
res.set('Cache-Control', 'public, max-age=604800'); |
|
|
} |
|
|
|
|
|
else if (path.match(/\.(woff|woff2|ttf|eot)$/)) { |
|
|
res.set('Cache-Control', 'public, max-age=31536000'); |
|
|
} |
|
|
|
|
|
else if (path.match(/\.(json|webmanifest)$/) || path.endsWith('sw.js')) { |
|
|
res.set('Cache-Control', 'public, max-age=86400'); |
|
|
} |
|
|
} |
|
|
})); |
|
|
|
|
|
|
|
|
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() }); |
|
|
}); |
|
|
|
|
|
|
|
|
app.post('/api/progress', (req, res) => { |
|
|
|
|
|
res.json({ success: true, message: 'Progress saved' }); |
|
|
}); |
|
|
|
|
|
app.get('/api/progress/:day', (req, res) => { |
|
|
|
|
|
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`); |
|
|
}); |
|
|
|
|
|
|
|
|
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); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
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); |
|
|
}); |
|
|
|