1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
|
"use client"
import { useState, useEffect } from "react"
import { createSupabaseBrowserClient } from "@/lib/supabase/client"
export function MfaChallenge({ onVerified }: { onVerified: () => void }) {
const [verificationCode, setVerificationCode] = useState("")
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const [isVerifying, setIsVerifying] = useState(false)
const [factorIdentifier, setFactorIdentifier] = useState<string | null>(null)
const supabaseClient = createSupabaseBrowserClient()
useEffect(() => {
async function loadFactor() {
const { data } = await supabaseClient.auth.mfa.listFactors()
if (data?.totp && data.totp.length > 0) {
const verifiedFactor = data.totp.find(
(factor) => factor.status === "verified"
)
if (verifiedFactor) {
setFactorIdentifier(verifiedFactor.id)
}
}
}
loadFactor()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
async function handleVerify(event?: React.FormEvent) {
event?.preventDefault()
if (!factorIdentifier || verificationCode.length !== 6) return
setIsVerifying(true)
setErrorMessage(null)
const { data: challengeData, error: challengeError } =
await supabaseClient.auth.mfa.challenge({
factorId: factorIdentifier,
})
if (challengeError) {
setIsVerifying(false)
setErrorMessage("failed to create challenge — please try again")
return
}
const { error: verifyError } = await supabaseClient.auth.mfa.verify({
factorId: factorIdentifier,
challengeId: challengeData.id,
code: verificationCode,
})
setIsVerifying(false)
if (verifyError) {
setErrorMessage("invalid code — please try again")
setVerificationCode("")
return
}
onVerified()
}
return (
<div className="flex h-screen items-center justify-center bg-background-primary">
<div className="w-full max-w-sm space-y-6 px-4">
<div className="space-y-2">
<h1 className="text-lg text-text-primary">two-factor authentication</h1>
<p className="text-text-secondary">
enter the 6-digit code from your authenticator app
</p>
</div>
<form onSubmit={handleVerify} className="space-y-4">
<input
type="text"
inputMode="numeric"
pattern="[0-9]*"
maxLength={6}
value={verificationCode}
onChange={(event) => {
const filtered = event.target.value.replace(/\D/g, "")
setVerificationCode(filtered)
}}
placeholder="000000"
className="w-full border border-border bg-background-secondary px-3 py-3 text-center font-mono text-2xl tracking-[0.5em] text-text-primary outline-none placeholder:text-text-dim focus:border-text-dim"
autoFocus
/>
{errorMessage && (
<p className="text-status-error">{errorMessage}</p>
)}
<button
type="submit"
disabled={isVerifying || verificationCode.length !== 6 || !factorIdentifier}
className="w-full border border-border bg-background-tertiary px-4 py-2 text-text-primary transition-colors hover:bg-border disabled:opacity-50"
>
{isVerifying ? "verifying ..." : "verify"}
</button>
</form>
</div>
</div>
)
}
|