Email Bestätigung

This commit is contained in:
cay 2026-03-28 08:20:24 +00:00
parent 193f46ba62
commit b2784e7b63
8 changed files with 248 additions and 37 deletions

View File

@ -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;

View File

@ -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 `<!DOCTYPE html>
<html lang="de">
<head><meta charset="UTF-8"></head>
<body style="font-family:Outfit,Arial,sans-serif;background:#f8f9ff;margin:0;padding:20px">
<div style="max-width:600px;margin:0 auto;background:white;border-radius:16px;overflow:hidden;box-shadow:0 2px 16px rgba(0,0,0,0.08)">
<div style="background:#2d2dcc;padding:32px;text-align:center">
<h1 style="color:white;margin:0;font-size:1.8rem">Plusfit<span style="color:#a5b4fc">24</span></h1>
<p style="color:#c7d2fe;margin:8px 0 0">Mitgliedschaft bestätigen</p>
</div>
<div style="padding:32px">
<p>Hallo ${member.first_name} ${member.last_name},</p>
<p>vielen Dank für deine Anmeldung bei PlusFit24! Um deine Mitgliedschaft zu aktivieren, bestätige bitte deine E-Mail-Adresse:</p>
<div style="text-align:center;margin:32px 0">
<a href="${confirmLink}"
style="display:inline-block;background:#2d2dcc;color:white;padding:16px 40px;border-radius:12px;text-decoration:none;font-weight:700;font-size:1.05rem">
Mitgliedschaft jetzt bestätigen
</a>
</div>
<div style="background:#f8f9ff;border-radius:10px;padding:16px;margin-bottom:20px">
<p style="margin:0 0 8px;font-weight:700">Deine Vertragsdaten:</p>
<p style="margin:4px 0;color:#374151">📋 Tarif: ${member.tariff_name}</p>
<p style="margin:4px 0;color:#374151">💰 Monatsbeitrag: ${Number(member.agreed_price).toFixed(2).replace('.', ',')} </p>
<p style="margin:4px 0;color:#374151">📦 Startpaket: ${Number(member.start_package_price).toFixed(2).replace('.', ',')} </p>
</div>
<p style="color:#6b7280;font-size:0.85rem">
Dieser Link ist <strong>24 Stunden</strong> gültig. Falls du diese Anmeldung nicht durchgeführt hast, ignoriere diese E-Mail.<br><br>
<strong>PlusFit24 UG</strong> · Moosleiten 12 · 84089 Aiglsbach
</p>
</div>
</div>
</body></html>`;
}
// ============================================
// 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;

View File

@ -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');

View File

@ -34,9 +34,14 @@
<div class="detail-header-title">
<h1><%= member.first_name %> <%= member.last_name %></h1>
<span class="member-id-badge">Mitglied #<%= member.id %></span>
<span class="status-badge <%= member.status === 'active' ? 'active' : (member.status === 'paused' ? 'warning' : 'inactive') %>">
<%= member.status === 'active' ? '✅ Aktiv' : member.status === 'paused' ? '⏸ Pausiert' : '❌ Inaktiv' %>
<span class="status-badge <%= member.status === 'active' ? 'active' : member.status === 'paused' ? 'warning' : member.status === 'pending' ? 'warning' : 'inactive' %>">
<%= member.status === 'active' ? '✅ Aktiv' : member.status === 'paused' ? '⏸ Pausiert' : member.status === 'pending' ? '⏳ Ausstehend' : '❌ Inaktiv' %>
</span>
<% if (member.status === 'pending') { %>
<form method="POST" action="/admin/members/<%= member.id %>/confirm" style="display:inline">
<button type="submit" class="btn btn-sm btn-success">✅ Manuell bestätigen</button>
</form>
<% } %>
<% if (member.is_minor) { %>
<span class="status-badge warning">⚠️ Minderjährig</span>
<% } %>

View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PlusFit24 Link ungültig</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header class="site-header">
<div class="header-inner"><div class="logo">Plusfit<span>24</span></div></div>
</header>
<main class="success-main">
<div class="success-card">
<div class="success-icon">⚠️</div>
<h1>Link ungültig oder abgelaufen</h1>
<p>Dieser Bestätigungslink ist nicht mehr gültig.</p>
<p class="success-sub">Bitte kontaktiere uns direkt im Studio oder ruf uns an.</p>
<a href="/" class="btn btn-outline" style="margin-top:16px">Zur Startseite</a>
</div>
</main>
<footer class="site-footer">
<p>© 2024 PlusFit24 UG</p>
</footer>
</body>
</html>

View File

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PlusFit24 E-Mail bestätigen</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header class="site-header">
<div class="header-inner"><div class="logo">Plusfit<span>24</span></div></div>
</header>
<main class="success-main">
<div class="success-card">
<div class="success-icon">📧</div>
<h1>Fast geschafft!</h1>
<p>Wir haben eine Bestätigungs-E-Mail an</p>
<p><strong><%= email %></strong></p>
<p>gesendet. Bitte klicke auf den Link in der E-Mail um deine Mitgliedschaft zu aktivieren.</p>
<p class="success-sub">Der Link ist 24 Stunden gültig.</p>
<div class="info-box" style="margin-top:16px;text-align:left">
<strong> Keine E-Mail erhalten?</strong><br>
Bitte prüfe deinen Spam-Ordner oder kontaktiere uns direkt im Studio.
</div>
</div>
</main>
<footer class="site-footer">
<p>© 2024 PlusFit24 UG</p>
</footer>
</body>
</html>

View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PlusFit24 Bestätigt!</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header class="site-header">
<div class="header-inner"><div class="logo">Plusfit<span>24</span></div></div>
</header>
<main class="success-main">
<div class="success-card">
<div class="success-icon">🎉</div>
<h1>Herzlich Willkommen!</h1>
<p>Deine Mitgliedschaft bei PlusFit24 wurde erfolgreich bestätigt und ist jetzt aktiv.</p>
<p class="success-sub">Wir freuen uns auf dich im Studio!</p>
<a href="/" class="btn btn-primary" style="margin-top:16px">Zurück zur Startseite</a>
</div>
</main>
<footer class="site-footer">
<p>© 2024 PlusFit24 UG</p>
</footer>
</body>
</html>

View File

@ -457,7 +457,7 @@
const data = await res.json();
if (data.success) {
window.location.href = '/erfolg';
window.location.href = data.pending ? '/bestaetigung-ausstehend?email=' + encodeURIComponent(document.getElementById('email').value) : '/erfolg';
} else {
errorDiv.textContent = data.error || 'Fehler beim Absenden.';
errorDiv.classList.remove('hidden');