const express = require('express'); const session = require('express-session'); const bcrypt = require('bcrypt'); const crypto = require('crypto'); const nodemailer = require('nodemailer'); const { Pool } = require('pg'); const path = require('path'); const moment = require('moment'); require('dotenv').config(); const app = express(); const port = process.env.PORT; // Middleware app.use(express.static(path.join(__dirname, 'public'))); app.use(express.json()); app.set('view engine', 'ejs'); app.use(express.urlencoded({ extended: false })); // Session-Konfiguration app.use(session({ secret: process.env.SESSIONSECRET, resave: false, saveUninitialized: false, cookie: { maxAge: 1800000 } })); // Authentifizierungs-Middleware const requireAuth = (req, res, next) => { if (!req.session.userId) { return res.redirect('/login'); } next(); }; const requireAdmin = (req, res, next) => { if (req.session.role !== 'admin') { return res.status(403).send('Access denied'); } next(); }; // Email-Konfiguration const transporter = nodemailer.createTransport({ host: process.env.MAILHOST, port: 465, secure: true, auth: { user: process.env.MAILUSER, pass: process.env.MAILPASS } }); // Datenbankverbindung const pool = new Pool({ connectionString: process.env.DATABASE_URL }); // Altersberechnung const calculateAge = (birthdate) => { const today = new Date(); const birthDate = new Date(birthdate); let age = today.getFullYear() - birthDate.getFullYear(); const m = today.getMonth() - birthDate.getMonth(); if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) { age--; } return age; }; // Datumsformatierung const formatDate = (date) => { const d = new Date(date); const day = String(d.getDate()).padStart(2, '0'); const month = String(d.getMonth() + 1).padStart(2, '0'); const year = d.getFullYear(); return `${day}.${month}.${year}`; }; // Funktion zum Laden des aktuellen oder letzten Trainings const getTraining = async (selectedDate) => { const today = selectedDate ? new Date(selectedDate) : new Date(); const dayOfWeek = today.getDay(); // Wenn heute Donnerstag ist if (dayOfWeek === 4) { const result = await pool.query(` SELECT tr.*, g1.name AS geraet_riege_1_name, g2.name AS geraet_riege_2_name, g3.name AS geraet_riege_3_name, g4.name AS geraet_riege_4_name, g5.name AS geraet_riege_5_name, t1.name AS aufwaermleiter_name, t2.name AS spielleiter_name, sp1.name AS aufwaermen_name, sp2.name AS spiel_name FROM trainings tr LEFT JOIN geraete g1 ON tr.geraet_riege_1 = g1.id LEFT JOIN geraete g2 ON tr.geraet_riege_2 = g2.id LEFT JOIN geraete g3 ON tr.geraet_riege_3 = g3.id LEFT JOIN geraete g4 ON tr.geraet_riege_4 = g4.id LEFT JOIN geraete g5 ON tr.geraet_riege_5 = g5.id LEFT JOIN teilnehmende t1 ON tr.aufwaermleiter = t1.id LEFT JOIN teilnehmende t2 ON tr.spielleiter = t2.id LEFT JOIN spiele sp1 ON tr.aufwaermen = sp1.id LEFT JOIN spiele sp2 ON tr.spiel = sp2.id WHERE tr.datum = $1 LIMIT 1 `, [today]); return result.rows[0]; } else { const result = await pool.query(` SELECT tr.*, g1.name AS geraet_riege_1_name, g2.name AS geraet_riege_2_name, g3.name AS geraet_riege_3_name, g4.name AS geraet_riege_4_name, g5.name AS geraet_riege_5_name, t1.name AS aufwaermleiter_name, t2.name AS spielleiter_name, sp1.name AS aufwaermen_name, sp2.name AS spiel_name FROM trainings tr LEFT JOIN geraete g1 ON tr.geraet_riege_1 = g1.id LEFT JOIN geraete g2 ON tr.geraet_riege_2 = g2.id LEFT JOIN geraete g3 ON tr.geraet_riege_3 = g3.id LEFT JOIN geraete g4 ON tr.geraet_riege_4 = g4.id LEFT JOIN geraete g5 ON tr.geraet_riege_5 = g5.id LEFT JOIN teilnehmende t1 ON tr.aufwaermleiter = t1.id LEFT JOIN teilnehmende t2 ON tr.spielleiter = t2.id LEFT JOIN spiele sp1 ON tr.aufwaermen = sp1.id LEFT JOIN spiele sp2 ON tr.spiel = sp2.id WHERE tr.datum <= $1 ORDER BY tr.datum DESC LIMIT 1 `, [today]); return result.rows[0]; } }; // Funktion zum Laden aller Spiele const getAllSpiele = async () => { const result = await pool.query(` SELECT * FROM spiele ORDER BY name ASC; `); return result.rows; }; // Funktion zum Hinzufügen eines neuen Spiels, falls es noch nicht existiert const addNewSpiel = async (spielName) => { try { const result = await pool.query('INSERT INTO spiele (name) VALUES ($1) RETURNING id', [spielName]); return result.rows[0].id; } catch (error) { console.error('Error adding new spiel:', error); throw error; } }; // Funktion zum Laden der vier Leute, die am längsten nicht Aufwärmen bzw. Spiel geleitet haben // Funktion zum Laden der vier Leute, die am längsten nicht Aufwärmen geleitet haben const getCandidatesForAufwaermleiter = async () => { const result = await pool.query(` SELECT t.id, t.name, COALESCE(EXTRACT(EPOCH FROM (NOW() - MAX(tr.datum))) / 604800, EXTRACT(EPOCH FROM (NOW() - '1970-01-01'::date)) / 604800) AS weeks_since_last FROM teilnehmende t LEFT JOIN trainings tr ON t.id = tr.aufwaermleiter WHERE t.helfer = true GROUP BY t.id ORDER BY weeks_since_last DESC; `); return result.rows; }; // Funktion zum Laden der vier Leute, die am längsten nicht Spiel geleitet haben const getCandidatesForSpielleiter = async () => { const result = await pool.query(` SELECT t.id, t.name, COALESCE(EXTRACT(EPOCH FROM (NOW() - MAX(tr.datum))) / 604800, EXTRACT(EPOCH FROM (NOW() - '1970-01-01'::date)) / 604800) AS weeks_since_last FROM teilnehmende t LEFT JOIN trainings tr ON t.id = tr.spielleiter WHERE t.helfer = true GROUP BY t.id ORDER BY weeks_since_last DESC; `); return result.rows; }; // Funktion zum Laden der Riegenteilnehmer absteigend nach Alter sortiert const getRiegenMitgliederSortedByAge = async () => { const result = await pool.query(` SELECT r.riegennummer, t.name, t.alter FROM riegen r JOIN teilnehmende t ON r.fremdid_teilnehmende = t.id ORDER BY r.riegennummer, t.alter DESC `); return result.rows; }; // Registrierung app.post('/register', async (req, res) => { const { username, password } = req.body; try { const hashedPassword = await bcrypt.hash(password, 10); await pool.query('INSERT INTO users (username, password) VALUES ($1, $2)', [username, hashedPassword]); res.redirect('/login'); } catch (error) { console.error('Error registering user:', error); res.status(500).send('Internal Server Error'); } }); // Login app.post('/login', async (req, res) => { const { username, password } = req.body; try { const userResult = await pool.query('SELECT * FROM users WHERE username = $1', [username]); if (userResult.rows.length > 0) { const user = userResult.rows[0]; const match = await bcrypt.compare(password, user.password); if (match) { req.session.userId = user.id; req.session.role=user.role; res.redirect('/'); } else { res.redirect('/login'); } } else { res.redirect('/login'); } } catch (error) { console.error('Error logging in:', error); res.status(500).send('Internal Server Error'); } }); // Logout app.post('/logout', (req, res) => { req.session.destroy(err => { if (err) { return res.status(500).send('Internal Server Error'); } res.redirect('/login'); }); }); // Benutzer freischalten (nur Admin) app.post('/activate', requireAuth, requireAdmin, async (req, res) => { const { userId } = req.body; try { await pool.query('UPDATE users SET is_active = TRUE WHERE id = $1', [userId]); res.redirect('/admin'); } catch (error) { console.error('Error activating user:', error); res.status(500).send('Internal Server Error'); } }); // Passwort-Zurücksetzung anfordern app.post('/forgot-password', async (req, res) => { const { email } = req.body; console.log ( email ); try { const userResult = await pool.query('SELECT * FROM users WHERE email = $1', [email]); if (userResult.rows.length > 0) { const user = userResult.rows[0]; const token = crypto.randomBytes(20).toString('hex'); const resetLink = `http://tkd.boergmann.it/reset-password/${token}`; console.log(resetLink); await pool.query('UPDATE users SET reset_password_token = $1, reset_password_expires = $2 WHERE id = $3', [token, Date.now() + 3600, user.id]); const mailOptions = { to: user.email, from: 'admin@boergmann.it', subject: 'Password Reset', text: `Click the following link to reset your password: ${resetLink}` }; transporter.sendMail(mailOptions, (err) => { if (err) { console.error('Error sending email:', err); res.status(500).send('Error sending email'); } else { res.send('Password reset link sent'); } }); } else { res.status(400).send('Email not found'); } } catch (error) { console.error('Error in forgot-password:', error); res.status(500).send('Internal Server Error'); } }); app.get('/forgot-password', async (req, res) => { res.render('forgot-password', {session: req.session, token : '123'}) }) // Passwort zurücksetzen app.get('/reset-password/:token', async (req, res) => { const { token } = req.params; try { const userResult = await pool.query('SELECT * FROM users WHERE reset_password_token = $1 AND reset_password_expires > $2', [token, Date.now()]); if (userResult.rows.length > 0) { res.render('reset-password', { token }); // Stelle sicher, dass es eine reset-password.ejs gibt } else { res.status(400).send('Password reset token is invalid or has expired'); } } catch (error) { console.error('Error in reset-password:', error); res.status(500).send('Internal Server Error'); } }); app.post('/reset-password/:token', async (req, res) => { const { token } = req.params; const { password } = req.body; try { const userResult = await pool.query('SELECT * FROM users WHERE reset_password_token = $1 AND reset_password_expires > $2', [token, Date.now()]); if (userResult.rows.length > 0) { const user = userResult.rows[0]; const hashedPassword = await bcrypt.hash(password, 10); await pool.query('UPDATE users SET password = $1, reset_password_token = NULL, reset_password_expires = NULL WHERE id = $2', [hashedPassword, user.id]); res.redirect('/login'); } else { res.status(400).send('Password reset token is invalid or has expired'); } } catch (error) { console.error('Error in reset-password:', error); res.status(500).send('Internal Server Error'); } }); // Profilseite app.get('/profile', requireAuth, (req, res) => { res.render('profile', { session: req.session}); // Stelle sicher, dass es eine profile.ejs gibt }); app.post('/profile', 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.session.userId]); } if (password) { const hashedPassword = await bcrypt.hash(password, 10); await pool.query('UPDATE users SET password = $1 WHERE id = $2', [hashedPassword, req.session.userId]); } res.redirect('/profile'); } catch (error) { console.error('Error updating profile:', error); res.status(500).send('Internal Server Error'); } }); app.post('/update-training', async (req, res) => { const { trainingId, type, spielName } = req.body; let spielId; const spiel = await pool.query(`SELECT * FROM spiele WHERE name = $1`, [spielName]); if (spiel.rows.length > 0) { spielId = spiel.rows[0].id; console.log('Spiel existiert, die ID ist $1', [ spielId ]); } else { const newSpiel = await pool.query('INSERT INTO spiele (name) VALUES ($1) RETURNING id', [spielName]); spielId = newSpiel.rows[0].id; console.log('Spiel existiert nicht, wurde mitID $1 angelegt', [ spielId ]); } try { if (type === 'spiel') { await pool.query('UPDATE trainings SET spiel = $1 WHERE id = $2', [ spielId, trainingId ]); } else if (type === 'aufwaermen') { await pool.query('UPDATE trainings SET aufwaermen = $1 WHERE id = $2', [ spielId, trainingId ]); } res.redirect('/'); } catch (err) { console.error(err); res.send("Error " + err); } }); app.post('/update-leader', async (req, res) => { const { trainingId, type, leaderId } = req.body; try { if (type === 'aufwaermleiter') { await pool.query('UPDATE trainings SET aufwaermleiter = $1 WHERE id = $2', [leaderId, trainingId]); } else if (type === 'spielleiter') { await pool.query('UPDATE trainings SET spielleiter = $1 WHERE id = $2', [leaderId, trainingId]); } res.redirect('/'); } catch (err) { console.error(err); res.send("Error " + err); } }); // Admin-Seite app.get('/admin', requireAuth, requireAdmin, async (req, res) => { const usersResult = await pool.query('SELECT * FROM users WHERE is_active = FALSE'); res.render('admin', { users: usersResult.rows, session: req.session }); // Stelle sicher, dass es eine admin.ejs gibt }); // Teilnehmer_innen app.get('/', requireAuth, async (req, res) => { try { let dateParam = req.query.date; let selectedDate; if (dateParam) { selectedDate = moment(dateParam, 'DD.MM.YYYY').toDate(); } else { selectedDate = moment().toDate(); } const training =await getTraining(selectedDate); const trainingsResult = await pool.query('SELECT datum FROM trainings ORDER BY datum ASC'); const trainingsDates = trainingsResult.rows.map(tr => ({ datum: formatDate(tr.datum), rawDatum: tr.datum })); // Vorheriges Training ermitteln const previousTrainingResult = await pool.query('SELECT * FROM trainings WHERE datum < $1 ORDER BY datum DESC LIMIT 1',[ selectedDate ]); const nextTrainingResult = await pool.query('SELECT * FROM trainings WHERE datum > $1 ORDER BY datum ASC LIMIT 1',[ selectedDate ]); const previousTraining = previousTrainingResult.rows.length > 0 ? previousTrainingResult.rows[0] : null; const nextTraining = nextTrainingResult.rows.length > 0 ? nextTrainingResult.rows[0] : null; if (training) { training.datum = formatDate(training.datum); } const aufwaermleiterCandidates = await getCandidatesForAufwaermleiter(); const spielleiterCandidates = await getCandidatesForSpielleiter(); const spielCandidates = await getAllSpiele(); const aufwaermenCandidates = await getAllSpiele(); res.render('trainings', { training, trainingsDates, selectedDate: training.datum, aufwaermleiterCandidates, spielleiterCandidates, aufwaermenCandidates, // Übergeben der Kandidaten für Aufwärmleiter spielCandidates, // Übergeben der Spiele session: req.session, previousTraining, nextTraining, moment }); } catch (err) { console.error(err); res.send("Error " + err); } }); app.get('/riege', requireAuth, async (req, res) => { try { // Abrufen der Riegendaten einschließlich der Teilnehmer und deren Altersberechnung const result = await pool.query(` SELECT r.riegennummer, t.id, t.name, t.geburtsdatum, r.helfer FROM riegen r JOIN teilnehmende t ON r.fremdID_Teilnehmende = t.id ORDER BY r.riegennummer, t.geburtsdatum ASC `); // Gruppieren der Riegenteilnehmer nach Riegennummer const riegen = {}; result.rows.forEach(row => { const age = calculateAge(row.geburtsdatum); if (!riegen[row.riegennummer]) { riegen[row.riegennummer] = []; } riegen[row.riegennummer].push({ id: row.id, name: row.name, age: age, helfer: row.helfer, }); }); res.render('riegen', { riegen: riegen, session: req.session }); } catch (error) { console.error('Error fetching riegen:', error); res.status(500).send('Internal Server Error'); } }); app.get('/teilnehmer', requireAuth, async (req, res) => { try { const teilnehmendeResult = await pool.query('SELECT * FROM teilnehmende'); const teilnehmende = teilnehmendeResult.rows.map(t => ({ ...t, age: calculateAge(t.geburtsdatum) })).sort((a, b) => b.age - a.age); res.render('teilnehmer', { teilnehmende, session: req.session }); } catch (err) { console.error(err); res.send("Error " + err); } }); app.get('/mitglied/:id', requireAuth, async (req, res) => { const { id } = req.params; try { const userResult = await pool.query('SELECT * FROM teilnehmende WHERE id = $1', [id]); if (userResult.rows.length > 0) { const mitglied = userResult.rows.map(t => ({ ...t, age: calculateAge(t.geburtsdatum) })) res.render('mitglied', { id, mitglied: mitglied[0], session: req.session }); } else { res.status(400).send('Mitglied existiert nicht'); } } catch (error) { console.error('Error in Mitglied:', error); res.status(500).send('Internal Server Error'); } }); // Login und Registrierung anzeigen app.get('/login', (req, res) => { res.render('login', {session: req.session}); // Stelle sicher, dass es eine login.ejs gibt }); app.get('/register', (req, res) => { res.render('register'); // Stelle sicher, dass es eine register.ejs gibt }); const server = app.listen(port, '0.0.0.0', () => { console.log(`Server is running on http://localhost:${port}/`); });