diff --git a/.gitignore b/.gitignore index 28c7082..2273082 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,10 @@ client/dist/ dist/ docroot/dist/ +# Temporäre Bilder (werden nach 6 Stunden automatisch gelöscht) +tmp/ +uploads/ + # Node.js node_modules/ npm-debug.log* diff --git a/client/src/components/ChatInput.vue b/client/src/components/ChatInput.vue index e7f9cd4..5746226 100644 --- a/client/src/components/ChatInput.vue +++ b/client/src/components/ChatInput.vue @@ -95,21 +95,33 @@ async function handleImageUpload(event) { } try { - // Lese Bild als Base64 - const reader = new FileReader(); - reader.onload = (e) => { - const base64Image = e.target.result; - // Sende Bild als Nachricht - chatStore.sendImage(chatStore.currentConversation, base64Image, file.type); - }; - reader.onerror = (error) => { - console.error('Fehler beim Lesen des Bildes:', error); - alert('Fehler beim Lesen des Bildes'); - }; - reader.readAsDataURL(file); + // Erstelle FormData für Upload + const formData = new FormData(); + formData.append('image', file); + + // Lade Bild hoch + const response = await fetch('/api/upload-image', { + method: 'POST', + body: formData, + credentials: 'include' // Wichtig für Session-Cookies + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: 'Unbekannter Fehler' })); + throw new Error(errorData.error || 'Fehler beim Hochladen des Bildes'); + } + + const data = await response.json(); + + if (data.success && data.code) { + // Sende nur den Code über Socket.IO + chatStore.sendImage(chatStore.currentConversation, data.code, data.url); + } else { + throw new Error('Ungültige Antwort vom Server'); + } } catch (error) { console.error('Fehler beim Bild-Upload:', error); - alert('Fehler beim Bild-Upload'); + alert('Fehler beim Bild-Upload: ' + error.message); } // Input zurücksetzen, damit das gleiche Bild erneut ausgewählt werden kann diff --git a/client/src/stores/chat.js b/client/src/stores/chat.js index a025542..9a65b6b 100644 --- a/client/src/stores/chat.js +++ b/client/src/stores/chat.js @@ -226,17 +226,19 @@ export const useChatStore = defineStore('chat', () => { case 'message': // Debug-Logging für empfangene Nachrichten if (data.isImage) { - console.log('[Bild empfangen] Von:', data.from, 'Typ:', data.imageType, 'Größe:', data.message ? data.message.length : 0, 'Zeichen'); + console.log('[Bild empfangen] Von:', data.from, 'URL:', data.imageUrl || data.message); } if (currentConversation.value === data.from) { const newMessage = { from: data.from, - message: data.message, + message: data.imageUrl || data.message, // Verwende URL für Bilder timestamp: data.timestamp, self: false, isImage: data.isImage || false, - imageType: data.imageType || null + imageType: data.imageType || null, + imageUrl: data.imageUrl || null, + imageCode: data.imageCode || null }; console.log('[Nachricht hinzugefügt]', newMessage); @@ -254,11 +256,13 @@ export const useChatStore = defineStore('chat', () => { currentConversation.value = data.with; messages.value = data.messages.map(msg => ({ from: msg.from, - message: msg.message, + message: msg.imageUrl || msg.message, // Verwende URL für Bilder timestamp: msg.timestamp, self: msg.from === userName.value, isImage: msg.isImage || false, - imageType: msg.imageType || null + imageType: msg.imageType || null, + imageUrl: msg.imageUrl || null, + imageCode: msg.imageCode || null })); break; case 'searchResults': @@ -355,7 +359,7 @@ export const useChatStore = defineStore('chat', () => { resetTimeoutTimer(); } - function sendImage(toUserName, imageData, imageType) { + function sendImage(toUserName, imageCode, imageUrl) { if (!socket.value || !socket.value.connected) { console.error('Socket.IO nicht verbunden'); return; @@ -369,20 +373,20 @@ export const useChatStore = defineStore('chat', () => { const messageId = Date.now().toString(); socket.value.emit('message', { toUserName, - message: imageData, // Base64-kodiertes Bild + message: imageCode, // Nur der Code, nicht das gesamte Bild messageId, isImage: true, - imageType: imageType + imageUrl: imageUrl // URL für das Bild }); - // Lokal hinzufügen + // Lokal hinzufügen (mit URL) messages.value.push({ from: userName.value, - message: imageData, + message: imageUrl, // Verwende URL statt Code für lokale Anzeige timestamp: new Date().toISOString(), self: true, isImage: true, - imageType: imageType + imageCode: imageCode }); // Timeout zurücksetzen bei Aktivität diff --git a/server/broadcast.js b/server/broadcast.js index 3dc18a4..05625cb 100644 --- a/server/broadcast.js +++ b/server/broadcast.js @@ -249,17 +249,23 @@ export function setupBroadcast(io) { socket.emit('connected', connectedData); socket.on('disconnect', (reason) => { + console.log(`[Disconnect] Socket getrennt für Session-ID: ${sessionId}, Grund: ${reason}`); const client = clients.get(sessionId); if (client) { + console.log(`[Disconnect] Client gefunden: ${client.userName || 'unbekannt'}, Socket war verbunden: ${client.socket ? client.socket.connected : 'null'}`); + // Setze Socket auf null, damit keine Nachrichten mehr an diesen Client gesendet werden // ABER: Lösche den Client NICHT, damit die Session beim Reload wiederhergestellt werden kann client.socket = null; // Aktualisiere Benutzerliste, damit andere Clients sehen, dass dieser Benutzer offline ist if (client.userName) { + console.log(`[Disconnect] Aktualisiere Benutzerliste nach Disconnect von ${client.userName}`); broadcastUserList(); } // Client bleibt in der Map, damit Session-Wiederherstellung funktioniert + } else { + console.log(`[Disconnect] Kein Client gefunden für Session-ID: ${sessionId}`); } }); @@ -569,7 +575,7 @@ export function setupBroadcast(io) { return; } - const { toUserName, message, messageId, isImage, imageType } = data; + const { toUserName, message, messageId, isImage, imageType, imageUrl } = data; if (!toUserName) { socket.emit('error', { message: 'Empfänger fehlt' }); @@ -615,36 +621,63 @@ export function setupBroadcast(io) { conversation.push({ from: client.userName, to: toUserName, - message, + message: isImage && imageUrl ? imageUrl : message, // Verwende URL für Bilder messageId, timestamp: new Date().toISOString(), read: false, isImage: isImage || false, - imageType: imageType || null + imageType: imageType || null, + imageUrl: imageUrl || null, + imageCode: isImage ? message : null }); // Sende an Empfänger (wenn online) const messagePayload = { from: client.userName, - message, + message: isImage && imageUrl ? imageUrl : message, // Verwende URL für Bilder messageId, timestamp: new Date().toISOString(), isImage: isImage || false, - imageType: imageType || null + imageType: imageType || null, + imageUrl: imageUrl || null, + imageCode: isImage ? message : null // Code für Server-Referenz }; // Debug-Logging für Bilder if (isImage) { console.log(`[Bild] Sende Bild von ${client.userName} an ${toUserName}, Größe: ${message ? message.length : 0} Zeichen, Typ: ${imageType || 'unbekannt'}`); + console.log(`[Bild] Absender Socket verbunden: ${socket.connected}, Empfänger Socket verbunden: ${receiver.socket ? receiver.socket.connected : 'null'}`); } - receiver.socket.emit('message', messagePayload); + // Prüfe, ob Absender noch verbunden ist + if (!socket.connected) { + console.error(`[Bild] Absender ${client.userName} Socket nicht mehr verbunden beim Senden!`); + return; + } - // Bestätigung an Absender - socket.emit('messageSent', { - messageId, - to: toUserName - }); + // Prüfe, ob Empfänger-Socket noch existiert und verbunden ist + if (!receiver.socket || !receiver.socket.connected) { + console.error(`[Bild] Empfänger ${toUserName} Socket nicht mehr verbunden beim Senden!`); + socket.emit('error', { message: 'Empfänger ist nicht mehr online' }); + return; + } + + try { + receiver.socket.emit('message', messagePayload); + + // Bestätigung an Absender (nur wenn noch verbunden) + if (socket.connected) { + socket.emit('messageSent', { + messageId, + to: toUserName + }); + } + } catch (error) { + console.error(`[Bild] Fehler beim Senden der Nachricht:`, error); + if (socket.connected) { + socket.emit('error', { message: 'Fehler beim Senden der Nachricht' }); + } + } // Aktualisiere ungelesene Nachrichten für den Empfänger updateUnreadCount(receiver); diff --git a/server/routes.js b/server/routes.js index 65249b1..a4931ac 100644 --- a/server/routes.js +++ b/server/routes.js @@ -1,15 +1,171 @@ -import { readFileSync } from 'fs'; +import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync } from 'fs'; import { join } from 'path'; import { parse } from 'csv-parse/sync'; +import multer from 'multer'; +import crypto from 'crypto'; import axios from 'axios'; import { getSessionStatus, getClientsMap, getSessionIdForSocket } from './broadcast.js'; +// Bild-Upload-Konfiguration (temporäres Verzeichnis) +const uploadsDir = join(__dirname, '../tmp'); +if (!existsSync(uploadsDir)) { + mkdirSync(uploadsDir, { recursive: true }); +} + +// Map: Code -> Bild-Info (für temporäre Speicherung) +const imageCodes = new Map(); + +// Multer-Konfiguration für Bild-Upload +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, uploadsDir); + }, + filename: (req, file, cb) => { + // Generiere eindeutigen Code + const code = crypto.randomBytes(16).toString('hex'); + const ext = file.originalname.split('.').pop(); + cb(null, `${code}.${ext}`); + } +}); + +const upload = multer({ + storage: storage, + limits: { + fileSize: 5 * 1024 * 1024 // 5MB Limit + }, + fileFilter: (req, file, cb) => { + // Nur Bilder erlauben + if (file.mimetype.startsWith('image/')) { + cb(null, true); + } else { + cb(new Error('Nur Bilder sind erlaubt'), false); + } + } +}); + export function setupRoutes(app, __dirname) { // Health-Check-Endpoint app.get('/api/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); + + // Bild-Upload-Endpoint + app.post('/api/upload-image', upload.single('image'), (req, res) => { + try { + if (!req.file) { + return res.status(400).json({ error: 'Kein Bild hochgeladen' }); + } + + // Prüfe, ob Benutzer eingeloggt ist + const sessionId = req.sessionID; + const clientsMap = getClientsMap(); + const client = clientsMap.get(sessionId); + + if (!client || !client.userName) { + // Lösche hochgeladenes Bild, wenn nicht eingeloggt + unlinkSync(req.file.path); + return res.status(401).json({ error: 'Nicht eingeloggt' }); + } + + // Generiere eindeutigen Code für das Bild + const code = req.file.filename.split('.')[0]; + const imageInfo = { + code: code, + filename: req.file.filename, + originalName: req.file.originalname, + mimetype: req.file.mimetype, + size: req.file.size, + uploadedBy: client.userName, + uploadedAt: new Date().toISOString(), + expiresAt: new Date(Date.now() + 6 * 60 * 60 * 1000) // 6 Stunden + }; + + // Speichere Bild-Info + imageCodes.set(code, imageInfo); + + console.log(`[Bild-Upload] Bild hochgeladen von ${client.userName}, Code: ${code}`); + + res.json({ + success: true, + code: code, + url: `/api/image/${code}` + }); + } catch (error) { + console.error('Fehler beim Bild-Upload:', error); + if (req.file && existsSync(req.file.path)) { + unlinkSync(req.file.path); + } + res.status(500).json({ error: 'Fehler beim Hochladen des Bildes' }); + } + }); + + // Bild-Download-Endpoint (mit Code) + app.get('/api/image/:code', (req, res) => { + try { + const { code } = req.params; + const imageInfo = imageCodes.get(code); + + if (!imageInfo) { + return res.status(404).json({ error: 'Bild nicht gefunden' }); + } + + // Prüfe Ablaufzeit + if (new Date() > imageInfo.expiresAt) { + // Lösche abgelaufenes Bild + const filePath = join(uploadsDir, imageInfo.filename); + if (existsSync(filePath)) { + unlinkSync(filePath); + } + imageCodes.delete(code); + return res.status(410).json({ error: 'Bild abgelaufen' }); + } + + // Prüfe, ob Benutzer eingeloggt ist (optional, für zusätzliche Sicherheit) + const sessionId = req.sessionID; + const clientsMap = getClientsMap(); + const client = clientsMap.get(sessionId); + + // Wenn eingeloggt, prüfe ob Benutzer berechtigt ist (optional) + // Für jetzt erlauben wir allen eingeloggten Benutzern den Zugriff + + const filePath = join(uploadsDir, imageInfo.filename); + if (!existsSync(filePath)) { + imageCodes.delete(code); + return res.status(404).json({ error: 'Bilddatei nicht gefunden' }); + } + + // Setze Content-Type + res.setHeader('Content-Type', imageInfo.mimetype); + res.setHeader('Content-Disposition', `inline; filename="${imageInfo.originalName}"`); + + // Sende Bild + const imageData = readFileSync(filePath); + res.send(imageData); + } catch (error) { + console.error('Fehler beim Laden des Bildes:', error); + res.status(500).json({ error: 'Fehler beim Laden des Bildes' }); + } + }); + + // Cleanup-Funktion für abgelaufene Bilder (wird regelmäßig aufgerufen) + setInterval(() => { + const now = new Date(); + let deletedCount = 0; + for (const [code, info] of imageCodes.entries()) { + if (now > info.expiresAt) { + const filePath = join(uploadsDir, info.filename); + if (existsSync(filePath)) { + unlinkSync(filePath); + } + imageCodes.delete(code); + deletedCount++; + } + } + if (deletedCount > 0) { + console.log(`[Bild-Cleanup] ${deletedCount} abgelaufene Bild(er) gelöscht`); + } + }, 30 * 60 * 1000); // Alle 30 Minuten prüfen // Session-Status-Endpoint app.get('/api/session', (req, res) => {