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>
117 lines
3.4 KiB
JavaScript
117 lines
3.4 KiB
JavaScript
import { config } from '../config.js'
|
||
|
||
function appwriteHeaders() {
|
||
return {
|
||
'Content-Type': 'application/json',
|
||
'X-Appwrite-Project': config.appwrite.projectId,
|
||
}
|
||
}
|
||
|
||
async function appwriteFetch(path, { method = 'GET', body } = {}) {
|
||
const url = `${config.appwrite.endpoint}${path}`
|
||
const response = await fetch(url, {
|
||
method,
|
||
headers: appwriteHeaders(),
|
||
body: body ? JSON.stringify(body) : undefined,
|
||
})
|
||
|
||
let data = null
|
||
const text = await response.text()
|
||
if (text) {
|
||
try {
|
||
data = JSON.parse(text)
|
||
} catch {
|
||
data = { message: text }
|
||
}
|
||
}
|
||
|
||
if (!response.ok) {
|
||
const error = new Error(data?.message || `Appwrite ${response.status}`)
|
||
error.status = response.status
|
||
error.code = data?.code
|
||
error.type = data?.type
|
||
if (response.status === 429 || data?.type === 'general_rate_limit_exceeded') {
|
||
error.message =
|
||
'Zu viele Anmeldeversuche. Bitte warte einige Minuten, bevor du es erneut versuchst.'
|
||
error.status = 429
|
||
}
|
||
throw error
|
||
}
|
||
|
||
return data
|
||
}
|
||
|
||
/**
|
||
* Login via Appwrite Auth REST. userId kommt aus der Session – kein users.read nötig.
|
||
*/
|
||
let appwriteLoginBlockedUntil = 0
|
||
const APPWRITE_LOGIN_COOLDOWN_MS = 5 * 60 * 1000
|
||
const APPWRITE_RATE_LIMIT_COOLDOWN_MS = 15 * 60 * 1000
|
||
|
||
export function getLoginCooldownRemainingSec() {
|
||
const left = appwriteLoginBlockedUntil - Date.now()
|
||
return left > 0 ? Math.ceil(left / 1000) : 0
|
||
}
|
||
|
||
export function clearLoginCooldown() {
|
||
appwriteLoginBlockedUntil = 0
|
||
}
|
||
|
||
export async function loginWithAppwrite(email, password) {
|
||
const now = Date.now()
|
||
if (now < appwriteLoginBlockedUntil) {
|
||
const waitSec = Math.ceil((appwriteLoginBlockedUntil - now) / 1000)
|
||
const error = new Error(
|
||
`Zu viele Anmeldeversuche. Bitte warte noch ${waitSec} Sekunden.`
|
||
)
|
||
error.status = 429
|
||
throw error
|
||
}
|
||
|
||
let session
|
||
try {
|
||
session = await appwriteFetch('/account/sessions/email', {
|
||
method: 'POST',
|
||
body: { email, password },
|
||
})
|
||
} catch (err) {
|
||
if (err.status === 429) {
|
||
appwriteLoginBlockedUntil = Date.now() + APPWRITE_RATE_LIMIT_COOLDOWN_MS
|
||
}
|
||
const error = new Error(err.message || 'Anmeldung fehlgeschlagen')
|
||
error.status = err.status || 401
|
||
throw error
|
||
}
|
||
|
||
if (!session?.userId) {
|
||
const error = new Error('Appwrite-Session ohne userId.')
|
||
error.status = 500
|
||
throw error
|
||
}
|
||
|
||
const user = { $id: session.userId, email, name: '' }
|
||
|
||
clearLoginCooldown()
|
||
return user
|
||
}
|
||
|
||
/**
|
||
* Prueft ein Passwort ueber eine Appwrite-Session (Passwort aendern im Kundenbereich). Anders als
|
||
* loginWithAppwrite ohne die globale Login-Sperre: tippt ein Kunde sein aktuelles Passwort falsch,
|
||
* darf das die Anmeldung der anderen nicht blockieren. Die Session loescht der Aufrufer wieder.
|
||
* @returns {Promise<{ userId: string, sessionId: string } | null>} null = Passwort falsch
|
||
*/
|
||
export async function verifyAppwritePassword(email, password) {
|
||
try {
|
||
const session = await appwriteFetch('/account/sessions/email', {
|
||
method: 'POST',
|
||
body: { email, password },
|
||
})
|
||
return { userId: session?.userId || '', sessionId: session?.$id || '' }
|
||
} catch (err) {
|
||
// 400: kuerzer als 8 Zeichen (kann nie stimmen), 401: falsche Zugangsdaten; gesperrt u. a. weiterreichen
|
||
if (err.status === 400 || (err.status === 401 && err.type === 'user_invalid_credentials')) return null
|
||
throw err
|
||
}
|
||
}
|