/* Kundenansicht neu (P2): Passwort ändern (Etage #passwort, aus dem Konto-Menü). Dieselben Regeln prüft der Server (server/services/customerPassword.js); danach sind andere Geräte abgemeldet. Regeln und Texte aus app.js (passwordProblem, initPasswordView). */ import { api } from './api.js' import { h, leeren } from './dom.js' import { K, zuAnsicht, navigiere } from './ansicht-hilfen.js' const TEXT_ANSICHTSMODUS = 'Im Ansichtsmodus kann das Passwort nicht geändert werden.' const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs let beobachter = null // räumt die Felder, wenn die Seite verlassen wird /** → [feldId, text] oder null. Reihenfolge und Texte aus app.js passwordProblem. */ export function passwortProblem(aktuell, neu, wiederholung, email) { if (!aktuell) return ['pw-current', 'Bitte gib dein aktuelles Passwort ein.'] if (!neu) return ['pw-new', 'Bitte gib ein neues Passwort ein.'] if ([...neu].length < 8) return ['pw-new', 'Das neue Passwort muss mindestens 8 Zeichen haben.'] if (new TextEncoder().encode(neu).length > 256) return ['pw-new', 'Das neue Passwort ist zu lang (höchstens 256 Zeichen).'] if (!neu.trim()) return ['pw-new', 'Das neue Passwort darf nicht nur aus Leerzeichen bestehen.'] if (neu === aktuell) return ['pw-new', 'Das neue Passwort muss sich vom aktuellen unterscheiden.'] if (email && neu.trim().toLowerCase() === email.trim().toLowerCase()) return ['pw-new', 'Bitte nimm nicht deine E-Mail-Adresse als Passwort.'] if (neu !== wiederholung) return ['pw-repeat', 'Die beiden neuen Passwörter stimmen nicht überein.'] return null } export async function zeigePasswort(wurzel, { route, me, nav } = {}) { const nr = (laeufe.get(wurzel) ?? 0) + 1 laeufe.set(wurzel, nr) beobachter?.disconnect?.() beobachter = null const viewAs = me?.viewAs === true const email = me?.customer?.email || '' const benutzername = h('input', { type: 'text', id: 'pw-username', name: 'username', autocomplete: 'username', hidden: true, value: email }) const aktuell = h('input', { type: 'password', id: 'pw-current', class: K.input, name: 'current-password', autocomplete: 'current-password', required: true, enterkeyhint: 'next' }) const neu = h('input', { type: 'password', id: 'pw-new', class: K.input, name: 'new-password', autocomplete: 'new-password', minlength: 8, required: true, 'aria-describedby': 'pw-rule', enterkeyhint: 'next' }) const wdh = h('input', { type: 'password', id: 'pw-repeat', class: K.input, name: 'new-password-repeat', autocomplete: 'new-password', required: true, enterkeyhint: 'done' }) const zeigen = h('input', { type: 'checkbox', id: 'pw-show' }) const fehler = h('p', { class: K.fehler, id: 'pw-error', role: 'alert', hidden: true }) const senden = h('button', { type: 'submit', class: K.primaer, id: 'pw-submit' }, 'Passwort ändern') const felder = [aktuell, neu, wdh] const feldPaar = (label, steuerung, ...nach) => h('div', { class: K.feld }, h('label', { for: steuerung.getAttribute('id') }, label), steuerung, ...nach) const form = h('form', { id: 'pw-form', class: 'kn-pw-form', novalidate: true }, benutzername, feldPaar('Aktuelles Passwort', aktuell), feldPaar('Neues Passwort', neu, h('small', { class: 'kn-feld-hinweis', id: 'pw-rule' }, 'Mindestens 8 Zeichen. Am sichersten ist ein langer Satz, den nur du kennst.')), feldPaar('Neues Passwort wiederholen', wdh), h('label', { class: 'kn-check' }, zeigen, ' Passwörter anzeigen'), fehler, h('div', { class: 'kn-aktionen' }, senden, viewAs ? h('span', { class: K.meldung }, TEXT_ANSICHTSMODUS) : null), h('p', { class: 'kn-hinweis' }, 'Danach bist du auf deinen anderen Geräten abgemeldet. Aktuelles Passwort vergessen? ', h('a', zuAnsicht('chat'), 'Schreib uns im Chat'), ' – wir setzen es zurück.')) const fertig = h('button', { type: 'button', class: K.knopf, id: 'pw-done-btn' }, 'Fertig') const erfolg = h('div', { class: 'kn-pw-erfolg', id: 'pw-done', role: 'status', tabindex: '-1', hidden: true }, h('strong', null, 'Dein Passwort wurde geändert.'), h('p', null, 'Melde dich ab jetzt mit dem neuen Passwort an. Auf deinen anderen Geräten bist du abgemeldet.'), fertig) const leereFelder = () => { for (const f of felder) { f.value = ''; f.type = 'password' } zeigen.checked = false fehler.hidden = true } zeigen.addEventListener('change', () => { for (const f of felder) f.type = zeigen.checked ? 'text' : 'password' }) fertig.addEventListener('click', () => { if (nav === 'push') window.history.back() else navigiere('#') }) form.addEventListener('submit', async (ev) => { ev.preventDefault() if (viewAs || senden.disabled) return fehler.hidden = true const problem = passwortProblem(aktuell.value, neu.value, wdh.value, email) if (problem) { fehler.textContent = problem[1] fehler.hidden = false felder.find((f) => f.getAttribute('id') === problem[0])?.focus() return } senden.disabled = true senden.textContent = 'Wird gespeichert…' try { await api('/api/auth/password', { method: 'POST', body: { currentPassword: aktuell.value, newPassword: neu.value } }) leereFelder() form.hidden = true erfolg.hidden = false erfolg.focus() } catch (f) { fehler.textContent = f.message fehler.hidden = false if (f.code === 'wrong_password' || f.code === 'current_missing') { aktuell.focus(); aktuell.select?.() } else if (f.code === 'new_invalid') neu.focus() } finally { senden.disabled = false senden.textContent = 'Passwort ändern' } }) leeren(wurzel).append(h('div', { class: 'kn-panel kn-pw' }, form, erfolg)) if (viewAs) for (const e of wurzel.querySelectorAll('input, button')) e.disabled = true // Wer die Seite verlässt, findet beim nächsten Mal ein leeres Formular (keine Passwörter im DOM) if (typeof MutationObserver === 'function' && typeof document !== 'undefined' && document.body) { beobachter = new MutationObserver(() => { // nur räumen, wenn noch etwas darin steht: das Räumen ändert selbst `hidden` und darf den Beobachter nicht endlos wecken const benutzt = felder.some((f) => f.value) || !erfolg.hidden || !fehler.hidden || form.hidden if (laeufe.get(wurzel) === nr && benutzt && wurzel.closest('[hidden]')) { leereFelder(); form.hidden = false; erfolg.hidden = true } }) beobachter.observe(document.body, { attributes: true, subtree: true, attributeFilter: ['hidden'] }) } }