Email Bestätigung
This commit is contained in:
parent
193f46ba62
commit
b2784e7b63
@ -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;
|
||||
|
||||
169
routes/api.js
169
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 `<!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;
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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>
|
||||
<% } %>
|
||||
|
||||
27
views/confirmation-invalid.ejs
Normal file
27
views/confirmation-invalid.ejs
Normal 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>
|
||||
32
views/confirmation-pending.ejs
Normal file
32
views/confirmation-pending.ejs
Normal 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>
|
||||
27
views/confirmation-success.ejs
Normal file
27
views/confirmation-success.ejs
Normal 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>
|
||||
@ -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');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user