- Updated styles in style.css to improve overall design consistency and introduced CSS variables for better theming. - Refined ChatWindow.vue with improved no-conversation styling and adjusted image borders for a cleaner look. - Enhanced HistoryView.vue and InboxView.vue with new panel styles for better user experience and readability. - Revamped LoginForm.vue to provide a more engaging user interface with a landing page layout and cookie-based profile persistence. - Improved MenuBar.vue and SearchView.vue with active state indicators and refined item displays for better navigation. - Added logout functionality in chat store and server routes to manage user sessions effectively. - Introduced a new mockup view route for design previews. These changes collectively enhance the user experience and visual appeal of the application.
421 lines
15 KiB
JavaScript
421 lines
15 KiB
JavaScript
import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync } from 'fs';
|
|
import { join, dirname } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import { parse } from 'csv-parse/sync';
|
|
import multer from 'multer';
|
|
import crypto from 'crypto';
|
|
|
|
import axios from 'axios';
|
|
import { getSessionStatus, getClientsMap, getSessionIdForSocket, extractSessionId } from './broadcast.js';
|
|
|
|
// __dirname für ES-Module
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
|
|
// 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() });
|
|
});
|
|
|
|
app.post('/api/logout', (req, res) => {
|
|
try {
|
|
const sessionId = req.sessionID;
|
|
const clientsMap = getClientsMap();
|
|
const client = clientsMap.get(sessionId);
|
|
|
|
if (client?.socket) {
|
|
try {
|
|
client.socket.disconnect(true);
|
|
} catch (error) {
|
|
console.warn('Logout: Socket konnte nicht sauber getrennt werden:', error);
|
|
}
|
|
}
|
|
|
|
if (sessionId) {
|
|
clientsMap.delete(sessionId);
|
|
}
|
|
|
|
req.session.destroy((error) => {
|
|
if (error) {
|
|
console.error('Logout: Session konnte nicht zerstört werden:', error);
|
|
return res.status(500).json({ success: false });
|
|
}
|
|
|
|
res.clearCookie('connect.sid');
|
|
res.json({ success: true });
|
|
});
|
|
} catch (error) {
|
|
console.error('Logout-Fehler:', error);
|
|
res.status(500).json({ success: false });
|
|
}
|
|
});
|
|
|
|
// 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' });
|
|
}
|
|
|
|
// WICHTIG: Modifiziere req.session, damit Express-Session das Cookie setzt
|
|
// (saveUninitialized: false bedeutet, dass das Cookie nur gesetzt wird, wenn req.session modifiziert wird)
|
|
if (!req.session.initialized) {
|
|
req.session.initialized = true;
|
|
}
|
|
|
|
// Prüfe, ob Benutzer eingeloggt ist
|
|
// Versuche zuerst, Session-ID aus Cookie zu extrahieren (wie beim Login)
|
|
let sessionId = extractSessionId(req);
|
|
|
|
// Wenn extractSessionId eine UUID generiert hat (kein Cookie gefunden),
|
|
// versuche req.sessionID zu verwenden
|
|
const isUUID = sessionId && sessionId.length === 36 && sessionId.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
|
|
|
|
if (isUUID) {
|
|
// extractSessionId hat eine UUID generiert, verwende req.sessionID stattdessen
|
|
sessionId = req.sessionID;
|
|
if (sessionId && sessionId.startsWith('s:')) {
|
|
const parts = sessionId.split('.');
|
|
if (parts.length > 0) {
|
|
sessionId = parts[0].substring(2); // Entferne 's:' Präfix
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(`[Bild-Upload] Session-ID: ${sessionId}, req.sessionID (roh): ${req.sessionID}, Cookie vorhanden: ${!isUUID}, Alle Clients:`, Array.from(getClientsMap().keys()));
|
|
|
|
const clientsMap = getClientsMap();
|
|
let client = clientsMap.get(sessionId);
|
|
|
|
// Wenn kein Client gefunden wurde und req.sessionID vorhanden ist, versuche auch mit bereinigter req.sessionID
|
|
if (!client && req.sessionID) {
|
|
const cleanedSessionId = req.sessionID.startsWith('s:') ? req.sessionID.split('.')[0].substring(2) : req.sessionID;
|
|
if (cleanedSessionId !== sessionId) {
|
|
client = clientsMap.get(cleanedSessionId);
|
|
if (client) {
|
|
console.log(`[Bild-Upload] Client gefunden mit bereinigter req.sessionID: ${cleanedSessionId}`);
|
|
sessionId = cleanedSessionId;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Wenn kein Client gefunden wurde, versuche Fallback: Finde den zuletzt aktiven Client mit aktivem Socket
|
|
if (!client || !client.userName) {
|
|
console.log(`[Bild-Upload] Client nicht gefunden für Session-ID: ${sessionId}`);
|
|
console.log(`[Bild-Upload] Cookies: ${req.headers.cookie || 'keine'}`);
|
|
|
|
// Fallback: Suche nach dem zuletzt aktiven Client mit aktivem Socket
|
|
let fallbackClient = null;
|
|
let latestActivity = 0;
|
|
|
|
for (const [sid, c] of clientsMap.entries()) {
|
|
if (c.userName && c.socket && c.socket.connected) {
|
|
const activityTime = c.lastActivity ? c.lastActivity.getTime() : 0;
|
|
if (activityTime > latestActivity) {
|
|
latestActivity = activityTime;
|
|
fallbackClient = c;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Wenn ein Fallback-Client gefunden wurde und er kürzlich aktiv war (innerhalb der letzten 5 Minuten)
|
|
if (fallbackClient && latestActivity > Date.now() - 5 * 60 * 1000) {
|
|
console.log(`[Bild-Upload] Fallback: Verwende zuletzt aktiven Client: ${fallbackClient.userName} (Session-ID: ${fallbackClient.sessionId})`);
|
|
client = fallbackClient;
|
|
} else {
|
|
// 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) => {
|
|
try {
|
|
// WICHTIG: Modifiziere req.session, damit Express-Session das Cookie setzt
|
|
// (saveUninitialized: false bedeutet, dass das Cookie nur gesetzt wird, wenn req.session modifiziert wird)
|
|
if (!req.session.initialized) {
|
|
req.session.initialized = true;
|
|
}
|
|
|
|
const sessionId = req.sessionID;
|
|
console.log('Session-Check - SessionID:', sessionId);
|
|
console.log('Session-Check - Alle Clients:', Array.from(getClientsMap().keys()));
|
|
|
|
// Prüfe zuerst in der clients Map
|
|
const clientsMap = getClientsMap();
|
|
let client = clientsMap.get(sessionId);
|
|
|
|
// Wenn kein Client mit dieser Session-ID gefunden wurde, aber es gibt Clients,
|
|
// die bereits eingeloggt sind, könnte es sein, dass die Session-ID beim Reload geändert wurde.
|
|
// In diesem Fall sollten wir den Client mit der Express-Session-ID verknüpfen.
|
|
// Aber wir können nicht sicher sein, welcher Client zu welcher Session gehört,
|
|
// daher geben wir einfach die Session-ID zurück und lassen den Client beim setSessionId
|
|
// den richtigen Client finden.
|
|
|
|
if (client && client.userName) {
|
|
console.log('Session-Check - Client gefunden:', client.userName);
|
|
res.json({
|
|
loggedIn: true,
|
|
sessionId: sessionId, // Wichtig: Sende Session-ID zurück
|
|
user: {
|
|
sessionId: client.sessionId,
|
|
userName: client.userName,
|
|
gender: client.gender,
|
|
age: client.age,
|
|
country: client.country,
|
|
isoCountryCode: client.isoCountryCode
|
|
}
|
|
});
|
|
} else {
|
|
console.log('Session-Check - Kein Client gefunden für SessionID:', sessionId);
|
|
// Prüfe auch alle Clients, um zu sehen, ob es ein Mismatch gibt
|
|
for (const [sid, c] of clientsMap.entries()) {
|
|
if (c.userName) {
|
|
console.log('Session-Check - Gefundener Client:', sid, c.userName);
|
|
}
|
|
}
|
|
// Sende Session-ID zurück, auch wenn nicht eingeloggt (für Login)
|
|
res.json({ loggedIn: false, sessionId: sessionId });
|
|
}
|
|
} catch (error) {
|
|
console.error('Fehler beim Prüfen der Session:', error);
|
|
res.json({ loggedIn: false });
|
|
}
|
|
});
|
|
|
|
// Länderliste-Endpoint
|
|
app.get('/api/countries', async (req, res) => {
|
|
try {
|
|
// Versuche zuerst, die CSV-Datei zu laden
|
|
const csvPath = join(__dirname, '../docroot/countries.csv');
|
|
let countries = {};
|
|
|
|
try {
|
|
const fileContent = readFileSync(csvPath, 'utf-8');
|
|
const records = parse(fileContent, {
|
|
columns: true,
|
|
skip_empty_lines: true,
|
|
quote: '"',
|
|
trim: true,
|
|
relax_quotes: true,
|
|
relax_column_count: true
|
|
});
|
|
|
|
records.forEach(record => {
|
|
if (record.Name && record.Code) {
|
|
// Entferne alle Anführungszeichen (auch am Anfang/Ende)
|
|
const name = record.Name.replace(/^["']+|["']+$/g, '').replace(/["']/g, '').trim();
|
|
const code = record.Code.replace(/^["']+|["']+$/g, '').replace(/["']/g, '').trim();
|
|
if (name && code) {
|
|
countries[name] = code.toLowerCase();
|
|
}
|
|
}
|
|
});
|
|
} catch (fileError) {
|
|
// Wenn die Datei nicht existiert, lade von der URL
|
|
console.log('CSV-Datei nicht gefunden, lade von URL...');
|
|
const response = await axios.get('https://pkgstore.datahub.io/core/country-list/data_csv/data/d7c9d7cfb42cb69f4422dec222dbbaa8/data_csv.csv');
|
|
const lines = response.data.split('\n');
|
|
|
|
for (let i = 1; i < lines.length; i++) {
|
|
const line = lines[i].trim();
|
|
if (!line) continue;
|
|
|
|
// Parse CSV-Zeile mit Berücksichtigung von Anführungszeichen
|
|
const parseCSVLine = (line) => {
|
|
const result = [];
|
|
let current = '';
|
|
let inQuotes = false;
|
|
|
|
for (let i = 0; i < line.length; i++) {
|
|
const char = line[i];
|
|
if (char === '"') {
|
|
// Ignoriere Anführungszeichen, sie werden nicht zum Wert hinzugefügt
|
|
inQuotes = !inQuotes;
|
|
} else if (char === ',' && !inQuotes) {
|
|
result.push(current.trim());
|
|
current = '';
|
|
} else {
|
|
current += char;
|
|
}
|
|
}
|
|
result.push(current.trim());
|
|
return result;
|
|
};
|
|
|
|
const [name, code] = parseCSVLine(line);
|
|
if (name && code) {
|
|
// Entferne alle Anführungszeichen (auch am Anfang/Ende)
|
|
const cleanName = name.replace(/^["']+|["']+$/g, '').replace(/["']/g, '').trim();
|
|
const cleanCode = code.replace(/^["']+|["']+$/g, '').replace(/["']/g, '').trim();
|
|
if (cleanName && cleanCode) {
|
|
countries[cleanName] = cleanCode.toLowerCase();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
res.json(countries);
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Länderliste:', error);
|
|
res.status(500).json({ error: 'Fehler beim Laden der Länderliste' });
|
|
}
|
|
});
|
|
|
|
// Partners-Links
|
|
app.get('/api/partners', (req, res) => {
|
|
try {
|
|
const csvPath = join(__dirname, '../docroot/links.csv');
|
|
const fileContent = readFileSync(csvPath, 'utf-8');
|
|
const records = parse(fileContent, {
|
|
columns: true,
|
|
skip_empty_lines: true,
|
|
quote: '"'
|
|
});
|
|
|
|
res.json(records);
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Partner-Links:', error);
|
|
res.status(500).json({ error: 'Fehler beim Laden der Partner-Links' });
|
|
}
|
|
});
|
|
}
|