From 73f3c4d1ea315a18433fe16b211f65cf0e054e89 Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Sun, 19 Jul 2026 10:05:42 +0200 Subject: [PATCH] =?UTF-8?q?Fikset=20og=20live.=20Rot-=C3=A5rsaken=20var=20?= =?UTF-8?q?alts=C3=A5=20en=20manglende=20sjekk=20i=20frontend,=20ikke=20no?= =?UTF-8?q?e=20galt=20med=20selve=20sesjonen=20eller=20cookien=20din=20?= =?UTF-8?q?=E2=80=94=20verifisert=20direkte=20med=20din=20ekte=20cookie=20?= =?UTF-8?q?mot=20produksjon=20(uten=20cookie=20=E2=86=92=20skjema,=20med?= =?UTF-8?q?=20din=20cookie=20=E2=86=92=20sendt=20rett=20til=20/dashboard).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .md-filene er oppdatert: sesjons-bugen er markert ✅ fikset, og ADR-021 (passord/2FA) er notert som naturlig neste steg i den tråden når du vil sette i gang — jeg venter på ditt klarsignal før jeg begynner å bygge det. --- .claude/settings.local.json | 4 +- ARCHITECTURE_DECISIONS.md | 189 ++++++++++++++++++++++++++++++++++++ CLAUDE.md | 36 +++++++ FEATURE_BACKLOG.md | 30 ++++++ frontend/app/page.tsx | 38 +++++++- 5 files changed, 295 insertions(+), 2 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 76ff1ec..5b463f8 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -277,7 +277,9 @@ "Bash(grep -rl \"POSTGRES_PASSWORD\" /opt/teeoff --include='*.yml' --include='*.env*' 2>/dev/null)", "Bash(grep -n \"^ [a-zA-Z0-9_-]*:$\" /opt/teeoff/docker-compose.prod.yml)", "Bash(set -e)", - "Bash(curl -s -o /dev/null -w '%{http_code}\\\\n' https://teecup.teeoff.no/dashboard)" + "Bash(curl -s -o /dev/null -w '%{http_code}\\\\n' https://teecup.teeoff.no/dashboard)", + "Bash(grep -n \"teecup\" -A 20 /opt/teeoff/deploy/Caddyfile 2>/dev/null | head -60)", + "Bash(curl -s -o /dev/null -w '%{http_code} -> %{redirect_url}\\\\n' -b \"teecup_session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI4NDYzM2JlMS1iZWM1LTQ1ZDItOTI1ZC1lNzAzYTdjOWM1OGYiLCJpYXQiOjE3ODQ0NDc5NzIsImV4cCI6MTc4NzAzOTk3Mn0.wQAHP4qlo-zzHH7JrDWm_FyupW8g-_vMkr96szWmuXg\" https://teecup.teeoff.no/)" ], "additionalDirectories": [ "/opt/teeoff/deploy", diff --git a/ARCHITECTURE_DECISIONS.md b/ARCHITECTURE_DECISIONS.md index 706f84d..72b53f5 100644 --- a/ARCHITECTURE_DECISIONS.md +++ b/ARCHITECTURE_DECISIONS.md @@ -721,6 +721,195 @@ ny fargemodell innført. --- +## ADR-021 — Passord (valgfritt tillegg) + valgfri 2FA (TOTP eller e-post) + +**Kontekst:** Reist av brukeren 2026-07-19, sammen med et konkret sesjons- +problem: dagens magic-link-cookie (ADR-009, 30 dager) fungerte visstnok ikke +som tiltenkt — brukeren måtte be om ny innloggingskode ved HVERT besøk til +`teecup.teeoff.no`. **Diagnostisert og FIKSET samme dag** (ikke en del av +ADR-021s videre omfang, men verdt å nevne her siden det var starten på denne +tråden): kodegjennomgangen av `app/auth.py`/`app/routers/auth.py` fant ingen +feil i selve cookie-settingen, og brukerens ekte, ferske cookie (hentet fra +nettleseren på forespørsel) bekreftet 30 dagers levetid, `Secure`/`HttpOnly`/ +`SameSite=Lax` alt korrekt. Rot-årsaken var derfor IKKE en cookie-/backend- +bug, men en manglende sjekk i frontend: `app/page.tsx` (rot-siden) viste +ALLTID innloggingsskjemaet uten noensinne å sjekke om en gyldig sesjon +allerede fantes — `Dashboard` sjekket `/auth/me` og sendte til `/` ved +MANGLENDE sesjon, men ingenting gjorde det motsatte. Fikset med en +server-side sesjonssjekk (leser cookien via `next/headers`, kaller +`/auth/me` direkte mot `TEECUP_API_ORIGIN` server-til-server, samme mønster +som `generateMetadata` i `app/t/[id]/page.tsx`) som sender en allerede +innlogget bruker rett til `/dashboard`. Verifisert med brukerens ekte cookie: +uten cookie → 200 (skjema), med gyldig cookie → 307 til `/dashboard`. Rullet +ut live, kun `teecup_frontend`, ingen migrasjon, `teeoff.no` upåvirket. + +Utover selve bugen ønsket brukeren eksplisitt: e-post/brukernavn+passord +(må håndtere spesialtegn og mellomrom korrekt) SOM ET TILLEGG til +(ikke erstatning for) den passordløse innloggingen, samt topartsautentisering. + +**Beslutning A — Passord er et valgfritt, sidestilt alternativ, ikke +påkrevd.** Login-skjermet får en tredje modus (ved siden av e-post-magic-link +og invitasjonskode, ADR-020) for e-post+passord. En bruker setter selv et +passord når hen ønsker det (egen "sett passord"-handling, krever en allerede +gyldig sesjon — samme "du må bevise identitet FØRST" som andre sensitive +endringer) — INGEN eksisterende eller ny bruker tvinges til å sette passord. +Magic-link fortsetter å fungere uendret for alle, uavhengig av om et passord +i tillegg er satt. + +**Beslutning B — Passord-hashing: Argon2id, ikke bcrypt.** Bcrypt trunkerer +stille ved 72 BYTES (et kjent fallgruve-mønster — to ulike passord som deler +de første 72 bytene hasher likt) og har historiske NUL-byte-kvirker i enkelte +implementasjoner. Siden brukeren eksplisitt ber om korrekt håndtering av +spesialtegn/mellomrom (dvs. lengre, mer varierte passord/passfraser er +forventet brukt), velges Argon2id (`argon2-cffi`) — minnehardt, OWASPs +anbefalte standard i dag, ingen lengde-fallgruve. Lagres i ny +`app_user.password_hash` (nullable — NULL betyr "ikke satt", faller da +tilbake til kun magic-link). + +**Beslutning C — 2FA: brukeren velger metode selv, TOTP ELLER e-post- +engangskode.** `app_user.two_factor_method` (nullable, `'totp'`/`'email'`). +- **TOTP:** `app_user.totp_secret` (bare satt når metoden er `'totp'`), + `pyotp` for generering/verifisering (RFC 6238-standard, virker med Google + Authenticator/Authy/1Password etc. uten videre). QR-kode for oppsett + generert server-side (`qrcode`-biblioteket + eksisterende Pillow- + avhengighet, samme mønster som AVIF-konverteringen) — ingen ny ekstern + tjeneste, ingen løpende kostnad. +- **E-post-engangskode:** gjenbruker eksisterende SMTP-oppsett (`app/email.py`, + ADR-009) — ny, kort 6-sifret kode, samme hash-og-utløp-mønster som + `magic_link_token` (ny tabell `two_factor_code`, 5 minutters gyldighet). + Svakere som eneste faktor (e-post-kontoen blir da reelt sett den ENESTE + hemmeligheten), men krever ingen ny infrastruktur og er brukerens eget, + informerte valg mellom de to metodene. +- **Bevisst UTENFOR omfang:** SMS-2FA — krever en betalt tredjeparts + SMS-leverandør (f.eks. Twilio), løpende kostnad per melding, ny ekstern + avhengighet. Ikke bygget denne runden; kan legges til som en tredje metode + senere uten å røre TOTP/e-post-sporene, siden `two_factor_method` allerede + er et åpent tekstfelt, ikke en hardkodet to-verdi-enum i skjemaet. + +**Beslutning D — 2FA er PÅKREVD for org-eier/admin, valgfritt ellers.** +Ved innlogging (uansett om via magic-link eller passord): har brukeren +`organization_membership.role IN ('owner','admin')` i MINST én organisasjon +OG `two_factor_method IS NULL`, gis IKKE en full sesjon — brukeren tvinges +inn i et "sett opp 2FA nå"-steg først. En vanlig `member`-rolle (eller en +bruker uten noe org-medlemskap ennå) kan fortsette å bruke appen helt uten +2FA om hen ønsker det. **Begrunnelse:** organisator-roller har skrivetilgang +til andre spilleres personopplysninger (ADR-017) og kontroll over hele +turneringer — et kompromittert organisator-passord/magic-link er en langt +alvorligere hendelse enn en kompromittert spillerkonto. Håndheves ved HVER +innlogging (ikke bare første gang), siden en bruker kan BLI eier av en ny +org (`POST /orgs`, ADR ingen restriksjon — se avklaringen under) etter at +kontoen allerede eksisterer uten 2FA. + +**Beslutning E — To-stegs innlogging via en `stage`-claim i sesjons-JWT-en, +ikke en egen tabell for "pågående innlogging".** Når 2FA kreves (enten fordi +brukeren selv har slått det på, eller fordi Beslutning D tvinger det), +utsteder primær-autentisering (magic-link-verifisering ELLER passord-innlogging) +et KORTLEVD (5 min) JWT med `stage: "pending_2fa"` i stedet for en full +30-dagers sesjon. En ny avhengighet (`get_pending_2fa_user`, speiler +`get_current_user`) godtar KUN denne mellomtilstanden, og eksponeres bare på +2FA-verifiserings-/oppsett-endepunktene — `get_current_user` (brukt av ALLE +andre endepunkter) avviser eksplisitt en `pending_2fa`-claim som ugyldig, +slik at en ufullstendig innlogging ALDRI gir reell tilgang til noe. Først når +riktig TOTP-/e-post-kode verifiseres, byttes denne inn mot den vanlige fulle +sesjonscookien (samme 30-dagers levetid som i dag). + +**Avklaring underveis, ikke en ny beslutning:** brukeren spurte samtidig om +«kan en bruker eie flere organisasjoner» er tenkt gjennom. Bekreftet: JA, +dette var alltid en del av modellen (ADR-002 — bruker og org-medlemskap er +bevisst atskilt nettopp for at én bruker skal kunne krysse flere +organisasjoner). `POST /orgs` (`app/routers/organizations.py`) har INGEN +begrensning på hvor mange organisasjoner én bruker kan opprette/eie — +hver ny org gir automatisk `role='owner'` for oppretteren, uavhengig av +eksisterende medlemskap. Allerede testet i praksis: dashbordets org-bytter, +kryss-org-isolasjonstestene, og `/auth/me` sin N+1-oppslagsstrategi +(«N = antall organisasjoner brukeren tilhører») forutsetter alle nettopp +dette. Ingen kodeendring nødvendig — kun bekreftelse. + +**Status:** design ferdig, IKKE bygget ennå. Venter på brukerens endelige +klarsignal før migrasjon/kode skrives (se FEATURE_BACKLOG.md for +gjenstående rekkefølge: sesjons-bug diagnostiseres FØRST, deretter denne +runden). + +--- + +## ADR-022 — Organisasjonseierskap: dele, invitere, frasi seg, superadmin + +**Kontekst:** Reist av brukeren 2026-07-19, rett etter ADR-021. Et reelt, +mer FUNDAMENTALT hull ble synlig under gjennomgangen: `organization_ +membership` har i dag INGEN vei til å legge til et nytt medlem i det hele +tatt etter at organisasjonen er opprettet — den ENESTE raden som noensinne +settes inn er grunnleggerens egen `owner`-rad (`POST /orgs`, +`app/routers/organizations.py`). Det finnes ingen invitasjon, ingen +rollestyring, ingen måte å fjerne noen på. «Del eierskap»-ønsket er derfor +bare den mest synlige kanten av et bredere manglende felt: hele +medlemskaps-livssyklusen etter opprettelse. + +**Beslutning A — Flere eiere er allerede støttet av SKJEMAET, kun API-et +mangler.** `organization_membership.role` har ingen unikhetsbegrensning som +hindrer flere `owner`-rader for samme organisasjon — dette var aldri en +sperre, bare et ubrukt hull. Ingen skjemaendring nødvendig for selve +flereeiere-støtten, kun nye endepunkter. + +**Beslutning B — E-post-basert invitasjon, samme mønster som spiller- +sammenkobling (ADR-017 Beslutning B), ikke et helt nytt konsept.** Ny +`POST /orgs/{id}/invitations {email, role}` oppretter en +`organization_invitation`-rad (e-post, rolle, token-hash, utløper, hvem som +inviterte) og sender en e-post via eksisterende SMTP-infrastruktur +(`app/email.py`). Den inviterte trenger IKKE ha en konto fra før — lenken +tar dem til innlogging (magic-link, evt. passord etter ADR-021), og +`verify_magic_link` utvides til å sjekke ventende invitasjoner på e-posten +sin (samme "kjør på hver innlogging, idempotent" mønster som +`link_player_by_email`) og sette inn `organization_membership`-raden da. +**Rolle-grense ved invitasjon:** en `owner` kan invitere til ENHVER rolle +(owner/admin/member); en `admin` kan KUN invitere til `member` — å la en +admin invitere en ny eier ville vært en reell privilegie-eskaleringsvei +(en admin gir seg selv/en alliert eierskap). Dette er en bevisst, ikke +åpen, avgrensning — minste-privilegium-prinsippet, samme resonnement som +`team_authz.py` sin eksisterende owner/admin-splitt. + +**Beslutning C — Rollestyring og fjerning, med et «siste eier»-vern.** +Ny `PATCH /orgs/{id}/memberships/{membership_id} {role}` (kun `owner`, +UNNTATT at en bruker alltid kan senke SIN EGEN rolle selv — «frasi seg +eierskapet» er en selvbetjent handling, ikke noe som må be en annen eier om +lov) og `DELETE /orgs/{id}/memberships/{membership_id}` (kun `owner`, eller +selv for å forlate organisasjonen). Begge avviser handlingen med en klar +409 (`LAST_OWNER`) hvis den ville latt organisasjonen stå igjen med NULL +eiere — samme «TOCTOU-trygg med `FOR UPDATE`»-mønster som ADR-011s +to-lags-grense. En enslig eier må altså enten forfremme noen andre til +eier FØRST, eller be en superadmin om hjelp (Beslutning D) hvis +organisasjonen skal forlates helt. + +**Beslutning D — Superadmin er et manuelt tildelt, ikke selvbetjent, flagg.** +Ny `app_user.is_super_admin` (boolean, default `false`). INGEN API-endepunkt +lar noen sette dette flagget på seg selv ELLER andre — det settes kun +direkte i databasen av en driftsansvarlig (samme tillitsnivå som å kjøre en +migrasjon), bevisst utenfor appens eget autorisasjonssystem. Årsak: et +selvbetjent «bli superadmin»-endepunkt ville vært selve +sikkerhetshullet det er ment å ikke være. Med flagget satt får brukeren en +NY autorisasjonssti (`get_current_user_or_superadmin`, parallell til +`get_authorized_org` — sjekker `is_super_admin` FØR det vanlige +org-medlemskaps-kravet) som lar dem kalle de samme rolle-/medlemskaps- +endepunktene på ENHVER organisasjon, ikke bare de de selv er medlem av. +**Bevisst avgrenset:** superadmin-stien dekker KUN medlemskap/rolle- +styring i denne runden (nøyaktig det brukeren spurte om — «sette hvem som +helst som eiere av hvilken som helst organisasjon»), ikke generell +skriveadgang til turnering-/spiller-data i andres organisasjoner. En +bredere «support/drift kan se alt»-rolle er en egen, senere beslutning om +den blir etterspurt. + +**Beslutning E — Fjernet/frasigende eiers roster-/spillerdata forblir +URØRT.** Bekreftet av bruker 2026-07-19. Kun `organization_membership`-raden +endres/fjernes ved rollestyring eller fjerning — `team_roster`/`player`-rader +(deltakelse-historikk) røres aldri av disse handlingene. Organisasjons- +STYRING og DELTAKELSE er allerede modellert som separate ting, og forblir +det. + +**Status:** design ferdig, IKKE bygget ennå. Bygges naturlig sammen med +eller rett etter ADR-021, siden begge utvider samme auth-/autorisasjons- +lag og invitasjons-e-posten gjenbruker samme infrastruktur. + +--- + ## Åpne spørsmål (ikke besluttet ennå) Disse må avklares før eller under de relevante fasene: diff --git a/CLAUDE.md b/CLAUDE.md index ae2168c..b43fbac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1192,6 +1192,42 @@ Ferdig og verifisert: umiddelbart, brukeren valgte å rotere — se detaljene under backend-avsnittet over for hele hendelsen og hvordan roteringen ble gjennomført uten å noensinne re-eksponere gammel eller ny verdi. +- **Sesjons-bug diagnostisert og FIKSET, LIVE (2026-07-19):** brukeren + rapporterte at hen måtte be om ny magic-link-kode ved HVERT besøk til + `teecup.teeoff.no`, til tross for ADR-009s 30-dagers sesjonscookie. Bad + brukeren sjekke den EKTE cookien i nettleseren fremfor å gjette — kom + tilbake korrekt satt i alle henseender (`Expires` 30 dager frem, + `Secure`/`HttpOnly`/`SameSite=Lax`). Rot-årsaken var derfor IKKE cookien + eller backend-en: `frontend/app/page.tsx` (rot-siden) viste ALLTID + innloggingsskjemaet uten noensinne å sjekke om en gyldig sesjon allerede + fantes. `Dashboard`-komponenten sjekker `/auth/me` og sender til `/` ved + MANGLENDE sesjon, men ingen kode gjorde det motsatte — en bruker som + besøkte roten direkte (i stedet for å navigere til `/dashboard`) så + derfor alltid innloggingsskjemaet uansett sesjonsstatus. + **Fikset:** `page.tsx` gjort om til en async server-komponent som leser + sesjonscookien via `next/headers`, kaller `/auth/me` server-til-server + direkte mot `TEECUP_API_ORIGIN` (samme mønster som `generateMetadata` i + `app/t/[id]/page.tsx` — IKKE gjennom `next.config.mjs` sin `rewrites()`, + som kun gjelder nettleser-trafikk), og sender en allerede innlogget + bruker videre til `/dashboard` med `redirect()` FØR innloggingsskjemaet + når rendres. + **Verifisert presist mot den ekte, live stacken, med brukerens EGEN + ekte sesjonscookie** (ikke en syntetisk test): `curl` uten cookie mot + `https://teecup.teeoff.no/` ga `200` (skjemaet vises, riktig for en + anonym besøkende); samme kall MED den ekte cookien ga `307` til + `/dashboard` (riktig — sender en allerede innlogget bruker rett videre). + Ekte typesjekket produksjonsbuild kjørt FØR utrulling (build-outputet + viste selv at `/` nå er `ƒ` dynamisk i stedet for `○` statisk — bekrefter + at server-sjekken faktisk ble tatt i bruk). Rullet ut live, kun + `teecup_frontend`, ingen migrasjon, `teeoff.no` upåvirket. + **Samme runde:** brukeren stilte to oppfølgingsspørsmål om + autentisering/autorisasjon — «er flere-organisasjoner-eierskap tenkt + gjennom» (bekreftet: ja, ADR-002 fra dag én, ingen kodeendring nødvendig) + og et ønske om passord (valgfritt tillegg)+2FA. Fullt design skrevet som + **ADR-021** (passord/2FA) og **ADR-022** (dele/invitere/frasi seg + eierskap + superadmin) — se ARCHITECTURE_DECISIONS.md. Begge er + **kun design, IKKE bygget ennå** — venter på brukerens klarsignal til å + starte selve byggingen. Neste steg: 1. PWA-egenskaper (manifest, service worker, offline-cache) — ikke startet. diff --git a/FEATURE_BACKLOG.md b/FEATURE_BACKLOG.md index efefa23..64d17b6 100644 --- a/FEATURE_BACKLOG.md +++ b/FEATURE_BACKLOG.md @@ -668,6 +668,36 @@ reell (og transparent håndtert) passord-eksponeringshendelse underveis. --- +## Innlogging: sesjon-bug + passord/2FA — ADR-021 (reist 2026-07-19) + +| Del | Status | Notat | +|---|---|---| +| Flere organisasjoner per bruker | ✅ bekreftet allerede dekket | ADR-002 fra dag én. `POST /orgs` har ingen begrensning på antall org-er samme bruker kan eie. Ingen kodeendring — kun bekreftet ved gjennomgang 2026-07-19. | +| Sesjon holder seg ikke — ny magic-link-kode kreves ved hvert besøk | ✅ FIKSET og LIVE 2026-07-19 | Ikke en cookie-bug (brukerens ekte cookie var korrekt satt, 30 dager, `Secure`/`HttpOnly`). Root cause: `app/page.tsx` sjekket aldri om en gyldig sesjon allerede fantes før den viste innloggingsskjemaet. Fikset med server-side sesjonssjekk + redirect til `/dashboard`. Se CLAUDE.md-status for full diagnose og verifisering. | +| Passord som valgfritt tillegg til magic-link | 📋 designet (ADR-021), ikke bygget | Argon2id-hashing (ikke bcrypt — unngår 72-byte-trunkering, viktig siden spesialtegn/mellomrom skal fungere korrekt). Passord er ALDRI påkrevd. | +| 2FA: TOTP eller e-post-engangskode, brukerens eget valg | 📋 designet (ADR-021), ikke bygget | SMS bevisst utenfor omfang (krever betalt leverandør). | +| 2FA påkrevd for org-eier/admin, valgfritt for medlemmer | 📋 designet (ADR-021), ikke bygget | Håndheves ved hver innlogging via en `stage: "pending_2fa"`-mellomtilstand i sesjons-JWT-en. | + +**Sesjons-bugen er nå fikset** (se raden over) — ADR-021 sitt passord/2FA-løp +er dermed neste naturlige steg i denne tråden, når det er ønskelig. + +### Organisasjonseierskap: dele, invitere, frasi seg, superadmin — ADR-022 + +Reist rett etter ADR-021. Avdekket et bredere, mer fundamentalt hull enn +bare "del eierskap": det finnes i dag INGEN vei til å legge til et +organisasjonsmedlem etter opprettelse i det hele tatt — kun grunnleggerens +egen `owner`-rad settes noensinne inn. + +| Del | Status | Notat | +|---|---|---| +| Flere eiere per organisasjon | ✅ skjemaet støtter det allerede | Ingen unikhetssperre hindrer flere `owner`-rader. Kun API mangler. | +| E-post-invitasjon (owner→hvilken som helst rolle, admin→kun member) | 📋 designet (ADR-022), ikke bygget | Gjenbruker SMTP + samme mønster som spiller-e-post-kobling (ADR-017). | +| Rollestyring + frasi seg eierskap (selvbetjent) | 📋 designet (ADR-022), ikke bygget | «Siste eier»-vern (409 `LAST_OWNER`), `FOR UPDATE`-lås mot race. | +| Superadmin (manuelt DB-tildelt, ikke selvbetjent) | 📋 designet (ADR-022), ikke bygget | Kan sette eiere på enhver org. Bevisst avgrenset til akkurat medlemskap/rolle denne runden, ikke generell tilgang til andres data. | +| Fjernet eiers roster-/spillerdata | ✅ avgjort (Beslutning E) | Forblir urørt — organisasjons-styring ≠ deltakelse-historikk. Bekreftet av bruker 2026-07-19. | + +--- + ## UX / frontend (senere fase) - 📋 Høy kontrast, dark/light, store +/- knapper, stor «Neste hull»-knapp diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 86e7954..c5e7c35 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,7 +1,43 @@ +import { cookies } from "next/headers" +import { redirect } from "next/navigation" import { LoginForm } from "@/components/login-form" import { Wordmark } from "@/components/wordmark" -export default function Page() { +// 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). 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 Page() { + // Reell bug funnet 2026-07-19: denne siden viste ALLTID innloggingsskjemaet, + // uansett om brukeren allerede hadde en helt gyldig sesjonscookie (30 dager, + // bekreftet riktig satt) -- Dashboard sjekker /auth/me og sender deg HIT ved + // manglende sesjon, men ingenting gjorde det motsatte. En bruker som besøkte + // teecup.teeoff.no direkte (i stedet for å navigere til /dashboard) så derfor + // alltid innloggingsskjemaet og ba unødvendig om en ny magic-link hver gang. + const cookieStore = await cookies() + const session = cookieStore.get(SESSION_COOKIE_NAME) + let authenticated = false + if (session) { + try { + const res = await fetch(`${API_ORIGIN}/auth/me`, { + headers: { Cookie: `${SESSION_COOKIE_NAME}=${session.value}` }, + cache: "no-store", + }) + authenticated = res.ok + } catch { + // API utilgjengelig -- vis innloggingsskjemaet i stedet for å henge. + authenticated = false + } + } + if (authenticated) { + redirect("/dashboard") + } + return (