"use client" import type React from "react" import { useEffect, useRef, useState } from "react" import { useRouter } from "next/navigation" import { Mail, ArrowLeft, CheckCircle2, KeyRound, Lock } from "lucide-react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { TwoFactorSetupForm, TwoFactorVerifyForm } from "@/components/two-factor-flow" const RESEND_COOLDOWN = 30 // seconds 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" } function isValidEmail(value: string) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim()) } // ADR-021 Beslutning E: primær-autentisering (magic-link ELLER passord) kan // returnere tre ulike utfall -- delt mellom denne komponenten og // verify-form.tsx sin håndtering av magic-link-svaret. type PostAuthMode = "2fa-verify" | "2fa-setup" | null export function LoginForm() { const router = useRouter() const [mode, setMode] = useState<"email" | "code" | "password">("email") const [postAuth, setPostAuth] = useState(null) const [twoFactorMethod, setTwoFactorMethod] = useState<"totp" | "email" | null>(null) const [email, setEmail] = useState("") const [touched, setTouched] = useState(false) const [sent, setSent] = useState(false) const [sending, setSending] = useState(false) const [cooldown, setCooldown] = useState(0) const [error, setError] = useState(null) function handleLoginResult(result: LoginResult) { if (result.status === "success") { router.replace("/dashboard") } else if (result.status === "2fa_required") { setTwoFactorMethod(result.two_factor_method ?? "totp") setPostAuth("2fa-verify") } else { setPostAuth("2fa-setup") } } function handleTwoFactorSuccess() { router.replace("/dashboard") } const emailValid = isValidEmail(email) const showError = touched && email.length > 0 && !emailValid // Countdown timer for the resend cooldown. useEffect(() => { if (cooldown <= 0) return const id = setInterval(() => { setCooldown((c) => (c <= 1 ? 0 : c - 1)) }, 1000) return () => clearInterval(id) }, [cooldown]) async function sendLink() { setSending(true) setError(null) try { // Alltid samme suksess-respons uansett om e-posten finnes (anti- // enumerering, se ADR-009) -- kun nettverks-/serverfeil havner i catch. const res = await fetch("/auth/request-link", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ email: email.trim(), locale: "nb" }), }) if (!res.ok) throw new Error(`request-link: ${res.status}`) setSent(true) setCooldown(RESEND_COOLDOWN) } catch { setError("Klarte ikke å sende lenken. Sjekk tilkoblingen og prøv igjen.") } finally { setSending(false) } } function handleSubmit(e: React.FormEvent) { e.preventDefault() setTouched(true) if (!emailValid || sending) return void sendLink() } function handleResend() { if (cooldown > 0 || sending) return void sendLink() } function handleReset() { setSent(false) setTouched(false) } return (
{postAuth === "2fa-verify" ? ( setPostAuth(null)} /> ) : postAuth === "2fa-setup" ? ( ) : sent ? ( ) : mode === "code" ? ( setMode("email")} /> ) : mode === "password" ? ( setMode("email")} /> ) : (
{showError && (

Skriv inn en gyldig e-postadresse.

)}
{error && (

{error}

)}

Ingen passord nødvendig. Vi sender deg en sikker lenke på e-post.

)}
) } // --- Passord-innlogging (ADR-021 Beslutning A) -- et sidestilt, valgfritt // alternativ til magic-link, ALDRI en erstatning. ----------------------- function PasswordLoginForm({ onResult, onBack, }: { onResult: (result: LoginResult) => void onBack: () => void }) { const [email, setEmail] = useState("") const [password, setPassword] = useState("") const [submitting, setSubmitting] = useState(false) const [error, setError] = useState(null) async function handleSubmit(e: React.FormEvent) { e.preventDefault() if (!email.trim() || !password || submitting) return setSubmitting(true) setError(null) try { const res = await fetch("/auth/login-password", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ email: email.trim(), password }), }) if (!res.ok) { const body = await res.json().catch(() => null) throw new Error(body?.detail?.message ?? "E-post eller passord er feil.") } onResult(await res.json()) } catch (err) { setError(err instanceof Error ? err.message : "Noe gikk galt. Prøv igjen.") } finally { setSubmitting(false) } } return (
{error && (

{error}

)}
) } // --- Kode-innlogging (ADR-020): tar deg rett til en turnering via en kort, // menneske-skrivbar kode -- FØR innlogging, og uansett turneringens // synlighet (koden ER selve invitasjonen, se ADR-020 Beslutning A). --------- function JoinByCode({ onBack }: { onBack: () => void }) { const router = useRouter() const [code, setCode] = useState("") const [loading, setLoading] = useState(false) const [error, setError] = useState(null) async function handleSubmit(e: React.FormEvent) { e.preventDefault() const trimmed = code.trim() if (!trimmed || loading) return setLoading(true) setError(null) try { const res = await fetch(`/public/tournaments/by-code/${encodeURIComponent(trimmed)}`) if (!res.ok) { const body = await res.json().catch(() => null) setError(body?.detail?.message ?? "Fant ingen turnering med denne koden.") return } const data: { tournament_id: string } = await res.json() router.push(`/t/${data.tournament_id}?code=${encodeURIComponent(trimmed)}`) } catch { setError("Klarte ikke å slå opp koden. Sjekk tilkoblingen og prøv igjen.") } finally { setLoading(false) } } return (
{error && (

{error}

)}

Fått en kode muntlig eller på en lapp? Skriv den inn her — du trenger ikke logge inn for å se turneringen eller melde deg på.

) } function ConfirmationState({ email, cooldown, sending, error, onResend, onReset, }: { email: string cooldown: number sending: boolean error: string | null onResend: () => void onReset: () => void }) { const liveRef = useRef(null) useEffect(() => { liveRef.current?.focus() }, []) return (

Sjekk innboksen din

Vi har sendt en innloggingslenke til{" "} {email}. Åpne den på denne enheten for å logge inn.

{error && (

{error}

)}
{cooldown > 0 ? (

Fikk du ingen e-post? Send på nytt om{" "} {cooldown}s

) : ( )}
) }