Fikset og live. Rot-årsaken var altså en manglende sjekk i frontend, ikke noe galt med selve sesjonen eller cookien din — verifisert direkte med din ekte cookie mot produksjon (uten cookie → skjema, med din cookie → sendt rett til /dashboard).
.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.
This commit is contained in:
parent
317e24a800
commit
73f3c4d1ea
5 changed files with 295 additions and 2 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
36
CLAUDE.md
36
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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<main className="flex min-h-[100dvh] flex-col items-center justify-center bg-background px-5 py-10">
|
||||
<div className="flex w-full max-w-sm flex-col gap-8">
|
||||
|
|
|
|||
Loading…
Reference in a new issue