cors
This commit is contained in:
@@ -17,6 +17,7 @@ const PORT = process.env.PORT || 3000;
|
||||
app.set('trust proxy', true);
|
||||
|
||||
// Security middleware with relaxed CSP for SPA
|
||||
// Note: CSP is relaxed to allow assets to load properly
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: {
|
||||
@@ -26,20 +27,26 @@ app.use(
|
||||
"'self'",
|
||||
"'unsafe-inline'", // Required for Vite HMR and some inline scripts
|
||||
"'unsafe-eval'", // Required for Vite dev mode
|
||||
"https:", // Allow scripts from HTTPS sources
|
||||
"http:", // Allow scripts from HTTP sources (for development)
|
||||
],
|
||||
styleSrc: [
|
||||
"'self'",
|
||||
"'unsafe-inline'", // Required for inline styles
|
||||
"https:", // Allow styles from HTTPS sources
|
||||
"http:", // Allow styles from HTTP sources (for development)
|
||||
],
|
||||
imgSrc: ["'self'", "data:", "https:"],
|
||||
fontSrc: ["'self'", "data:", "https:"],
|
||||
imgSrc: ["'self'", "data:", "https:", "http:"],
|
||||
fontSrc: ["'self'", "data:", "https:", "http:"],
|
||||
connectSrc: ["'self'", "https:", "http:", "ws:", "wss:"], // Allow API calls
|
||||
frameSrc: ["'none'"],
|
||||
objectSrc: ["'none'"],
|
||||
// upgradeInsecureRequests removed - causes issues with reverse proxy
|
||||
// Instead, we rely on the reverse proxy to handle HTTPS
|
||||
},
|
||||
},
|
||||
crossOriginEmbedderPolicy: false, // Disable for better compatibility
|
||||
crossOriginResourcePolicy: { policy: "cross-origin" }, // Allow cross-origin resources
|
||||
})
|
||||
);
|
||||
|
||||
@@ -175,14 +182,37 @@ if (fs.existsSync(frontendDistPath)) {
|
||||
}
|
||||
|
||||
// Serve static files with proper headers for SPA
|
||||
// Use middleware to set headers with access to request object
|
||||
// Middleware to set CORS and security headers for all static assets
|
||||
app.use((req, res, next) => {
|
||||
// Set CORS headers for assets if needed
|
||||
// Get the origin from request (for CORS)
|
||||
const origin = req.headers.origin;
|
||||
const protocol = req.protocol; // 'http' or 'https' (respects X-Forwarded-Proto)
|
||||
const host = req.get('host');
|
||||
|
||||
// Set CORS headers for assets (allow same-origin and configured origins)
|
||||
if (origin) {
|
||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
||||
// Check if origin is allowed
|
||||
const allowedOrigins = getAllowedOrigins();
|
||||
if (isDevelopment || allowedOrigins.includes(origin) || origin.includes(host)) {
|
||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
}
|
||||
} else {
|
||||
// Same-origin request (no origin header), allow it
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
}
|
||||
|
||||
// Security headers for all static assets
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
res.setHeader('X-Frame-Options', 'DENY');
|
||||
|
||||
// Cache control for assets
|
||||
if (req.path.match(/\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$/)) {
|
||||
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
@@ -190,15 +220,20 @@ if (fs.existsSync(frontendDistPath)) {
|
||||
maxAge: '1y', // Cache static assets
|
||||
etag: true,
|
||||
lastModified: true,
|
||||
setHeaders: (res, filePath) => {
|
||||
setHeaders: (res, filePath, stat) => {
|
||||
// Set proper content type for JS/CSS files
|
||||
if (filePath.endsWith('.js')) {
|
||||
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
||||
} else if (filePath.endsWith('.css')) {
|
||||
res.setHeader('Content-Type', 'text/css; charset=utf-8');
|
||||
} else if (filePath.endsWith('.html')) {
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
}
|
||||
|
||||
// Ensure CORS headers are set (in case middleware didn't catch it)
|
||||
if (!res.getHeader('Access-Control-Allow-Origin')) {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
}
|
||||
// Security headers for assets
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
@@ -262,16 +297,57 @@ if (fs.existsSync(frontendDistPath)) {
|
||||
logger.info(`SPA fallback: serving index.html for ${req.path}`);
|
||||
}
|
||||
|
||||
// Set proper headers for index.html
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
|
||||
// Set CORS headers for index.html if needed
|
||||
const origin = req.headers.origin;
|
||||
if (origin) {
|
||||
const allowedOrigins = getAllowedOrigins();
|
||||
if (isDevelopment || allowedOrigins.includes(origin) || origin.includes(req.get('host'))) {
|
||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent caching of index.html (assets are cached, but HTML should not be)
|
||||
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||
res.setHeader('Pragma', 'no-cache');
|
||||
res.setHeader('Expires', '0');
|
||||
|
||||
// Serve frontend SPA
|
||||
res.sendFile(indexHtmlPath, (err) => {
|
||||
// Read the file and potentially rewrite HTTP URLs to HTTPS if needed
|
||||
fs.readFile(indexHtmlPath, 'utf8', (err, html) => {
|
||||
if (err) {
|
||||
logger.error(`❌ Failed to send index.html: ${err.message}`);
|
||||
logger.error(`❌ Failed to read index.html: ${err.message}`);
|
||||
logger.error(`Error stack: ${err.stack}`);
|
||||
res.status(500).json({
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to load frontend',
|
||||
});
|
||||
}
|
||||
|
||||
// If the request is HTTPS (via reverse proxy), rewrite HTTP asset URLs to HTTPS
|
||||
// This fixes mixed content issues
|
||||
const protocol = req.protocol; // Will be 'https' if X-Forwarded-Proto is set correctly
|
||||
if (protocol === 'https') {
|
||||
// Replace absolute HTTP URLs with HTTPS (but keep relative URLs as-is)
|
||||
html = html.replace(/href="http:\/\/([^"]+)"/g, (match, url) => {
|
||||
if (url.includes(req.get('host'))) {
|
||||
return `href="https://${url}"`;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
html = html.replace(/src="http:\/\/([^"]+)"/g, (match, url) => {
|
||||
if (url.includes(req.get('host'))) {
|
||||
return `src="https://${url}"`;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
}
|
||||
|
||||
res.send(html);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user