initial commit

This commit is contained in:
klaas 2025-03-15 19:29:07 +01:00
commit 74d115f9de
15 changed files with 3941 additions and 0 deletions

13
.env_default Normal file
View File

@ -0,0 +1,13 @@
DATABASE_URL=postgres://user:psw@localhost:5432/postgres
SESSIONSECRET=Your Secret
JWT_SECRET=
MAILHOST=
MAILUSER=
MAILFROM=
MAILPASS=
PORT=2000
TELEBOT=
TELECHAT=
HOST=
WEEKDAY=4
ABTEILUNG=KiTu/ABENTEUERSPIELPLATZ/KKT

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.env
node_modules/
logs/*
.DS_Store
.vscode/

0
README.md Normal file
View File

79
app.js Normal file
View File

@ -0,0 +1,79 @@
const express = require("express");
const session = require("express-session");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
const path = require("path");
const moment = require("moment");
require("dotenv").config();
const log = require("node-file-logger");
const app = express();
const port = process.env.PORT;
const options = {
timeZone: "europe/Berlin",
folderPath: "./logs/",
dateBasedFileNaming: true,
fileNamePrefix: "DailyLogs_",
fileNameExtension: ".log",
dateFormat: "YYYY_MM_DD",
timeFormat: "H:mm:ss",
};
log.SetUserOptions(options);
// Middleware
app.use(express.static(path.join(__dirname, "public")));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// Session-Konfiguration
app.use(
session({
secret: process.env.SESSIONSECRET,
resave: false,
saveUninitialized: true,
cookie: { maxAge: 1000 * 60 * 60 * 24 * 2 },
})
);
const userRoutes = require("./routes/user");
const spieleRoutes = require("./routes/spiele");
const featureRoutes = require("./routes/feature");
const memberRoutes = require("./routes/member");
const trainingRoutes = require("./routes/training");
const anwesendRoutes = require("./routes/anwesend");
app.use("/user", userRoutes);
app.use("/spiele", spieleRoutes);
app.use("/feature", featureRoutes);
app.use(
"/:abteilung?/member",
(req, res, next) => {
req.abteilung = req.params.abteilung; // `abteilung` in req speichern
next();
},
memberRoutes
);
app.use(
"/:abteilung/training",
(req, res, next) => {
req.abteilung = req.params.abteilung;
next();
},
trainingRoutes
);
app.use(
"/:abteilung/anwesend",
(req, res, next) => {
req.abteilung = req.params.abteilung;
next();
},
anwesendRoutes
);
const server = app.listen(port, "0.0.0.0", () => {
console.log(`Server is running on ${process.env.HOST}:${port}/`);
log.Info(`Server is running on ${process.env.HOST}:${port}/`);
});

14
db.js Normal file
View File

@ -0,0 +1,14 @@
const { Pool } = require("pg");
const bcrypt = require("bcryptjs");
const pool = new Pool({
connectionString: process.env.DATABASE_URL + process.env.DBNAME,
});
// Fehlerbehandlung bei der Datenbankverbindung
pool.on("error", (err) => {
console.error("Fehler in der PostgreSQL-Datenbankverbindung:", err);
});
// Exportiere den Pool für die Nutzung in anderen Dateien
module.exports = pool;

40
middleware/auth.js Normal file
View File

@ -0,0 +1,40 @@
const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");
const pool = require("../db"); // Stelle sicher, dass der DB-Pool importiert wird
// Authentifizierungs-Middleware
const requireAuth = (req, res, next) => {
const token = req.headers.authorization?.split(" ")[1];
if (!token) {
return res.status(401).json({ error: "Kein Token vorhanden" });
}
try {
// JWT entschlüsseln
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // Benutzerinformationen im Request speichern
next();
} catch (err) {
return res.status(401).json({ error: "Ungültiges Token" });
}
};
const requireAdmin = async (req, res, next) => {
try {
const result = await pool.query("SELECT role FROM users WHERE id = $1", [
req.user.id,
]);
if (result.rows.length === 0 || result.rows[0].role !== "admin") {
return res.status(403).json({ error: "Nicht autorisiert" });
}
next();
} catch (err) {
console.error(err);
return res.status(500).json({ error: "Interner Serverfehler" });
}
};
module.exports = { requireAuth, requireAdmin };

32
middleware/mail.js Normal file
View File

@ -0,0 +1,32 @@
const nodemailer = require("nodemailer");
// Email-Konfiguration
const transporter = nodemailer.createTransport({
host: process.env.MAILHOST,
port: 465,
secure: true,
auth: {
user: process.env.MAILUSER,
pass: process.env.MAILPASS,
},
});
const sendActivationEmail = async (email, username) => {
if (!email) return; // Falls keine E-Mail hinterlegt ist, wird nichts gesendet
const mailOptions = {
from: process.env.MAILUSER, // Absender-E-Mail
to: email,
subject: "Dein Konto wurde aktiviert!",
text: `Hallo ${username},\n\nDu wurdest soeben freigeschaltet und kannst dich nun unter https://${process.env.HOST} anmelden.\n\nViele Grüße,\nDein Team`,
};
try {
await transporter.sendMail(mailOptions);
console.log(`Aktivierungs-E-Mail an ${email} gesendet.`);
} catch (err) {
console.error("Fehler beim Senden der E-Mail:", err);
}
};
module.exports = { transporter, sendActivationEmail };

2439
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "training",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"start": "node app.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"bcrypt": "^5.1.1",
"bcryptjs": "^3.0.2",
"crypto": "^1.0.1",
"dotenv": "^16.4.5",
"ejs": "^3.1.10",
"express": "^4.19.2",
"express-session": "^1.18.0",
"fs": "^0.0.1-security",
"jsonwebtoken": "^9.0.2",
"moment": "^2.30.1",
"node-file-logger": "^0.9.5",
"nodemailer": "^6.9.13",
"pg": "^8.11.5",
"telebot": "^1.4.1"
}
}

89
routes/anwesend.js Normal file
View File

@ -0,0 +1,89 @@
const express = require("express");
const pool = require("../db"); // PostgreSQL-Datenbankverbindung
const { requireAuth, requireAdmin } = require("../middleware/auth"); // Auth-Middleware
const router = express.Router();
// **1. Alle Anwesenheiten eines Trainings abrufen**
router.get("/:id", requireAuth, async (req, res) => {
const { id } = req.params; // Training-ID
const abteilung = req.abteilung; // Abteilung aus Middleware
if (!abteilung) {
return res.status(400).json({ error: "Abteilung ist erforderlich" });
}
try {
const result = await pool.query(
`SELECT a.fid_mitglied, m.vorname, m.nachname
FROM anwesend a
JOIN mitglieder m ON a.fid_mitglied = m.id
WHERE a.fid_training = $1`,
[id]
);
if (result.rows.length === 0) {
return res.json([]); // Falls keine Anwesenheiten vorhanden sind
}
return res.json(result.rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
// **2. Anwesenheit für ein Training setzen**
router.post("/:id", requireAuth, async (req, res) => {
const { id } = req.params; // Training-ID
const { inriege, anw } = req.body; // Arrays mit IDs
if (!Array.isArray(inriege) || !Array.isArray(anw)) {
return res
.status(400)
.json({ error: "Daten müssen als Arrays übergeben werden" });
}
try {
await pool.query("BEGIN"); // Transaktion starten
// **1. Alle aktuellen Einträge für das Training abrufen**
const existingEntries = await pool.query(
`SELECT fid_mitglied FROM anwesend WHERE fid_training = $1`,
[id]
);
const existingSet = new Set(
existingEntries.rows.map((row) => row.fid_mitglied)
);
const anwSet = new Set(anw);
// **2. Anwesenheit setzen**
for (const memberId of inriege) {
if (anwSet.has(memberId) && !existingSet.has(memberId)) {
// **Fall 1: ID ist in `anw`, aber nicht in der Tabelle → Neu hinzufügen**
await pool.query(
`INSERT INTO anwesend (fid_mitglied, fid_training) VALUES ($1, $2)`,
[memberId, id]
);
} else if (!anwSet.has(memberId) && existingSet.has(memberId)) {
// **Fall 2: ID ist nicht in `anw`, aber in der Tabelle → Löschen**
await pool.query(
`DELETE FROM anwesend WHERE fid_mitglied = $1 AND fid_training = $2`,
[memberId, id]
);
}
// **Fall 3 & 4: Keine Änderung nötig (Eintrag bleibt gleich oder existiert nicht)**
}
await pool.query("COMMIT"); // Transaktion abschließen
res.json({ message: "Anwesenheit erfolgreich aktualisiert" });
} catch (err) {
await pool.query("ROLLBACK"); // Falls Fehler, Transaktion rückgängig machen
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
module.exports = router;

151
routes/feature.js Normal file
View File

@ -0,0 +1,151 @@
const express = require("express");
const pool = require("../db"); // PostgreSQL-Datenbankverbindung
const { requireAuth } = require("../middleware/auth"); // Auth-Middleware
const router = express.Router();
// **1. Alle Features abrufen (Falls ID gegeben: Nur dieses Feature)**
router.get("/:id?", requireAuth, async (req, res) => {
const { id } = req.params;
try {
let result;
if (id) {
result = await pool.query("SELECT * FROM features WHERE id = $1", [id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: "Feature nicht gefunden" });
}
return res.json(result.rows[0]);
} else {
result = await pool.query(
"SELECT id, title FROM features ORDER BY datetime DESC"
);
return res.json(result.rows);
}
} catch (err) {
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
// **2. Feature anlegen oder aktualisieren**
router.put("/:id?", requireAuth, async (req, res) => {
const { id } = req.params;
const { title, body, type, urgency, fid_user, datetime, done } = req.body;
try {
let result;
if (id) {
// **Feature aktualisieren**
const featureResult = await pool.query(
"SELECT fid_user FROM features WHERE id = $1",
[id]
);
if (featureResult.rows.length === 0) {
return res.status(404).json({ error: "Feature nicht gefunden" });
}
const featureOwner = featureResult.rows[0].fid_user;
if (req.user.id !== featureOwner && req.user.id !== 1) {
return res
.status(403)
.json({ error: "Keine Berechtigung, dieses Feature zu bearbeiten" });
}
if (done !== undefined && req.user.id !== 1) {
return res
.status(403)
.json({ error: 'Nur User mit ID 1 darf das Feld "done" ändern' });
}
if (done !== undefined) {
result = await pool.query(
"UPDATE features SET done = $1 WHERE id = $2 RETURNING *",
[done, id]
);
} else {
if (!title || !body || !type || !urgency ) {
return res
.status(400)
.json({ error: "Alle Felder sind erforderlich" });
}
result = await pool.query(
`UPDATE features
SET title = $1, body = $2, type = $3, urgency = $4
WHERE id = $5
RETURNING *`,
[title, body, type, urgency, id]
);
}
return res.json({
message: "Feature erfolgreich aktualisiert",
feature: result.rows[0],
});
} else {
// **Neues Feature anlegen**
if (!title || !body || !type || !urgency || !fid_user || !datetime) {
return res
.status(400)
.json({ error: 'Alle Felder außer "done" sind erforderlich' });
}
result = await pool.query(
`INSERT INTO features (title, body, type, urgency, fid_user, datetime, done)
VALUES ($1, $2, $3, $4, $5, $6, false)
RETURNING *`,
[title, body, type, urgency, fid_user, datetime]
);
return res
.status(201)
.json({
message: "Feature erfolgreich angelegt",
feature: result.rows[0],
});
}
} catch (err) {
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
// **3. Feature löschen (Nur durch anlegenden Benutzer oder User ID 1)**
router.delete("/:id", requireAuth, async (req, res) => {
const { id } = req.params;
try {
const featureResult = await pool.query(
"SELECT fid_user FROM features WHERE id = $1",
[id]
);
if (featureResult.rows.length === 0) {
return res.status(404).json({ error: "Feature nicht gefunden" });
}
const featureOwner = featureResult.rows[0].fid_user;
if (req.user.id !== featureOwner && req.user.id !== 1) {
return res
.status(403)
.json({ error: "Keine Berechtigung, dieses Feature zu löschen" });
}
await pool.query("DELETE FROM features WHERE id = $1", [id]);
res.json({ message: "Feature erfolgreich gelöscht" });
} catch (err) {
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
module.exports = router;

346
routes/member.js Normal file
View File

@ -0,0 +1,346 @@
const express = require("express");
const pool = require("../db"); // PostgreSQL-Datenbankverbindung
const { requireAuth, requireAdmin } = require("../middleware/auth"); // Auth-Middleware
const router = express.Router();
router.get("/:id?", requireAuth, async (req, res) => {
const { id } = req.params;
const abteilung = req.abteilung;
try {
let query;
let values = [];
if (id) {
// **Ein einzelnes Mitglied abrufen**
if (abteilung) {
// Falls Abteilung gesetzt ist, prüfen ob das Mitglied dazugehört
query = `
SELECT m.*
FROM mitglieder m
JOIN abteilungszuordnung dm ON m.id = dm.fid_mitglied
WHERE m.id = $1 AND dm.fid_abteilung = $2
`;
values = [id, abteilung];
} else {
// Mitglied ohne Abteilungseinschränkung abrufen
query = `SELECT * FROM mitglieder WHERE id = $1`;
values = [id];
}
} else if (abteilung) {
// **Alle Mitglieder einer Abteilung abrufen**
query = `
SELECT m.*
FROM mitglieder m
JOIN abteilungszuordnung dm ON m.id = dm.fid_mitglied
WHERE dm.fid_abteilung = $1
`;
values = [abteilung];
} else {
// **Alle Mitglieder abrufen**
query = `SELECT * FROM mitglieder`;
}
const result = await pool.query(query, values);
if (result.rows.length === 0) {
return res.status(404).json({ error: "Keine Mitglieder gefunden" });
}
return res.json(result.rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
// **2. Mitglied anlegen oder aktualisieren (Nur für Admins)**
router.put("/:id?", requireAuth, requireAdmin, async (req, res) => {
const { id } = req.params;
const {
vorname,
nachname,
geburtsdatum,
adresse,
mit_seit,
mit_num,
helfer,
probe,
} = req.body;
try {
let result;
if (id) {
// **Mitglied aktualisieren**
if (
!vorname ||
!nachname ||
geburtsdatum === undefined ||
adresse === undefined ||
mit_seit === undefined ||
mit_num === undefined ||
helfer === undefined ||
probe === undefined
) {
return res.status(400).json({ error: "Alle Felder sind erforderlich" });
}
result = await pool.query(
`UPDATE mitglieder
SET vorname = $1, nachname = $2, geburtsdatum = $3, adresse = $4,
mit_seit = $5, mit_num = $6, helfer = $7, probe = $8
WHERE id = $9
RETURNING *`,
[
vorname,
nachname,
geburtsdatum,
adresse,
mit_seit,
mit_num,
helfer,
probe,
id,
]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: "Mitglied nicht gefunden" });
}
return res.json({
message: "Mitglied erfolgreich aktualisiert",
member: result.rows[0],
});
} else {
// **Neues Mitglied anlegen**
if (
!vorname ||
!nachname ||
geburtsdatum === undefined ||
adresse === undefined ||
mit_seit === undefined ||
mit_num === undefined ||
helfer === undefined ||
probe === undefined
) {
return res.status(400).json({ error: "Alle Felder sind erforderlich" });
}
result = await pool.query(
`INSERT INTO mitglieder (vorname, nachname, geburtsdatum, adresse, mit_seit, mit_num, helfer, probe)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *`,
[
vorname,
nachname,
geburtsdatum,
adresse,
mit_seit,
mit_num,
helfer,
probe,
]
);
return res.status(201).json({
message: "Mitglied erfolgreich angelegt",
member: result.rows[0],
});
}
} catch (err) {
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
// **3. Mitglied löschen (Nur für Admins)**
router.delete("/:id", requireAuth, requireAdmin, async (req, res) => {
const { id } = req.params;
try {
const memberResult = await pool.query(
"SELECT id FROM mitglieder WHERE id = $1",
[id]
);
if (memberResult.rows.length === 0) {
return res.status(404).json({ error: "Mitglied nicht gefunden" });
}
await pool.query("DELETE FROM mitglieder WHERE id = $1", [id]);
res.json({ message: "Mitglied erfolgreich gelöscht" });
} catch (err) {
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
// **1. notfallnummern abrufen (Falls ID gegeben: Nur die Nummern eines Mitglieds)**
router.get("/phone/:id", requireAuth, async (req, res) => {
const { id } = req.params;
try {
const result = await pool.query(
"SELECT * FROM notfallnummern WHERE fid_mitglied = $1",
[id]
);
if (result.rows.length === 0) {
return res
.status(404)
.json({ error: "Keine notfallnummern für dieses Mitglied gefunden" });
}
return res.json(result.rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
// **2. Telefonnummer anlegen oder aktualisieren (Nur für Admins)**
router.put("/phone/:id?", requireAuth, requireAdmin, async (req, res) => {
const { id } = req.params;
const { fid_teilnehmer, name, nummer, verbindung, stand } = req.body;
try {
let result;
if (id) {
// **Telefonnummer aktualisieren**
if (!fid_teilnehmer || !nummer) {
return res
.status(400)
.json({ error: "Telefonnumer und Mitglieds ID sind erforderlich" });
}
result = await pool.query(
`UPDATE notfallnummern
SET fid_mitglied = $1, name = $2, nummer = $3, verbindung = $4, stand = COALESCE($5, stand)
WHERE id = $6
RETURNING *`,
[
fid_teilnehmer,
name || null,
nummer,
verbindung || null,
stand || new Date(),
id,
]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: "Telefonnummer nicht gefunden" });
}
return res.json({
message: "Telefonnummer erfolgreich aktualisiert",
phone: result.rows[0],
});
} else {
// **Neue Telefonnummer anlegen**
if (!fid_teilnehmer || !nummer) {
return res
.status(400)
.json({ error: 'Alle Felder außer "stand" sind erforderlich' });
}
result = await pool.query(
`INSERT INTO notfallnummern (fid_mitglied, name, nummer, verbindung, stand)
VALUES ($1, $2, $3, $4, COALESCE($5, NOW()))
RETURNING *`,
[fid_teilnehmer, name, nummer, verbindung, stand]
);
return res.status(201).json({
message: "Telefonnummer erfolgreich angelegt",
phone: result.rows[0],
});
}
} catch (err) {
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
// **3. Telefonnummer löschen (Nur für Admins)**
router.delete("/phone/:id", requireAuth, requireAdmin, async (req, res) => {
const { id } = req.params;
try {
const phoneResult = await pool.query(
"SELECT id FROM notfallnummern WHERE id = $1",
[id]
);
if (phoneResult.rows.length === 0) {
return res.status(404).json({ error: "Telefonnummer nicht gefunden" });
}
await pool.query("DELETE FROM notfallnummern WHERE id = $1", [id]);
res.json({ message: "Telefonnummer erfolgreich gelöscht" });
} catch (err) {
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
router.post("/riege/:id", requireAuth, requireAdmin, async (req, res) => {
const { id } = req.params;
const abteilung = req.abteilung;
const { neue_riege } = req.body;
if (!neue_riege) {
return res.status(400).json({ error: "Neue Riege ist erforderlich" });
}
try {
await pool.query("BEGIN"); // Transaktion starten
// **1. Prüfen, ob das Mitglied bereits in einer Riege dieser Abteilung ist**
const aktuelleRiege = await pool.query(
`SELECT rz.id, rz.fid_mitglied, rz.fid_riege
FROM riegenzuordnung rz
JOIN riegen r ON rz.fid_riege = r.id
WHERE rz.fid_mitglied = $1 AND r.fid_abteilung = $2 AND rz.bis IS NULL`,
[id, abteilung]
);
if (aktuelleRiege.rows.length > 0) {
const alteRiegenId = aktuelleRiege.rows[0].fid_riege;
// **2. Falls Mitglied bereits in einer Riege ist → Diesen Eintrag beenden**
await pool.query(
`UPDATE riegenzuordnung
SET bis = NOW()
WHERE fid_mitglied = $1 AND fid_riege = $2 AND bis IS NULL`,
[id, alteRiegenId]
);
}
// **3. Neuen Riegen-Eintrag für das Mitglied erstellen**
const result = await pool.query(
`INSERT INTO riegenzuordnung (fid_mitglied, fid_riege, von)
VALUES ($1, $2, NOW())
RETURNING *`,
[id, neue_riege]
);
await pool.query("COMMIT"); // Transaktion abschließen
res.json({
message: "Mitglied erfolgreich in neue Riege verschoben",
riegenzuordnung: result.rows[0],
});
} catch (err) {
await pool.query("ROLLBACK"); // Falls Fehler, Transaktion rückgängig machen
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
module.exports = router;

119
routes/spiele.js Normal file
View File

@ -0,0 +1,119 @@
const { requireAuth, requireAdmin } = require("../middleware/auth"); // Falls Middleware in einer extra Datei liegt
const express = require("express");
const pool = require("../db"); // Stelle sicher, dass dein DB-Pool importiert wird
const router = express.Router();
// Spieleliste
router.get("/:id?", async (req, res) => {
const { id } = req.params;
try {
let result;
if (id) {
// Falls eine ID übergeben wurde, geben wir alle Felder des Spiels zurück
result = await pool.query("SELECT * FROM spiele WHERE id = $1", [id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: "Spiel nicht gefunden" });
}
return res.json(result.rows[0]);
} else {
// Falls keine ID übergeben wurde, geben wir nur ID und Name zurück
result = await pool.query(
"SELECT id, name FROM spiele ORDER BY name ASC"
);
return res.json(result.rows);
}
} catch (err) {
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
// **2. Spiel löschen (Nur für Admins)**
router.delete("/:id", requireAuth, requireAdmin, async (req, res) => {
const { id } = req.params;
try {
// Prüfen, ob das Spiel existiert
const spielResult = await pool.query(
"SELECT id, name FROM spiele WHERE id = $1",
[id]
);
if (spielResult.rows.length === 0) {
return res.status(404).json({ error: "Spiel nicht gefunden" });
}
// Spiel löschen
await pool.query("DELETE FROM spiele WHERE id = $1", [id]);
res.json({
message: "Spiel erfolgreich gelöscht",
spiel: spielResult.rows[0],
});
} catch (err) {
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
router.put("/:id?", requireAuth, requireAdmin, async (req, res) => {
const { id } = req.params;
const { name, material, regeln, variationen, dauer, type } = req.body;
try {
let result;
if (id) {
// **Spiel aktualisieren** → Alle Felder müssen übergeben werden
if (!name || !material || !regeln || !variationen || !dauer || !type) {
return res.status(400).json({ error: "Alle Felder sind erforderlich" });
}
result = await pool.query(
`UPDATE spiele
SET name = $1, material = $2, regeln = $3, variationen = $4, dauer = $5, type = $6
WHERE id = $7
RETURNING *`,
[name, material, regeln, variationen, dauer, type, id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: "Spiel nicht gefunden" });
}
return res.json({
message: "Spiel erfolgreich aktualisiert",
spiel: result.rows[0],
});
} else {
// **Neues Spiel anlegen** → Nur `name` ist erforderlich
if (!name) {
return res
.status(400)
.json({ error: "Der Name des Spiels ist erforderlich" });
}
result = await pool.query(
`INSERT INTO spiele (name)
VALUES ($1)
RETURNING *`,
[name]
);
return res
.status(201)
.json({ message: "Spiel erfolgreich angelegt", spiel: result.rows[0] });
}
} catch (err) {
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
module.exports = router;

244
routes/training.js Normal file
View File

@ -0,0 +1,244 @@
const express = require("express");
const pool = require("../db"); // PostgreSQL-Datenbankverbindung
const { requireAuth, requireAdmin } = require("../middleware/auth"); // Auth-Middleware
const router = express.Router();
// **Trainings abrufen (mit Geräten & Leitern)**
router.get("/:year?/:kw?", requireAuth, async (req, res) => {
const { year, kw } = req.params;
const abteilung = req.abteilung; // Abteilung aus Middleware
if (!abteilung) {
return res.status(400).json({ error: "Abteilung ist erforderlich" });
}
try {
let query;
let values = [abteilung];
if (year && kw) {
// **Trainings einer bestimmten Woche abrufen**
query = `
SELECT * FROM trainings
WHERE abteilung = $1 AND year = $2 AND kw = $3
ORDER BY year DESC, kw DESC
`;
values.push(year, kw);
} else if (year) {
// **Alle Trainings eines yeares abrufen**
query = `
SELECT * FROM trainings
WHERE abteilung = $1 AND year = $2
ORDER BY kw ASC
`;
values.push(year);
} else {
// **Alle Trainings der Abteilung abrufen**
query = `
SELECT * FROM trainings
WHERE abteilung = $1
ORDER BY year DESC, kw DESC
`;
}
const result = await pool.query(query, values);
if (result.rows.length === 0) {
return res.status(404).json({ error: "Keine Trainings gefunden" });
}
return res.json(result.rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
router.post("/leiten/:id", requireAuth, async (req, res) => {
const { id } = req.params; // Trainings-ID
const abteilung = req.abteilung; // Abteilung aus Middleware
const { fid_helfer, fid_spiel, typ, action } = req.body; // Daten aus POST-Body
if (!action || !["new", "update"].includes(action)) {
return res.status(400).json({
error: 'Es muss "new" oder "update" als action angegeben werden',
});
}
if (!fid_helfer || !typ) {
return res
.status(400)
.json({ error: "fid_helfer und typ sind erforderlich" });
}
if (typ !== "a" && typ !== "s") {
return res
.status(400)
.json({ error: 'Typ muss "a" (Aufwärmen) oder "s" (Spiel) sein' });
}
try {
if (action === "new") {
// **Neuen Eintrag erstellen**
const result = await pool.query(
`INSERT INTO leiten (fid_training, fid_helfer, fid_spiel, typ)
VALUES ($1, $2, $3, $4)
RETURNING *`,
[id, fid_helfer, fid_spiel || null, typ]
);
return res.status(201).json({
message: "Leiten erfolgreich hinzugefügt",
leiten: result.rows[0],
});
} else {
// **Vorhandenen Eintrag aktualisieren**
const checkLeiten = await pool.query(
`SELECT * FROM leiten WHERE fid_training = $1 AND fid_helfer = $2 AND typ = $3`,
[id, fid_helfer, typ]
);
if (checkLeiten.rows.length === 0) {
return res
.status(404)
.json({ error: "Kein passender Leiten-Eintrag gefunden" });
}
const updateResult = await pool.query(
`UPDATE leiten
SET fid_spiel = $1
WHERE fid_training = $2 AND fid_helfer = $3 AND typ = $4
RETURNING *`,
[fid_spiel || null, id, fid_helfer, typ]
);
return res.json({
message: "Leiten erfolgreich aktualisiert",
leiten: updateResult.rows[0],
});
}
} catch (err) {
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
router.post("/new/:jahr/:kw", requireAuth, requireAdmin, async (req, res) => {
const { jahr, kw } = req.params;
const abteilung = req.abteilung; // Abteilung aus Middleware
if (!abteilung) {
return res.status(400).json({ error: "Abteilung ist erforderlich" });
}
try {
await pool.query("BEGIN"); // Transaktion starten
// **1. Prüfen, ob bereits ein Training in dieser Woche existiert**
const existingTraining = await pool.query(
`SELECT id FROM trainings WHERE year = $1 AND kw = $2 AND abteilung = $3`,
[jahr, kw, abteilung]
);
if (existingTraining.rows.length > 0) {
await pool.query("ROLLBACK");
return res
.status(400)
.json({ error: "Training für diese Woche existiert bereits" });
}
// **2. Prüfen, ob die Abteilung Riegen hat**
const abteilungResult = await pool.query(
`SELECT riegen, geraete FROM abteilungen WHERE id = $1`,
[abteilung]
);
if (abteilungResult.rows.length === 0) {
return res.status(404).json({ error: "Abteilung nicht gefunden" });
}
const { riegen, geraete } = abteilungResult.rows[0]; // `riegen = true/false`, `geraete` = int[]
// **3. Neues Training anlegen**
const trainingResult = await pool.query(
`INSERT INTO trainings (year, kw, abteilung) VALUES ($1, $2, $3) RETURNING id`,
[jahr, kw, abteilung]
);
const trainingId = trainingResult.rows[0].id;
let geraeteplan = [];
if (riegen) {
// **4. Die letzten zwei Trainings abrufen**
const lastTrainings = await pool.query(
`SELECT g.geraete
FROM trainings t
JOIN geraeteplan g ON t.id = g.fid_training
WHERE t.abteilung = $1
ORDER BY t.year DESC, t.kw DESC
LIMIT 2`,
[abteilung]
);
let lastUsed1 =
lastTrainings.rows.length > 0 ? lastTrainings.rows[0].geraete : [];
let lastUsed2 =
lastTrainings.rows.length > 1 ? lastTrainings.rows[1].geraete : [];
// **5. Falls eines der letzten beiden Trainings Geräte 1113 enthielt → Letztes gültiges Training nehmen**
if (lastUsed1.some((g) => g >= 11 && g <= 13)) {
lastUsed1 = [];
}
if (lastUsed2.some((g) => g >= 11 && g <= 13)) {
lastUsed2 = [];
}
// **6. Bestimmen, ob hochgezählt werden soll**
let nextGeraete = [];
if (
lastUsed1.length > 0 &&
JSON.stringify(lastUsed1) === JSON.stringify(lastUsed2)
) {
// Hochzählen, weil die letzten beiden Trainings identisch waren
let lastDeviceIndex = geraete.findIndex((g) => g === lastUsed1[0]);
// Falls letztes Gerät 10 war, dann zurück auf 1
let nextDevice = geraete[(lastDeviceIndex + 1) % geraete.length];
if (lastUsed1[0] === 10) {
nextDevice = 1;
}
nextGeraete = [nextDevice, ...lastUsed1.slice(0, 4)];
} else if (lastUsed1.length > 0) {
// Letztes Training wiederholen
nextGeraete = lastUsed1;
} else {
// Erstes Training → Starte mit den ersten fünf Geräten
nextGeraete = geraete.slice(0, 5);
}
// **7. Geräteplan speichern als int[]**
await pool.query(
`INSERT INTO geraeteplan (fid_training, geraete) VALUES ($1, $2)`,
[trainingId, nextGeraete]
);
geraeteplan = nextGeraete;
}
await pool.query("COMMIT"); // Transaktion abschließen
res.status(201).json({
message: "Training erfolgreich erstellt",
training: { id: trainingId, year: jahr, kw, abteilung },
geraeteplan,
});
} catch (err) {
await pool.query("ROLLBACK"); // Falls Fehler, Transaktion rückgängig machen
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
module.exports = router;

341
routes/user.js Normal file
View File

@ -0,0 +1,341 @@
const express = require("express");
const pool = require("../db"); // Stelle sicher, dass dein DB-Pool importiert wird
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
const nodemailer = require("nodemailer");
const { requireAuth, requireAdmin } = require("../middleware/auth"); // Falls Middleware in einer extra Datei liegt
const { transporter, sendActivationEmail } = require("../middleware/mail");
const router = express.Router();
// Registrierung
router.post("/register", async (req, res) => {
const { username, email, password } = req.body;
try {
const hashedPassword = await bcrypt.hash(password, 10);
await pool.query(
"INSERT INTO users (username, email, password) VALUES ($1, $2, $3)",
[username, email, hashedPassword]
);
const message =
"Registrierung erfolgreich. Ein Admin wird dich in kürze freischalten";
const mailOptions = {
to: "admin@boergmann.it",
from: "admin@boergmann.it",
subject: "Neue Registrierung",
text: `${username} hat sich registriert`,
};
transporter.sendMail(mailOptions, (error) => {
if (error) {
console.error("Error sending email:", error);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
res.status(201).json({ message: message });
} catch (error) {
console.error("Error registering user:", error);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
// Login
router.post("/login", async (req, res) => {
const { username, password } = req.body;
try {
const userResult = await pool.query(
"SELECT *, CASE WHEN admin_temp IS NOT NULL AND (now() - admin_temp) > interval '22 hours' THEN 'expired' ELSE 'valid' END AS admin_status FROM users WHERE username = $1",
[username]
);
if (userResult.rows.length > 0) {
const user = userResult.rows[0];
console.log(user);
const match = await bcrypt.compare(password, user.password);
if (match) {
if (user.is_active) {
const token = jwt.sign(
{ id: user.id, username: user.username, role: user.role },
process.env.JWT_SECRET,
{
expiresIn: "24h",
}
);
if (user.admin_status === "expired") {
await pool.query(
"UPDATE users SET role = $1, admin_temp = NULL WHERE id = $2",
["user", user.id]
);
}
res.json({ token });
} else {
return res.status(401).json({ error: "Auf Freischlatung warten" });
}
} else {
const message = "Falsches Passwort";
return res.status(401).json({ error: "Ungültige Anmeldedaten" });
}
} else {
const message = "Unbekannter Benutzer";
return res.status(401).json({ error: "Ungültige Anmeldedaten" });
}
} catch (error) {
console.error("Error logging in:", error);
const message = "Error logging in:" + error;
return res.status(401).json({ error: "Ungültige Anmeldedaten" });
}
});
// Reset initiieren
router.post("/reset-password", async (req, res) => {
const { email } = req.body;
if (!email) {
return res.status(400).json({ error: "E-Mail-Adresse ist erforderlich" });
}
try {
// Prüfen, ob die E-Mail existiert
const userResult = await pool.query(
"SELECT id, username FROM users WHERE email = $1",
[email]
);
if (userResult.rows.length === 0) {
return res
.status(404)
.json({ error: "Benutzer mit dieser E-Mail nicht gefunden" });
}
const { id, username } = userResult.rows[0];
// Token generieren (random 32 Byte)
const token = crypto.randomBytes(32).toString("hex");
const expires = new Date(Date.now() + 3600000); // Token 1 Stunde gültig
// Token und Ablaufdatum in die DB speichern
await pool.query(
"UPDATE users SET reset_password_token = $1, reset_password_expires = $2 WHERE id = $3",
[token, expires, id]
);
// Mail senden
const resetLink = `https://${process.env.HOST}/reset/${token}`;
const mailOptions = {
from: process.env.MAILUSER,
to: email,
subject: "Passwort zurücksetzen",
text: `Hallo ${username},\n\nKlicke auf den folgenden Link, um dein Passwort zurückzusetzen:\n\n${resetLink}\n\nDer Link ist 1 Stunde gültig.\n\nFalls du das Zurücksetzen nicht beantragt hast, ignoriere diese Nachricht.`,
};
await transporter.sendMail(mailOptions);
res.json({ message: "Passwort-Reset-Link wurde per E-Mail gesendet." });
} catch (err) {
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
// Reset Abschließen
router.post("/reset/:token", async (req, res) => {
const { token } = req.params;
const { password } = req.body;
if (!password) {
return res.status(400).json({ error: "Neues Passwort ist erforderlich" });
}
try {
// Token in der Datenbank suchen und prüfen, ob es noch gültig ist
const result = await pool.query(
"SELECT id FROM users WHERE reset_password_token = $1 AND reset_password_expires > NOW()",
[token]
);
if (result.rows.length === 0) {
return res
.status(400)
.json({ error: "Ungültiges oder abgelaufenes Token" });
}
const userId = result.rows[0].id;
// Passwort hashen
const hashedPassword = await bcrypt.hash(password, 10);
// Passwort speichern und Token-Felder leeren
await pool.query(
"UPDATE users SET password = $1, reset_password_token = NULL, reset_password_expires = NULL WHERE id = $2",
[hashedPassword, userId]
);
res.json({ message: "Passwort erfolgreich zurückgesetzt" });
} catch (err) {
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
// Email und/oder Passwort ändern
router.post("/update", requireAuth, async (req, res) => {
const { email, password } = req.body;
try {
if (email) {
await pool.query("UPDATE users SET email = $1 WHERE id = $2", [
email,
req.user.id,
]);
}
if (password) {
const hashedPassword = await bcrypt.hash(password, 10);
await pool.query("UPDATE users SET password = $1 WHERE id = $2", [
hashedPassword,
req.user.id,
]);
}
res.json({
message: "Benutzerdaten erfolgreich aktualisiert",
});
} catch (error) {
console.error("Error updating profile:", error);
const message = "Error updating profile:" + error;
res.status(400).json({ error: "Keine Änderungen vorgenommen" });
}
});
// Liste der User mit Rollen und is_active
router.get("/admin", requireAuth, requireAdmin, async (req, res) => {
try {
const result = await pool.query(
"SELECT id, username, email, role, is_active FROM users ORDER BY is_active ASC, role DESC"
);
res.json(result.rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
router.put("/activate/:id", requireAuth, requireAdmin, async (req, res) => {
const { id } = req.params;
try {
// Benutzer-Daten abrufen (inklusive E-Mail und Benutzername)
const userResult = await pool.query(
"SELECT username, email FROM users WHERE id = $1",
[id]
);
if (userResult.rows.length === 0) {
return res.status(404).json({ error: "Benutzer nicht gefunden" });
}
const { username, email } = userResult.rows[0];
// Benutzer aktivieren
const result = await pool.query(
"UPDATE users SET is_active = true WHERE id = $1 RETURNING id, username, is_active",
[id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: "Benutzer nicht gefunden" });
}
// E-Mail senden (wenn vorhanden)
await sendActivationEmail(email, username);
res.json({
message: "Benutzer erfolgreich aktiviert",
user: result.rows[0],
});
} catch (err) {
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
router.delete("/:id", requireAuth, requireAdmin, async (req, res) => {
const { id } = req.params;
try {
// Prüfen, ob der Benutzer existiert
const userResult = await pool.query(
"SELECT id, username FROM users WHERE id = $1",
[id]
);
if (userResult.rows.length === 0) {
return res.status(404).json({ error: "Benutzer nicht gefunden" });
}
// Verhindern, dass sich Admins selbst löschen
if (req.user.id == id) {
return res
.status(400)
.json({ error: "Ein Admin kann sich nicht selbst löschen" });
}
// Benutzer löschen
await pool.query("DELETE FROM users WHERE id = $1", [id]);
res.json({
message: "Benutzer erfolgreich gelöscht",
user: userResult.rows[0],
});
} catch (err) {
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
// User zu Admins machen
router.put("/admin/:id", requireAuth, requireAdmin, async (req, res) => {
const { id } = req.params;
const { temporary } = req.body; // erwartet { "temporary": true } oder { "temporary": false }
if (temporary === undefined) {
return res.status(400).json({
error: "Es muss angegeben werden, ob der Admin-Zugang temporär sein soll",
});
}
try {
let query, values;
if (temporary) {
query = `
UPDATE users
SET role = 'admin', admin_temp = NOW() + INTERVAL '48 hours'
WHERE id = $1 RETURNING id, username, role, admin_temp
`;
values = [id];
} else {
query = `
UPDATE users
SET role = 'admin', admin_temp = NULL
WHERE id = $1 RETURNING id, username, role, admin_temp
`;
values = [id];
}
const result = await pool.query(query, values);
if (result.rows.length === 0) {
return res.status(404).json({ error: "Benutzer nicht gefunden" });
}
res.json({
message: "Benutzer wurde zum Admin gemacht",
user: result.rows[0],
});
} catch (err) {
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
}
});
module.exports = router;