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 */} -
- @@ -494,12 +533,8 @@ export function TeeCupAuth() { placeholder="F.EKS. RYDER-25" value={code} onChange={(e) => setCode(e.target.value.toUpperCase())} - aria-describedby="invite-hint" className={cn(inputBase, "border-clubhouse-border font-mono tracking-[0.15em] uppercase")} /> -

- Prøv TEECUP for en demo. -

Be om en ny lenke