Spaces:
Runtime error
Runtime error
| import express from 'express'; | |
| import cors from 'cors'; | |
| import morgan from 'morgan'; | |
| import helmet from 'helmet'; | |
| import { errorHandler, notFoundHandler } from './middleware/errorHandler.js'; | |
| import { apiLimiter } from './middleware/rateLimiter.js'; | |
| import userRoutes from './routes/users.js'; | |
| import assessmentRoutes from './routes/assessments.js'; | |
| import recommendationRoutes from './routes/recommendations.js'; | |
| import simulationRoutes from './routes/simulations.js'; | |
| import aiRoutes from './routes/ai.js'; | |
| const app = express(); | |
| // Trust Railway/Vercel reverse proxy for accurate IP and HTTPS | |
| app.set('trust proxy', 1); | |
| // ββ Security Headers (Helmet) βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // BACKEND_PUBLIC_URL: the public URL of this API server. | |
| // Keeping it in an env var means the CSP isn't hard-coded to one Railway deployment. | |
| // Falls back to the original Railway URL so existing deployments keep working. | |
| const backendPublicUrl = | |
| process.env.BACKEND_PUBLIC_URL || 'https://carbon-production-49fd.up.railway.app'; | |
| app.use( | |
| helmet({ | |
| contentSecurityPolicy: { | |
| directives: { | |
| defaultSrc: ["'self'"], | |
| scriptSrc: ["'self'"], | |
| // NOTE: 'unsafe-inline' is required for styleSrc to allow: | |
| // 1. Vite's dynamic style injection in development. | |
| // 2. Tailwind's utility class injection at runtime. | |
| // 3. Google Fonts dynamic CSS stylesheet generation. | |
| styleSrc: ["'self'", 'https://fonts.googleapis.com', "'unsafe-inline'"], | |
| fontSrc: ["'self'", 'https://fonts.gstatic.com'], | |
| connectSrc: ["'self'", backendPublicUrl, '*.vercel.app', 'http://localhost:*'], | |
| imgSrc: ["'self'", 'data:'], | |
| objectSrc: ["'none'"], | |
| frameSrc: ["'none'"], | |
| upgradeInsecureRequests: [], | |
| }, | |
| }, | |
| crossOriginEmbedderPolicy: false, | |
| crossOriginOpenerPolicy: { policy: 'same-origin-allow-popups' }, | |
| crossOriginResourcePolicy: { policy: 'same-origin' }, | |
| hsts: { maxAge: 31536000, includeSubDomains: true, preload: true }, | |
| noSniff: true, | |
| // xssFilter removed β deprecated in Helmet 7 and no longer effective in modern browsers. | |
| referrerPolicy: { policy: 'strict-origin-when-cross-origin' }, | |
| // permittedCrossDomainPolicies removed β was a no-op (false is not a valid option). | |
| frameguard: { action: 'deny' }, | |
| }) | |
| ); | |
| // ββ Permissions-Policy Header βββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Restrict access to sensitive browser APIs that this application does not use. | |
| app.use((req, res, next) => { | |
| res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), payment=()'); | |
| next(); | |
| }); | |
| // ββ CORS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const corsOrigin = process.env.CORS_ORIGIN || 'http://localhost:5173'; | |
| const corsOrigins = corsOrigin | |
| .split(',') | |
| .map((o) => o.trim()) | |
| .filter(Boolean); | |
| app.use( | |
| cors({ | |
| origin: (origin, callback) => { | |
| if (!origin) return callback(null, true); | |
| if (origin.startsWith('http://localhost:') || origin.startsWith('http://127.0.0.1:')) { | |
| return callback(null, true); | |
| } | |
| const isAllowed = | |
| corsOrigins.some((allowed) => origin === allowed) || origin.endsWith('.vercel.app'); | |
| callback(null, isAllowed); | |
| }, | |
| methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], | |
| allowedHeaders: ['Content-Type', 'Authorization'], | |
| credentials: true, | |
| }) | |
| ); | |
| // ββ Body Parsing ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.use(express.json({ limit: '100kb' })); | |
| app.use(express.urlencoded({ extended: true, limit: '100kb' })); | |
| // ββ Logging βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if (process.env.NODE_ENV !== 'test') { | |
| app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev')); | |
| } | |
| // ββ Rate Limiting βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.use('/api/', apiLimiter); | |
| // ββ Health Check ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.get('/health', (req, res) => { | |
| res.json({ | |
| status: 'ok', | |
| timestamp: new Date().toISOString(), | |
| environment: process.env.NODE_ENV || 'development', | |
| version: '1.0.0', | |
| }); | |
| }); | |
| // ββ API Routes ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.use('/api/users', userRoutes); | |
| app.use('/api/assessments', assessmentRoutes); | |
| app.use('/api/recommendations', recommendationRoutes); | |
| app.use('/api/simulations', simulationRoutes); | |
| app.use('/api/ai', aiRoutes); | |
| // ββ 404 & Error Handling ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.use(notFoundHandler); | |
| app.use(errorHandler); | |
| export default app; | |