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
Backend og frontend er grundig scratch-verifisert — inkludert tre reelle bugs jeg fant og fikset underveis (en UUID-serialiseringsfeil i magic-link-innlogging, og to tilfeller av en uendelig 2FA-løkke der en nettopp bekreftet kode ble sjekket på nytt). Alle sikkerhetsvern testet eksplisitt: siste-eier-vern, blokkert selv-forfremmelse, admin kan ikke gi eierskap, superadmin fungerer/avvises riktig, tvungen 2FA for nye eiere, passord med spesialtegn/mellomrom, og bakoverkompatibilitet med eksisterende magic-link-flyt.
Klar til utrulling mot ekte systemer:
Migrasjon: 012_password_2fa_and_org_invitations.sql mot ekte teecup_db
Redeploy: både teecup_api og teecup_frontend
AskUserQuestion
ADR-017 sin backend er nå komplett: registrerings-API-et og kontosammenkoblingen fungerer sammen — en spiller organisator la inn på forhånd, i én eller flere klubber, kobles automatisk til riktig konto første gang hen logger inn, uten å bli medlem av noe hun ikke ba om.
Bygget: ny app/email.py (send_magic_link_email, kjører smtplib via asyncio.to_thread siden det er synkront, håndterer både implisitt TLS (port 465) og STARTTLS dynamisk siden jeg bevisst ikke leste TEECUP_SMTP_PORT-verdien). app/config.py fikk nye, valgfrie innstillinger — SMTP_CONFIGURED er IKKE _required, så scratch-/dev-testing fortsatt fungerer uendret uten SMTP satt opp.
Sikkerhetsdesign: en driftsfeil i selve utsendingen (feil passord, SMTP nede, eller ingenting konfigurert) logges kun server-side og endrer aldri klientresponsen — bevarer request-link sitt anti-enumereringsvern.
Verifisert i to trinn:
Dev-log-flyten uendret uten SMTP satt (regresjonstest).
Én ekte test-e-post sendt til hei@erol.no, med credentials videreført fra .env til scratch-containeren uten at jeg noensinne leste verdiene — du bekreftet mottak.
Dette er første gang noe i prosjektet er bevist ved ekte, ekstern levering fremfor bare curl/scratch-container. CLAUDE.md/FEATURE_BACKLOG.md oppdatert.
Fiksen: ny migrasjon 005_rls_null_guard.sql — én delt STABLE SQL-funksjon app_current_org() (NULLIF(current_setting('app.current_org', true), '')::uuid) erstatter det rå uttrykket i alle 15 RLS-policyer (14 org_isolation + org_self) via ALTER POLICY. NULLIF konverterer tomstreng til NULL før cast, så "trygg standard: se ingenting" gjenopprettes uansett GUC-tilstand.
Verifisert to ganger, ulikt:
Tre nye regresjonstester i test_isolation.sql (Test 10–12): tomstreng-lesing gir 0 rader ikke krasj, tomstreng-skriving avvises av RLS ikke krasj, og org-bootstrap-innsetting (tidligere aldri testet) fungerer rett etter tomstreng-tilstand.
Faktisk gjenskaping av original-buggen mot en ekte container (pool-størrelse tvunget til 1 for å garantere tilkoblings-gjenbruk): varmet opp med et vanlig org_connection()-kall, kalte deretter /auth/me på samme tilkobling — gikk fra 500 til 200.
En viktig ting jeg tok feil om i forrige runde, oppdaget ved å faktisk teste i stedet for å anta: fiksen gjør ikke at /auth/me trygt kan joine organization direkte via plain_connection(). Den løser tomstreng-krasjen, men org_self-policyen krever fortsatt en matchende app.current_org for å vise noen rad i det hele tatt — riktig RLS-design, ikke noe fiksen skulle endre. Siden en bruker kan tilhøre flere organisasjoner, finnes det ingen én kontekst å sette. Rettet til riktig løsning: /auth/me slår nå opp hvert org-navn enkeltvis via org_connection() (verifisert at det faktisk returnerer navnet korrekt).
Alt dokumentert i CLAUDE.md/FEATURE_BACKLOG.md.
Ny flyt: magic-link (POST /auth/request-link → POST /auth/verify-link) + JWT-sesjon i HttpOnly/SameSite=Lax/dynamisk-Secure-cookie (30 dager), pluss /auth/logout og /auth/me. Ny migrasjon 004_auth.sql (unik e-post-indeks + magic_link_token-tabell).
Sikkerhetsdesignet fra Plan-agent-gjennomgangen holdt gjennom testing:
Token: secrets.token_urlsafe(32), kun SHA-256-hash lagres
Atomisk forbruk (UPDATE...RETURNING, ikke les-sjekk-skriv) — hindrer replay
Generisk respons uansett om e-posten finnes — hindrer enumerering
app_user opprettes først ved vellykket verifisering, ikke ved forespørsel — hindrer massopprettelse
Gamle uforbrukte lenker ugyldiggjøres når en ny utstedes
PyJWT (byttet fra python-jose pga. bredere sårbarhetsflate) med eksplisitt algorithms=["HS256"]
Ekte eksistens-sjekk mot app_user på hvert kall — en slettet bruker mister tilgang umiddelbart, ikke etter 30 dager
Alle 12 planlagte tester bestått, inkludert cooldown, token-ugyldiggjøring, utløp, tuklet JWT, slettet bruker, og at debug-headeren nå er helt uten effekt.
To ting funnet og fikset/dokumentert underveis:
ON CONFLICT (email) matchet ikke den nye partielle unike indeksen uten eksplisitt WHERE-klausul — fikset.
En reell, dypere RLS-bug (dokumentert i FEATURE_BACKLOG.md, ikke fikset her): organization-tabellens RLS-policy kaster en 500 i stedet for "se ingenting" når app.current_org leses tilbake som tomstreng (ikke NULL) på en gjenbrukt pool-tilkobling. Berører trolig alle 15 RLS-policyer i skjemaet — for stort og sensitivt (ADR-003-grunnmuren) til å hastefikse her, så jeg mitigerte det lokalt i /auth/me og satte det som punkt 1 i neste-steg-listen.