184 lines
7.5 KiB
JavaScript
184 lines
7.5 KiB
JavaScript
const express = require('express');
|
|
const crypto = require('crypto');
|
|
const router = express.Router();
|
|
const dns = require('dns').promises;
|
|
const db = require('../config/database');
|
|
|
|
// ============================================
|
|
// 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 };
|
|
}
|
|
|
|
// 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 prüfen
|
|
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 — MUSS vor Berechnungen stehen
|
|
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
|
|
const startPackagePrice = parseFloat(tariff.start_package_price) || 35.00;
|
|
|
|
// Vertragsbeginn = heute + 15 Tage
|
|
const contractStart = new Date();
|
|
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 firstPaymentAmt = Math.round((partialMonth + startPackagePrice) * 100) / 100;
|
|
|
|
// Vertragsende
|
|
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');
|
|
|
|
// In DB speichern
|
|
// Spalten: 26, Werte: 26
|
|
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,
|
|
agreed_price, agreed_duration,
|
|
start_package_price,
|
|
signup_date, contract_start, contract_end, effective_end,
|
|
first_payment_date, first_payment_amt)
|
|
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,
|
|
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
|
|
]);
|
|
|
|
res.json({ success: true });
|
|
|
|
} catch (err) {
|
|
console.error('Submit error:', err);
|
|
res.json({ success: false, error: 'Serverfehler. Bitte versuche es später erneut.' });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|