import express from 'express'; import { createServer } from 'http'; import { Server as SocketIOServer } from 'socket.io'; import cookieParser from 'cookie-parser'; import session from 'express-session'; import cors from 'cors'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { setupBroadcast } from './broadcast.js'; import { setupRoutes } from './routes.js'; import { setupSEORoutes } from './routes-seo.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const app = express(); const server = createServer(app); // Umgebungsvariablen const NODE_ENV = process.env.NODE_ENV || 'development'; const PORT = process.env.PORT || (NODE_ENV === 'production' ? 4000 : 3300); const IS_PRODUCTION = NODE_ENV === 'production'; const PRIMARY_HOST = 'www.ypchat.net'; const LEGACY_HOSTS = new Set(['ypchat.net']); // CORS-Origins konfigurieren const allowedOrigins = IS_PRODUCTION ? ['https://ypchat.net', 'https://www.ypchat.net'] : ['http://localhost:5175', 'http://127.0.0.1:5175']; // Socket.IO auf dem gleichen HTTP-Server wie Express const io = new SocketIOServer(server, { cors: { origin: allowedOrigins, credentials: true, methods: ['GET', 'POST'] }, transports: ['websocket', 'polling'], allowEIO3: true, maxHttpBufferSize: 10 * 1024 * 1024, // 10MB für große Bilder (Base64-kodiert) pingTimeout: 60000, pingInterval: 25000 }); console.log('Socket.IO Server initialisiert auf Express-Server'); console.log('Umgebung:', NODE_ENV); console.log('CORS erlaubt für:', allowedOrigins); // CORS-Konfiguration app.use(cors({ origin: (origin, callback) => { // Erlaube Requests ohne Origin (z.B. Postman, mobile Apps) if (!origin) return callback(null, true); if (allowedOrigins.includes(origin) || !IS_PRODUCTION) { callback(null, true); } else { callback(new Error('Nicht erlaubt durch CORS')); } }, credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'] })); // Middleware app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cookieParser()); // Session-Konfiguration const sessionSecret = process.env.SESSION_SECRET || 'singlechat-secret-key-change-in-production'; app.use(session({ secret: sessionSecret, resave: false, saveUninitialized: false, cookie: { secure: IS_PRODUCTION, // true für HTTPS in Production httpOnly: true, maxAge: 24 * 60 * 60 * 1000, // 24 Stunden sameSite: IS_PRODUCTION ? 'lax' : false // Lax für HTTPS, false für Development } })); // Trust Proxy für Apache Reverse Proxy (muss vor Routes stehen) if (IS_PRODUCTION) { app.set('trust proxy', 1); // Vertraue dem ersten Proxy (Apache) // SEO-Fallback: erzwinge kanonischen Host + HTTPS, falls der Proxy es nicht bereits tut. app.use((req, res, next) => { const forwardedHost = String(req.headers['x-forwarded-host'] || '') .split(',')[0] .trim() .toLowerCase(); const rawHost = String(req.headers.host || '') .split(':')[0] .trim() .toLowerCase(); const host = forwardedHost || rawHost; const forwardedProto = String(req.headers['x-forwarded-proto'] || '') .split(',')[0] .trim() .toLowerCase(); const isHttps = forwardedProto ? forwardedProto === 'https' : req.secure; const isKnownPublicHost = host === PRIMARY_HOST || LEGACY_HOSTS.has(host); if (!isKnownPublicHost) { next(); return; } if (!isHttps || host !== PRIMARY_HOST) { res.redirect(301, `https://${PRIMARY_HOST}${req.originalUrl || '/'}`); return; } next(); }); } // Statische Dateien aus docroot app.use('/static', express.static(join(__dirname, '../docroot'))); // SEO-Routes (robots.txt, sitemap.xml, Pre-Rendering) // Müssen vor anderen Routes stehen, damit sie nicht vom SPA-Fallback abgefangen werden setupSEORoutes(app, __dirname); // API Routes (müssen vor SPA-Fallback stehen) setupRoutes(app, __dirname); // Socket.IO-Handling setupBroadcast(io, __dirname); // In Production: Serviere auch die gebauten Client-Dateien // SPA-Fallback muss nach allen anderen Routen stehen if (IS_PRODUCTION) { const distPath = join(__dirname, '../docroot/dist'); const KNOWN_SPA_ROUTES = new Set(['/', '/partners', '/feedback', '/mockup-redesign']); app.use(express.static(distPath)); // Fallback für Vue Router (SPA) - muss am Ende stehen app.get('*', (req, res) => { // Überspringe SEO-Routes in Production (werden bereits von setupSEORoutes behandelt) if (IS_PRODUCTION && (req.path === '/' || req.path === '/partners')) { return; // Route wurde bereits behandelt } // In Production: /src/ Pfade sollten nicht existieren (404) if (IS_PRODUCTION && req.path.startsWith('/src/')) { res.status(404).send('Not found'); return; } // Nur bekannte SPA-Routen als index.html ausliefern, unbekannte Routen mit 404 beantworten if ( !req.path.startsWith('/api') && !req.path.startsWith('/static') && req.path !== '/robots.txt' && req.path !== '/sitemap.xml' && KNOWN_SPA_ROUTES.has(req.path) ) { res.sendFile(join(distPath, 'index.html')); } else { res.status(404).send('Not found'); } }); } // Server starten const HOST = '127.0.0.1'; // Nur localhost, da Apache als Reverse Proxy fungiert server.listen(PORT, HOST, () => { console.log(`Server läuft auf http://${HOST}:${PORT}`); console.log(`Umgebung: ${NODE_ENV}`); console.log(`CORS erlaubt für: ${allowedOrigins.join(', ')}`); if (IS_PRODUCTION) { console.log('Production-Modus: HTTPS über Apache Reverse Proxy'); } });