volleyball-dev-backend/index.js

983 lines
24 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const express = require("express");
const { Pool } = require("pg");
const os = require("os");
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const cors = require('cors');
const app = express();
const port = process.env.PORT || 3000;
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const JWT_SECRET = "supergeheimes_tg_cms_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://localhost:8080", "http://192.168.50.65:8080"],
methods: ["GET", "POST", "PUT", "DELETE"],
credentials: true
}));
app.use(express.json());
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
// PostgreSQL Verbindung aufbauen
const pool = new Pool({
user: "tgadmin",
host: "db", // Docker Container Name!
database: "tg-cms",
password: "secretpass",
port: 5432,
});
// 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 });
});
//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");
}
});
//Carousell Bilder für Teams hochladen
const multer = require("multer");
const path = require("path");
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, "uploads/carousel");
},
filename: function (req, file, cb) {
const uniqueName = Date.now() + "-" + file.originalname;
cb(null, uniqueName);
},
});
const upload = multer({ storage });
router.post("/api/teams/:id/carousel-upload", upload.single("image"), (req, res) => {
if (!req.file) return res.status(400).json({ error: "Keine Datei hochgeladen" });
// z.B. /uploads/carousel/171654543-name.jpg
const filePath = `/uploads/carousel/${req.file.filename}`;
res.json({ path: filePath });
});
//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}`);
});
});