const express = require("express"); const { Pool } = require("pg"); const os = require("os"); const multer = require('multer'); const path = require('path'); const fs = require('fs'); //GeoIP const geoip = require('geoip-lite'); const accessLogPath = path.join(__dirname, 'logs', 'access_log.txt'); const NodeCache = require('node-cache'); const ipCache = new NodeCache({stdTTL: 3600}); //Geoblocker const geoBlocker = require("./geoBlocker"); const cors = require('cors'); const app = express(); const port = process.env.PORT || 3000; const pool = new Pool({ user: process.env.DB_USER, host: process.env.DB_HOST, database: process.env.DB_NAME, password: process.env.DB_PASSWORD, port: process.env.DB_PORT, }); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const JWT_SECRET = process.env.JWT_SECRET; //Rate Limiter fuer Logins const rateLimit = require("express-rate-limit"); const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5, message: "Zu viele Login-Versuche. Bitte in 15 Minuten erneut versuchen.", standardHeaders: true, legacyHeaders: false, }); // Bodyparser für JSON aktivieren app.use(cors({ origin: ["http://volleyball.marc-wieland.de", "https://volleyball.marc-wieland.de"], methods: ["GET", "POST", "PUT", "DELETE"], credentials: true })); app.use(express.json()); app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); //Geoip implementation app.use((req, res, next) => { const ipRaw = req.headers['x-forwarded-for'] || req.socket.remoteAddress || ''; const ip = ipRaw.split(',')[0].trim().replace(/^::ffff:/, ''); // Prüfen ob IP schon gecached ist if (ipCache.has(ip)) { return next(); // IP ist schon drin → kein Logging } ipCache.set(ip, true); // IP merken const geo = geoip.lookup(ip); const timestamp = new Date().toISOString(); const logEntry = `[${timestamp}] IP: ${ip} ${geo ? `(${geo.country})` : '(unbekannt)'}\n`; fs.appendFile(accessLogPath, logEntry, (err) => { if (err) console.error("Fehler beim Loggen:", err); }); next(); }); //GeoBlocker aktivieren app.use(geoBlocker); // Testroute API-Check app.get("/api/hello", (req, res) => { res.send("Hello World from TG CMS API! 🚀"); }); // Testroute DB-Check app.get("/api/test-db", async (req, res) => { try { const result = await pool.query("SELECT NOW()"); res.json({ time: result.rows[0].now }); } catch (err) { console.error(err); res.status(500).send("Fehler bei DB-Verbindung"); } }); // Alle News abrufen app.get("/api/news", async (req, res) => { try { const result = await pool.query('SELECT * FROM news ORDER BY created_at DESC'); res.json(result.rows); } catch (err) { console.error(err); res.status(500).send("Fehler beim Abrufen der News"); } }); // Neue News anlegen app.post("/api/news", async (req, res) => { const { title, description, image_url, team } = req.body; if (!title || !description) { return res.status(400).send("Titel und Beschreibung sind Pflichtfelder"); } try { const result = await pool.query( 'INSERT INTO news (title, description, image_url, team) VALUES ($1, $2, $3, $4) RETURNING *', [title, description, image_url, team] ); res.status(201).json(result.rows[0]); } catch (err) { console.error(err); res.status(500).send("Fehler beim Anlegen der News"); } }); // News bearbeiten app.put("/api/news/:id", async (req, res) => { const { id } = req.params; const { title, description, image_url, team } = req.body; try { const result = await pool.query( 'UPDATE news SET title = $1, description = $2, image_url = $3, team = $4 WHERE id = $5 RETURNING *', [title, description, image_url, team, id] ); if (result.rowCount === 0) { return res.status(404).send("News-Eintrag nicht gefunden"); } res.json(result.rows[0]); } catch (err) { console.error(err); res.status(500).send("Fehler beim Aktualisieren der News"); } }); // News löschen app.delete("/api/news/:id", async (req, res) => { const { id } = req.params; try { const result = await pool.query( 'DELETE FROM news WHERE id = $1', [id] ); if (result.rowCount === 0) { return res.status(404).send("News-Eintrag nicht gefunden"); } res.send("News-Eintrag erfolgreich gelöscht"); } catch (err) { console.error(err); res.status(500).send("Fehler beim Löschen der News"); } }); // Neuen Benutzer anlegen app.post("/api/users", async (req, res) => { const { username, password, role, email } = req.body; if (!username || !password || !email) { return res.status(400).send("Username, Email & Passwort sind Pflicht"); } try { // Passwort hashen const hashedPassword = await bcrypt.hash(password, 10); // In DB speichern const result = await pool.query( 'INSERT INTO users (username, password, role, email) VALUES ($1, $2, $3, $4) RETURNING id, username, role, email, created_at', [username, hashedPassword, role ||'user', email] ); res.status(201).json(result.rows[0]); } catch (err) { console.error(err); res.status(500).send("Fehler beim Anlegen des Benutzers"); } }); // Benutzer Login app.post("/api/login", loginLimiter, async (req, res) => { const { username, password } = req.body; if (!username || !password) { return res.status(400).send("Username und Passwort erforderlich"); } try { const result = await pool.query( 'SELECT * FROM users WHERE username = $1', [username] ); const user = result.rows[0]; if (!user) { return res.status(400).send("Benutzer existiert nicht"); } // Passwort prüfen const validPassword = await bcrypt.compare(password, user.password); if (!validPassword) { return res.status(401).send("Passwort falsch"); } await pool.query( 'UPDATE users SET last_logged = NOW() WHERE id = $1', [user.id] ); // JWT Token erzeugen const token = jwt.sign( { id: user.id, username: user.username, role: user.role }, JWT_SECRET, { expiresIn: "6h" } ); res.json({ token }); } catch (err) { console.error(err); res.status(500).send("Fehler beim Login"); } }); // Alle Benutzer abrufen app.get("/api/users", async (req, res) => { try { const result = await pool.query( 'SELECT id, username, role, email, created_at FROM users ORDER BY created_at DESC' ); res.json(result.rows); } catch (err) { console.error(err); res.status(500).send("Fehler beim Abrufen der Benutzer"); } }); // Benutzer l�loeschen app.delete("/api/users/:id", async (req, res) => { const { id } = req.params; try { // Prüfen, ob der Benutzer existiert const checkResult = await pool.query('SELECT * FROM users WHERE id = $1', [id]); if (checkResult.rowCount === 0) { return res.status(404).send("Benutzer nicht gefunden"); } // Benutzer löschen await pool.query('DELETE FROM users WHERE id = $1', [id]); res.send("Benutzer erfolgreich gelöscht"); } catch (err) { console.error(err); res.status(500).send("Fehler beim Löschen des Benutzers"); } }); // Benutzer bearbeiten app.put("/api/users/:id", async (req, res) => { const { id } = req.params; const { username, password, role } = req.body; try { // Passwort optional neu setzen let query = 'UPDATE users SET username = $1, role = $2'; let params = [username, role, id]; if (password) { const hashedPassword = await bcrypt.hash(password, 10); query = 'UPDATE users SET username = $1, password = $2, role = $3 WHERE id = $4'; params = [username, hashedPassword, role, id]; } else { query += ' WHERE id = $3'; } const result = await pool.query(query, params); if (result.rowCount === 0) { return res.status(404).send("Benutzer nicht gefunden"); } res.send("Benutzer erfolgreich aktualisiert"); } catch (err) { console.error(err); res.status(500).send("Fehler beim Aktualisieren des Benutzers"); } }); // Speicherort definieren const newsStorage = multer.diskStorage({ destination: (req, file, cb) => { const dir = "./uploads/news"; fs.mkdirSync(dir, { recursive: true }); cb(null, dir); }, filename: (req, file, cb) => { const ext = path.extname(file.originalname); const filename = Date.now() + ext; cb(null, filename); }, }); const playerStorage = multer.diskStorage({ destination: (req, file, cb) => { const dir = "./uploads/players"; fs.mkdirSync(dir, { recursive: true }); cb(null, dir); }, filename: (req, file, cb) => { const ext = path.extname(file.originalname); const filename = Date.now() + ext; cb(null, filename); }, }); const uploadNewsImage = multer({ storage: newsStorage }); const uploadPlayerImage = multer({ storage: playerStorage }); //Neues Bild zu den News hinzufügen app.post("/api/upload-news-image", uploadNewsImage.single("image"), (req, res) => { if (!req.file) { return res.status(400).send("Kein Bild hochgeladen"); } const imageUrl = `/uploads/news/${req.file.filename}`; res.json({ imageUrl }); }); //Teams abfragen app.get("/api/teams", async (req, res) => { try { const result = await pool.query(` SELECT t.id, t.name, t.liga, t.beschreibung, t.trainingszeiten, t.sucht_spieler, t.social_media, t.trainer_name, t.karussell_bilder, COUNT(pt.player_id) AS player_count FROM teams t LEFT JOIN player_teams pt ON pt.team_id = t.id GROUP BY t.id, t.name, t.liga, t.beschreibung, t.trainingszeiten, t.sucht_spieler, t.social_media, t.trainer_name, t.karussell_bilder ORDER BY t.name ASC `); res.json(result.rows); } catch (err) { console.error("Fehler beim Abrufen der Teams:", err); res.status(500).send("Fehler beim Abrufen der Teams"); } }); //Team aktualisieren app.put("/api/teams/:id", async (req, res) => { const { id } = req.params; const { name, liga, sucht_spieler, social_media, karussell_bilder, trainer_name, trainingszeiten, trainingsort, kontakt_name, kontakt_email, teamfarben, beschreibung, tabellenlink } = req.body; try { const result = await pool.query( `UPDATE teams SET name = $1, liga = $2, sucht_spieler = $3, social_media = $4, karussell_bilder = $5, trainer_name = $6, trainingszeiten = $7, trainingsort = $8, kontakt_name = $9, kontakt_email = $10, teamfarben = $11, beschreibung = $12, tabellenlink = $13 WHERE id = $14 RETURNING *`, [ name, liga, sucht_spieler, social_media, karussell_bilder, trainer_name, trainingszeiten, trainingsort, kontakt_name, kontakt_email, teamfarben, beschreibung, tabellenlink, id ] ); if (result.rowCount === 0) { return res.status(404).send("Team nicht gefunden"); } res.json(result.rows[0]); } catch (err) { console.error("Fehler beim Aktualisieren des Teams:", err); res.status(500).send("Fehler beim Aktualisieren des Teams"); } }); //Teams anlegen app.post("/api/teams", async (req, res) => { const { name, liga, sucht_spieler, social_media, karussell_bilder, trainer_name, trainingszeiten, trainingsort, kontakt_name, kontakt_email, teamfarben, beschreibung, tabellenlink } = req.body; if (!name) return res.status(400).send("Teamname ist erforderlich"); try { const result = await pool.query( `INSERT INTO teams ( name, liga, sucht_spieler, social_media, karussell_bilder, trainer_name, trainingszeiten, trainingsort, kontakt_name, kontakt_email, teamfarben, beschreibung, tabellenlink ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13 ) RETURNING *`, [ name, liga, sucht_spieler, social_media, karussell_bilder, trainer_name, trainingszeiten, trainingsort, kontakt_name, kontakt_email, teamfarben, beschreibung, tabellenlink ] ); res.status(201).json(result.rows[0]); } catch (err) { console.error("Fehler beim Anlegen des Teams:", err); res.status(500).send("Fehler beim Anlegen des Teams"); } }); //Team loeschen app.delete("/api/teams/:id", async (req, res) => { const { id } = req.params; try { const result = await pool.query("DELETE FROM teams WHERE id = $1", [id]); if (result.rowCount === 0) { return res.status(404).send("Team nicht gefunden"); } res.send("Team gelöscht"); } catch (err) { console.error("Fehler beim Löschen:", err); res.status(500).send("Fehler beim Löschen"); } }); //Spieler anlegen app.post("/api/players", async (req, res) => { const { name, nickname, birthdate, position, jersey_number, favorite_food, image_url, status, team_ids } = req.body; if (!name || !position) { return res.status(400).send("Name und Position sind Pflichtfelder"); } const client = await pool.connect(); try { await client.query("BEGIN"); const result = await client.query( `INSERT INTO players (name, nickname, birthdate, position, jersey_number, favorite_food, image_url, status) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`, [name, nickname, birthdate, position, jersey_number, favorite_food, image_url, status || "aktiv"] ); const playerId = result.rows[0].id; if (team_ids && team_ids.length > 0) { const teamInsert = team_ids.map((teamId) => `(${playerId}, ${teamId})`).join(","); await client.query(`INSERT INTO player_teams (player_id, team_id) VALUES ${teamInsert}`); } await client.query("COMMIT"); res.status(201).json({ id: playerId }); } catch (err) { await client.query("ROLLBACK"); console.error("Fehler beim Anlegen des Spielers:", err); res.status(500).send("Fehler beim Anlegen des Spielers"); } finally { client.release(); } }); // Einzelnes Team inkl. Metadaten + Spieler laden app.get("/api/teams/:id", async (req, res) => { const { id } = req.params; try { // Team-Daten holen const teamResult = await pool.query( `SELECT id, name, liga, sucht_spieler, social_media, trainer_name, karussell_bilder, trainingszeiten, trainingsort, kontakt_name, kontakt_email, teamfarben, beschreibung, tabellenlink FROM teams WHERE id = $1`, [id] ); if (teamResult.rowCount === 0) { return res.status(404).send("Team nicht gefunden"); } // Spieler für dieses Team holen const playerResult = await pool.query(` SELECT p.id, p.name, p.nickname, p.position, p.jersey_number, p.image_url FROM players p INNER JOIN player_teams pt ON p.id = pt.player_id WHERE pt.team_id = $1 ORDER BY p.name ASC `, [id]); res.json({ ...teamResult.rows[0], players: playerResult.rows, }); } catch (err) { console.error("Fehler beim Laden des Teams:", err); res.status(500).send("Fehler beim Laden des Teams"); } }); //Alle Spieler abrufen app.get("/api/players", async (req, res) => { try { const result = await pool.query("SELECT * FROM players ORDER BY name ASC"); res.json(result.rows); } catch (err) { console.error("Fehler beim Abrufen der Spieler:", err); res.status(500).send("Fehler beim Abrufen der Spieler"); } }); //Upload Player Image app.post("/api/upload-player-image", uploadPlayerImage.single("image"), (req, res) => { if (!req.file) { return res.status(400).send("Kein Bild hochgeladen"); } const imageUrl = `/uploads/players/${req.file.filename}`; res.json({ imageUrl }); }); //Carousell Bilder für Teams hochladen const carouselStorage = multer.diskStorage({ destination: function (req, file, cb) { const dir = "./uploads/carousel"; fs.mkdirSync(dir, { recursive: true }); cb(null, dir); }, filename: function (req, file, cb) { const uniqueName = Date.now() + "-" + file.originalname; cb(null, uniqueName); }, }); const uploadCarouselImage = multer({ storage: carouselStorage }); app.post("/api/teams/:id/carousel-upload", uploadCarouselImage.single("image"), (req, res) => { if (!req.file) return res.status(400).json({ error: "Keine Datei hochgeladen" }); const filePath = `/uploads/carousel/${req.file.filename}`; res.json({ path: filePath }); }); //Spieler bearbeiten // Spieler aktualisieren app.put("/api/players/:id", async (req, res) => { const { id } = req.params; const { name, nickname, birthdate, position, jersey_number, favorite_food, status, image_url, } = req.body; if (!name || !position) { return res.status(400).send("Name und Position sind Pflichtfelder"); } try { const result = await pool.query( `UPDATE players SET name = $1, nickname = $2, birthdate = $3, position = $4, jersey_number = $5, favorite_food = $6, status = $7, image_url = $8 WHERE id = $9 RETURNING *`, [ name, nickname || null, birthdate || null, position, jersey_number ? Number(jersey_number) : null, favorite_food || null, status || "aktiv", image_url || null, id, ] ); if (result.rowCount === 0) { return res.status(404).send("Spieler nicht gefunden"); } res.json(result.rows[0]); } catch (err) { console.error("Fehler beim Aktualisieren des Spielers:", err); res.status(500).send("Fehler beim Aktualisieren des Spielers"); } }); //Einzelnen Spieler abrufen app.get("/api/players/:id", async (req, res) => { const { id } = req.params; try { const result = await pool.query( "SELECT * FROM players WHERE id = $1", [id] ); if (result.rowCount === 0) { return res.status(404).send("Spieler nicht gefunden"); } const player = result.rows[0]; // 🔥 Team-IDs abfragen const teamResult = await pool.query( "SELECT team_id FROM player_teams WHERE player_id = $1", [id] ); const team_ids = teamResult.rows.map(r => r.team_id); res.json({ ...player, team_ids }); } catch (err) { console.error("Fehler beim Abrufen des Spielers:", err); res.status(500).send("Fehler beim Abrufen des Spielers"); } }); // Spieler aus einem Team entfernen app.delete("/api/teams/:teamId/players/:playerId", async (req, res) => { const { teamId, playerId } = req.params; try { const result = await pool.query( "DELETE FROM player_teams WHERE team_id = $1 AND player_id = $2", [teamId, playerId] ); if (result.rowCount === 0) { return res.status(404).send("Spieler nicht im Team gefunden"); } res.send("Spieler erfolgreich aus dem Team entfernt"); } catch (err) { console.error("Fehler beim Entfernen des Spielers:", err); res.status(500).send("Fehler beim Entfernen des Spielers"); } }); // Existierenden Spieler einem Team hinzufügen app.post("/api/teams/:teamId/players", async (req, res) => { const { teamId } = req.params; const { playerId } = req.body; if (!playerId) { return res.status(400).send("playerId ist erforderlich"); } try { const result = await pool.query( "INSERT INTO player_teams (player_id, team_id) VALUES ($1, $2)", [playerId, teamId] ); res.status(201).send("Spieler erfolgreich dem Team zugeordnet"); } catch (err) { if (err.code === "23505") { return res.status(400).send("Spieler ist bereits im Team"); } console.error("Fehler beim Hinzufügen des Spielers zum Team:", err); res.status(500).send("Fehler beim Hinzufügen des Spielers"); } }); // Alle Teams abrufen app.get("/api/all-teams", async (req, res) => { try { const result = await pool.query("SELECT id, name FROM teams ORDER BY name ASC"); res.json(result.rows); } catch (err) { console.error("Fehler beim Laden der Teams:", err); res.status(500).send("Fehler beim Laden der Teams"); } }); //Spieler einem Team zuweisen app.post("/api/players/:id/assign-team", async (req, res) => { const { id } = req.params; const { team_id } = req.body; if (!team_id) return res.status(400).send("team_id erforderlich"); try { await pool.query("DELETE FROM player_teams WHERE player_id = $1", [id]); await pool.query("INSERT INTO player_teams (player_id, team_id) VALUES ($1, $2)", [id, team_id]); res.send("Teamzugehörigkeit aktualisiert"); } catch (err) { console.error("Fehler beim Aktualisieren der Teamzugehörigkeit:", err); res.status(500).send("Fehler beim Aktualisieren"); } }); //Multer Storage fuer Galleriebilder const galleryStorage = multer.diskStorage({ destination: (req, file, cb) => { const dir = "./uploads/gallery"; fs.mkdirSync(dir, { recursive: true }); cb(null, dir); }, filename: (req, file, cb) => { const ext = path.extname(file.originalname); const filename = Date.now() + ext; cb(null, filename); }, }); const uploadGalleryImage = multer({ storage: galleryStorage }); //Gallery Image Upload app.post("/api/gallery", uploadGalleryImage.single("image"), async (req, res) => { const { title } = req.body; if (!req.file) return res.status(400).send("Kein Bild hochgeladen"); const imageUrl = `/uploads/gallery/${req.file.filename}`; try { const result = await pool.query( "INSERT INTO gallery_images (image_url, title) VALUES ($1, $2) RETURNING *", [imageUrl, title || null] ); res.status(201).json(result.rows[0]); } catch (err) { console.error("Fehler beim Speichern des Galeriebilds:", err); res.status(500).send("Fehler beim Speichern"); } }); //Get all gallery images app.get("/api/gallery", async (req, res) => { try { const result = await pool.query("SELECT * FROM gallery_images ORDER BY uploaded_at DESC"); res.json(result.rows); } catch (err) { console.error("Fehler beim Laden der Galerie:", err); res.status(500).send("Fehler beim Laden"); } }); //Delete image app.delete("/api/gallery/:id", async (req, res) => { const { id } = req.params; try { const result = await pool.query("DELETE FROM gallery_images WHERE id = $1 RETURNING *", [id]); if (result.rowCount === 0) return res.status(404).send("Bild nicht gefunden"); // Optional: Datei auch vom Dateisystem löschen const filePath = path.join(__dirname, result.rows[0].image_url); if (fs.existsSync(filePath)) fs.unlinkSync(filePath); res.send("Bild erfolgreich gelöscht"); } catch (err) { console.error("Fehler beim Löschen des Bildes:", err); res.status(500).send("Fehler beim Löschen"); } }); //Event laden app.get("/api/events", async (req, res) => { const { showPrivate } = req.query; const query = showPrivate === "true" ? "SELECT * FROM events ORDER BY created_at DESC" : "SELECT * FROM events WHERE is_private = FALSE ORDER BY created_at DESC"; const result = await pool.query(query); res.json(result.rows); }); //Event erstellen app.post("/api/events", async (req, res) => { const { title, description, max_participants, fee, address, is_private } = req.body; const result = await pool.query( `INSERT INTO events (title, description, max_participants, fee, address, is_private) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`, [title, description, max_participants, fee, address, is_private] ); res.status(201).json(result.rows[0]); }); //Event editieren app.put("/api/events/:id", async (req, res) => { const { id } = req.params; const { title, description, max_participants, fee, address, is_private } = req.body; const result = await pool.query( `UPDATE events SET title = $1, description = $2, max_participants = $3, fee = $4, address = $5, is_private = $6 WHERE id = $7 RETURNING *`, [title, description, max_participants, fee, address, is_private, id] ); if (result.rowCount === 0) { return res.status(404).json({ message: "Event not found" }); } res.json(result.rows[0]); }); //Event loeschen app.delete("/api/events/:id", async (req, res) => { const { id } = req.params; const result = await pool.query(`DELETE FROM events WHERE id = $1`, [id]); if (result.rowCount === 0) { return res.status(404).json({ message: "Event not found" }); } res.status(204).send(); // No Content }); // Server starten app.listen(port, () => { const nets = os.networkInterfaces(); const addresses = []; for (const name of Object.keys(nets)) { for (const net of nets[name]) { if (net.family === 'IPv4' && !net.internal) { addresses.push(net.address); } } } console.log("Backend erreichbar unter:"); addresses.forEach(ip => { console.log(`http://${ip}:${port}`); }); });