Update Todos
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.
This commit is contained in:
parent
89a803140e
commit
a216e3945a
4 changed files with 134 additions and 23 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
111
CLAUDE.md
111
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.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue