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(); 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; }; const putInRiege = async (riege, teilnehmerID) => { try { const resultRiege = await pool.query('SELECT * FROM riegen WHERE fremdid_teilnehmende = $1;', [teilnehmerID]); if (resultRiege.rows.length > 0) { const resultRiege = await pool.query('UPDATE riegen SET riegennummer = $1 WHERE fremdid_teilnehmende = $2', [riege, teilnehmerID]); console.log("Mitglied ist jetzt in Riege $1", [riege]); } else { const resultRiege = await pool.query('INSERT INTO riegen (fremdid_teilnehmende, riegennummer) VALUES ($1, $2)', [teilnehmerID, riege]); console.log("Mitglied neu in Riege $1 eingefügt.", [riege]); } } catch (error) { console.error('Error adding new spiel:', error); throw error; } }; // 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, 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); const message = 'Error sending Mail:' + error; res.render('error', {session: req.session, message}); } }); res.render('error', {session: req.session, message}); } catch (error) { console.error('Error registering user:', error); const message = 'Error registering user:' + error; res.render('error', {session: req.session, message}); } }); // Login app.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]; const match = await bcrypt.compare(password, user.password); if (match) { if (user.is_active) { req.session.userId = user.id; if (user.admin_status === 'expired') { await pool.query('UPDATE users SET role = $1, admin_temp = NULL WHERE id = $2', ['user', user.id]); req.session.role='user'; } else { req.session.role=user.role; } res.redirect('/training'); } else { res.redirect('/freischaltung') } } else { const message = 'Falscher Benutzername oder falsches Passwort'; res.render('login', {session: req.session, username, message}); } } else { const message = 'Falscher Benutzername oder falsches Passwort'; res.render('login', {session: req.session, username, message}); } } catch (error) { console.error('Error logging in:', error); const message = 'Error logging in:' + error; res.render('error', {session: req.session, message}); } }); //Wird angezeigt, wenn ein nicht freigeschalteter User sich anmelden will. app.get('/freischaltung', async (req, res) => { res.render('freischaltung', { session: req.session}); }) // Logout app.get('/logout', (req, res) => { req.session.destroy(err => { if (err) { return res.status(500).send('Internal Server Error'); } res.redirect('/'); }); }); // Benutzer freischalten (nur Admin) app.post('/userrights', requireAuth, requireAdmin, async (req, res) => { const { userId, type } = req.body; try { if (type === 'activate'){ await pool.query('UPDATE users SET is_active = TRUE WHERE id = $1', [userId]); const userResult = await pool.query('SELECT * FROM users WHERE id = $1', [userId]); if (userResult.rows.length > 0) { if (userResult.rows[0].email){ const mailOptions = { to: userResult.rows[0].email, from: 'admin@boergmann.it', subject: 'Freischaltung', text: `Hallo ${userResult.rows[0].username}, du wurdest soeben freigeschaltet.` }; transporter.sendMail(mailOptions, (error) => { if (error) { console.error('Error sending email:', error); const message = 'Error sending Mail:' + error; res.render('error', {session: req.session, message}); } })}} } else if (type === 'admin') { await pool.query('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]); } else if (type === 'admint') { await pool.query('UPDATE users SET role = $1, admin_temp = $2 WHERE id = $3', ['admin', moment().toDate() , userId]); } else if (type === 'delete') { await pool.query('DELETE FROM users WHERE id = $1', [userId]); } res.redirect('/admin'); } catch (error) { console.error('Error activating user:', error); const message = 'Error activating user:' + error; res.render('error', {session: req.session, message}); } }); // Passwort-Zurücksetzung anfordern app.post('/send-password', async (req, res) => { const { email } = req.body; 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}`; await pool.query('UPDATE users SET reset_password_token = $1, reset_password_expires = $2 WHERE id = $3', [token, selectedDate = moment().add(1,'d').toDate() , 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); const message = 'Error sending Mail:' + error; res.render('error', {session: req.session, message}); } else { const message = 'Password reset link sent'; res.render('error', {session: req.session, message}); } }); } else { const message = 'Email not found'; res.render('error', {session: req.session, message}); } } catch (error) { console.error('Error in forgot-password:', error); const message = 'Error in forgot-password'; res.render('error', {session: req.session, message}); } }); app.get('/forgot-password', async (req, res) => { res.render('forgot-password', {session: req.session}) }) // 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 { const message = 'Token ungültig oder abgelaufen'; res.render('error', {session: req.session, message}); } } catch (error) { console.error('Error in reset-password:', error); const message = 'Error in reset-password'; res.render('error', {session: req.session, message}); } }); 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 { const message = 'Token ungültig oder abgelaufen'; res.render('error', {session: req.session, message}); } } catch (error) { console.error('Error in reset-password:', error); const message = 'Error in reset-password'; res.render('error', {session: req.session, message}); } }); // 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); const message = 'Error updating profile:' + error; res.render('error', {session: req.session, message}); } }); app.post('/update-training', requireAuth, 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('/training'); } catch (error) { console.error(error); const message = 'Error:' + error; res.render('error', {session: req.session, message}); } }); app.post('/update-leader', requireAuth , 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('/training'); } catch (error) { console.error(error); const message = 'Error:' + error; res.render('error', {session: req.session, message}); } }); // Admin-Seite app.get('/admin', requireAuth, requireAdmin, async (req, res) => { const usersResult = await pool.query('SELECT * FROM users'); res.render('admin', { users: usersResult.rows, session: req.session }); // Stelle sicher, dass es eine admin.ejs gibt }); app.post('/new-member', requireAuth, requireAdmin ,async(req, res) => { const {vorname, nachname, geburt, riege, adresse} = req.body; const name = vorname + ' ' + nachname; console.log(name + ', ' + geburt + ', ' + riege + ', ' + adresse); try { const teilnehmerID = await pool.query('INSERT INTO teilnehmende (name, geburtsdatum, adresse, vorname, nachname) VALUES ($1, $2, $3, $4, $5) RETURNING id', [name, geburt, adresse, vorname, nachname]); console.log(teilnehmerID.rows[0].id); putInRiege(riege, teilnehmerID.rows[0].id); res.redirect('/riege'); }catch (error) { const message = 'Error:' + error; res.render('error', {session: req.session, message}); } }) // Teilnehmer_innen app.get('/training', 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 (error) { console.error(error); const message = 'Error:' + error; res.render('error', {session: req.session, message}); } }); 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.vorname, t.nachname, 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, vorname: row.vorname, nachname: row.nachname, age: age, helfer: row.helfer, }); }); res.render('riegen', { riegen: riegen, session: req.session }); } catch (error) { console.error('Error fetching riegen:', error); const message = 'Error fetching riegen:' + error; res.render('error', {session: req.session, message}); } }); 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 (error) { console.error(error); const message = 'Error:' + error; res.render('error', {session: req.session, message}); } }); 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]); const riegeResult = await pool.query('SELECT * FROM riegen WHERE fremdid_teilnehmende = $1', [id]); const riege = riegeResult.rows[0].riegennummer; const numberResult = await pool.query('SELECT telefonnummern.*, telverbindung.bezeichnung FROM telefonnummern JOIN telverbindung ON telefonnummern.verbindung = telverbindung.id WHERE telefonnummern.fid_teilnehmer = $1', [id]); const numbers = numberResult.rows if (userResult.rows.length > 0) { const mitglied = userResult.rows.map(t => ({ ...t, age: calculateAge(t.geburtsdatum) })) res.render('mitglied', { id, mitglied: mitglied[0], numbers, riege, session: req.session }); } else { res.status(400).send('Mitglied existiert nicht'); } } catch (error) { console.error('Error in Mitglied:', error); const message = 'Error:' + error; res.render('error', {session: req.session, message}); } }); app.post('/set-riege', requireAuth, async (req, res) => { const { riege, id } = req.body; putInRiege(riege, id); res.redirect('/riege'); }) app.post('/update-mitglied', requireAdmin, async (req, res) => { const { type, id, telid, adresse, name, verbindung, nummer } = req.body; try{ if (type=="tel"){ console.log('INSERT INTO telefonnummern (fid_teilnehmer, name, verbindung, nummer, stand) VALUES ($1, $2, $3, $4, $5)', [id, name, verbindung, nummer, moment().toDate()]); await pool.query('INSERT INTO telefonnummern (fid_teilnehmer, name, verbindung, nummer, stand) VALUES ($1, $2, $3, $4, $5)', [id, name, verbindung, nummer, moment().toDate()]); } else if (type=="tel-delete"){ await pool.query('DELETE FROM telefonnummern WHERE id = $1', [telid]); console.log('delete numer $1', [telid]); } else if (type=="adresse"){ await pool.query('UPDATE teilnehmende SET adresse = $1 WHERE id = $2', [adresse, id]); } res.redirect('/mitglied/' + id); } catch (error) { console.error(error); const message = 'Error:' + error; res.render('error', {session: req.session, message}); } }); // Login und Registrierung anzeigen app.get('/login', (req, res) => { res.render('login', {session: req.session}); // Stelle sicher, dass es eine login.ejs gibt }); // Registrierung app.get('/register', (req, res) => { res.render('register', {session: req.session}); // Stelle sicher, dass es eine register.ejs gibt }); // Spieleliste app.get('/spiele', async (req, res) => { try { const spieleResult = await pool.query('SELECT * FROM spiele ORDER BY name ASC;'); const spiele = spieleResult.rows; res.render('spiele', { spiele, session: req.session }); } catch (error) { console.error('Error in Mitglied:', error); const message = 'Error:' + error; res.render('error', {session: req.session, message}); } }) app.post('/delete-spiel', requireAdmin ,async (req, res) => { const { spielId } = req.body; console.log(spielId); await pool.query('DELETE FROM spiele WHERE id = $1', [spielId]); res.redirect('/spiele'); }) // Gerenderte Seite für gewähltes Spiel app.get('/spiel/:id', async (req, res) => { const { id } = req.params; try { const spieleResult = await pool.query('SELECT * FROM spiele WHERE id = $1', [id]); const spiel = spieleResult.rows[0]; res.render('spiel', { spiel, session: req.session }); } catch (error) { console.error('Error in Mitglied:', error); const message = 'Error:' + error; res.render('error', {session: req.session, message}); } }) // Postseite für Änderungen app.post('/edit-spiel', requireAuth, async (req, res) => { const { material, dauer, regeln, variationen, type, id } = req.body; try { const queryResult = await pool.query('UPDATE spiele set material = $1, regeln = $2, dauer = $3, variationen = $4, type = $5 Where id = $6 ', [material, regeln, dauer, variationen, type, id]); res.redirect('/spiel/' + id); } catch (error) { console.error('Error in edit Spiel:', error); const message = 'Error:' + error; res.render('error', {session: req.session, message}); } }) // Startseite app.get('/', (req, res) => { res.render('index', {session: req.session}); }); const server = app.listen(port, '0.0.0.0', () => { console.log(`Server is running on http://localhost:${port}/`); });