"use client" // Delt mellom teecup-auth.tsx (passord-modus, /logg-inn) og verify-form.tsx // (magic-link) -- begge kan returnere status "2fa_required"/ // "2fa_setup_required" fra backend (ADR-021 Beslutning E), og trenger // identisk oppfølging. import type React from "react" import { useEffect, useState } from "react" import { ArrowLeft, KeyRound, Mail, ShieldCheck } from "lucide-react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" type SessionUser = { id: string email: string display_name: string preferred_locale: string } type LoginResult = { status: "success" | "2fa_required" | "2fa_setup_required" user?: SessionUser two_factor_method?: "totp" | "email" } // --- Verifisering av en PÅGÅENDE innlogging (status "2fa_required") -------- export function TwoFactorVerifyForm({ method, onSuccess, onBack, }: { method: "totp" | "email" onSuccess: (user: SessionUser) => void onBack: () => void }) { const [code, setCode] = useState("") const [submitting, setSubmitting] = useState(false) const [sendingEmail, setSendingEmail] = useState(false) const [emailSent, setEmailSent] = useState(false) const [error, setError] = useState(null) async function requestEmailCode() { setSendingEmail(true) setError(null) try { const res = await fetch("/auth/2fa/email/request", { method: "POST", credentials: "include" }) if (!res.ok) throw new Error("request failed") setEmailSent(true) } catch { setError("Klarte ikke å sende koden. Prøv igjen.") } finally { setSendingEmail(false) } } useEffect(() => { if (method === "email") void requestEmailCode() // eslint-disable-next-line react-hooks/exhaustive-deps }, [method]) async function handleSubmit(e: React.FormEvent) { e.preventDefault() if (!code.trim() || submitting) return setSubmitting(true) setError(null) try { const res = await fetch("/auth/2fa/verify", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ code: code.trim() }), }) if (!res.ok) { const body = await res.json().catch(() => null) throw new Error(body?.detail?.message ?? "Feil eller utløpt kode.") } const result: LoginResult = await res.json() if (result.status === "success" && result.user) { onSuccess(result.user) } else { throw new Error("Uventet svar fra serveren.") } } catch (err) { setError(err instanceof Error ? err.message : "Noe gikk galt. Prøv igjen.") } finally { setSubmitting(false) } } return (

Topartsautentisering

{method === "totp" ? "Skriv inn koden fra autentisator-appen din." : emailSent ? "Vi har sendt en engangskode til e-posten din." : "Sender en engangskode til e-posten din …"}

{method === "totp" ? (
{error && (

{error}

)} {method === "email" && ( )}
) } // --- Oppsett av 2FA (status "2fa_setup_required" ELLER frivillig fra // kontoinnstillinger) ------------------------------------------------- type SetupStartResult = { status: "totp_ready" | "email_sent" secret?: string otpauth_uri?: string qr_code_data_uri?: string } export function TwoFactorSetupForm({ forced, onSuccess, }: { forced: boolean onSuccess: (user: SessionUser) => void }) { const [method, setMethod] = useState<"totp" | "email" | null>(null) const [starting, setStarting] = useState(false) const [setupInfo, setSetupInfo] = useState(null) const [code, setCode] = useState("") const [confirming, setConfirming] = useState(false) const [error, setError] = useState(null) async function chooseMethod(chosen: "totp" | "email") { setMethod(chosen) setStarting(true) setError(null) try { const res = await fetch("/auth/2fa/setup/start", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ method: chosen }), }) if (!res.ok) throw new Error("start failed") setSetupInfo(await res.json()) } catch { setError("Klarte ikke å starte oppsettet. Prøv igjen.") setMethod(null) } finally { setStarting(false) } } async function handleConfirm(e: React.FormEvent) { e.preventDefault() if (!method || !code.trim() || confirming) return setConfirming(true) setError(null) try { const res = await fetch("/auth/2fa/setup/confirm", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ method, code: code.trim(), secret: setupInfo?.secret }), }) if (!res.ok) { const body = await res.json().catch(() => null) throw new Error(body?.detail?.message ?? "Feil kode.") } const user: SessionUser = await res.json() onSuccess(user) } catch (err) { setError(err instanceof Error ? err.message : "Noe gikk galt. Prøv igjen.") } finally { setConfirming(false) } } if (!method) { return (

Sett opp topartsautentisering

{forced ? "Som organisasjonseier/administrator må du sette opp 2FA før du kan fortsette." : "Velg hvilken metode du ønsker."}

{error && (

{error}

)}
) } return (

Bekreft oppsettet

{method === "totp" && setupInfo?.qr_code_data_uri && (
{/* eslint-disable-next-line @next/next/no-img-element -- data-URI, ingen next/image-fordel */} QR-kode for autentisator-app

Skann QR-koden med autentisator-appen din, eller skriv inn denne koden manuelt:{" "} {setupInfo.secret}

)} {method === "email" && (

Vi har sendt en engangskode til e-posten din.

)}
setCode(e.target.value)} className="h-14 rounded-2xl text-base tracking-widest" />
{error && (

{error}

)}
) }