Kundenbereich: Passwort selbst ändern
Im Konto-Menü (Profilbild) gibt es unter "Konto" jetzt "Passwort ändern". Die Seite fragt aktuelles Passwort, neues Passwort und Wiederholung ab (mindestens 8 Zeichen, "Passwörter anzeigen"). Am Handy ist sie eine Unterseite des Tabs, aus dem man kam; Direktlink /dashboard.html#passwort. Im Ansichtsmodus ist sie gesperrt. Server (POST /api/auth/password): - Das aktuelle Passwort wird über eine kurze Appwrite-Session geprüft, die gleich wieder gelöscht wird. Tippfehler lösen nicht die globale Login-Sperre aus. Es gibt höchstens 5 Prüfungen je Kunde und Stunde, das bleibt unter Appwrites Grenze (10 Anmeldungen je E-Mail und Stunde). - Das neue Passwort wird über die Users-API gesetzt. Fehler von Appwrite (zuletzt benutzt, zu häufig, persönliche Daten) kommen als verständliche Meldung. - Die Klartext-Kopie in customers.portalPassword und ein Alt-Eintrag in den Notizen werden geleert. Ein selbst gewähltes Passwort wird nie im Klartext gespeichert; das Ticketsystem zeigt dann "–". - Der Kunde bekommt eine Bestätigungsmail (Kopie an die Admins wie bei allen Kundenmails). Andere Geräte werden abgemeldet: Kunden-Sessions merken sich beim Login den Passwortstand (Appwrite passwordUpdate, pwv). requireSession und /api/auth/me prüfen ihn, 60 s zwischengespeichert. Das gilt auch, wenn wir im Ticketsystem ein neues Passwort setzen. Ist Appwrite nicht erreichbar, bleibt die Session gültig. Sessions von vor diesem Update haben keinen Stand und laufen normal aus (höchstens 7 Tage). Login-Seite: Hinweis auf "Passwort ändern" statt "über das Ticketsystem", dazu "Passwort vergessen?" mit support@webklar.com. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
63ddbf55cc
commit
81119c19bc
12 files changed
+585
-13
No files matched your search
+138
-7
@@ -1,3 +1,6 @@
|
||||
// Sitzung beendet (Passwort auf einem anderen Gerät geändert): nur einmal zur Anmeldung wechseln
|
||||
let sessionEndRedirected = false
|
||||
|
||||
async function api(path, options = {}) {
|
||||
const response = await fetch(path, {
|
||||
credentials: 'same-origin',
|
||||
@@ -12,7 +15,13 @@ async function api(path, options = {}) {
|
||||
: `Fehler ${response.status}`
|
||||
const err = new Error(data.error || fallback)
|
||||
err.status = response.status
|
||||
if (data.code) err.code = data.code
|
||||
if (data.retryAfterSeconds) err.retryAfterSeconds = data.retryAfterSeconds
|
||||
// Passwort auf einem anderen Gerät geändert: diese Sitzung gilt nicht mehr
|
||||
if (data.code === 'session_revoked' && document.body.classList.contains('dashboard-page') && !sessionEndRedirected) {
|
||||
sessionEndRedirected = true
|
||||
window.location.href = `/login.html?error=${encodeURIComponent(err.message)}`
|
||||
}
|
||||
throw err
|
||||
}
|
||||
return data
|
||||
@@ -1272,11 +1281,122 @@ async function loadTakeoverCard({ abo, notice = '' } = {}) {
|
||||
}))
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------- Passwort ändern --- */
|
||||
// Seite "Passwort ändern" aus dem Konto-Menü. Dieselben Regeln prüft der Server
|
||||
// (server/services/customerPassword.js); danach sind andere Geräte abgemeldet.
|
||||
function passwordProblem(current, next, repeat, email) {
|
||||
if (!current) return ['pw-current', 'Bitte gib dein aktuelles Passwort ein.']
|
||||
if (!next) return ['pw-new', 'Bitte gib ein neues Passwort ein.']
|
||||
if ([...next].length < 8) return ['pw-new', 'Das neue Passwort muss mindestens 8 Zeichen haben.']
|
||||
if (new TextEncoder().encode(next).length > 256) return ['pw-new', 'Das neue Passwort ist zu lang (höchstens 256 Zeichen).']
|
||||
if (!next.trim()) return ['pw-new', 'Das neue Passwort darf nicht nur aus Leerzeichen bestehen.']
|
||||
if (next === current) return ['pw-new', 'Das neue Passwort muss sich vom aktuellen unterscheiden.']
|
||||
if (email && next.trim().toLowerCase() === email.trim().toLowerCase()) {
|
||||
return ['pw-new', 'Bitte nimm nicht deine E-Mail-Adresse als Passwort.']
|
||||
}
|
||||
if (next !== repeat) return ['pw-repeat', 'Die beiden neuen Passwörter stimmen nicht überein.']
|
||||
return null
|
||||
}
|
||||
|
||||
const passwordState = { email: '', ready: false, viewAs: false }
|
||||
|
||||
// Kunde bekannt (/api/auth/me): Zugang für Passwort-Manager, im Ansichtsmodus gesperrt
|
||||
function setPasswordAccount(customer, viewAs) {
|
||||
passwordState.email = customer.email || ''
|
||||
passwordState.viewAs = viewAs
|
||||
passwordState.ready = true
|
||||
document.getElementById('pw-username').value = passwordState.email
|
||||
if (!viewAs) return
|
||||
const submit = document.getElementById('pw-submit')
|
||||
document.querySelectorAll('#pw-form input').forEach((f) => { f.disabled = true })
|
||||
submit.disabled = true
|
||||
submit.insertAdjacentHTML('afterend', '<span class="abo-note">Im Ansichtsmodus kann das Passwort nicht geändert werden.</span>')
|
||||
}
|
||||
|
||||
// Gleich beim Start: das Formular darf nie ohne Script-Handler abgeschickt werden
|
||||
function initPasswordView() {
|
||||
const form = document.getElementById('pw-form')
|
||||
if (!form) return
|
||||
const done = document.getElementById('pw-done')
|
||||
const errorEl = document.getElementById('pw-error')
|
||||
const submit = document.getElementById('pw-submit')
|
||||
const show = document.getElementById('pw-show')
|
||||
const fields = ['pw-current', 'pw-new', 'pw-repeat'].map((id) => document.getElementById(id))
|
||||
const [current, next, repeat] = fields
|
||||
let returnTo = 'projects'
|
||||
|
||||
const clearFields = () => {
|
||||
fields.forEach((f) => { f.value = ''; f.type = 'password' })
|
||||
show.checked = false
|
||||
hideError(errorEl)
|
||||
}
|
||||
const showDone = (on) => {
|
||||
form.classList.toggle('hidden', on)
|
||||
done.classList.toggle('hidden', !on)
|
||||
}
|
||||
// Wer die Seite verlässt, findet beim nächsten Mal ein leeres Formular (keine Passwörter im DOM)
|
||||
document.addEventListener('kb:view', (ev) => {
|
||||
if (ev.detail.name !== 'passwort') {
|
||||
returnTo = ev.detail.name
|
||||
clearFields()
|
||||
showDone(false)
|
||||
} else if (!passwordState.viewAs && !coarsePointer.matches) {
|
||||
// Am Desktop gleich lostippen; auf dem Handy nicht ungefragt die Tastatur öffnen
|
||||
current.focus({ preventScroll: true })
|
||||
}
|
||||
})
|
||||
|
||||
show.addEventListener('change', () => {
|
||||
fields.forEach((f) => { f.type = show.checked ? 'text' : 'password' })
|
||||
})
|
||||
|
||||
form.addEventListener('submit', async (ev) => {
|
||||
ev.preventDefault()
|
||||
if (submit.disabled || !passwordState.ready || passwordState.viewAs) return
|
||||
hideError(errorEl)
|
||||
const problem = passwordProblem(current.value, next.value, repeat.value, passwordState.email)
|
||||
if (problem) {
|
||||
showError(errorEl, problem[1])
|
||||
document.getElementById(problem[0]).focus()
|
||||
return
|
||||
}
|
||||
submit.disabled = true
|
||||
submit.textContent = 'Wird gespeichert…'
|
||||
try {
|
||||
await api('/api/auth/password', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ currentPassword: current.value, newPassword: next.value }),
|
||||
})
|
||||
clearFields()
|
||||
showDone(true)
|
||||
done.focus()
|
||||
} catch (err) {
|
||||
showError(errorEl, err.message)
|
||||
if (err.code === 'wrong_password' || err.code === 'current_missing') {
|
||||
current.focus()
|
||||
current.select()
|
||||
} else if (err.code === 'new_invalid') {
|
||||
next.focus()
|
||||
}
|
||||
} finally {
|
||||
submit.disabled = false
|
||||
submit.textContent = 'Passwort ändern'
|
||||
}
|
||||
})
|
||||
|
||||
// Fertig: zurück, woher man kam (App-Ansicht über den Verlauf wie der Zurück-Pfeil)
|
||||
document.getElementById('pw-done-btn')?.addEventListener('click', () => {
|
||||
if (window.history.state?.kbSub === 'passwort') window.history.back()
|
||||
else showView(APP_PARENT.passwort || returnTo)
|
||||
})
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------- Navigation --- */
|
||||
// Ansichten im Inhaltsbereich; Hash fuer Direktlinks (z. B. aus Mails: #abo, #gutschrift).
|
||||
// "wartung" gibt es nur in der App-Ansicht (Tab), am Desktop zeigt sie das Wartungs-Abo.
|
||||
// "projekt" (Projektdetails als eigene Seite) gibt es ebenfalls nur in der App-Ansicht.
|
||||
const VIEW_HASHES = { projects: '', activity: 'fortschritt', invoices: 'rechnungen', chat: 'chat', wartung: 'wartung', abo: 'abo', takeover: 'gutschrift', projekt: 'projekt' }
|
||||
// "passwort" oeffnet das Konto-Menue (Profilbild).
|
||||
const VIEW_HASHES = { projects: '', activity: 'fortschritt', invoices: 'rechnungen', chat: 'chat', wartung: 'wartung', abo: 'abo', takeover: 'gutschrift', projekt: 'projekt', passwort: 'passwort' }
|
||||
const viewLoaders = { activity: loadCustomerActivity, chat: initChat }
|
||||
const loadedViews = new Set()
|
||||
|
||||
@@ -1319,6 +1439,7 @@ async function initDashboardPage() {
|
||||
const loadError = document.getElementById('load-error')
|
||||
|
||||
initAppShell()
|
||||
initPasswordView()
|
||||
|
||||
// Konto-Menü (Profilbild): Abmelden und "Abo verwalten & Rechnungen" gibt es nur dort
|
||||
document.getElementById('kb-sheet-logout')?.addEventListener('click', async () => {
|
||||
@@ -1345,7 +1466,10 @@ async function initDashboardPage() {
|
||||
const me = await api('/api/auth/me')
|
||||
|
||||
if (!me.authenticated) {
|
||||
window.location.href = loginUrl
|
||||
const target = new URL(loginUrl, window.location.origin)
|
||||
// Passwort auf einem anderen Gerät geändert: Hinweis auf der Anmeldeseite
|
||||
if (me.reason === 'password_changed' && me.message) target.searchParams.set('error', me.message)
|
||||
window.location.href = target.pathname + target.search
|
||||
return
|
||||
}
|
||||
if (me.role === 'admin' && !me.customer) {
|
||||
@@ -1361,6 +1485,7 @@ async function initDashboardPage() {
|
||||
chatState.viewAs = Boolean(me.viewAs)
|
||||
takeoverState.customer = customer
|
||||
renderViewAsBanner(me)
|
||||
setPasswordAccount(customer, chatState.viewAs)
|
||||
|
||||
const displayName = customer.companyName || customer.name || customer.email
|
||||
document.getElementById('customer-name').textContent = displayName
|
||||
@@ -1424,9 +1549,10 @@ async function initDashboardPage() {
|
||||
// Gleiche Bedingung wie der Abschnitt "App-Ansicht" in kundenbereich.css (auch Handys quer)
|
||||
const appLayout = window.matchMedia('(max-width: 880px), (pointer: coarse) and (max-height: 500px)')
|
||||
const coarsePointer = window.matchMedia('(pointer: coarse)')
|
||||
// Unterseite -> Tab, zu dem sie gehoert (und Text neben dem Zurueck-Pfeil)
|
||||
// Unterseite -> Tab, zu dem sie gehoert (und Text neben dem Zurueck-Pfeil).
|
||||
// "passwort" setzt onAppViewShown: die Ansicht, aus der das Konto-Menue geoeffnet wurde.
|
||||
const APP_PARENT = { abo: 'wartung', takeover: 'wartung', projekt: 'projects' }
|
||||
const APP_PARENT_LABEL = { wartung: 'Wartung', projects: 'Projekte' }
|
||||
const APP_PARENT_LABEL = { wartung: 'Wartung', projects: 'Projekte', activity: 'Fortschritt', invoices: 'Rechnungen', chat: 'Chat' }
|
||||
const appShell = { view: 'projects', scroll: {}, fromHistory: false, sideCards: [], installPrompt: null }
|
||||
const TEXT_INPUT = 'textarea, input:not([type=checkbox]):not([type=radio]):not([type=file]):not([type=date]):not([type=button]):not([type=submit]):not([type=range]):not([type=color])'
|
||||
|
||||
@@ -1498,15 +1624,18 @@ function onAppViewShown(name) {
|
||||
return
|
||||
}
|
||||
const prev = appShell.view
|
||||
// "Passwort ändern" (Konto-Menü) ist in der App-Ansicht eine Unterseite der Ansicht, in der man gerade war
|
||||
if (name === 'passwort' && prev !== 'passwort') APP_PARENT.passwort = isAppLayout() ? prev : undefined
|
||||
const parent = APP_PARENT[name]
|
||||
const nav = name === prev ? 'same' : parent === prev ? 'push' : APP_PARENT[prev] === name ? 'back' : 'tab'
|
||||
appShell.view = name
|
||||
document.body.dataset.kbView = name
|
||||
document.body.dataset.kbNav = nav
|
||||
|
||||
// Unterseiten markieren ihren Tab
|
||||
// Unterseiten markieren ihren Tab (Passwort aus einer Unterseite: deren Tab)
|
||||
const tab = APP_PARENT[parent] || parent || name
|
||||
document.querySelectorAll('.kb-nav-item').forEach((b) => {
|
||||
const on = b.dataset.view === (parent || name)
|
||||
const on = b.dataset.view === tab
|
||||
b.classList.toggle('active', on)
|
||||
if (on) b.setAttribute('aria-current', 'page')
|
||||
else b.removeAttribute('aria-current')
|
||||
@@ -1654,7 +1783,9 @@ function initAccountSheet() {
|
||||
sheet.addEventListener('close', () => {
|
||||
sheet.classList.remove('is-closing')
|
||||
document.documentElement.classList.remove('kb-sheet-open')
|
||||
avatar.focus({ preventScroll: true })
|
||||
// Fokus zurück aufs Profilbild – außer die geöffnete Ansicht hat ihn schon gesetzt ("Passwort ändern")
|
||||
const active = document.activeElement
|
||||
if (!active || active === document.body || sheet.contains(active)) avatar.focus({ preventScroll: true })
|
||||
})
|
||||
// Klick auf den Hintergrund, das X oder eine Ansicht (z. B. "Nachricht schreiben") schließt
|
||||
sheet.addEventListener('click', (ev) => {
|
||||
|
||||
@@ -77,6 +77,10 @@
|
||||
<a class="legal-strong" href="/widerrufen.html">Vertrag widerrufen</a>
|
||||
</nav>
|
||||
<p class="error hidden" id="kb-sheet-error"></p>
|
||||
<p class="kb-label kb-sheet-label">Konto</p>
|
||||
<nav class="kb-sheet-links" aria-label="Konto">
|
||||
<button type="button" data-view="passwort">Passwort ändern</button>
|
||||
</nav>
|
||||
<nav class="kb-sheet-links" aria-label="Rechtliches">
|
||||
<a href="/datenschutz.html" target="_blank" rel="noopener">Datenschutz</a>
|
||||
<a href="https://webklar.com/impressum" target="_blank" rel="noopener">Impressum</a>
|
||||
@@ -202,6 +206,48 @@
|
||||
<div id="takeover-card" aria-live="polite"><p class="kb-empty">Lade…</p></div>
|
||||
</section>
|
||||
|
||||
<!-- Passwort ändern (Konto-Menü; app.js initPasswordView). In der App-Ansicht eine Unterseite
|
||||
des Tabs, aus dem das Menü geöffnet wurde -->
|
||||
<section class="kb-view" id="view-passwort">
|
||||
<header class="kb-head">
|
||||
<h1>Passwort ändern</h1>
|
||||
<p>Für deine Anmeldung im Kundenbereich.</p>
|
||||
</header>
|
||||
<div class="kb-panel pw-panel">
|
||||
<form id="pw-form" class="pw-form" method="post" action="/api/auth/password" novalidate>
|
||||
<!-- Für Passwort-Manager: zu welchem Zugang das neue Passwort gehört -->
|
||||
<input type="text" id="pw-username" name="username" autocomplete="username" hidden>
|
||||
<div class="tk-field">
|
||||
<label for="pw-current">Aktuelles Passwort</label>
|
||||
<input type="password" id="pw-current" name="current-password" autocomplete="current-password" required enterkeyhint="next">
|
||||
</div>
|
||||
<div class="tk-field">
|
||||
<label for="pw-new">Neues Passwort</label>
|
||||
<input type="password" id="pw-new" name="new-password" autocomplete="new-password" minlength="8" required aria-describedby="pw-rule" enterkeyhint="next">
|
||||
<small class="pw-rule" id="pw-rule">Mindestens 8 Zeichen. Am sichersten ist ein langer Satz, den nur du kennst.</small>
|
||||
</div>
|
||||
<div class="tk-field">
|
||||
<label for="pw-repeat">Neues Passwort wiederholen</label>
|
||||
<input type="password" id="pw-repeat" name="new-password-repeat" autocomplete="new-password" required enterkeyhint="done">
|
||||
</div>
|
||||
<label class="abo-check pw-show"><input type="checkbox" id="pw-show"> Passwörter anzeigen</label>
|
||||
<p class="error hidden" id="pw-error" role="alert"></p>
|
||||
<div class="abo-actions">
|
||||
<button type="submit" class="abo-btn" id="pw-submit">Passwort ändern</button>
|
||||
</div>
|
||||
<p class="kb-note pw-foot">Danach bist du auf deinen anderen Geräten abgemeldet. Aktuelles Passwort vergessen? <button type="button" class="kb-link" data-view="chat">Schreib uns im Chat</button> – wir setzen es zurück.</p>
|
||||
</form>
|
||||
<div class="pw-done hidden" id="pw-done" role="status" tabindex="-1">
|
||||
<svg class="pw-done-ico" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M8 12.5l2.6 2.6L16 9.5"/></svg>
|
||||
<div>
|
||||
<strong>Dein Passwort wurde geändert.</strong>
|
||||
<p>Melde dich ab jetzt mit dem neuen Passwort an. Auf deinen anderen Geräten bist du abgemeldet.</p>
|
||||
<button type="button" class="kb-btn-outline" id="pw-done-btn">Fertig</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="kb-legal">
|
||||
<a href="/abo-bedingungen.html" target="_blank" rel="noopener">Vertragsbedingungen</a>
|
||||
<a href="/datenschutz.html" target="_blank" rel="noopener">Datenschutz</a>
|
||||
|
||||
@@ -426,6 +426,22 @@
|
||||
.tk-question { margin: 10px 0 0; padding: 10px 12px; border-radius: 12px; white-space: pre-wrap; background: rgba(251, 191, 36, 0.1); border: 1px solid rgba(251, 191, 36, 0.4); }
|
||||
.tk-reply { margin-top: 10px; display: grid; gap: 8px; }
|
||||
|
||||
/* -------------------------------------------------------- Passwort aendern --- */
|
||||
/* Seite aus dem Konto-Menue (app.js initPasswordView); Felder wie im Gutschrift-Formular */
|
||||
#view-passwort .kb-panel { max-width: 560px; }
|
||||
.pw-form { display: grid; gap: 16px; }
|
||||
.pw-form .tk-field input { font-size: 16px; }
|
||||
.pw-rule { display: block; margin-top: 6px; font-size: 12.5px; line-height: 1.45; }
|
||||
.pw-show { margin: -4px 0 0; }
|
||||
#pw-error { text-align: left; }
|
||||
.pw-foot { margin: 0; line-height: 1.5; }
|
||||
.pw-done { display: flex; gap: 14px; align-items: flex-start; outline: none; }
|
||||
.pw-done-ico {
|
||||
flex: none; width: 34px; height: 34px; fill: none; stroke: #86efac; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round;
|
||||
}
|
||||
.pw-done strong { display: block; margin-top: 5px; font-size: 17px; }
|
||||
.pw-done p { margin: 6px 0 16px; font-size: 14px; line-height: 1.5; color: var(--kb-muted); }
|
||||
|
||||
/* ---------------------------------------------------------- Pflichtlinks --- */
|
||||
.kb-legal { margin-top: 40px; padding-top: 18px; border-top: 1px solid var(--kb-border); display: flex; flex-wrap: wrap; gap: 8px 18px; font-size: 13px; }
|
||||
.kb-legal a { color: var(--kb-muted); }
|
||||
|
||||
+2
-1
@@ -74,7 +74,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<p class="login-hint">
|
||||
Zugangsdaten erhältst du von Webklar. Passwortänderungen erfolgen über das Ticketsystem.
|
||||
Zugangsdaten erhältst du von Webklar. Dein Passwort kannst du nach der Anmeldung selbst ändern (Profilbild oben rechts, „Passwort ändern“).
|
||||
Passwort vergessen? Schreib uns an <a href="mailto:support@webklar.com" style="color:inherit">support@webklar.com</a>.
|
||||
</p>
|
||||
<p class="login-hint">
|
||||
<a href="/kuendigen.html" style="color:#fff;font-weight:700">Verträge hier kündigen</a> ·
|
||||
|
||||
Reference in new issue
Block a user