- Added a middleware to enforce HTTPS and canonical host for known public hosts, enhancing security and SEO. - Introduced a function to generate the sitemap.xml dynamically, improving the delivery of SEO data to crawlers. - Updated the sitemap route to utilize the new function, ensuring stable delivery and error handling. These changes collectively enhance the application's SEO capabilities and ensure secure access in production environments.
179 lines
5.6 KiB
JavaScript
179 lines
5.6 KiB
JavaScript
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');
|
|
}
|
|
});
|
|
|