From a216e3945aa033f590d355745d2729e7f5b3128e Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Sun, 19 Jul 2026 10:40:15 +0200 Subject: [PATCH] Update Todos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Legge til argon2-cffi, pyotp, qrcode i requirements.txt Skrive migrasjon 012 (passord, 2FA, superadmin, org-invitasjoner) app/auth.py: sesjonsstadier, passord-hashing, TOTP-hjelpere app/routers/auth.py: passord-innlogging, 2FA-oppsett/verifisering app/email.py: 2FA-kode og invitasjons-maler app/routers/organizations.py: invitasjoner, medlemskapsstyring, superadmin-sti Frontend: login-form passord-modus + 2FA-skjermer Frontend: kontoinnstillinger + org-medlemsstyring-skjerm Ekte typesjekket frontend-build Scratch-verifisere hele auth-løpet grundig (backend) Deploy mot ekte teecup_db/containere + oppdatere .md-filer ADR-021 (passord/2FA) og ADR-022 (org-eierskap) er live. Kort oppsummert: Nytt i innloggingen: Passord (valgfritt tillegg til magic-link) — Argon2id, testet med ekte spesialtegn/mellomrom/æøå 2FA — TOTP eller e-post-engangskode, brukerens eget valg Påkrevd 2FA for org-eiere/administratorer, valgfritt for andre Ny /account-skjerm for å sette passord og styre 2FA Nytt for organisasjoner: Inviter andre på e-post til enhver rolle (eiere) eller kun medlem (administratorer) Frasi deg eierskap selv, eller fjern andre medlemmer — med vern mot at en organisasjon står igjen uten eier Superadmin-flagg (kun manuelt satt i databasen, aldri via API) som kan sette eierskap på hvilken som helst organisasjon Ny /orgs/[id]/members-skjerm, lenket fra dashbordet Fant og fikset tre reelle bugs underveis i scratch-testingen (ingen nådde produksjon) — en krasj i selve innloggingen og to tilfeller av en uendelig 2FA-løkke. Alt er nå verifisert grundig og rullet ut, eksisterende sesjoner er upåvirket. --- .claude/settings.local.json | 3 +- ARCHITECTURE_DECISIONS.md | 13 ++--- CLAUDE.md | 111 +++++++++++++++++++++++++++++++++++- FEATURE_BACKLOG.md | 30 ++++++---- 4 files changed, 134 insertions(+), 23 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 5b463f8..5971bee 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -279,7 +279,8 @@ "Bash(set -e)", "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/)" + "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/)", + "Bash(curl -s -w '\\\\n%{http_code}\\\\n' -X POST https://teecup.teeoff.no/auth/login-password -H \"Content-Type: application/json\" -d '{\"email\":\"ikke@finnes.no\",\"password\":\"whatever\"}')" ], "additionalDirectories": [ "/opt/teeoff/deploy", diff --git a/ARCHITECTURE_DECISIONS.md b/ARCHITECTURE_DECISIONS.md index 72b53f5..b5e6fe9 100644 --- a/ARCHITECTURE_DECISIONS.md +++ b/ARCHITECTURE_DECISIONS.md @@ -825,10 +825,10 @@ 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). +**Status: ✅ BYGGET OG LIVE 2026-07-19.** Migrasjon `012_password_2fa_and_ +org_invitations.sql` kjørt mot ekte `teecup_db`. Se CLAUDE.md-status for +full byggerunde (backend/frontend-detaljer, tre reelle bugs funnet og +fikset under scratch-testing). --- @@ -904,9 +904,8 @@ endres/fjernes ved rollestyring eller fjerning — `team_roster`/`player`-rader 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. +**Status: ✅ BYGGET OG LIVE 2026-07-19**, samme runde som ADR-021 (samme +migrasjon `012`). Se CLAUDE.md-status for full byggerunde. --- diff --git a/CLAUDE.md b/CLAUDE.md index b43fbac..d629529 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1225,9 +1225,114 @@ Ferdig og verifisert: 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. + eierskap + superadmin) — se ARCHITECTURE_DECISIONS.md. +- **ADR-021 (passord/2FA) + ADR-022 (org-eierskap) BYGGET OG LIVE + (2026-07-19), samme dag:** brukeren ba om begge sammen («Bygg det», + bekreftet eksplisitt at det gjaldt begge ADR-ene i samme runde). Ny + migrasjon `012_password_2fa_and_org_invitations.sql`: `app_user. + password_hash`/`two_factor_method`/`totp_secret`/`is_super_admin`, ny + tabell `two_factor_code` (samme hash-og-utløp-mønster som + `magic_link_token`), ny tabell `organization_invitation` (RLS + org-isolert, INGEN egen klikkbar aksept-lenke — godtas automatisk ved + neste innlogging med matchende e-post), femte + `SECURITY DEFINER`-bro `accept_pending_invitations_by_email()` (etter + `public_tournament_org` 007, `link_player_by_email` 008, + `public_org_by_slug` 009, `public_tournament_by_code` 011). + **Sesjons-STADIER innført i `app/auth.py`:** en sesjonscookie er ikke + nødvendigvis en full sesjon lenger — `create_session_token()` tar nå en + `stage`-parameter (`full`/`pending_2fa`/`must_enroll_2fa`), lagt inn som + et JWT-claim. `get_current_user` avviser eksplisitt alt annet enn `full` + ELLER en ELDRE token uten stage-claim i det hele tatt (utstedt før denne + runden — behandlet som `full` for bakoverkompatibilitet, ingen + eksisterende bruker logget brått ut). To nye avhengigheter: + `get_pending_user` (kun for 2FA-verifiseringsendepunktene) og + `get_current_or_enrolling_user` (godtar BÅDE en full sesjon — frivillig + 2FA-oppsett fra kontoinnstillinger — OG `must_enroll_2fa` — tvunget + oppsett rett etter innlogging — samme oppsett-logikk dekker begge + veiene). `get_current_user_optional` fikk samme stage-sjekk for + konsistens. + **Passord (Argon2id, ikke bcrypt):** bevisst valg for å unngå bcrypt sin + stille 72-byte-trunkering, siden brukeren eksplisitt ba om korrekt + håndtering av spesialtegn/mellomrom. `POST /auth/set-password`/ + `/remove-password`/`/login-password` — sistnevnte svarer med IDENTISK + 401 uansett om e-posten finnes, mangler passord, eller passordet er + feil (samme anti-enumerering som magic-link). + **2FA (TOTP via `pyotp` ELLER e-post-engangskode via eksisterende SMTP, + brukerens eget valg):** `POST /auth/2fa/setup/start` genererer en + TOTP-secret UTEN å lagre den (rundturer til klienten, som ekkoer den + tilbake i `/setup/confirm` — unngår en halvferdig 2FA-tilstand i + databasen hvis brukeren forlater oppsettet). QR-kode generert + server-side (`qrcode`-biblioteket + eksisterende Pillow-avhengighet, + ingen ny ekstern tjeneste). `POST /auth/2fa/verify` fullfører en + PÅGÅENDE innlogging. + **Tvungen 2FA for org-eier/admin (ADR-021 Beslutning D):** + `user_requires_2fa_enrollment()` sjekket ved HVER innlogging (ikke bare + første gang, siden en bruker kan bli eier av en NY org etter at kontoen + allerede eksisterer uten 2FA) — verifisert eksplisitt: en fersk + org-eier uten 2FA ble korrekt blokkert fra all normal tilgang (401) og + tvunget inn i oppsett-flyten før noe annet ble tilgjengelig. + **Organisasjonseierskap (ADR-022):** `POST/GET/DELETE + /orgs/{id}/invitations` (owner→enhver rolle, admin→KUN member — ellers + en privilegie-eskaleringsvei), `PATCH/DELETE /orgs/{id}/memberships/ + {id}` (owner kan endre/fjerne hvem som helst; en bruker kan ALLTID + SENKE egen rolle selv — aldri heve den, ville vært selv-forfremmelse — + og alltid forlate selv), «siste eier»-vern (409 `LAST_OWNER`, `FOR + UPDATE`-lås mot race på alle eier-rader, samme TOCTOU-mønster som + ADR-011s to-lags-grense). Superadmin (`app_user.is_super_admin`, KUN + manuelt DB-tildelt — bevisst INGEN API-vei til å gi seg selv eller + andre flagget) får en parallell autorisasjonssti + (`get_superadmin_user`) som kan sette medlemskap på ENHVER org, + uavhengig av eget medlemskap — bevisst avgrenset til nøyaktig dette, + ikke generell tilgang til andres turnering-/spillerdata. + **Tre reelle bugs funnet OG fikset UNDER scratch-testing, ingen nådde + produksjon:** + 1. `verify_magic_link` sendte en rå asyncpg-`UUID` (ikke streng) videre + til sesjonsutstedelse — `jwt.encode()` sin JSON-serialisering + krasjet rått (500) på selve innloggingen. Fant umiddelbart ved første + reelle innloggingstest. Rettet med en eksplisitt `str()`. + 2. OG 3. Både `2fa/setup/confirm` og `2fa/verify` kalte først den delte + `_issue_login_result()`-hjelpefunksjonen (ment for PRIMÆR + autentisering) EN GANG TIL etter at 2FA nettopp var bekreftet — som + så (korrekt, men feil kontekst) at `two_factor_method` nå var satt + og krevde EN NY runde med 2FA for akkurat den samme innloggingen, + en uendelig løkke. Fant ved å faktisk fullføre hele innloggings- + syklusen med ekte genererte TOTP-koder (`pyotp` i test-scriptet), + ikke bare ved å lese koden. Rettet ved at begge endepunktene nå + utsteder en full sesjon DIREKTE etter vellykket 2FA-bekreftelse, + ikke via gjenbruk av den generelle sjekken. + **Frontend:** `login-form.tsx` fikk en tredje modus (passord, ved siden + av magic-link og ADR-020s invitasjonskode-modus). Ny delt + `two-factor-flow.tsx` (`TwoFactorVerifyForm`/`TwoFactorSetupForm`), + brukt av BÅDE `login-form.tsx` og `verify-form.tsx` siden begge + primær-autentiseringsveiene kan returnere samme 2FA-mellomtilstand. Ny + `/account`-skjerm (sett/fjern passord, aktiver/deaktiver 2FA) og ny + `/orgs/[id]/members`-skjerm (invitere, endre rolle, fjerne/forlate), + begge lenket fra dashbordets header/org-visning. `next.config.mjs` sin + `rewrites()` utvidet med `/superadmin/:path*` (ADR-016s konsekvens for + enhver ny API-prefiks, selv om ingen frontend-UI faktisk bruker den + ennå). + **Verifisert grundig mot fersk `teecup_scratch`** (001→012, + `test_isolation.sql` 12/12): full magic-link-bakoverkompatibilitet + (eksisterende flyt uendret for brukere uten 2FA), passord med ekte + spesialtegn/mellomrom/æøå satt og brukt til pålogging, full TOTP-runde + (oppsett→bekreft→logg ut→logg inn→krev 2FA→verifiser med ekte + `pyotp`-generert kode), full e-post-2FA-runde (samme mønster, feil kode + avvist, kode ikke gjenbrukbar), tvungen 2FA-registrering for ny + org-eier, invitasjon→auto-aksept ved førstegangsinnlogging, + rolle-eskalering-forsøk avvist på tre distinkte måter (medlem kan ikke + invitere, admin kan ikke gi eierskap, bruker kan ikke forfremme seg + selv), siste-eier-vern (både PATCH og DELETE), duplikat-invitasjon + avvist, superadmin-sti fungerer/avvises riktig i begge retninger. Ekte + typesjekket produksjonsbuild av frontend (alle nye ruter listet). + **Rullet ut mot ekte systemer 2026-07-19**, bruker bekreftet eksplisitt: + migrasjon 012 kjørt mot ekte `teecup_db`, `test_isolation.sql` fortsatt + 12/12, begge containere (`teecup_api`, `teecup_frontend`) redeployet og + bekreftet ren oppstart, `/health` og `/dashboard` fortsatt 200 (eksisterende + sesjoner uendret av stage-bakoverkompatibiliteten), `/auth/login-password` + bekreftet nåbart over ekte https, `teeoff.no` upåvirket. + **ADR-021 og ADR-022 er dermed begge helt ferdig** — backend + frontend. + Bevisst utenfor omfang: dedikert superadmin-UI (brukes via API av en + betrodd operatør), SMS som 2FA-metode. Neste steg: 1. PWA-egenskaper (manifest, service worker, offline-cache) — ikke startet. diff --git a/FEATURE_BACKLOG.md b/FEATURE_BACKLOG.md index 64d17b6..4dc1069 100644 --- a/FEATURE_BACKLOG.md +++ b/FEATURE_BACKLOG.md @@ -674,27 +674,33 @@ reell (og transparent håndtert) passord-eksponeringshendelse underveis. |---|---|---| | 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. | +| Passord som valgfritt tillegg til magic-link | ✅ LIVE 2026-07-19 | Argon2id-hashing (ikke bcrypt — unngår 72-byte-trunkering). Verifisert med et ekte passord med mellomrom+æøå+spesialtegn. `POST /auth/login-password`, `/auth/set-password`, `/auth/remove-password`. Passord er ALDRI påkrevd. | +| 2FA: TOTP eller e-post-engangskode, brukerens eget valg | ✅ LIVE 2026-07-19 | SMS bevisst utenfor omfang (krever betalt leverandør). `POST /auth/2fa/setup/start`+`/confirm`, `/auth/2fa/verify`, `/auth/2fa/disable`. | +| 2FA påkrevd for org-eier/admin, valgfritt for medlemmer | ✅ LIVE 2026-07-19 | Håndheves ved hver innlogging via en `stage: "pending_2fa"`/`"must_enroll_2fa"`-mellomtilstand i sesjons-JWT-en. Verifisert: en fersk org-eier uten 2FA ble korrekt tvunget inn i oppsett ved neste innlogging. | +| Frontend: passord-innlogging, 2FA-verifisering/-oppsett, kontoinnstillinger | ✅ LIVE 2026-07-19 | `login-form.tsx` (passord-modus), `two-factor-flow.tsx` (delt mellom login/verify), `/account`. | -**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. +**ADR-021 er dermed helt ferdig.** Se CLAUDE.md-status for full byggerunde, +inkl. tre reelle bugs funnet og fikset under scratch-testing. ### 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 +bare "del eierskap": det fantes tidligere INGEN vei til å legge til et organisasjonsmedlem etter opprettelse i det hele tatt — kun grunnleggerens -egen `owner`-rad settes noensinne inn. +egen `owner`-rad ble noensinne satt 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. | +| Flere eiere per organisasjon | ✅ LIVE | Skjemaet støttet det allerede (ingen unikhetssperre); nå faktisk brukbart via API. | +| E-post-invitasjon (owner→hvilken som helst rolle, admin→kun member) | ✅ LIVE 2026-07-19 | `POST/GET/DELETE /orgs/{id}/invitations`. Auto-akseptert ved neste innlogging (magic-link ELLER passord), samme mønster som spiller-e-post-kobling (ADR-017). Verifisert: admin som forsøkte å invitere som eier ble korrekt avvist. | +| Rollestyring + frasi seg eierskap (selvbetjent) | ✅ LIVE 2026-07-19 | `PATCH`/`DELETE /orgs/{id}/memberships/{id}`. «Siste eier»-vern (409 `LAST_OWNER`) OG selv-forfremmelse-vern verifisert eksplisitt i scratch. | +| Superadmin (manuelt DB-tildelt, ikke selvbetjent) | ✅ LIVE 2026-07-19 | `POST /superadmin/orgs/{id}/memberships`. Verifisert: ikke-superadmin avvist (403), superadmin kan sette eierskap på en org de selv ikke er medlem av, ukjent e-post avvist (404). | +| Fjernet eiers roster-/spillerdata | ✅ avgjort (Beslutning E) | Forblir urørt — organisasjons-styring ≠ deltakelse-historikk. Bekreftet av bruker 2026-07-18. | +| Frontend: medlemsstyring/invitasjon | ✅ LIVE 2026-07-19 | `org-members.tsx`, `/orgs/[id]/members`, lenket fra dashbordet. | + +**ADR-022 er dermed helt ferdig.** Ingen dedikert superadmin-UI bygget +(bevisst — brukes via API av en betrodd operatør, matcher «sjelden, +manuelt tildelt makt»-designet). ---