diff --git a/public/css/style.css b/public/css/style.css index d56b740..5057f56 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1006,3 +1006,24 @@ body { /* Hover auf Mitglieder-Tabellenzeilen */ .member-row:hover td { background: #f0f0ff !important; } + +/* ---- IBAN Validierung ---- */ +.iban-message { + font-size: 0.8rem; + margin-top: 4px; + padding-left: 4px; + min-height: 18px; +} +.iban-message.success { color: var(--success); } +.iban-message.error { color: var(--error); } + +.input-wrap input.input-valid, +.karte-input.input-valid { + border-color: var(--success) !important; + background: #f0fdf4 !important; +} +.input-wrap input.input-error, +.karte-input.input-error { + border-color: var(--error) !important; + background: #fff5f5 !important; +} diff --git a/public/js/iban.js b/public/js/iban.js new file mode 100644 index 0000000..b984fd6 --- /dev/null +++ b/public/js/iban.js @@ -0,0 +1,83 @@ +/** + * PlusFit24 – IBAN Validierung + * Prüft: Format, Ländercode, Länge, Prüfziffer (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 formatIBAN(value) { + const clean = value.replace(/[^A-Z0-9]/gi, '').toUpperCase(); + return clean.replace(/(.{4})/g, '$1 ').trim(); +} + +function validateIBAN(iban) { + 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 checkDigits = clean.substring(2, 4); + if (!/^\d{2}$/.test(checkDigits)) return { valid: false, error: 'Prüfziffern ungültig' }; + + const expectedLength = IBAN_LENGTHS[country]; + if (!expectedLength) return { valid: false, error: 'Ländercode "' + country + '" nicht unterstützt' }; + if (clean.length !== expectedLength) { + return { valid: false, error: country + '-IBAN muss ' + expectedLength + ' Zeichen haben (aktuell: ' + clean.length + ')' }; + } + + if (!/^[A-Z0-9]+$/.test(clean.substring(4))) { + return { valid: false, error: 'IBAN enthält ungültige Zeichen' }; + } + + // Modulo-97 Prüfung + 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: 'Prüfziffer falsch – IBAN ungültig' }; + + return { valid: true, formatted: formatIBAN(clean) }; +} + +function attachIBANValidation(inputEl, statusEl, messageEl) { + inputEl.addEventListener('input', function () { + const rawClean = this.value.replace(/[^A-Z0-9]/gi, '').toUpperCase(); + this.value = formatIBAN(rawClean); + + if (rawClean.length < 5) { + if (statusEl) statusEl.textContent = ''; + if (messageEl) { messageEl.textContent = ''; messageEl.className = 'iban-message'; } + this.classList.remove('input-error', 'input-valid'); + return; + } + + const result = validateIBAN(rawClean); + if (result.valid) { + if (statusEl) statusEl.textContent = '✅'; + if (messageEl) { messageEl.textContent = 'IBAN gültig'; messageEl.className = 'iban-message success'; } + this.classList.remove('input-error'); this.classList.add('input-valid'); + } else { + if (statusEl) statusEl.textContent = '❌'; + if (messageEl) { messageEl.textContent = result.error; messageEl.className = 'iban-message error'; } + this.classList.remove('input-valid'); this.classList.add('input-error'); + } + }); +} diff --git a/routes/api.js b/routes/api.js index 753db82..6eb7c2e 100644 --- a/routes/api.js +++ b/routes/api.js @@ -20,6 +20,38 @@ async function verifyEmailDomain(email) { } } + +// Server-seitige IBAN Prüfung (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 }; // Optional – kein IBAN = ok + 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; @@ -46,6 +78,14 @@ router.post('/submit-membership', async (req, res) => { return res.json({ success: false, error: 'E-Mail-Adresse ist nicht erreichbar: ' + emailCheck.reason }); } + // IBAN prüfen (falls angegeben) + if (iban && iban.trim()) { + const ibanCheck = validateIBANServer(iban.trim()); + if (!ibanCheck.valid) { + return res.json({ success: false, error: 'IBAN ungültig: ' + ibanCheck.error }); + } + } + // 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.' }); diff --git a/views/admin/dashboard.ejs b/views/admin/dashboard.ejs index 2d5905a..dfcc00c 100644 --- a/views/admin/dashboard.ejs +++ b/views/admin/dashboard.ejs @@ -8,7 +8,7 @@
-