Vertragsverwaltung_Plusfit24/routes/api.js
2026-03-28 09:28:41 +00:00

240 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
// ============================================
async function verifyEmailDomain(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) return { valid: false, reason: 'Ungültiges E-Mail-Format' };
const domain = email.split('@')[1];
try {
const records = await dns.resolveMx(domain);
if (records && records.length > 0) return { valid: true };
return { valid: false, reason: 'Domain hat keine E-Mail-Server (MX-Records fehlen)' };
} catch (err) {
return { valid: false, reason: 'E-Mail-Domain konnte nicht verifiziert werden' };
}
}
// ============================================
// IBAN Validierung (Modulo-97)
// ============================================
const IBAN_LENGTHS = {
AL:28,AD:24,AT:20,AZ:28,BH:22,BE:16,BA:20,BR:29,BG:22,CR:22,HR:21,
CY:28,CZ:24,DK:18,DO:28,EE:20,FO:18,FI:18,FR:27,GE:22,DE:22,GI:23,
GL:18,GT:28,HU:28,IS:26,IE:22,IL:23,IT:27,JO:30,KZ:20,KW:30,LV:21,
LB:28,LI:21,LT:20,LU:20,MK:19,MT:31,MR:27,MU:30,MC:27,MD:24,ME:22,
NL:18,NO:15,PK:24,PS:29,PL:28,PT:25,QA:29,RO:24,SM:27,SA:24,RS:22,
SK:24,SI:19,ES:24,SE:24,CH:21,TN:24,TR:26,AE:23,GB:22,VG:24
};
function validateIBANServer(iban) {
if (!iban) return { valid: true };
const clean = iban.replace(/\s/g, '').toUpperCase();
if (clean.length < 5) return { valid: false, error: 'IBAN zu kurz' };
const country = clean.substring(0, 2);
if (!/^[A-Z]{2}$/.test(country)) return { valid: false, error: 'Ungültiger Ländercode' };
const expectedLen = IBAN_LENGTHS[country];
if (!expectedLen) return { valid: false, error: 'Ländercode nicht unterstützt' };
if (clean.length !== expectedLen) return { valid: false, error: country + '-IBAN muss ' + expectedLen + ' Zeichen haben' };
const rearranged = clean.substring(4) + clean.substring(0, 4);
const numeric = rearranged.split('').map(c => {
const code = c.charCodeAt(0);
return code >= 65 ? (code - 55).toString() : c;
}).join('');
let remainder = 0;
for (let i = 0; i < numeric.length; i++) {
remainder = (remainder * 10 + parseInt(numeric[i])) % 97;
}
if (remainder !== 1) return { valid: false, error: 'IBAN Prüfziffer ungültig' };
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' });
const result = await verifyEmailDomain(email);
res.json(result);
});
// ============================================
// POST /api/submit-membership
// ============================================
router.post('/submit-membership', async (req, res) => {
try {
const {
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
} = req.body;
// 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.' });
}
if (!agb_accepted || !datenschutz_accepted || !data_correct) {
return res.json({ success: false, error: 'Bitte alle Einverständniserklärungen bestätigen.' });
}
// E-Mail validieren
const emailCheck = await verifyEmailDomain(email);
if (!emailCheck.valid) {
return res.json({ success: false, error: 'E-Mail-Adresse ist nicht erreichbar: ' + emailCheck.reason });
}
// IBAN prüfen
if (iban && iban.trim()) {
const ibanCheck = validateIBANServer(iban.trim());
if (!ibanCheck.valid) {
return res.json({ success: false, error: 'IBAN ungültig: ' + ibanCheck.error });
}
}
// Alter berechnen
const birthDateObj = new Date(birth_date);
const today = new Date();
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) {
return res.json({ success: false, error: 'Bei Minderjährigen ist die Einverständniserklärung der Erziehungsberechtigten erforderlich.' });
}
if (age < 14) {
return res.json({ success: false, error: 'Das Mindestalter für eine Mitgliedschaft beträgt 14 Jahre.' });
}
// 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];
// Berechnungen
const startPackagePrice = parseFloat(tariff.start_package_price) || 35.00;
const contractStart = new Date(today);
contractStart.setDate(contractStart.getDate() + 15);
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;
const contractEnd = new Date(contractStart);
contractEnd.setMonth(contractEnd.getMonth() + tariff.duration_months);
contractEnd.setDate(contractEnd.getDate() - 1);
// Tokens generieren
const access_token = crypto.randomBytes(16).toString('hex');
const confirmation_token = crypto.randomBytes(32).toString('hex');
// In DB speichern — Status: pending
await db.query(`
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, confirmation_token,
agreed_price, agreed_duration,
start_package_price,
signup_date, contract_start, contract_end, effective_end,
first_payment_date, first_payment_amt,
status, reviewed)
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, confirmation_token,
tariff.price_monthly, tariff.duration_months,
startPackagePrice,
today.toISOString().split('T')[0],
contractStart.toISOString().split('T')[0],
contractEnd.toISOString().split('T')[0],
contractEnd.toISOString().split('T')[0],
contractStart.toISOString().split('T')[0],
firstPaymentAmt,
'pending', 0
]);
// 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);
res.json({ success: false, error: 'Serverfehler. Bitte versuche es später erneut.' });
}
});
module.exports = router;