summaryrefslogtreecommitdiff
path: root/apps/web/app/reader/_components/mfa-challenge.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/app/reader/_components/mfa-challenge.tsx')
-rw-r--r--apps/web/app/reader/_components/mfa-challenge.tsx108
1 files changed, 108 insertions, 0 deletions
diff --git a/apps/web/app/reader/_components/mfa-challenge.tsx b/apps/web/app/reader/_components/mfa-challenge.tsx
new file mode 100644
index 0000000..347d8b4
--- /dev/null
+++ b/apps/web/app/reader/_components/mfa-challenge.tsx
@@ -0,0 +1,108 @@
+"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()
+ }, [])
+
+ 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>
+ )
+}