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:
ClaudeandClaude Opus 5.5 committed 2026-09-27 09:22:37 +00:00
1 parent 63ddbf55cc
commit 81119c19bc
12 files changed
+585 -13

No files matched your search

+36 -3
View File
@@ -14,9 +14,13 @@ import {
} from '../services/appwriteAdmin.js'
import { loginWithAppwrite, getLoginCooldownRemainingSec } from '../services/appwriteClient.js'
import {
SESSION_REVOKED_MESSAGE,
clearPortalSession,
requireSession,
setPortalSession,
} from '../middleware/session.js'
import { changeCustomerPassword } from '../services/customerPassword.js'
import { isSessionPasswordCurrent, rememberPasswordVersion } from '../services/passwordVersion.js'
import {
PREVIEW_COOKIE_NAME,
clearPreviewSession,
@@ -50,8 +54,7 @@ function sanitizeCustomer(customer) {
* Gibt null zurueck, wenn der Login kein Mitarbeiter ist (dann greift der
* normale Kunden-Login).
*/
async function resolveStaffAccount(appwriteUserId, email) {
const account = await getUserById(appwriteUserId).catch(() => null)
async function resolveStaffAccount(appwriteUserId, email, account) {
const isAdmin = Array.isArray(account?.labels) && account.labels.includes('admin')
let employee = await getEmployeeByUserId(appwriteUserId).catch(() => null)
@@ -176,9 +179,11 @@ router.post('/login', async (req, res) => {
try {
const user = await loginWithAppwrite(email.trim(), password)
// Appwrite-Konto: Label "admin" und Passwortstand fuer die Kunden-Session (passwordVersion.js)
const account = await getUserById(user.$id).catch(() => null)
// Ticketsystem-Mitarbeiter (employees-Collection) bekommen die Admin-Ansicht
const staff = await resolveStaffAccount(user.$id, email.trim())
const staff = await resolveStaffAccount(user.$id, email.trim(), account)
if (staff) {
setPortalSession(res, {
role: 'admin',
@@ -212,7 +217,9 @@ router.post('/login', async (req, res) => {
appwriteUserId: user.$id,
name: customer.name || user.name || '',
email: customer.email || user.email || email,
pwv: account?.passwordUpdate || undefined,
})
if (account) rememberPasswordVersion(user.$id, account.passwordUpdate)
const previewToken = await createPreviewSessionToken({
userId: user.$id,
@@ -254,6 +261,25 @@ router.post('/login', async (req, res) => {
}
})
// Passwort aendern (Konto-Menue "Passwort ändern"). Die eigene Session bekommt den neuen
// Passwortstand; alle anderen Sessions des Kunden gelten danach nicht mehr.
router.post('/password', requireSession, async (req, res) => {
if (req.session.role === 'admin') {
return res.status(403).json({ error: 'Im Ansichtsmodus kann das Passwort nicht geändert werden.' })
}
try {
const { passwordVersion } = await changeCustomerPassword(req.session, req.body)
setPortalSession(res, { ...req.session, pwv: passwordVersion || undefined })
return res.json({ success: true })
} catch (err) {
return res.status(err.status || 500).json({
error: err.message || 'Das Passwort konnte nicht geändert werden.',
...(err.code ? { code: err.code } : {}),
...(err.retryAfterSeconds ? { retryAfterSeconds: err.retryAfterSeconds } : {}),
})
}
})
router.post('/logout', (_req, res) => {
clearPortalSession(res)
clearPreviewSession(res)
@@ -389,6 +415,13 @@ router.get('/me', async (req, res) => {
return res.json({ authenticated: false })
}
// Passwort inzwischen geaendert (anderes Geraet oder durch uns): neu anmelden
if (!(await isSessionPasswordCurrent(session))) {
clearPortalSession(res)
clearPreviewSession(res)
return res.json({ authenticated: false, reason: 'password_changed', message: SESSION_REVOKED_MESSAGE })
}
let customer = await resolveSessionCustomer(session)
if (!customer) {
clearPortalSession(res)