From b0c65d983b6d6596475857a5e78ee6f6d9eae4e5 Mon Sep 17 00:00:00 2001
From: Erol Haagenrud
Date: Sat, 8 Aug 2026 12:34:50 +0200
Subject: [PATCH] =?UTF-8?q?Gj=C3=B8r=20/logg-inn=20til=20den=20ekte=20innl?=
=?UTF-8?q?oggingssiden?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
/logg-inn (clubhouse-palett, TeeCupAuth) var en foreldreløs V0-
utforskning -- ingenting i appen lenket eller redirectet dit, og
TeeCupAuth var 100% mock (sleep()-baserte stubber, hardkodet
demo-passord, falske invitasjonskoder, en falsk "2FA-kode sendes"-tekst
som aldri sendte noe). Flagget eksplisitt til bruker før arbeid startet,
siden dette er sikkerhetskritisk kode og omfanget var langt større enn
en ren redirect-ombytting.
Full port: ekte POST /auth/request-link, POST /auth/login-password
(identisk LoginResult-statushåndtering som den gamle LoginForm), GET
/public/tournaments/by-code/{code}. TwoFactorVerifyForm/
TwoFactorSetupForm gjenbrukt UENDRET (lavest mulig risiko for
sikkerhetskritisk 2FA-kode). app/page.tsx redusert til en tynn
videresending (autentisert -> /dashboard//account, uautentisert ->
/logg-inn), beholdt for gamle bokmerker. Gammel LoginForm slettet.
Alle ~11 uautentisert-redirect-steder (committet i forrige commit)
pekte allerede til /logg-inn.
Én reell bug funnet og rettet under scratch-verifisering:
app/logg-inn/page.tsx kalte redirect() INNI en try/catch som fanget
Next.js sin egen NEXT_REDIRECT-kontrollflyt-exception stille -- en
allerede innlogget bruker fikk se innloggingsskjemaet på nytt i stedet
for å bli sendt videre. Rettet ved å flytte redirect()-kallet utenfor
try/catch, samme mønster som den opprinnelige /-siden alltid brukte.
Se CHANGELOG.md punkt 42 for full verifiseringsdetalj.
---
frontend/app/logg-inn/page.tsx | 45 +-
frontend/app/page.tsx | 35 +-
frontend/components/login-form.tsx | 675 ---------------------
frontend/components/teecup/teecup-auth.tsx | 203 ++++---
frontend/components/two-factor-flow.tsx | 7 +-
frontend/components/verify-form.tsx | 2 +-
6 files changed, 179 insertions(+), 788 deletions(-)
delete mode 100644 frontend/components/login-form.tsx
diff --git a/frontend/app/logg-inn/page.tsx b/frontend/app/logg-inn/page.tsx
index d970a6f..a230387 100644
--- a/frontend/app/logg-inn/page.tsx
+++ b/frontend/app/logg-inn/page.tsx
@@ -1,4 +1,6 @@
import type { Metadata } from "next"
+import { cookies } from "next/headers"
+import { redirect } from "next/navigation"
import { TeeCupWordmark } from "@/components/teecup/wordmark"
import { TeeCupAuth } from "@/components/teecup/teecup-auth"
@@ -7,7 +9,48 @@ export const metadata: Metadata = {
description: "Logg inn på TeeCup for live-scoring og golfturneringer med vennegjengen, klubben eller bedriften.",
}
-export default function LoggInnPage() {
+// Server-side, IKKE nettleser-fetch -- går derfor IKKE gjennom
+// next.config.mjs sin rewrites() (samme mønster som generateMetadata i
+// app/t/[id]/page.tsx og den forrige app/page.tsx). Peker direkte på API-et.
+const API_ORIGIN = process.env.TEECUP_API_ORIGIN || "http://localhost:8000"
+// Må matche SESSION_COOKIE_NAME i app/auth.py -- ingen delt konstant på
+// tvers av Python/TypeScript, samme mønster som andre API-kontrakt-felt
+// som dupliseres bevisst i frontend-koden.
+const SESSION_COOKIE_NAME = "teecup_session"
+
+export default async function LoggInnPage() {
+ // Samme allerede-innlogget-sjekk som /-siden hadde (2026-08-08: /logg-inn
+ // overtok rollen som ekte innloggingsside, se CHANGELOG.md) -- en bruker
+ // med gyldig sesjonscookie skal aldri se innloggingsskjemaet på nytt.
+ const cookieStore = await cookies()
+ const session = cookieStore.get(SESSION_COOKIE_NAME)
+ let authenticated = false
+ let profileComplete = true
+ if (session) {
+ try {
+ const res = await fetch(`${API_ORIGIN}/auth/me`, {
+ headers: { Cookie: `${SESSION_COOKIE_NAME}=${session.value}` },
+ cache: "no-store",
+ })
+ authenticated = res.ok
+ if (res.ok) {
+ const data = await res.json()
+ profileComplete = Boolean(data.profile_complete)
+ }
+ } catch {
+ // API utilgjengelig -- vis innloggingsskjemaet i stedet for å henge.
+ authenticated = false
+ }
+ }
+ // redirect() kaster en egen NEXT_REDIRECT-kontrollflyt-exception som MÅ
+ // boble videre til Next.js -- må derfor stå UTENFOR try/catch-en over
+ // (en catch der ville slukt den stille og latt innloggingsskjemaet vises
+ // i stedet for å omdirigere, reell bug funnet under scratch-verifisering
+ // 2026-08-08, se CHANGELOG.md).
+ if (authenticated) {
+ redirect(profileComplete ? "/dashboard" : "/account")
+ }
+
return (
{/* Quiet turf-horizon backdrop — the one bit of atmosphere, kept subtle. */}
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx
index 02219fe..282196a 100644
--- a/frontend/app/page.tsx
+++ b/frontend/app/page.tsx
@@ -1,6 +1,5 @@
import { cookies } from "next/headers"
import { redirect } from "next/navigation"
-import { LoginForm } from "@/components/login-form"
// Server-side, IKKE nettleser-fetch -- går derfor IKKE gjennom
// next.config.mjs sin rewrites() (samme mønster som generateMetadata i
@@ -38,27 +37,15 @@ export default async function Page() {
authenticated = false
}
}
- if (authenticated) {
- // Obligatorisk profil-fullføring (2026-07-22): en innlogget bruker som
- // ennå ikke har fylt ut de obligatoriske profilfeltene sendes til
- // /account (som selv viser en fokusert fullførings-visning) i stedet
- // for dashbordet -- se AccountSettings sin profile_complete-gren.
- redirect(profileComplete ? "/dashboard" : "/account")
- }
-
- return (
-
-
- {/* Redesignet kort (2026-08-01, V0/Forest Green) har sin egen
- innebygde header (logo + tittel/undertekst per tilstand) --
- den forrige separate Wordmark+tagline-headeren over kortet er
- derfor fjernet for å unngå dobbel "TeeCup"-overskrift. */}
-
-
-
- Ved å logge inn godtar du våre vilkår og personvernregler.
-
-
-
- )
+ // Obligatorisk profil-fullføring (2026-07-22): en innlogget bruker som
+ // ennå ikke har fylt ut de obligatoriske profilfeltene sendes til
+ // /account (som selv viser en fokusert fullførings-visning) i stedet
+ // for dashbordet -- se AccountSettings sin profile_complete-gren.
+ //
+ // /logg-inn overtok rollen som ekte innloggingsside 2026-08-08 (se
+ // CHANGELOG.md) -- denne siden (root) er nå bare en tynn videresending,
+ // beholdt for gamle bokmerker/lenker til teecup.golf/. Selve
+ // innloggingsskjemaet (LoginForm, Forest Green) er fjernet -- erstattet
+ // av TeeCupAuth (clubhouse-palett) på /logg-inn.
+ redirect(authenticated ? (profileComplete ? "/dashboard" : "/account") : "/logg-inn")
}
diff --git a/frontend/components/login-form.tsx b/frontend/components/login-form.tsx
deleted file mode 100644
index a5a0b70..0000000
--- a/frontend/components/login-form.tsx
+++ /dev/null
@@ -1,675 +0,0 @@
-"use client"
-
-import type React from "react"
-import { useEffect, useRef, useState } from "react"
-import { useRouter } from "next/navigation"
-import {
- Mail,
- Lock,
- KeyRound,
- ArrowLeft,
- CheckCircle2,
- Eye,
- EyeOff,
- Flag,
-} from "lucide-react"
-import { TwoFactorSetupForm, TwoFactorVerifyForm } from "@/components/two-factor-flow"
-
-// Locked "Forest Green" palette (V0-redesign 2026-08-01, basert på Stitch --
-// se DESIGN_SYSTEM.md sitt notat om denne runden). Anvendt inline med vilje,
-// slik at ingenting tolkes om ved en senere V0-reeksport.
-const C = {
- card: "rgba(255, 255, 255, 0.72)",
- ink: "#012c11", // primærtekst + primærknapp-bakgrunn
- inkOn: "#ffffff",
- accent: "#1a4325", // mørkegrønne aksenter (header-gradient)
- accentMuted: "#bfeec4", // dempet tekst PÅ mørkegrønn -- ALDRI #84b089/#a4a5a5 her (målt ~4.5:1, under vårt AAA-mål)
- successBg: "#91f78e",
- successInk: "#00731e",
- muted: "#424941", // dempet brødtekst på hvit bakgrunn
- border: "#c1c9bf",
- fieldBg: "#ffffff",
-} as const
-
-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"
-}
-
-// 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
-
-type Mode = "default" | "sent" | "password" | "join" | "verify"
-
-function isValidEmail(value: string) {
- return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim())
-}
-
-export function LoginForm() {
- const router = useRouter()
- const [mode, setMode] = useState("default")
- const [postAuth, setPostAuth] = useState(null)
- const [twoFactorMethod, setTwoFactorMethod] = useState<"totp" | "email" | null>(null)
- const [email, setEmail] = useState("")
- const [emailTouched, setEmailTouched] = useState(false)
- const [sending, setSending] = useState(false)
- const [cooldown, setCooldown] = useState(0)
- const [error, setError] = useState(null)
-
- const emailValid = isValidEmail(email)
- const emailError = emailTouched && email.length > 0 && !emailValid
-
- 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")
- }
-
- // 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}`)
- setMode("sent")
- setCooldown(RESEND_COOLDOWN)
- } catch {
- setError("Klarte ikke å sende lenken. Sjekk tilkoblingen og prøv igjen.")
- } finally {
- setSending(false)
- }
- }
-
- function submitDefault(e: React.FormEvent) {
- e.preventDefault()
- setEmailTouched(true)
- if (!emailValid || sending) return
- void sendLink()
- }
-
- function resend() {
- if (cooldown > 0 || sending) return
- void sendLink()
- }
-
- function backToDefault() {
- setMode("default")
- setEmailTouched(false)
- setError(null)
- }
-
- return (
-
- {/* Soft green glow behind the card so the glass blur has something to work against */}
-
-
-
-
-
-
- {/* 2FA bruker de EKSISTERENDE, sikkerhetskritiske komponentene uendret
- (egen intern overskrift) -- kun logo-raden fra det nye headeret
- vises over dem, ikke tittel/undertekst-blokken, for å unngå
- dobbel overskrift. */}
-
-
-
- )
-}
-
-/* --- Shared shell -------------------------------------------------------- */
-
-const HEADINGS: Record = {
- default: {
- title: "Velkommen til TeeCup",
- subtitle: "Logg inn for å følge turneringen din live.",
- },
- sent: {
- title: "Sjekk innboksen din",
- subtitle: "Vi har sendt deg en sikker innloggingslenke.",
- },
- password: {
- title: "Logg inn med passord",
- subtitle: "Skriv inn e-post og passord for å fortsette.",
- },
- join: {
- title: "Gå rett til turneringen",
- subtitle: "Har du fått en invitasjonskode? Skriv den inn her.",
- },
- verify: {
- title: "Bekreft innloggingen",
- subtitle: "Vi har sendt en kode for å bekrefte at det er deg.",
- },
-}
-
-function CardHeader({ mode, showTitle }: { mode: Mode; showTitle: boolean }) {
- const { title, subtitle } = HEADINGS[mode]
- return (
-