1/** 2 * AJAX Auth Modal â Frontend Controller 3 * Kein jQuery, kein externes Framework. 4 * Kompatibel mit WPS Hide Login (alle Requests gehen an admin-ajax.php). 5 */ 6(function () { 7 'use strict'; 8 9 /* ========================================================================= 10 * Konfiguration aus PHP 11 * ====================================================================== */ 12 const cfg = window.aamConfig || {}; 13 const { 14 ajaxUrl, 15 nonces, 16 selectorsLogin = [], 17 selectorsRegister = [], 18 defaultTab = 'login', 19 closeOnOverlay = true, 20 closeOnEsc = true, 21 enableRegister = true, 22 enableLostPw = true, 23 isLoggedIn = false, 24 i18n = {}, 25 pwRules = {}, 26 oauthNonce = '', 27 homeUrl = '', 28 profileIncomplete = null, 29 } = cfg; 30 31 // Sichtbarkeits-Klassen + Logout-Links nach DOM-Ready setzen 32 function applyLoggedInClasses() { 33 // lrm-hide-if-logged-in / lrm-show-if-logged-in 34 document.querySelectorAll('[class*="lrm-hide-if-logged-in"]').forEach(el => { 35 el.style.display = isLoggedIn ? 'none' : ''; 36 }); 37 document.querySelectorAll('[class*="lrm-show-if-logged-in"]').forEach(el => { 38 el.style.display = isLoggedIn ? '' : 'none'; 39 }); 40 41 // lrm-logout / aam-logout: href mit wp_logout_url() befüllen. 42 // Klasse kann am <a> selbst ODER am umgebenden <li> sitzen (wp_nav_menu setzt 43 // die Klassen am <li>, nicht am <a>). 44 if (isLoggedIn && cfg.logoutUrl) { 45 const logoutSel = '[class*="lrm-logout"], [class*="aam-logout"]'; 46 document.querySelectorAll(logoutSel).forEach(el => { 47 // Entweder das Element selbst ist ein <a>, oder wir suchen das erste <a> darin 48 const link = el.tagName === 'A' ? el : el.querySelector('a'); 49 if (link && (!link.getAttribute('href') || link.getAttribute('href') === '#')) { 50 link.setAttribute('href', cfg.logoutUrl); 51 } 52 }); 53 } 54 } 55 56 if (document.readyState === 'loading') { 57 document.addEventListener('DOMContentLoaded', applyLoggedInClasses); 58 } else { 59 applyLoggedInClasses(); // DOM bereits bereit 60 } 61 62 if (isLoggedIn) return; // Modal-Logik nur für nicht-eingeloggte Nutzer 63 64 // Gesamte Modal-Logik nach DOMContentLoaded starten â 65 // das Modal-HTML wird erst in wp_footer gerendert und ist beim Script-Parse noch nicht da. 66 function initModal() { 67 68 /* ========================================================================= 69 * DOM-Referenzen 70 * ====================================================================== */ 71 const overlay = document.getElementById('aam-overlay'); 72 if (!overlay) return; 73 74 const modal = overlay.querySelector('.aam-modal'); 75 const btnClose = overlay.querySelector('.aam-close'); 76 const tabs = overlay.querySelectorAll('.aam-tab'); 77 const indicator = overlay.querySelector('.aam-tab-indicator'); 78 const panelLogin = document.getElementById('aam-panel-login'); 79 const panelRegister = document.getElementById('aam-panel-register'); 80 const panelLostPw = document.getElementById('aam-panel-lost-password'); 81 82 const formLogin = document.getElementById('aam-form-login'); 83 const formRegister = document.getElementById('aam-form-register'); 84 const formLostPw = document.getElementById('aam-form-lostpassword'); 85 86 /* ========================================================================= 87 * Hilfsfunktionen 88 * ====================================================================== */ 89 90 function show(el) { if (el) { el.hidden = false; } } 91 function hide(el) { if (el) { el.hidden = true; } } 92 93 // Nur <br> erlauben â verhindert XSS auch bei filter-injiziertem HTML vom Server 94 function showMessage(panel, text, type = 'error') { 95 const msg = panel.querySelector('.aam-message'); 96 if (!msg) return; 97 msg.className = `aam-message is-${type}`; 98 msg.textContent = ''; 99 String(text).split(/<br\s*\/?>/i).forEach((part, i, arr) => { 100 msg.appendChild(document.createTextNode(part)); 101 if (i < arr.length - 1) msg.appendChild(document.createElement('br')); 102 }); 103 show(msg); 104 msg.focus(); 105 } 106 107 function clearMessage(panel) { 108 const msg = panel.querySelector('.aam-message'); 109 if (msg) { hide(msg); msg.textContent = ''; msg.className = 'aam-message'; } 110 } 111 112 function setLoading(form, loading) { 113 const btn = form.querySelector('.aam-submit'); 114 const label = form.querySelector('.aam-submit-label'); 115 const spinner = form.querySelector('.aam-spinner'); 116 if (!btn) return; 117 btn.disabled = loading;
118 if (label) label.textContent = loading ? (i18n.loading || 'Bitte wartenâ¦') : btn.dataset.originalLabel; 119 if (spinner) { loading ? show(spinner) : hide(spinner); } 120 } 121 122 /** Holt Nonce frisch vom Server? Nein â wir nutzen die bereits eingebetteten 123 * Nonces. Da Nonces 24h gültig sind, reicht das für normale Sessions. */ 124 function getFreshNonce(action) { 125 return nonces[action] || ''; 126 } 127 128 /* ========================================================================= 129 * Modal öffnen / schlieÃen 130 * ====================================================================== */ 131 132 function openModal(tab) { 133 // Register-Tab sperren wenn Registrierung deaktiviert (WP-Kern oder Plugin-Option) 134 if ( tab === 'register' && !enableRegister ) tab = 'login'; 135 overlay.hidden = false; 136 // Kleines Delay damit CSS-Transition startet (nach hiddenâfalse) 137 requestAnimationFrame(() => { 138 requestAnimationFrame(() => { 139 overlay.classList.add('is-visible'); 140 }); 141 }); 142 switchTab(tab || defaultTab); 143 trapFocus(modal); 144 document.body.style.overflow = 'hidden'; 145 146 // Screenreader: Hintergrundinhalt deaktivieren 147 document.querySelectorAll('body > *:not(#aam-overlay)').forEach(el => { 148 el.setAttribute('aria-hidden', 'true'); 149 }); 150 } 151 152 function closeModal() { 153 // Profil-Vervollständigung ist Pflicht â Modal kann nicht geschlossen werden 154 if (_profilePanelActive) { 155 // Visuelles Feedback: Modal kurz schütteln 156 overlay.querySelector('.aam-modal')?.classList.add('aam-shake'); 157 setTimeout(() => overlay.querySelector('.aam-modal')?.classList.remove('aam-shake'), 500); 158 return; 159 } 160 overlay.classList.remove('is-visible'); 161 document.body.style.overflow = ''; 162 document.querySelectorAll('[aria-hidden="true"]').forEach(el => { 163 el.removeAttribute('aria-hidden'); 164 }); 165 releaseTrap(); 166 // Warte auf Transition, dann wirklich verstecken 167 overlay.addEventListener('transitionend', () => { 168 if (!overlay.classList.contains('is-visible')) { 169 overlay.hidden = true; 170 } 171 }, { once: true }); 172 } 173 174 /* ========================================================================= 175 * Tab-Switcher 176 * ====================================================================== */ 177 178 const panels = { 179 login: panelLogin, 180 register: panelRegister, 181 'lost-password': panelLostPw, 182 }; 183 184 function switchTab(target) { 185 Object.entries(panels).forEach(([key, panel]) => { 186 if (panel) panel.hidden = key !== target; 187 }); 188 189 // Tab-Buttons aktualisieren
190 tabs.forEach(btn => { 191 const isActive = btn.dataset.target === target; 192 btn.setAttribute('aria-selected', isActive ? 'true' : 'false'); 193 }); 194 195 // Indikator verschieben 196 if (indicator) { 197 indicator.classList.toggle('at-register', target === 'register'); 198 } 199 200 // Autofokus auf erstes Feld 201 const activePanel = panels[target]; 202 if (activePanel) { 203 setTimeout(() => { 204 const first = activePanel.querySelector('input:not([type="hidden"]):not([tabindex="-1"])'); 205 if (first) first.focus(); 206 }, 120); 207 } 208 } 209 210 /* ========================================================================= 211 * Formular-Absenden (AJAX) 212 * ====================================================================== */ 213 214 async function submitForm(form, action, panel) { 215 clearMessage(panel); 216 setLoading(form, true); 217 218 // Anti-Bot-Felder unmittelbar vor dem Lesen setzen (cache-sicher). 219 // _antiBotStart wird beim Init gesetzt; Fallback auf jetzt. 220 const tsField = form.querySelector('input[name="aam__ts"]'); 221 if (tsField && !tsField.value) { 222 tsField.value = String(Date.now() - (window._aamStart || Date.now())); 223 } 224 const jsField = form.querySelector('input[name="aam__js"]'); 225 if (jsField && !jsField.value) jsField.value = '1'; 226 227 const data = new FormData(form); 228 data.set('action', action); 229 230 // Nonce aktuell halten 231 const nonceKey = { 232 aam_login: 'login', 233 aam_register: 'register', 234 aam_lostpassword: 'lostpassword', 235 aam_password_reset: 'password_reset', 236 }[action]; 237 if (nonceKey) { 238 // Den POST-Feldname ermitteln (data-*-nonce im form) 239 const nonceField = { 240 aam_login: 'aam_login', 241 aam_register: 'aam_register', 242 aam_lostpassword: 'aam_lostpassword', 243 aam_password_reset: 'aam_password_reset', 244 }[action]; 245 if (nonceField) data.set(nonceField, getFreshNonce(nonceKey)); 246 } 247 248 // redirect_to aus data-Attribut des Triggers einsetzen 249 if (_pendingRedirect) { 250 data.set('redirect_to', _pendingRedirect); 251 } 252 253 let json; 254 try { 255 const resp = await fetch(ajaxUrl, { method: 'POST', body: data, credentials: 'same-origin' }); 256 json = await resp.json(); 257 } catch { 258 setLoading(form, false); 259 showMessage(panel, i18n.networkError || 'Netzwerkfehler.', 'error'); 260 return; 261 } 262 263 setLoading(form, false); 264 265 if (!json.success) { 266 showMessage(panel, json.data?.message || 'Unbekannter Fehler.', 'error'); 267 // Feld-spezifische Fehler 268 if (json.data?.field) { 269 const field = form.querySelector(`[name="${json.data.field}"]`); 270 if (field) { 271 field.classList.add('is-invalid'); 272 const err = field.closest('.aam-field')?.querySelector('.aam-field-error'); 273 if (err) err.textContent = json.data.message; 274 } 275 } 276 // Login mit unbestätigter E-Mail â Resend-Option anbieten 277 if (json.data?.unconfirmed) { 278 const account = form.querySelector('[name="username"]')?.value || ''; 279 showResendOption(panel, null, account); 280 } 281 // Custom-Event für Third-Party-Integration 282 document.dispatchEvent(new CustomEvent('aam:error', { detail: json.data })); 283 return; 284 } 285 286 // Erfolg 287 showMessage(panel, json.data?.message || 'Erfolgreich.', 'success'); 288 document.dispatchEvent(new CustomEvent('aam:success', { detail: { action, response: json.data } })); 289 290 // Double-Opt-In: "Mail erneut senden"-Option anbieten 291 if (json.data?.confirm) { 292 showResendOption(panel, form); 293 } 294 295 const rd = json.data?.redirect_to; 296 if (rd) { 297 window.location.href = rd; 298 } else if (json.data?.reload) { 299 window.location.reload(); 300 } 301 } 302 303 function showResendOption(panel, registerForm, accountOverride) { 304 // E-Mail aus dem Register-Formular oder explizit übergeben (Login-Fall) 305 const email = accountOverride 306 || registerForm?.querySelector('[name="email"]')?.value 307 || ''; 308 309 let box = panel.querySelector('.aam-resend-box'); 310 if (!box) { 311 box = document.createElement('div'); 312 box.className = 'aam-resend-box'; 313 panel.querySelector('.aam-message')?.after(box); 314 } 315 box.innerHTML = ''; 316 317 const text = document.createElement('span'); 318 text.textContent = 'Keine E-Mail erhalten? '; 319 const link = document.createElement('button'); 320 link.type = 'button'; 321 link.className = 'aam-link aam-resend-link'; 322 link.textContent = 'Erneut senden'; 323 box.append(text, link); 324 325 link.addEventListener('click', async () => { 326 link.disabled = true; 327 link.textContent = 'Wird gesendetâ¦';
328 try { 329 const data = new FormData(); 330 data.set('action', 'aam_resend_confirmation'); 331 data.set('aam_resend_confirmation', nonces.resend || ''); 332 data.set('account', email); 333 const resp = await fetch(ajaxUrl, { method: 'POST', body: data, credentials: 'same-origin' }); 334 const json = await resp.json(); 335 box.innerHTML = ''; 336 const msg = document.createElement('span'); 337 msg.className = 'aam-resend-done'; 338 msg.textContent = json.data?.message || 'Gesendet.'; 339 box.appendChild(msg); 340 } catch { 341 link.disabled = false; 342 link.textContent = 'Erneut senden'; 343 } 344 }); 345 } 346 347 /* ========================================================================= 348 * Passwort-Toggle 349 * ====================================================================== */ 350 351 overlay.addEventListener('click', e => { 352 const btn = e.target.closest('.aam-pw-toggle'); 353 if (!btn) return; 354 const input = btn.closest('.aam-pw-wrap')?.querySelector('input'); 355 if (!input) return; 356 const isText = input.type === 'text'; 357 input.type = isText ? 'password' : 'text'; 358 btn.classList.toggle('is-visible', !isText); 359 btn.setAttribute('aria-label', isText ? 'Passwort anzeigen' : 'Passwort verbergen'); 360 }); 361 362 /* ========================================================================= 363 * Passwort-Stärke 364 * ====================================================================== */ 365 366 const pwInput = document.getElementById('aam-reg-password'); 367 const pwStrength = panelRegister?.querySelector('.aam-pw-strength'); 368 369 if (pwInput && pwStrength) { 370 pwInput.addEventListener('input', () => { 371 const val = pwInput.value; 372 let score = 0; 373 if (val.length >= 8) score++; 374 if (/[A-Z]/.test(val)) score++; 375 if (/[0-9]/.test(val)) score++; 376 if (/[^A-Za-z0-9]/.test(val)) score++; 377 378 pwStrength.className = `aam-pw-strength${score ? ' s' + score : ''}`; 379 }); 380 } 381 382 /* ========================================================================= 383 * Feldvalidierung â live bei blur 384 * ====================================================================== */ 385 386 overlay.addEventListener('blur', e => { 387 const input = e.target; 388 if (!input.matches('input[required]')) return; 389 if (!input.value.trim()) { 390 input.classList.add('is-invalid'); 391 } else { 392 input.classList.remove('is-invalid'); 393 const err = input.closest('.aam-field')?.querySelector('.aam-field-error'); 394 if (err) err.textContent = ''; 395 } 396 }, true); 397 398 overlay.addEventListener('input', e => { 399 const input = e.target; 400 if (input.classList.contains('is-invalid') && input.value.trim()) { 401 input.classList.remove('is-invalid'); 402 const err = input.closest('.aam-field')?.querySelector('.aam-field-error'); 403 if (err) err.textContent = ''; 404 } 405 }); 406 407 /* ========================================================================= 408 * Form-Submit-Events 409 * ====================================================================== */ 410 411 if (formLogin) { 412 // Label vor Loading speichern 413 const lbl = formLogin.querySelector('.aam-submit-label'); 414 if (lbl) formLogin.querySelector('.aam-submit').dataset.originalLabel = lbl.textContent; 415 416 formLogin.addEventListener('submit', e => { 417 e.preventDefault(); 418 submitForm(formLogin, 'aam_login', panelLogin); 419 }); 420 } 421 422 if (formRegister) { 423 const lbl = formRegister.querySelector('.aam-submit-label'); 424 if (lbl) formRegister.querySelector('.aam-submit').dataset.originalLabel = lbl.textContent; 425 426 formRegister.addEventListener('submit', e => { 427 e.preventDefault(); 428 submitForm(formRegister, 'aam_register', panelRegister); 429 }); 430 } 431 432 if (formLostPw) { 433 const lbl = formLostPw.querySelector('.aam-submit-label'); 434 if (lbl) formLostPw.querySelector('.aam-submit').dataset.originalLabel = lbl.textContent; 435 436 formLostPw.addEventListener('submit', e => { 437 e.preventDefault(); 438 submitForm(formLostPw, 'aam_lostpassword', panelLostPw); 439 }); 440 } 441 442 /* ========================================================================= 443 * Tab-Klicks & "Passwort vergessen"-Links 444 * ====================================================================== */ 445 446 overlay.addEventListener('click', e => { 447 // Tab-Buttons 448 const tab = e.target.closest('.aam-tab'); 449 if (tab && tab.dataset.target) { 450 e.preventDefault(); 451 switchTab(tab.dataset.target); 452 return; 453 } 454 455 // Switch-Links (z.B. "Passwort vergessen", "Zurück zum Login") 456 const switcher = e.target.closest('.aam-switch-to'); 457 if (switcher && switcher.dataset.target) { 458 e.preventDefault(); 459 switchTab(switcher.dataset.target); 460 } 461 }); 462 463 /* ========================================================================= 464 * Modal schlieÃen 465 * ====================================================================== */ 466 467 if (btnClose) btnClose.addEventListener('click', closeModal); 468 469 if (closeOnOverlay) { 470 overlay.addEventListener('mousedown', e => { 471 if (e.target === overlay) closeModal(); 472 }); 473 } 474 475 if (closeOnEsc) { 476 document.addEventListener('keydown', e => { 477 if (e.key === 'Escape' && overlay.classList.contains('is-visible')) closeModal(); 478 }); 479 } 480 481 /* ========================================================================= 482 * Trigger-Elemente â öffnen Modal 483 * ====================================================================== */ 484 485 function bindTriggers() { 486 // Konfigurierbarer Selektor (Admin-Einstellung) 487 const allLoginSels = ['.aam-trigger', ...selectorsLogin].join(',');
488 const allRegisterSels = selectorsRegister.join(','); 489 490 document.addEventListener('click', e => { 491 // Login-Trigger 492 // try/catch: ungültige Admin-CSS-Selektoren würden sonst JS-Exception werfen 493 try { 494 if (allLoginSels && e.target.closest(allLoginSels)) { 495 const el = e.target.closest(allLoginSels); 496 // preventDefault auch wenn <a> innerhalb des matched Elements (z.B. <li class="lrm-login"><a>) 497 const clickedLink = e.target.closest('a, button'); 498 if (el?.tagName === 'A' || el?.tagName === 'BUTTON' || clickedLink) e.preventDefault(); 499 _pendingRedirect = el?.dataset.aamRedirect || ''; 500 const tab = el?.dataset.aamTab || 'login'; 501 openModal(tab); 502 return; 503 } 504 if (allRegisterSels && e.target.closest(allRegisterSels)) { 505 const el = e.target.closest(allRegisterSels); 506 if (el?.tagName === 'A' || el?.tagName === 'BUTTON') e.preventDefault(); 507 _pendingRedirect = el?.dataset.aamRedirect || ''; 508 openModal('register'); 509 } 510 } catch (_) { /* Ungültiger CSS-Selektor â still ignorieren */ } 511 }); 512 } 513 514 bindTriggers(); 515 bindPasswordStrength(); 516 bindPasswordGenerator(); 517 bindOAuth(); 518 bindProfileForm(); 519 setAntiBotToken(); 520 521 // Profil-Vervollständigung ausstehend (User hat Modal beim letzten Besuch weggeklickt 522 // oder Seite neu geladen) â Panel direkt öffnen 523 if (profileIncomplete) { 524 openProfilePanel( 525 profileIncomplete.token, 526 profileIncomplete.first_name || '', 527 profileIncomplete.last_name || '' 528 ); 529 } 530 531 /* ========================================================================= 532 * URL-Hash â Auto-Open (#login, #register, #reset-password) 533 * ====================================================================== */ 534 535 function handleHash() { 536 const hash = window.location.hash.replace('#', ''); 537 const map = { login: 'login', register: 'register', 'reset-password': 'lost-password' }; 538 if (map[hash]) { 539 openModal(map[hash]); 540 history.replaceState(null, '', window.location.pathname + window.location.search); 541 } 542 } 543 544 window.addEventListener('hashchange', handleHash); 545 setTimeout(handleHash, 300); // Auch beim initialen Laden 546 547 /* ========================================================================= 548 * Custom-Event API (für Developer) 549 * document.dispatchEvent(new CustomEvent('aam:open', { detail: { tab: 'register' } })) 550 * ====================================================================== */ 551 552 document.addEventListener('aam:open', e => { 553 openModal(e.detail?.tab || 'login'); 554 }); 555 document.addEventListener('aam:close', closeModal); 556 557 // Rückwärtskompatibilität mit LRM-Events 558 document.addEventListener('lrm_show_login', () => openModal('login')); 559 document.addEventListener('lrm_show_signup', () => openModal('register')); 560 document.addEventListener('lrm_show_register', () => openModal('register')); 561 562 /* ========================================================================= 563 * Focus-Trap für Barrierefreiheit 564 * ====================================================================== */ 565 566 let _pendingRedirect = ''; 567 let _profilePanelActive = false; // blockiert Modal-SchlieÃen während Profil-Pflichtschritt 568 let _focusTrapEl = null; 569 const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; 570 571 function trapFocus(container) { 572 releaseTrap(); 573 _focusTrapEl = container; 574 container.addEventListener('keydown', _focusTrapHandler); 575 } 576 577 function releaseTrap() { 578 if (_focusTrapEl) { 579 _focusTrapEl.removeEventListener('keydown', _focusTrapHandler); 580 _focusTrapEl = null; 581 } 582 } 583 584 function _focusTrapHandler(e) { 585 if (e.key !== 'Tab') return; 586 const focusable = [...modal.querySelectorAll(FOCUSABLE)].filter(el => !el.closest('[hidden]')); 587 if (!focusable.length) return; 588 const first = focusable[0];
589 const last = focusable[focusable.length - 1]; 590 if (e.shiftKey) { 591 if (document.activeElement === first) { e.preventDefault(); last.focus(); } 592 } else { 593 if (document.activeElement === last) { e.preventDefault(); first.focus(); } 594 } 595 } 596 597 // ========================================================================= 598 // Passwort-Stärke-Indikator 599 // ========================================================================= 600 601 function checkPasswordStrength(pw) { 602 const r = pwRules; 603 let score = 0; 604 const issues = []; 605 606 if (pw.length === 0) return { score: 0, issues: [] }; 607 608 if (pw.length >= (r.minLength || 8)) score++; 609 else issues.push(`Mindestens ${r.minLength || 8} Zeichen`); 610 611 if (r.requireUpper) { if (/[A-Z]/.test(pw)) score++; else issues.push('GroÃbuchstabe'); } 612 else if (/[A-Z]/.test(pw)) score++; 613 614 if (r.requireLower) { if (/[a-z]/.test(pw)) score++; else issues.push('Kleinbuchstabe'); } 615 else if (/[a-z]/.test(pw)) score++; 616 617 if (r.requireNumber) { if (/[0-9]/.test(pw)) score++; else issues.push('Ziffer (0â9)'); } 618 else if (/[0-9]/.test(pw)) score++; 619 620 if (r.requireSpecial) { if (/[^A-Za-z0-9]/.test(pw)) score++; else issues.push('Sonderzeichen'); } 621 else if (/[^A-Za-z0-9]/.test(pw)) score++; 622 623 return { score, issues }; 624 } 625 626 function updateStrengthBar(input) { 627 const wrap = input.closest('.aam-field'); 628 const bar = wrap?.querySelector('.aam-pw-strength'); 629 const error = wrap?.querySelector('.aam-field-error'); 630 if (!bar) return; 631 632 const pw = input.value; 633 const { score, issues } = checkPasswordStrength(pw); 634 const levels = ['', 'weak', 'fair', 'good', 'strong']; 635 636 bar.dataset.level = pw.length ? (levels[Math.min(score, 4)] || 'weak') : ''; 637 bar.querySelectorAll('span').forEach((s, i) => { 638 s.dataset.active = i < score ? '1' : '0'; 639 }); 640 641 if (error) { 642 error.textContent = (issues.length && pw.length > 0) ? 'Fehlt: ' + issues.join(', ') : ''; 643 } 644 } 645 646 function bindPasswordStrength() { 647 const pwField = document.getElementById('aam-reg-password'); 648 if (!pwField) return; 649 pwField.addEventListener('input', () => updateStrengthBar(pwField)); 650 } 651 652 // ========================================================================= 653 // Passwort-Generator 654 // ========================================================================= 655 656 function generatePassword() { 657 const r = pwRules; 658 const minLen = Math.max( r.minLength || 8, 12 ); // mind. 12 für generierte PWs 659 const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; // ohne I, O (lesbarkeit) 660 const lower = 'abcdefghjkmnpqrstuvwxyz'; // ohne i, l, o 661 const digits = '23456789'; // ohne 0, 1 662 const special = '!@#$%^&*-_=+?'; 663 const allChars = upper + lower + digits + special; 664 665 // Garantierte Pflichtzeichen zuerst einbauen 666 const required = []; 667 required.push( randomChar(upper) ); 668 required.push( randomChar(lower) ); 669 required.push( randomChar(digits) ); 670 required.push( randomChar(special) ); 671 672 // Rest auffüllen bis zur Mindestlänge 673 const pw = [...required]; 674 while (pw.length < minLen) { 675 pw.push( randomChar(allChars) ); 676 } 677 678 // Mischen (crypto-sicher) 679 return shuffleArray(pw).join(''); 680 } 681 682 function randomChar(chars) { 683 const arr = new Uint32Array(1); 684 crypto.getRandomValues(arr); 685 return chars[ arr[0] % chars.length ]; 686 } 687 688 function shuffleArray(arr) { 689 const a = [...arr]; 690 for (let i = a.length - 1; i > 0; i--) { 691 const buf = new Uint32Array(1); 692 crypto.getRandomValues(buf); 693 const j = buf[0] % (i + 1); 694 [a[i], a[j]] = [a[j], a[i]]; 695 } 696 return a; 697 } 698 699 function bindPasswordGenerator() { 700 const btn = document.getElementById('aam-pw-generate'); 701 const copyBtn = document.getElementById('aam-pw-copy'); 702 const pwField = document.getElementById('aam-reg-password'); 703 const pw2Field = document.getElementById('aam-reg-password2'); 704 if (!btn || !pwField) return; 705 706 btn.addEventListener('click', () => { 707 const pw = generatePassword(); 708 709 // Passwort eintragen + sichtbar machen 710 pwField.value = pw; 711 pwField.type = 'text'; 712 713 // Bestätigungsfeld synchron befüllen 714 if (pw2Field) { 715 pw2Field.value = pw; 716 pw2Field.type = 'text'; 717 } 718 719 // Toggle-Button-Zustand aktualisieren 720 pwField.closest('.aam-pw-wrap')?.querySelector('.aam-pw-toggle') 721 ?.classList.add('is-visible'); 722 723 // Stärke-Bar aktualisieren 724 updateStrengthBar(pwField); 725 726 // Kopier-Button einblenden 727 if (copyBtn) copyBtn.hidden = false;
728 }); 729 730 if (copyBtn) { 731 copyBtn.addEventListener('click', () => { 732 navigator.clipboard?.writeText(pwField.value).then(() => { 733 const label = copyBtn.querySelector('span'); 734 if (label) { 735 label.textContent = 'Kopiert â'; 736 setTimeout(() => { label.textContent = 'Kopieren'; }, 2000); 737 } 738 }); 739 }); 740 } 741 } 742 743 // ========================================================================= 744 // OAuth / SSO â Popup-Flow 745 // ========================================================================= 746 747 function bindOAuth() { 748 document.querySelectorAll('.aam-oauth-btn:not(.aam-oauth-apple)').forEach(btn => { 749 btn.addEventListener('click', () => handleOAuthClick(btn)); 750 }); 751 752 // Apple: JS-SDK-Button 753 document.querySelectorAll('.aam-oauth-apple').forEach(btn => { 754 btn.addEventListener('click', () => handleAppleClick(btn)); 755 }); 756 757 // postMessage vom Popup-Fenster empfangen 758 window.addEventListener('message', onOAuthMessage); 759 } 760 761 async function handleOAuthClick(btn) { 762 const provider = btn.dataset.provider; 763 btn.disabled = true; 764 btn.classList.add('aam-oauth-loading'); 765 766 try { 767 const data = new FormData(); 768 data.set('action', 'aam_oauth_init'); 769 data.set('provider', provider); 770 data.set('nonce', oauthNonce); 771 772 const resp = await fetch(ajaxUrl, { method: 'POST', body: data, credentials: 'same-origin' }); 773 const json = await resp.json(); 774 775 if (!json.success) throw new Error(json.data?.message || 'Fehler'); 776 777 if (json.data.use_js_sdk) return; // Apple â sollte nicht vorkommen 778 779 openOAuthPopup(json.data.auth_url, provider); 780 781 } catch (err) { 782 showOAuthError(btn, err.message); 783 } finally { 784 btn.disabled = false; 785 btn.classList.remove('aam-oauth-loading'); 786 } 787 } 788 789 async function handleAppleClick(btn) { 790 const clientId = btn.dataset.clientId; 791 const redirectUri = btn.dataset.redirectUri; 792 if (!clientId || typeof AppleID === 'undefined') { 793 showOAuthError(btn, 'Apple Sign In nicht verfügbar.'); 794 return; 795 } 796 797 btn.disabled = true; 798 btn.classList.add('aam-oauth-loading'); 799 800 try { 801 AppleID.auth.init({ 802 clientId, 803 scope: 'name email', 804 redirectURI: redirectUri, 805 usePopup: true, 806 }); 807 808 const result = await AppleID.auth.signIn(); 809 810 const data = new FormData(); 811 data.set('action', 'aam_oauth_apple'); 812 data.set('code', result.authorization?.code || ''); 813 data.set('id_token', result.authorization?.id_token || ''); 814 data.set('given_name', result.user?.name?.firstName || ''); 815 data.set('family_name', result.user?.name?.lastName || ''); 816 817 const resp = await fetch(ajaxUrl, { method: 'POST', body: data, credentials: 'same-origin' }); 818 const json = await resp.json(); 819 820 if (!json.success) throw new Error(json.data?.message || 'Apple-Anmeldung fehlgeschlagen.'); 821 822 handleLoginSuccess(json.data); 823 824 } catch (err) { 825 // Apple wirft einen Error wenn Nutzer abbricht â kein Fehler anzeigen 826 if (err?.error !== 'popup_closed_by_user') { 827 showOAuthError(btn, err.message || 'Apple-Anmeldung fehlgeschlagen.'); 828 } 829 } finally { 830 btn.disabled = false; 831 btn.classList.remove('aam-oauth-loading'); 832 } 833 } 834 835 function openOAuthPopup(authUrl, provider) { 836 const w = 520, h = 600; 837 const left = Math.round((screen.width - w) / 2); 838 const top = Math.round((screen.height - h) / 2); 839 window.open( 840 authUrl, 841 'aam_oauth_' + provider, 842 `width=${w},height=${h},left=${left},top=${top},toolbar=no,menubar=no,scrollbars=yes` 843 ); 844 } 845 846 function onOAuthMessage(event) { 847 if (homeUrl && !event.origin.startsWith(new URL(homeUrl).origin)) return; 848 const msg = event.data; 849 if (!msg || msg.type !== 'aam_oauth_result') return; 850 851 if (!msg.success) { 852 const msgBox = document.querySelector('#aam-panel-login .aam-message'); 853 if (msgBox) { msgBox.textContent = msg.message || 'Anmeldung fehlgeschlagen.'; msgBox.hidden = false; } 854 return; 855 } 856 857 // Profil unvollständig â Panel öffnen statt Modal schlieÃen 858 if (msg.needs_profile) { 859 openProfilePanel(msg.token, msg.first_name || '', msg.last_name || ''); 860 return; 861 } 862 863 handleLoginSuccess(msg); 864 } 865 866 function openProfilePanel(token, firstName, lastName) { 867 _profilePanelActive = true; // Modal-SchlieÃen blockieren 868 document.getElementById('aam-profile-token').value = token; 869 document.getElementById('aam-profile-firstname').value = firstName; 870 document.getElementById('aam-profile-lastname').value = lastName; 871 872 // Tabs ausblenden â dieser Schritt ist kein normaler Tab 873 overlay.querySelectorAll('.aam-tabs').forEach(t => t.hidden = true); 874 875 // Panel direkt einblenden 876 overlay.querySelectorAll('.aam-panel').forEach(p => { p.hidden = true; }); 877 const panel = document.getElementById('aam-panel-profile'); 878 if (panel) { 879 panel.hidden = false;
880 // Username-Vorschlag aus Vor+Nachname generieren 881 if (firstName) { 882 const suggestion = (firstName + (lastName ? lastName : '')).toLowerCase().replace(/[^a-z0-9]/g, ''); 883 document.getElementById('aam-profile-username').value = suggestion; 884 checkUsername(suggestion); 885 } 886 panel.querySelector('input:not([type=hidden])').focus(); 887 } 888 889 if (!overlay.classList.contains('is-visible')) { 890 overlay.hidden = false; 891 requestAnimationFrame(() => requestAnimationFrame(() => overlay.classList.add('is-visible'))); 892 } 893 } 894 895 function bindProfileForm() { 896 const form = document.getElementById('aam-form-profile'); 897 const usernameInput = document.getElementById('aam-profile-username'); 898 if (!form) return; 899 900 // Live-Verfügbarkeitsprüfung mit Debounce 901 let checkTimer; 902 usernameInput?.addEventListener('input', () => { 903 clearTimeout(checkTimer); 904 const status = document.querySelector('.aam-username-status'); 905 if (status) { status.textContent = ''; status.className = 'aam-username-status'; } 906 checkTimer = setTimeout(() => checkUsername(usernameInput.value), 500); 907 }); 908 909 form.addEventListener('submit', async (e) => { 910 e.preventDefault(); 911 const btn = form.querySelector('[type=submit]'); 912 btn.disabled = true; 913 const msgBox = document.querySelector('#aam-panel-profile .aam-message'); 914 915 try { 916 const data = new FormData(form); 917 data.set('action', 'aam_complete_profile'); 918 const resp = await fetch(ajaxUrl, { method: 'POST', body: data, credentials: 'same-origin' }); 919 const json = await resp.json(); 920 921 if (!json.success) { 922 const field = json.data?.field; 923 if (field) { 924 const input = form.querySelector(`[name="${field}"]`); 925 const err = input?.closest('.aam-field')?.querySelector('.aam-field-error'); 926 if (err) err.textContent = json.data.message; 927 } else if (msgBox) { 928 msgBox.textContent = json.data?.message || 'Fehler beim Speichern.'; 929 msgBox.hidden = false; 930 } 931 return; 932 } 933 934 // Tabs wieder einblenden für zukünftige Modal-Ãffnungen 935 overlay.querySelectorAll('.aam-tabs').forEach(t => t.hidden = false); 936 _profilePanelActive = false; 937 938 // Konto pending â Wartemeldung anzeigen, NICHT einloggen 939 if (json.data?.pending) { 940 showPendingMessage(json.data.message); 941 return; 942 } 943 944 handleLoginSuccess(json.data); 945 946 } catch (err) { 947 if (msgBox) { msgBox.textContent = i18n.networkError || 'Netzwerkfehler.'; msgBox.hidden = false; } 948 } finally { 949 btn.disabled = false; 950 } 951 }); 952 } 953 954 async function checkUsername(username) { 955 const status = document.querySelector('.aam-username-status'); 956 if (!status || username.length < 3) return; 957 958 status.textContent = 'â¦'; 959 status.className = 'aam-username-status'; 960 961 const data = new FormData(); 962 data.set('action', 'aam_check_username'); 963 data.set('username', username); 964 data.set('profile_token', document.getElementById('aam-profile-token')?.value || ''); 965 966 try { 967 const resp = await fetch(ajaxUrl, { method: 'POST', body: data, credentials: 'same-origin' }); 968 const json = await resp.json(); 969 status.textContent = json.data?.message || ''; 970 status.className = 'aam-username-status ' + (json.success ? 'is-available' : 'is-taken'); 971 } catch { 972 status.textContent = ''; 973 } 974 } 975 976 function handleLoginSuccess(data) { 977 closeModal(); 978 if (data.redirect_to) { 979 window.location.href = data.redirect_to; 980 } else if (data.reload) { 981 window.location.reload(); 982 } 983 } 984 985 function showPendingMessage(message) { 986 // Alle Panels ausblenden, neutrale Wartemeldung im Modal anzeigen 987 overlay.querySelectorAll('.aam-panel').forEach(p => { p.hidden = true; }); 988 overlay.querySelectorAll('.aam-tabs').forEach(t => t.hidden = true); 989 990 let box = overlay.querySelector('.aam-pending-notice'); 991 if (!box) { 992 box = document.createElement('div'); 993 box.className = 'aam-pending-notice'; 994 overlay.querySelector('.aam-modal-body, .aam-modal')?.appendChild(box); 995 } 996 box.innerHTML = ''; 997 const icon = document.createElement('div'); 998 icon.className = 'aam-pending-icon'; 999 icon.textContent = 'â'; 1000 const text = document.createElement('p'); 1001 text.textContent = message || 'Dein Konto wird geprüft.'; 1002 box.append(icon, text); 1003 box.hidden = false;
1004 } 1005 1006 function showOAuthError(btn, message) { 1007 const msgBox = btn.closest('.aam-panel')?.querySelector('.aam-message'); 1008 if (msgBox) { 1009 msgBox.textContent = message; 1010 msgBox.hidden = false; 1011 } 1012 } 1013 1014 // Setzt die Anti-Bot-Tokens. Cache-sicher, da rein clientseitig: 1015 // - aam__js: Marker dass JS läuft (Headless-Bots ohne JS senden leer) 1016 // - aam__ts: verstrichene Millisekunden seit Seitenladen (Zeit-Falle) 1017 function setAntiBotToken() { 1018 window._aamStart = Date.now(); 1019 const start = window._aamStart; 1020 1021 const fillJs = () => { 1022 document.querySelectorAll('input[name="aam__js"]').forEach(el => { 1023 if (!el.value) el.value = '1'; 1024 }); 1025 }; 1026 fillJs(); 1027 ['mousemove', 'keydown', 'touchstart', 'pointerdown'].forEach(ev => { 1028 document.addEventListener(ev, fillJs, { once: true, passive: true }); 1029 }); 1030 1031 // Beim Absenden eines Formulars die verstrichene Zeit eintragen. 1032 // capture=true, damit es vor dem submit-Handler läuft. 1033 document.addEventListener('submit', (e) => { 1034 const form = e.target; 1035 if (!form || !form.matches('.aam-form')) return; 1036 const tsField = form.querySelector('input[name="aam__ts"]'); 1037 if (tsField) tsField.value = String(Date.now() - start); 1038 const jsField = form.querySelector('input[name="aam__js"]'); 1039 if (jsField && !jsField.value) jsField.value = '1'; 1040 }, true); 1041 1042 // Fallback: Da die Formulare per JS (fetch) abgeschickt werden und nicht 1043 // immer ein natives submit-Event feuern, die Zeit auch bei Klick auf 1044 // Submit-Buttons setzen. 1045 document.addEventListener('click', (e) => { 1046 const btn = e.target.closest('.aam-form button[type="submit"], .aam-form .aam-submit'); 1047 if (!btn) return; 1048 const form = btn.closest('.aam-form'); 1049 if (!form) return; 1050 const tsField = form.querySelector('input[name="aam__ts"]'); 1051 if (tsField) tsField.value = String(Date.now() - start); 1052 const jsField = form.querySelector('input[name="aam__js"]'); 1053 if (jsField && !jsField.value) jsField.value = '1'; 1054 }, true); 1055 } 1056 1057 } // initModal() 1058 1059 if (document.readyState === 'loading') { 1060 document.addEventListener('DOMContentLoaded', initModal); 1061 } else { 1062 initModal(); 1063 } 1064 1065})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.