Files
Webklar-Kundenbereich/public/neu/ansicht-passwort.js
T
2026-10-16 17:12:32 +00:00

112 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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'] })
}
}