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 -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(grep -n \"^ [a-zA-Z0-9_-]*:$\" /opt/teeoff/docker-compose.prod.yml)",
|
||||||
"Bash(set -e)",
|
"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": [
|
"additionalDirectories": [
|
||||||
"/opt/teeoff/deploy",
|
"/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å)
|
## Åpne spørsmål (ikke besluttet ennå)
|
||||||
|
|
||||||
Disse må avklares før eller under de relevante fasene:
|
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
|
umiddelbart, brukeren valgte å rotere — se detaljene under
|
||||||
backend-avsnittet over for hele hendelsen og hvordan roteringen ble
|
backend-avsnittet over for hele hendelsen og hvordan roteringen ble
|
||||||
gjennomført uten å noensinne re-eksponere gammel eller ny verdi.
|
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:
|
Neste steg:
|
||||||
1. PWA-egenskaper (manifest, service worker, offline-cache) — ikke startet.
|
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)
|
## UX / frontend (senere fase)
|
||||||
|
|
||||||
- 📋 Høy kontrast, dark/light, store +/- knapper, stor «Neste hull»-knapp
|
- 📋 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 { LoginForm } from "@/components/login-form"
|
||||||
import { Wordmark } from "@/components/wordmark"
|
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 (
|
return (
|
||||||
<main className="flex min-h-[100dvh] flex-col items-center justify-center bg-background px-5 py-10">
|
<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">
|
<div className="flex w-full max-w-sm flex-col gap-8">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue