PageSourceSearch

https://www.l-iz.de/wp-content/plugins/ajax-auth-modal/assets/js/modal.js?ver=1.0.1

js l-iz.de collected 2026-10-01 11:41:17 UTC 38,463 bytes, 1,065 lines download raw bytes

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.