'use client' import { useState, useRef, useEffect, Suspense } from 'react' import { useRouter, useSearchParams } from 'next/navigation' import { createClient } from '@/lib/supabase/client' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { useToast } from '@/components/ui/use-toast' import { Loader2, ShieldCheck, Copy, Check, ArrowLeft } from 'lucide-react' import { getBranding } from '@/lib/branding/service' import { userHasPassword } from '@/lib/auth/has-password' import { safeReturnTo } from '@/lib/auth/safe-return-to' import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message' export default function MfaEnrollPage() { return ( ) } function MfaEnrollContent() { const [qrCode, setQrCode] = useState(null) const [secret, setSecret] = useState(null) const [factorId, setFactorId] = useState(null) const [code, setCode] = useState('') const [isEnrolling, setIsEnrolling] = useState(false) const [isVerifying, setIsVerifying] = useState(false) const [copied, setCopied] = useState(false) const inputRef = useRef(null) const { toast } = useToast() const router = useRouter() const searchParams = useSearchParams() const supabase = createClient() const returnTo = safeReturnTo(searchParams.get('returnTo'), '/') // UX defense: middleware already blocks this route for BankID-only users // without a password, but a stale tab might land here too. Bounce them to // the set-password flow before they enroll a factor they cannot later // un-enroll without AAL2. useEffect(() => { let cancelled = false ;(async () => { const { data: { user }, } = await supabase.auth.getUser() if (cancelled || !user) return if (!userHasPassword(user)) { router.replace( `/account/set-password?returnTo=${encodeURIComponent( `/mfa/enroll?returnTo=${encodeURIComponent(returnTo)}`, )}`, ) } })() return () => { cancelled = true } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const handleEnroll = async () => { setIsEnrolling(true) try { // Clean up any stale unverified factors from previous abandoned attempts const { data: existingFactors } = await supabase.auth.mfa.listFactors() if (existingFactors?.totp) { for (const factor of existingFactors.totp) { if (factor.status !== 'verified') { await supabase.auth.mfa.unenroll({ factorId: factor.id }) } } } const { data, error } = await supabase.auth.mfa.enroll({ factorType: 'totp', friendlyName: getBranding().appName.toLowerCase(), }) if (error) { toast({ title: 'Kunde inte aktivera 2FA', description: getUserErrorMessage(error), variant: 'destructive', }) setIsEnrolling(false) return } setQrCode(data.totp.qr_code) setSecret(data.totp.secret) setFactorId(data.id) // Focus the code input after render setTimeout(() => inputRef.current?.focus(), 100) } catch { toast({ title: 'Kunde inte aktivera 2FA', description: 'Ett oväntat fel uppstod.', variant: 'destructive', }) } finally { setIsEnrolling(false) } } const handleVerify = async (e: React.FormEvent) => { e.preventDefault() if (!factorId || code.length !== 6) return setIsVerifying(true) try { const { data: challenge, error: challengeError } = await supabase.auth.mfa.challenge({ factorId, }) if (challengeError) { toast({ title: 'Verifiering misslyckades', description: 'Kunde inte starta verifiering. Försök igen.', variant: 'destructive', }) setIsVerifying(false) return } const { error: verifyError } = await supabase.auth.mfa.verify({ factorId, challengeId: challenge.id, code, }) if (verifyError) { toast({ title: 'Fel kod', description: 'Kontrollera att koden stämmer och försök igen.', variant: 'destructive', }) setCode('') inputRef.current?.focus() setIsVerifying(false) return } toast({ title: 'Tvåfaktorsautentisering aktiverad', description: 'Ditt konto är nu skyddat med 2FA.', }) router.push(returnTo) router.refresh() } catch { toast({ title: 'Verifiering misslyckades', description: 'Ett oväntat fel uppstod. Försök igen.', variant: 'destructive', }) } finally { setIsVerifying(false) } } const copySecret = async () => { if (!secret) return await navigator.clipboard.writeText(secret) setCopied(true) setTimeout(() => setCopied(false), 2000) } // Step 1: Show enroll button if (!qrCode) { return (

Aktivera tvåfaktorsautentisering

Skydda ditt konto med en autentiseringsapp som Google Authenticator eller Authy

Du behöver en autentiseringsapp på din telefon. Appen genererar en tidsbegränsad kod som du anger vid varje inloggning.

) } // Step 2: Show QR code and verification return (

Skanna QR-koden

Öppna din autentiseringsapp och skanna koden nedan

{/* QR Code */}
{/* Manual secret */}

Kan du inte skanna? Ange denna nyckel manuellt:

{secret}
{/* Verification code */}
setCode(e.target.value.replace(/\D/g, '').slice(0, 6))} required disabled={isVerifying} className="h-11 text-center text-lg tracking-[0.5em] font-mono" />
) }