IBAN Überprüfung eingefügt

This commit is contained in:
cay 2026-03-27 11:37:37 +00:00
parent ad5f30920b
commit c5d08abbd8
6 changed files with 164 additions and 4 deletions

View File

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

83
public/js/iban.js Normal file
View File

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

View File

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

View File

@ -8,7 +8,7 @@
<link rel="stylesheet" href="/css/style.css">
</head>
<body class="admin-body">
<div class="admin-layout">
<div class="admin-layout">
<aside class="admin-sidebar">
<div class="logo admin-logo">Plusfit<span>24</span></div>

View File

@ -225,7 +225,8 @@
<div class="karte-row">
<div class="karte-field karte-field-full">
<label>IBAN</label>
<input type="text" name="iban" value="<%= member.iban ? member.iban.replace(/(.{4})/g, '$1 ').trim() : '' %>" disabled class="karte-input karte-iban">
<input type="text" name="iban" id="ibanInput" value="<%= member.iban ? member.iban.replace(/(.{4})/g, '$1 ').trim() : '' %>" disabled class="karte-input karte-iban" maxlength="34" autocomplete="off">
<div class="iban-message" id="ibanMessage"></div>
</div>
</div>
<div class="karte-row">
@ -291,11 +292,18 @@
</main>
</div>
<script src="/js/iban.js"></script>
<script>
const editableFields = document.querySelectorAll('.karte-input:not(.karte-readonly)');
function enableEdit() {
editableFields.forEach(f => f.removeAttribute('disabled'));
// IBAN Validierung aktivieren
const ibanInput = document.getElementById('ibanInput');
const ibanMessage = document.getElementById('ibanMessage');
if (ibanInput) {
attachIBANValidation(ibanInput, null, ibanMessage);
}
document.getElementById('editBtn').classList.add('hidden');
document.getElementById('saveBtn').classList.remove('hidden');
document.getElementById('cancelBtn').classList.remove('hidden');

View File

@ -176,8 +176,10 @@
<div class="form-group">
<div class="input-wrap">
<span class="input-icon">💳</span>
<input type="text" id="iban" placeholder="IBAN" maxlength="22">
<input type="text" id="iban" placeholder="DE00 0000 0000 0000 0000 00" maxlength="34" autocomplete="off">
<span class="email-status" id="ibanStatus"></span>
</div>
<div class="email-message" id="ibanMessage"></div>
</div>
<div class="step-buttons">
@ -278,6 +280,7 @@
<p>© 2024 PlusFit24 UG · <a href="https://plusfit24.de/datenschutz-2/" target="_blank">Datenschutz</a> · <a href="https://plusfit24.de/wp-content/uploads/2022/11/AG_PlusFit24.pdf" target="_blank">AGB</a></p>
</footer>
<script src="/js/iban.js"></script>
<script>
const TARIFF_ID = '<%= tariff.id %>';
let currentStep = 1;
@ -468,6 +471,11 @@
btn.textContent = 'Kostenpflichtige Mitgliedschaft abschließen';
}
}
// IBAN Validierung (iban.js wird vor diesem Script geladen)
const ibanInput = document.getElementById('iban');
if (ibanInput) {
attachIBANValidation(ibanInput, document.getElementById('ibanStatus'), document.getElementById('ibanMessage'));
}
</script>
</body>
</html>