Portal: Mitarbeiter-Login gesperrt (Hinweis aufs Ticketsystem), alte Mitarbeiter-Sitzungen enden
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d0ca3c41cd
commit
3a4fa581c0
13 files changed
+145
-559
No files matched your search
@@ -50,8 +50,8 @@ function readSessionCookie(response) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Appwrite-Aufruf im Namen einer Sitzung aus loginWithAppwrite (z. B. MFA-Challenge). Deutsche Sprache,
|
||||
* damit Appwrite die deutsche Mailvorlage nimmt.
|
||||
* Appwrite-Aufruf im Namen einer Sitzung aus loginWithAppwrite (z. B. Sitzung eines abgelehnten
|
||||
* Mitarbeiter-Logins beenden). Deutsche Sprache, damit Appwrite deutsche Texte liefert.
|
||||
*/
|
||||
export function appwriteSessionFetch(path, { method = 'GET', body, sessionCookie } = {}) {
|
||||
return appwriteFetch(path, {
|
||||
@@ -64,21 +64,6 @@ export function appwriteSessionFetch(path, { method = 'GET', body, sessionCookie
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Braucht die Sitzung noch einen zweiten Faktor (Appwrite-2FA)? Fragt Appwrite selbst statt des
|
||||
* mfa-Flags aus dem Admin-Abruf, damit ein gestoerter Abruf nicht ohne Code durchlaesst. Andere
|
||||
* Fehler werden geworfen (fail closed).
|
||||
*/
|
||||
export async function sessionNeedsSecondFactor(sessionCookie) {
|
||||
try {
|
||||
await appwriteSessionFetch('/account', { sessionCookie })
|
||||
return false
|
||||
} catch (err) {
|
||||
if (err.type === 'user_more_factors_required') return true
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Login via Appwrite Auth REST. userId kommt aus der Session – kein users.read nötig.
|
||||
*/
|
||||
@@ -131,7 +116,7 @@ export async function loginWithAppwrite(email, password) {
|
||||
throw error
|
||||
}
|
||||
|
||||
// sessionCookie: fuer den zweiten Faktor (Mitarbeiter-2FA) arbeitet der Server im Namen dieser Sitzung weiter
|
||||
// sessionCookie: lehnt der Login einen Mitarbeiter ab, beendet der Server damit diese Sitzung wieder
|
||||
const user = { $id: session.userId, email, name: '', sessionCookie }
|
||||
|
||||
clearLoginCooldown()
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import { randomBytes } from 'node:crypto'
|
||||
import { appwriteSessionFetch } from './appwriteClient.js'
|
||||
|
||||
/**
|
||||
* Offene Mitarbeiter-Anmeldungen mit E-Mail-Code (Appwrite-2FA). Das Passwort ist geprueft, die
|
||||
* Appwrite-Sitzung hat aber erst einen Faktor; erst der richtige Code aus der Mail macht daraus eine
|
||||
* Portal-Sitzung. Nur im Speicher: nach einem Neustart meldet man sich neu an.
|
||||
*/
|
||||
export const MFA_COOKIE = 'webklar_portal_mfa'
|
||||
export const MFA_TTL_MS = 10 * 60 * 1000
|
||||
const MAX_ATTEMPTS = 5
|
||||
|
||||
const pending = new Map()
|
||||
|
||||
function purgeExpired() {
|
||||
const now = Date.now()
|
||||
for (const [key, entry] of pending) {
|
||||
if (entry.expiresAt <= now) pending.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
function liveEntry(key) {
|
||||
purgeExpired()
|
||||
return (key && pending.get(key)) || null
|
||||
}
|
||||
|
||||
async function createChallenge(sessionCookie) {
|
||||
const challenge = await appwriteSessionFetch('/account/mfa/challenges', {
|
||||
method: 'POST',
|
||||
body: { factor: 'email' },
|
||||
sessionCookie,
|
||||
})
|
||||
return challenge.$id
|
||||
}
|
||||
|
||||
/** kenso@webklar.com -> k…@webklar.com */
|
||||
export function maskEmail(email) {
|
||||
const [local, domain] = String(email || '').split('@')
|
||||
if (!local || !domain) return ''
|
||||
return `${local[0]}…@${domain}`
|
||||
}
|
||||
|
||||
export async function startStaffMfa({ userId, email, sessionCookie }) {
|
||||
const challengeId = await createChallenge(sessionCookie)
|
||||
purgeExpired()
|
||||
const key = randomBytes(32).toString('hex')
|
||||
pending.set(key, { userId, email, sessionCookie, challengeId, expiresAt: Date.now() + MFA_TTL_MS, attempts: 0 })
|
||||
return { key, maskedEmail: maskEmail(email) }
|
||||
}
|
||||
|
||||
/** @returns {Promise<{ok: true, userId: string, email: string} | {ok: false, reason: 'invalid' | 'gone'}>} */
|
||||
export async function verifyStaffMfa(key, otp) {
|
||||
const entry = liveEntry(key)
|
||||
if (!entry || entry.attempts >= MAX_ATTEMPTS) return { ok: false, reason: 'gone' }
|
||||
// Versuch vor dem Appwrite-Aufruf reservieren, sonst zaehlen parallele Anfragen alle als erster Versuch
|
||||
entry.attempts += 1
|
||||
try {
|
||||
await appwriteSessionFetch('/account/mfa/challenges', {
|
||||
method: 'PUT',
|
||||
body: { challengeId: entry.challengeId, otp: String(otp || '').trim() },
|
||||
sessionCookie: entry.sessionCookie,
|
||||
})
|
||||
} catch (err) {
|
||||
if (err.type !== 'user_invalid_token') {
|
||||
entry.attempts -= 1
|
||||
throw err
|
||||
}
|
||||
if (entry.attempts >= MAX_ATTEMPTS) pending.delete(key)
|
||||
return { ok: false, reason: 'invalid' }
|
||||
}
|
||||
pending.delete(key)
|
||||
return { ok: true, userId: entry.userId, email: entry.email }
|
||||
}
|
||||
|
||||
export async function resendStaffMfa(key) {
|
||||
const entry = liveEntry(key)
|
||||
if (!entry) return false
|
||||
entry.challengeId = await createChallenge(entry.sessionCookie)
|
||||
return true
|
||||
}
|
||||
Reference in new issue
Block a user