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>
63 lines
4.0 KiB
JavaScript
63 lines
4.0 KiB
JavaScript
import { test } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
|
|
// Vor dem Import von config.js setzen: Appwrite unerreichbar (Port 9), damit kein Test ins Netz geht
|
|
process.env.APPWRITE_ENDPOINT = 'http://127.0.0.1:9/v1'
|
|
process.env.APPWRITE_API_KEY = 'test'
|
|
const { attemptState, newPasswordProblem, recordAttempt } = await import('../server/services/customerPassword.js')
|
|
const { forgetPasswordVersion, isSessionPasswordCurrent, rememberPasswordVersion } = await import('../server/services/passwordVersion.js')
|
|
|
|
test('Neues Passwort: Laenge in Zeichen, Grenze von Appwrite in Byte', () => {
|
|
assert.equal(newPasswordProblem('alt-alt-alt', ''), 'Bitte gib ein neues Passwort ein.')
|
|
assert.match(newPasswordProblem('alt-alt-alt', '1234567'), /mindestens 8 Zeichen/)
|
|
assert.equal(newPasswordProblem('alt-alt-alt', '1234567ä'), '')
|
|
// Emoji zaehlen als ein Zeichen (nicht als zwei UTF-16-Einheiten)
|
|
assert.match(newPasswordProblem('alt-alt-alt', '😀'.repeat(7)), /mindestens 8 Zeichen/)
|
|
assert.equal(newPasswordProblem('alt-alt-alt', '😀'.repeat(8)), '')
|
|
assert.equal(newPasswordProblem('alt-alt-alt', 'x'.repeat(256)), '')
|
|
assert.match(newPasswordProblem('alt-alt-alt', 'x'.repeat(257)), /zu lang/)
|
|
assert.equal(newPasswordProblem('alt-alt-alt', 'ü'.repeat(128)), '')
|
|
assert.match(newPasswordProblem('alt-alt-alt', 'ü'.repeat(129)), /zu lang/)
|
|
})
|
|
|
|
test('Neues Passwort: nicht leer, nicht das alte, nicht die E-Mail', () => {
|
|
assert.match(newPasswordProblem('alt-alt-alt', ' '.repeat(10)), /Leerzeichen/)
|
|
assert.match(newPasswordProblem('Altes-Passwort', 'Altes-Passwort'), /unterscheiden/)
|
|
assert.match(newPasswordProblem('alt-alt-alt', ' Kunde@Beispiel.de ', 'kunde@beispiel.de'), /E-Mail-Adresse/)
|
|
assert.equal(newPasswordProblem('alt-alt-alt', 'kunde@beispiel.de!', 'kunde@beispiel.de'), '')
|
|
assert.equal(newPasswordProblem('alt-alt-alt', 'Ein langer Satz, den nur ich kenne'), '')
|
|
})
|
|
|
|
test('Versuche: 5 je Stunde und Kunde, danach Sperre bis der erste eine Stunde alt ist', () => {
|
|
const t0 = Date.parse('2026-09-27T10:00:00Z')
|
|
const min = 60 * 1000
|
|
assert.deepEqual(attemptState('k1', t0), { left: 5, blockedFor: 0 })
|
|
for (let i = 0; i < 4; i++) recordAttempt('k1', t0 + i * min)
|
|
assert.deepEqual(attemptState('k1', t0 + 4 * min), { left: 1, blockedFor: 0 })
|
|
recordAttempt('k1', t0 + 4 * min)
|
|
assert.deepEqual(attemptState('k1', t0 + 5 * min), { left: 0, blockedFor: 55 * 60 })
|
|
// Nach einer Stunde ist der erste Versuch verfallen: wieder einer frei
|
|
assert.deepEqual(attemptState('k1', t0 + 60 * min), { left: 1, blockedFor: 0 })
|
|
assert.deepEqual(attemptState('k1', t0 + 65 * min), { left: 5, blockedFor: 0 })
|
|
// Andere Kunden sind nicht betroffen
|
|
assert.deepEqual(attemptState('k2', t0 + 5 * min), { left: 5, blockedFor: 0 })
|
|
})
|
|
|
|
test('Sessions: nach Passwortaenderung ungueltig, alte ohne Stand und Admins bleiben', async () => {
|
|
rememberPasswordVersion('u1', '2026-09-27T10:00:00.000+00:00')
|
|
assert.equal(await isSessionPasswordCurrent({ appwriteUserId: 'u1', customerId: 'c1', pwv: '2026-09-27T10:00:00.000+00:00' }), true)
|
|
assert.equal(await isSessionPasswordCurrent({ appwriteUserId: 'u1', customerId: 'c1', pwv: '2026-09-01T08:00:00.000+00:00' }), false)
|
|
// Session von vor dieser Pruefung (ohne pwv) laeuft normal aus
|
|
assert.equal(await isSessionPasswordCurrent({ appwriteUserId: 'u1', customerId: 'c1' }), true)
|
|
// "Als Kunde ansehen": appwriteUserId ist die des Admins
|
|
assert.equal(await isSessionPasswordCurrent({ role: 'admin', viewAs: true, appwriteUserId: 'u1', customerId: 'c1', pwv: 'x' }), true)
|
|
// Konto ohne Passwort (geleert): keine Session gilt mehr
|
|
rememberPasswordVersion('u2', '')
|
|
assert.equal(await isSessionPasswordCurrent({ appwriteUserId: 'u2', customerId: 'c2', pwv: '2026-09-27T10:00:00.000+00:00' }), false)
|
|
})
|
|
|
|
test('Sessions: ist Appwrite nicht erreichbar, bleibt man angemeldet', async () => {
|
|
forgetPasswordVersion('u3')
|
|
assert.equal(await isSessionPasswordCurrent({ appwriteUserId: 'u3', customerId: 'c3', pwv: 'irgendwas' }), true)
|
|
})
|