Link ungültig oder abgelaufen
+Dieser Bestätigungslink ist nicht mehr gültig.
+Bitte kontaktiere uns direkt im Studio oder ruf uns an.
+ Zur Startseite +diff --git a/routes/admin.js b/routes/admin.js index dc2e521..0f9e4dd 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -319,4 +319,18 @@ router.post('/members/:id/update-nfc', requireAdmin, async (req, res) => { } }); + +// Mitglied manuell bestätigen +router.post('/members/:id/confirm', requireAdmin, async (req, res) => { + try { + await db.query( + "UPDATE memberships SET status='active', confirmed_at=NOW() WHERE id=?", + [req.params.id] + ); + res.redirect(`/admin/members/${req.params.id}?success=Mitglied+manuell+bestätigt`); + } catch (err) { + res.redirect(`/admin/members/${req.params.id}?error=Fehler+bei+Bestätigung`); + } +}); + module.exports = router; diff --git a/routes/api.js b/routes/api.js index 5fdda5d..381b73c 100644 --- a/routes/api.js +++ b/routes/api.js @@ -1,8 +1,9 @@ -const express = require('express'); -const crypto = require('crypto'); -const router = express.Router(); -const dns = require('dns').promises; -const db = require('../config/database'); +const express = require('express'); +const crypto = require('crypto'); +const router = express.Router(); +const dns = require('dns').promises; +const db = require('../config/database'); +const mailer = require('../config/mailer'); // ============================================ // E-Mail Validierung via DNS MX-Record @@ -53,7 +54,49 @@ function validateIBANServer(iban) { return { valid: true }; } +// ============================================ +// Bestätigungs-E-Mail Template +// ============================================ +function confirmationEmailHtml(member, confirmLink) { + return ` + +
+ + +`; +} + +// ============================================ // POST /api/verify-email +// ============================================ router.post('/verify-email', async (req, res) => { const { email } = req.body; if (!email) return res.json({ valid: false, reason: 'Keine E-Mail angegeben' }); @@ -61,7 +104,9 @@ router.post('/verify-email', async (req, res) => { res.json(result); }); +// ============================================ // POST /api/submit-membership +// ============================================ router.post('/submit-membership', async (req, res) => { try { const { @@ -72,7 +117,7 @@ router.post('/submit-membership', async (req, res) => { guardian_consent } = req.body; - // Pflichtfelder prüfen + // Pflichtfelder if (!tariff_id || !first_name || !last_name || !birth_date || !email || !street || !zip || !city) { return res.json({ success: false, error: 'Bitte alle Pflichtfelder ausfüllen.' }); } @@ -100,7 +145,6 @@ router.post('/submit-membership', async (req, res) => { let age = today.getFullYear() - birthDateObj.getFullYear(); const mo = today.getMonth() - birthDateObj.getMonth(); if (mo < 0 || (mo === 0 && today.getDate() < birthDateObj.getDate())) age--; - const is_minor = age < 18 ? 1 : 0; if (is_minor && !guardian_consent) { @@ -110,58 +154,52 @@ router.post('/submit-membership', async (req, res) => { return res.json({ success: false, error: 'Das Mindestalter für eine Mitgliedschaft beträgt 14 Jahre.' }); } - // Tarif laden — MUSS vor Berechnungen stehen + // Tarif laden const [tariffs] = await db.query('SELECT * FROM tariffs WHERE id = ? AND active = 1', [tariff_id]); if (tariffs.length === 0) { return res.json({ success: false, error: 'Ungültiger oder inaktiver Tarif.' }); } const tariff = tariffs[0]; - // Startpaket-Preis + // Berechnungen const startPackagePrice = parseFloat(tariff.start_package_price) || 35.00; - - // Vertragsbeginn = heute + 15 Tage - const contractStart = new Date(); + const contractStart = new Date(today); contractStart.setDate(contractStart.getDate() + 15); - - // Anteiligen ersten Monatsbeitrag berechnen - const daysInMonth = new Date(contractStart.getFullYear(), contractStart.getMonth() + 1, 0).getDate(); - const remainingDays = daysInMonth - contractStart.getDate() + 1; - const dailyRate = parseFloat(tariff.price_monthly) / daysInMonth; - const partialMonth = Math.round(dailyRate * remainingDays * 100) / 100; - - // Erste Zahlung = anteiliger Monat + Startpaket + const daysInMonth = new Date(contractStart.getFullYear(), contractStart.getMonth() + 1, 0).getDate(); + const remainingDays = daysInMonth - contractStart.getDate() + 1; + const partialMonth = Math.round((parseFloat(tariff.price_monthly) / daysInMonth) * remainingDays * 100) / 100; const firstPaymentAmt = Math.round((partialMonth + startPackagePrice) * 100) / 100; - - // Vertragsende - const contractEnd = new Date(contractStart); + const contractEnd = new Date(contractStart); contractEnd.setMonth(contractEnd.getMonth() + tariff.duration_months); contractEnd.setDate(contractEnd.getDate() - 1); - // Zugangstoken generieren - const access_token = crypto.randomBytes(16).toString('hex'); + // Tokens generieren + const access_token = crypto.randomBytes(16).toString('hex'); + const confirmation_token = crypto.randomBytes(32).toString('hex'); - // In DB speichern - // Spalten: 26, Werte: 26 + // In DB speichern — Status: pending await db.query(` - INSERT INTO memberships + INSERT INTO memberships (tariff_id, salutation, title, first_name, last_name, birth_date, email, phone, street, address_addition, zip, city, bank_name, account_holder, iban, sepa_accepted, agb_accepted, datenschutz_accepted, data_correct, - guardian_consent, is_minor, access_token, + guardian_consent, is_minor, + access_token, confirmation_token, agreed_price, agreed_duration, start_package_price, signup_date, contract_start, contract_end, effective_end, - first_payment_date, first_payment_amt) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + first_payment_date, first_payment_amt, + status) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) `, [ tariff_id, salutation, title || '', first_name, last_name, birth_date, email, phone || '', street, address_addition || '', zip, city, bank_name || '', account_holder || '', iban || '', sepa_accepted ? 1 : 0, agb_accepted ? 1 : 0, datenschutz_accepted ? 1 : 0, data_correct ? 1 : 0, - guardian_consent ? 1 : 0, is_minor, access_token, + guardian_consent ? 1 : 0, is_minor, + access_token, confirmation_token, tariff.price_monthly, tariff.duration_months, startPackagePrice, today.toISOString().split('T')[0], @@ -169,10 +207,28 @@ router.post('/submit-membership', async (req, res) => { contractEnd.toISOString().split('T')[0], contractEnd.toISOString().split('T')[0], contractStart.toISOString().split('T')[0], - firstPaymentAmt + firstPaymentAmt, + 'pending' ]); - res.json({ success: true }); + // Bestätigungs-E-Mail senden + const baseUrl = process.env.APP_URL || 'https://plusfit24.software-joksch.com'; + const confirmLink = `${baseUrl}/confirm/${confirmation_token}`; + const memberData = { first_name, last_name, agreed_price: tariff.price_monthly, start_package_price: startPackagePrice, tariff_name: tariff.name }; + + try { + await mailer.sendMail({ + from: process.env.MAIL_FROM, + to: email, + subject: 'PlusFit24 – Bitte bestätige deine Mitgliedschaft', + html: confirmationEmailHtml(memberData, confirmLink) + }); + } catch (mailErr) { + console.error('E-Mail Fehler:', mailErr.message); + // Trotzdem Erfolg zurückgeben — Admin kann manuell bestätigen + } + + res.json({ success: true, pending: true }); } catch (err) { console.error('Submit error:', err); @@ -180,4 +236,49 @@ router.post('/submit-membership', async (req, res) => { } }); +// ============================================ +// GET /confirm/:token — E-Mail Bestätigung +// ============================================ +router.get('/confirm/:token', async (req, res) => { + try { + const [rows] = await db.query( + "SELECT * FROM memberships WHERE confirmation_token = ? AND status = 'pending'", + [req.params.token] + ); + + if (rows.length === 0) { + // Prüfen ob bereits bestätigt + const [confirmed] = await db.query( + "SELECT * FROM memberships WHERE confirmation_token = ? AND status = 'active'", + [req.params.token] + ); + if (confirmed.length > 0) { + return res.render('confirmation-success'); // Bereits bestätigt + } + return res.render('confirmation-invalid'); + } + + const member = rows[0]; + + // Prüfen ob Link abgelaufen (24h) + const createdAt = new Date(member.created_at); + const now = new Date(); + const hoursDiff = (now - createdAt) / (1000 * 60 * 60); + if (hoursDiff > 24) { + return res.render('confirmation-invalid'); + } + + // Mitgliedschaft aktivieren + await db.query( + "UPDATE memberships SET status='active', confirmed_at=NOW() WHERE id=?", + [member.id] + ); + + res.render('confirmation-success'); + } catch (err) { + console.error('Confirm error:', err); + res.render('error', { message: 'Fehler bei der Bestätigung.' }); + } +}); + module.exports = router; diff --git a/routes/index.js b/routes/index.js index 42d576e..a1f012d 100644 --- a/routes/index.js +++ b/routes/index.js @@ -32,6 +32,11 @@ router.get('/anmelden/:tariffId', async (req, res) => { } }); +// Bestätigung ausstehend +router.get('/bestaetigung-ausstehend', (req, res) => { + res.render('confirmation-pending', { email: req.query.email || '' }); +}); + // Erfolgsseite router.get('/erfolg', (req, res) => { res.render('success'); diff --git a/views/admin/member-detail.ejs b/views/admin/member-detail.ejs index dfe53f0..02c09dc 100644 --- a/views/admin/member-detail.ejs +++ b/views/admin/member-detail.ejs @@ -34,9 +34,14 @@Dieser Bestätigungslink ist nicht mehr gültig.
+Bitte kontaktiere uns direkt im Studio oder ruf uns an.
+ Zur Startseite +Wir haben eine Bestätigungs-E-Mail an
+<%= email %>
+gesendet. Bitte klicke auf den Link in der E-Mail um deine Mitgliedschaft zu aktivieren.
+Der Link ist 24 Stunden gültig.
+Deine Mitgliedschaft bei PlusFit24 wurde erfolgreich bestätigt und ist jetzt aktiv.
+Wir freuen uns auf dich im Studio!
+ Zurück zur Startseite +