diff options
Diffstat (limited to 'apps/web/app/reader/_components/mfa-challenge.tsx')
| -rw-r--r-- | apps/web/app/reader/_components/mfa-challenge.tsx | 108 |
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> + ) +} |