Login mit E-Mail-Code (Appwrite-2FA)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-09-30 11:07:00 +00:00
1 parent 21325c4037
commit b8e4de97c7
5 files changed
+253 -7

No files matched your search

+59 -2
View File
@@ -8,9 +8,12 @@ import {
Query,
hasAppwriteSession,
projectId as PROJECT_ID,
client,
} from '../lib/appwrite'
import { createMfaApi, isMoreFactorsError, maskEmail, mfaErrorMessage } from '../lib/mfa'
const AuthContext = createContext()
const mfa = createMfaApi(client)
function clearStaleAppwriteSessions() {
if (typeof window === 'undefined' || !window.localStorage) return
@@ -89,6 +92,9 @@ export function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const [employee, setEmployee] = useState(null)
const [loading, setLoading] = useState(true)
// 2FA: Passwort stimmt, Sitzung braucht noch den Code aus der E-Mail.
// challengeId null = noch keine Mail verschickt (z. B. nach dem Neuladen)
const [mfaPending, setMfaPending] = useState(null)
useEffect(() => {
checkUser()
@@ -106,6 +112,12 @@ export function AuthProvider({ children }) {
setUser(session)
setEmployee(await ensureEmployeeExists(session))
} catch (error) {
if (isMoreFactorsError(error)) {
// Halbe Sitzung: Code-Schritt zeigen, aber keine Mail von selbst (jedes Neuladen wuerde sonst eine ausloesen)
setMfaPending({ challengeId: null, email: '' })
setUser(null)
return
}
if (error.code !== 401 && error.code !== 404) {
console.error('Unexpected error checking user:', error)
}
@@ -126,7 +138,15 @@ export function AuthProvider({ children }) {
await createSessionWithCleanup(normalizedEmail, normalizedPassword)
const session = await account.get()
let session
try {
session = await account.get()
} catch (error) {
if (!isMoreFactorsError(error)) throw error
setMfaPending({ challengeId: null, email: maskEmail(normalizedEmail) })
const sent = await sendCode()
return { success: false, mfaRequired: true, error: sent.error }
}
setUser(session)
setEmployee(await ensureEmployeeExists(session))
@@ -156,6 +176,38 @@ export function AuthProvider({ children }) {
}
}
async function sendCode() {
try {
const challengeId = await mfa.createEmailChallenge()
setMfaPending((pending) => ({ email: '', ...pending, challengeId }))
return { success: true }
} catch (error) {
return { success: false, error: mfaErrorMessage(error) }
}
}
async function verifyCode(otp) {
if (!mfaPending?.challengeId) return { success: false, error: 'Bitte zuerst einen Code anfordern.' }
try {
await mfa.completeChallenge(mfaPending.challengeId, String(otp || '').trim())
const session = await account.get()
setUser(session)
setEmployee(await ensureEmployeeExists(session))
setMfaPending(null)
return { success: true }
} catch (error) {
return { success: false, error: mfaErrorMessage(error) }
}
}
async function cancelMfa() {
try {
await account.deleteSession('current')
} catch { /* Sitzung ggf. schon weg */ }
clearAllLocalAppwriteSessions()
setMfaPending(null)
}
async function logout() {
try {
await account.deleteSessions()
@@ -165,6 +217,7 @@ export function AuthProvider({ children }) {
clearAllLocalAppwriteSessions()
setUser(null)
setEmployee(null)
setMfaPending(null)
}
}
@@ -176,7 +229,11 @@ export function AuthProvider({ children }) {
loading,
login,
logout,
isAdmin
isAdmin,
mfaPending,
sendCode,
verifyCode,
cancelMfa,
}
return (