- Updated index.html with improved meta tags for SEO, including author and theme color. - Added a feedback dialog in ImprintContainer.vue for user feedback submission. - Refactored LoginForm.vue to utilize a utility for cookie management, simplifying profile persistence. - Introduced new routes and schemas for feedback in the router and server, enhancing SEO and user experience. - Improved ChatView.vue with better error handling and command table display. - Implemented feedback API endpoints in server routes for managing user feedback submissions and admin access. These changes collectively improve the application's SEO, user interaction, and feedback management capabilities.
519 lines
18 KiB
JavaScript
519 lines
18 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';
|
|
import { verifyChatUser } from './chat-auth.js';
|
|
import { loadFeedback, saveFeedback, createFeedbackEntry } from './feedback-store.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 });
|
|
}
|
|
});
|
|
|
|
app.get('/api/feedback', (req, res) => {
|
|
try {
|
|
const feedback = loadFeedback(__dirname)
|
|
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
|
res.json({
|
|
items: feedback,
|
|
admin: !!req.session.feedbackAdmin
|
|
});
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden des Feedbacks:', error);
|
|
res.status(500).json({ error: 'Fehler beim Laden des Feedbacks' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/feedback/admin-status', (req, res) => {
|
|
res.json({
|
|
authenticated: !!req.session.feedbackAdmin,
|
|
username: req.session.feedbackAdmin?.username || null
|
|
});
|
|
});
|
|
|
|
app.post('/api/feedback', (req, res) => {
|
|
try {
|
|
const ageValue = req.body.age === '' || req.body.age === null || req.body.age === undefined
|
|
? null
|
|
: Number.parseInt(req.body.age, 10);
|
|
|
|
const entry = createFeedbackEntry({
|
|
name: req.body.name,
|
|
age: Number.isNaN(ageValue) ? null : ageValue,
|
|
country: req.body.country,
|
|
gender: req.body.gender,
|
|
comment: req.body.comment
|
|
});
|
|
|
|
if (!entry.comment) {
|
|
return res.status(400).json({ error: 'Kommentar ist erforderlich.' });
|
|
}
|
|
|
|
const feedback = loadFeedback(__dirname);
|
|
feedback.push(entry);
|
|
saveFeedback(__dirname, feedback);
|
|
|
|
res.status(201).json({ success: true, item: entry });
|
|
} catch (error) {
|
|
console.error('Fehler beim Speichern des Feedbacks:', error);
|
|
res.status(500).json({ error: 'Fehler beim Speichern des Feedbacks' });
|
|
}
|
|
});
|
|
|
|
app.post('/api/feedback/admin-login', (req, res) => {
|
|
try {
|
|
const { username, password } = req.body || {};
|
|
const auth = verifyChatUser(__dirname, username, password);
|
|
|
|
if (!auth) {
|
|
return res.status(401).json({ error: 'Login fehlgeschlagen.' });
|
|
}
|
|
|
|
req.session.feedbackAdmin = {
|
|
username: auth.username
|
|
};
|
|
|
|
res.json({ success: true, username: auth.username });
|
|
} catch (error) {
|
|
console.error('Fehler beim Feedback-Admin-Login:', error);
|
|
res.status(500).json({ error: 'Fehler beim Login' });
|
|
}
|
|
});
|
|
|
|
app.post('/api/feedback/admin-logout', (req, res) => {
|
|
delete req.session.feedbackAdmin;
|
|
res.json({ success: true });
|
|
});
|
|
|
|
app.delete('/api/feedback/:id', (req, res) => {
|
|
try {
|
|
if (!req.session.feedbackAdmin) {
|
|
return res.status(403).json({ error: 'Nicht erlaubt.' });
|
|
}
|
|
|
|
const feedback = loadFeedback(__dirname);
|
|
const nextFeedback = feedback.filter((item) => item.id !== req.params.id);
|
|
|
|
if (nextFeedback.length === feedback.length) {
|
|
return res.status(404).json({ error: 'Eintrag nicht gefunden.' });
|
|
}
|
|
|
|
saveFeedback(__dirname, nextFeedback);
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('Fehler beim Löschen des Feedbacks:', error);
|
|
res.status(500).json({ error: 'Fehler beim Löschen des Feedbacks' });
|
|
}
|
|
});
|
|
|
|
// 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' });
|
|
}
|
|
});
|
|
}
|