232 lines
7.1 KiB
JavaScript
232 lines
7.1 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, URL as NodeURL } 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();
|
|
});
|
|
|
|
// Tracking-/Session-Query entfernen (wtd, js=no), damit Google keine Duplikat-URLs indexiert.
|
|
// Suchkonsole: "Alternative Seite mit richtigem kanonischen Tag" fuer /?wtd=... verschwindet nach Neu-Crawl.
|
|
app.use((req, res, next) => {
|
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
next();
|
|
return;
|
|
}
|
|
if (req.path.startsWith('/api')) {
|
|
next();
|
|
return;
|
|
}
|
|
const host = req.get('host') || 'localhost';
|
|
const proto = req.get('x-forwarded-proto') || req.protocol || 'https';
|
|
const raw = req.originalUrl.split('#')[0];
|
|
let u;
|
|
try {
|
|
u = new NodeURL(raw, `${proto}://${host}`);
|
|
} catch {
|
|
next();
|
|
return;
|
|
}
|
|
const hadWtd = u.searchParams.has('wtd');
|
|
const hadJsNo = u.searchParams.get('js') === 'no';
|
|
if (!hadWtd && !hadJsNo) {
|
|
next();
|
|
return;
|
|
}
|
|
if (hadWtd) u.searchParams.delete('wtd');
|
|
if (hadJsNo) u.searchParams.delete('js');
|
|
const search = u.searchParams.toString();
|
|
const dest = u.pathname + (search ? `?${search}` : '');
|
|
if (dest !== raw) {
|
|
res.redirect(301, dest);
|
|
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',
|
|
'/faq',
|
|
'/regeln',
|
|
'/sicherheit',
|
|
'/datenschutz',
|
|
'/ratgeber',
|
|
'/ratgeber/erste-nachricht',
|
|
'/ratgeber/profil-tipps',
|
|
'/ratgeber/sicher-chatten',
|
|
'/ratgeber/red-flags',
|
|
'/mockup-redesign'
|
|
]);
|
|
app.use(express.static(distPath));
|
|
// Fallback für Vue Router (SPA) - muss am Ende stehen
|
|
app.get('*', (req, res) => {
|
|
// Hinweis: SEO-Routen (/, /partners, …) werden bereits von setupSEORoutes registriert
|
|
// und greifen vor diesem Catch-All. Kein frühes return ohne res.* hier.
|
|
|
|
// 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
|
|
// In Production laeuft Apache als Reverse Proxy auf localhost.
|
|
// In Development kann HOST=0.0.0.0 gesetzt werden, damit echte Android-Geraete
|
|
// im selben WLAN den lokalen Server erreichen.
|
|
const HOST = process.env.HOST || '127.0.0.1';
|
|
|
|
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');
|
|
}
|
|
});
|