diff --git a/backend/server.js b/backend/server.js index 4a0e710..3d3b05f 100644 --- a/backend/server.js +++ b/backend/server.js @@ -60,12 +60,14 @@ const port = process.env.PORT || 3005; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +// CORS-Konfiguration - Socket.IO hat seine eigene CORS-Konfiguration app.use(cors({ origin: true, credentials: true, methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'authcode', 'userid'] })); + app.use(express.json()); // Request Logging Middleware - loggt alle API-Requests @@ -117,6 +119,11 @@ app.use('/api/training-times', trainingTimeRoutes); // Middleware für dynamischen kanonischen Tag (vor express.static) const setCanonicalTag = (req, res, next) => { + // Socket.IO-Requests komplett ignorieren + if (req.path.startsWith('/socket.io/')) { + return next(); + } + // Nur für HTML-Anfragen (nicht für API, Assets, etc.) if (req.path.startsWith('/api') || req.path.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|mp3|webmanifest|xml|txt)$/)) { return next(); @@ -316,13 +323,13 @@ app.use((err, req, res, next) => { // Erstelle HTTP-Server für API const httpServer = createServer(app); - httpServer.listen(port, () => { - console.log(`🚀 HTTP-Server läuft auf Port ${port}`); - }); - + + // WICHTIG: Socket.IO muss VOR dem Server-Start initialisiert werden + // damit es Upgrade-Requests abfangen kann + let socketIOInitialized = false; + // Erstelle HTTPS-Server für Socket.IO (direkt mit SSL) const httpsPort = process.env.HTTPS_PORT || 3051; - let socketIOInitialized = false; // Prüfe, ob SSL-Zertifikate vorhanden sind const sslKeyPath = '/etc/letsencrypt/live/tt-tagebuch.de/privkey.pem'; @@ -335,28 +342,51 @@ app.use((err, req, res, next) => { cert: fs.readFileSync(sslCertPath) }; + // Erstelle HTTPS-Server mit Express-App const httpsServer = https.createServer(httpsOptions, app); - // Initialisiere Socket.IO auf HTTPS-Server + // Initialisiere Socket.IO auf HTTPS-Server VOR dem Listen initializeSocketIO(httpsServer); socketIOInitialized = true; httpsServer.listen(httpsPort, '0.0.0.0', () => { console.log(`🚀 HTTPS-Server für Socket.IO läuft auf Port ${httpsPort}`); + console.log(` Socket.IO Endpoint: https://tt-tagebuch.de:${httpsPort}/socket.io/`); + }); + + // Error-Handling für HTTPS-Server + httpsServer.on('error', (err) => { + console.error('❌ HTTPS-Server Error:', err.message); + console.error(' Code:', err.code); + }); + + httpsServer.on('clientError', (err, socket) => { + if (socket && !socket.destroyed) { + console.error('❌ HTTPS-Server Client Error:', err.message); + socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); + } }); } catch (err) { console.error('⚠️ HTTPS-Server konnte nicht gestartet werden:', err.message); + console.error(' Stack:', err.stack); console.log(' → Socket.IO läuft auf HTTP-Server (nur für Entwicklung)'); } } else { console.log('ℹ️ SSL-Zertifikate nicht gefunden - Socket.IO läuft auf HTTP-Server (nur für Entwicklung)'); + console.log(` Erwartete Pfade: ${sslKeyPath}, ${sslCertPath}`); } // Fallback: Socket.IO auf HTTP-Server (wenn noch nicht initialisiert) + // WICHTIG: VOR dem httpServer.listen() initialisieren if (!socketIOInitialized) { initializeSocketIO(httpServer); console.log(' ✅ Socket.IO erfolgreich auf HTTP-Server initialisiert'); } + + // HTTP-Server starten NACH Socket.IO-Initialisierung + httpServer.listen(port, () => { + console.log(`🚀 HTTP-Server läuft auf Port ${port}`); + }); } catch (err) { console.error('Unable to synchronize the database:', err); } diff --git a/backend/services/socketService.js b/backend/services/socketService.js index 0a8e1bc..03418a3 100644 --- a/backend/services/socketService.js +++ b/backend/services/socketService.js @@ -3,46 +3,140 @@ import { Server } from 'socket.io'; let io = null; export const initializeSocketIO = (httpServer) => { + // Verhindere doppelte Initialisierung + if (io) { + console.warn('⚠️ Socket.IO wurde bereits initialisiert. Überspringe erneute Initialisierung.'); + return io; + } + io = new Server(httpServer, { cors: { - origin: true, + origin: (origin, callback) => { + // Erlaube alle Origins für Socket.IO (da wir direkten HTTPS-Zugang haben) + callback(null, true); + }, credentials: true, - methods: ['GET', 'POST'] + methods: ['GET', 'POST'], + allowedHeaders: ['Content-Type', 'Authorization', 'authcode', 'userid'] }, // Wichtig für Reverse Proxy (Apache/Nginx): Path und Transports explizit setzen path: '/socket.io/', - transports: ['websocket', 'polling'], + transports: ['polling', 'websocket'], // Polling zuerst für bessere Kompatibilität // Erlaube Upgrade von polling zu websocket allowUpgrades: true, - // Timeout für Upgrade - upgradeTimeout: 10000, + // Timeout für Upgrade - erhöht für bessere Kompatibilität + upgradeTimeout: 30000, // Ping-Timeout für Verbindungen pingTimeout: 60000, pingInterval: 25000, // Wichtig für Reverse Proxy: Erlaube WebSocket-Upgrades auch ohne Origin-Header allowEIO3: true, // Erlaube WebSocket-Upgrades auch wenn der Request von einem Proxy kommt - serveClient: false + serveClient: false, + // Verbesserte Cookie-Handling für Cross-Origin + cookie: { + name: 'io', + httpOnly: false, + sameSite: 'lax', + path: '/' + }, + // Erlaube Cross-Origin-Verbindungen + connectTimeout: 45000, + // Erlaube WebSocket-Upgrades auch bei Cross-Origin + perMessageDeflate: false, // Deaktiviert für bessere Kompatibilität + maxHttpBufferSize: 1e6 // 1MB + }); + + // Verbesserte WebSocket-Upgrade-Logging + io.engine.on('upgrade', (req, socket, head) => { + console.log(`🔄 Socket.IO Upgrade-Versuch: ${req.url}`); + console.log(` Headers:`, req.headers); + }); + + io.engine.on('upgradeError', (err, req, socket, head) => { + console.error('❌ Socket.IO Upgrade Error:', err.message || err); + if (req) { + console.error(' URL:', req.url); + console.error(' Headers:', req.headers); + console.error(' Method:', req.method); + } + if (err.stack) { + console.error(' Stack:', err.stack); + } + }); + + // Error-Handling für Socket.IO + io.engine.on('connection_error', (err) => { + // "Session ID unknown" ist normal nach Server-Neustarts - weniger alarmierend loggen + if (err.message && err.message.includes('Session ID unknown')) { + console.warn('⚠️ Socket.IO: Unbekannte Session-ID (normal nach Server-Neustart)'); + if (err.context && err.context.sid) { + console.warn(` Session-ID: ${err.context.sid}`); + } + // Client wird automatisch reconnecten - kein weiteres Action nötig + return; + } + + // Andere Fehler normal loggen + console.error('❌ Socket.IO Connection Error:', err.message || err); + if (err.req) { + console.error(' Request:', err.req.url); + console.error(' Method:', err.req.method); + } + console.error(' Code:', err.code); + console.error(' Type:', err.type); + console.error(' Context:', err.context); + if (err.stack) { + console.error(' Stack:', err.stack); + } + }); + + // Detailliertes Request-Logging für Debugging + io.engine.on('initial_headers', (headers, req) => { + // Logge nur bei Problemen + if (process.env.DEBUG_SOCKETIO === 'true') { + console.log('📡 Socket.IO Request:', req.method, req.url); + } }); io.on('connection', (socket) => { + console.log(`✅ Socket.IO Client verbunden: ${socket.id}`); + console.log(` Transport: ${socket.conn.transport.name}`); + console.log(` Origin: ${socket.handshake.headers.origin || 'unknown'}`); + + // Logge Transport-Upgrades + socket.conn.on('upgrade', () => { + console.log(`🔄 Socket.IO Transport Upgrade für ${socket.id}: ${socket.conn.transport.name}`); + }); + + socket.conn.on('upgradeError', (error) => { + console.error(`❌ Socket.IO Transport Upgrade Error für ${socket.id}:`, error.message); + }); + // Client tritt einem Club-Raum bei socket.on('join-club', (clubId) => { const room = `club-${clubId}`; socket.join(room); + console.log(` Client ${socket.id} tritt Raum bei: ${room}`); }); // Client verlässt einen Club-Raum socket.on('leave-club', (clubId) => { const room = `club-${clubId}`; socket.leave(room); + console.log(` Client ${socket.id} verlässt Raum: ${room}`); }); - socket.on('disconnect', () => { - // Socket getrennt + socket.on('disconnect', (reason) => { + console.log(`❌ Socket.IO Client getrennt: ${socket.id}, Grund: ${reason}`); + }); + + socket.on('error', (error) => { + console.error(`❌ Socket.IO Client Error für ${socket.id}:`, error); }); }); + console.log('✅ Socket.IO erfolgreich initialisiert'); return io; }; diff --git a/frontend/src/services/socketService.js b/frontend/src/services/socketService.js index 85c9517..f251f6b 100644 --- a/frontend/src/services/socketService.js +++ b/frontend/src/services/socketService.js @@ -2,12 +2,61 @@ import { io } from 'socket.io-client'; import { backendBaseUrl } from '../apiClient.js'; let socket = null; +let isReloading = false; +let suppressLogs = false; + +// Cleanup beim Seitenreload +if (typeof window !== 'undefined') { + window.addEventListener('beforeunload', () => { + isReloading = true; + suppressLogs = true; + if (socket) { + // Trenne die Verbindung beim Reload, aber unterdrücke Logs + socket.removeAllListeners(); + socket.disconnect(); + socket = null; + } + }); + + // Nach dem Reload Logs wieder erlauben (nach kurzer Verzögerung) + window.addEventListener('load', () => { + setTimeout(() => { + isReloading = false; + suppressLogs = false; + }, 1000); + }); +} export const connectSocket = (clubId) => { + // Beim ersten Connect nach Reload, entferne alte Socket.IO Cookies + if (typeof document !== 'undefined' && !socket) { + // Entferne alte Socket.IO Session-Cookies + document.cookie.split(';').forEach((cookie) => { + const eqPos = cookie.indexOf('='); + const name = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim(); + if (name === 'io') { + document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`; + } + }); + } + + // Wenn bereits eine Verbindung existiert, aber nicht verbunden ist, trenne sie + if (socket && !socket.connected) { + socket.removeAllListeners(); + socket.disconnect(); + socket = null; + } + if (socket && socket.connected) { // Wenn bereits verbunden, verlasse den alten Club-Raum und trete dem neuen bei - if (socket.currentClubId) { + if (socket.currentClubId && socket.currentClubId !== clubId) { socket.emit('leave-club', socket.currentClubId); + socket.currentClubId = clubId; + socket.emit('join-club', clubId); + return socket; + } else if (socket.currentClubId === clubId) { + // Bereits im richtigen Club-Raum - keine Aktion nötig + return socket; } } else { // Neue Verbindung erstellen @@ -33,27 +82,98 @@ export const connectSocket = (clubId) => { transports: ['polling', 'websocket'], // Polling zuerst für bessere Kompatibilität, dann WebSocket reconnection: true, reconnectionDelay: 1000, - reconnectionAttempts: 5, + reconnectionDelayMax: 5000, + reconnectionAttempts: Infinity, // Unbegrenzte Versuche timeout: 20000, upgrade: true, forceNew: false, secure: isHttps, // Nur für HTTPS - rejectUnauthorized: false // Für selbst-signierte Zertifikate (nur Entwicklung) + rejectUnauthorized: false, // Für selbst-signierte Zertifikate (nur Entwicklung) + // Verbesserte Cookie-Handling + withCredentials: true, + // Auto-Connect + autoConnect: true }); socket.on('connect', () => { + console.log('✅ Socket.IO verbunden:', socket.id); + console.log(' Transport:', socket.io.engine.transport.name); // Wenn bereits ein Club ausgewählt war, trete dem Raum bei if (socket.currentClubId) { socket.emit('join-club', socket.currentClubId); } }); - socket.on('disconnect', () => { - // Socket getrennt + socket.on('disconnect', (reason) => { + // Beim Reload oder wenn Logs unterdrückt werden sollen, keine Logs + if (isReloading || suppressLogs) { + return; + } + + // "transport error" ist normal beim Reload - unterdrücke diese Logs komplett + if (reason === 'transport error' || reason === 'ping timeout') { + return; // Keine Logs für normale Transport-Fehler + } + + // Nur wichtige Disconnects loggen + if (reason !== 'io client disconnect' && reason !== 'transport close') { + console.log('❌ Socket.IO getrennt:', reason); + } + + if (reason === 'io server disconnect') { + // Server hat die Verbindung getrennt, neu verbinden + socket.connect(); + } }); socket.on('connect_error', (error) => { - console.error('Socket.IO Verbindungsfehler:', error); + // Beim Reload keine Error-Logs + if (isReloading || suppressLogs) { + return; + } + + // "xhr poll error" oder "Session ID unknown" sind normal - unterdrücke diese + if (error.message && ( + error.message.includes('Session ID unknown') || + error.message.includes('xhr poll error') || + error.type === 'TransportError' + )) { + // Keine Logs für normale Verbindungsfehler + return; + } + + // Nur unerwartete Fehler loggen + console.error('❌ Socket.IO Verbindungsfehler:', error.message); + console.error(' Type:', error.type); + console.error(' Description:', error.description); + }); + + socket.on('reconnect', (attemptNumber) => { + // Beim Reload keine Reconnect-Logs + if (!isReloading && !suppressLogs) { + console.log('✅ Socket.IO wieder verbunden nach', attemptNumber, 'Versuch(en)'); + } + }); + + socket.on('reconnect_attempt', (attemptNumber) => { + // Reconnect-Versuche nicht loggen - zu viele Logs + // if (!isReloading && !suppressLogs) { + // console.log('🔄 Socket.IO Verbindungsversuch', attemptNumber); + // } + }); + + socket.on('reconnect_error', (error) => { + // Beim Reload keine Error-Logs + if (!isReloading && !suppressLogs) { + console.error('❌ Socket.IO Wiederverbindungsfehler:', error.message); + } + }); + + socket.on('reconnect_failed', () => { + // Nur kritische Fehler loggen + if (!isReloading && !suppressLogs) { + console.error('❌ Socket.IO Wiederverbindung fehlgeschlagen - alle Versuche aufgebraucht'); + } }); } @@ -71,6 +191,7 @@ export const disconnectSocket = () => { if (socket.currentClubId) { socket.emit('leave-club', socket.currentClubId); } + socket.removeAllListeners(); socket.disconnect(); socket = null; } diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 2e3703e..f24d931 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -13,6 +13,11 @@ export default defineConfig({ port: 5000, watch: { usePolling: true, + }, + hmr: { + protocol: 'ws', + host: 'localhost', + port: 5000, } }, });