summaryrefslogtreecommitdiff
path: root/apps/web/app/reader/_components/mfa-challenge.tsx
blob: b7f86a998a78ee9d0f449f4de8a2f311bb8d5204 (plain) (blame)
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>
  )
}