teecup/CLAUDE.md
Erol Haagenrud fbd3f58a1c Ekte autentisering er bygget og verifisert. X-Debug-User-Id-stubben er helt fjernet, ingen fallback beholdt.
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.
2026-07-16 15:16:53 +02:00

8 KiB

CLAUDE.md — arbeidsinstruks for TeeCup

Les dette først i hver økt. Det koder hva vi har bestemt og hvordan vi jobber.

Autoritative kilder (les før du gjør noe)

  • ARCHITECTURE_DECISIONS.md — hva som er bestemt og hvorfor (ADR-001…014). Fasit.
  • FEATURE_BACKLOG.md — hva som gjenstår, hva som er utsatt, hva som mangler.
  • Endres en beslutning: legg til en ny ADR, ikke slett historikk. Hold begge filene oppdatert når noe avgjøres.

Sikkerhetsregler (ufravikelige)

  • Rør ALDRI teeoff-databasen eller den ekte teecup_db uten at brukeren eksplisitt har bekreftet det i samme økt. Test alltid migrasjoner mot en egen scratch-database først, og rydd opp etterpå.
  • Vis planen (hvilke kommandoer, mot hvilken database) FØR du kjører noe som skriver, migrerer eller sletter. Vent på bekreftelse.
  • Hemmeligheter (passord, secrets) bor i .env (filrettigheter 600), dekkes av .gitignore, committes aldri, og skrives aldri i klartekst i chatten eller i SQL-filer. Generer dem på serveren (openssl rand -base64 32).
  • Kjør appen som databaserollen teecup_app (NOSUPERUSER, NOBYPASSRLS) — aldri som teeoff_admin/superuser i runtime.

Arkitektur-invarianter (ikke bryt uten en ny ADR)

  • Tenant = organisasjon. organization_id på alle domenetabeller, håndhevet av RLS. App-koden setter app.current_org med SET LOCAL per transaksjon.
  • Verifiser at brukeren er medlem av organisasjonen FØR org-konteksten settes. RLS stoler blindt på app.current_org.
  • Egen innlogging (uavhengig av teeoff). Banedata hentes fra teeoff via lesende API, ikke delt database.
  • v1 = nøyaktig to lag (Ryder Cup-format), håndhevet i app-laget. Match-modellen holdes generell (to sider) så knockout/flere lag kan komme senere.
  • Handicap-/matchlogikk skal ligge i handicap_engine.py (rent, testet, uten db/API-avhengigheter). Allowances er konfig, ikke hardkodet.
  • Media (bilder/video) skal i objektlagring (MinIO), ikke i Postgres. Postgres holder bare metadata + nøkkel.

Arbeidsmåte

  • Inkrementelt. Ingenting tas for gitt før det er testet. Bekreft hvert steg før du går videre.
  • Bruk git (remote: brukerens Forgejo). Commit i logiske steg med tydelige meldinger.
  • Er du usikker på omfang eller en beslutning: spør heller enn å gjette.

Status (oppdater denne når ting endres)

Ferdig og verifisert:

  • Handicap-motor + tester (24/24, R&A-verifisert).
  • Skjema 001 + roller 002 + scoring/blind draw 003. Isolasjon bevist med test_isolation.sql (RLS-oppførsel, ikke bare at skjemaet kjører).
  • 002 hadde en reell bug (psql interpolerer ikke :'var' inne i DO $$...$$) — permanent fikset, verifisert mot scratch to ganger.
  • API-et kjørt for ekte (ikke bare syntaks-sjekket) i en engangs Docker- container mot en scratch-database, RLS bevist gjennom hele asyncpg-pool-stacken (ikke bare i rå SQL).
  • Oppsett-endepunktene er bygget og verifisert: app/routers/players.py (spillerpool), tournaments.py (turnering/lag/roster/økter, ADR-011 to-lags-grense håndhevet med FOR UPDATE-lås), matches.py (matcher/ deltakere/blind draw-lås, ADR-013-synlighet push-down i SQL). Delt feiloversettelse i app/errors.py, delte synlighetsspørringer i app/blind_draw.py. main.py er nå bare app-factory + include_router.
  • Scoring-runden er bygget og verifisert for ekte mot scratch-db (18-hulls bane med tee_rating, 4 spillere for fourball-testing): app/handicap.py (ADR-014 fire brytere via parse_allowance_config, handicap beregnes i compute_and_store_side_handicaps rett etter deltaker-innsetting — singles/fourball per spiller umiddelbart, foursome/greensome/scramble kun når siden er komplett), app/routers/scoring.py (hole-scores/ hole-results-upsert, scorecard-GET, matchstatus-recompute med FOR UPDATE-lås mot race og SAMMENHENGENDE-prefiks-regel for uferdige hull). app/team_authz.py skilt ut fra matches.py (delt med scoring.py). Alle 10 planlagte tester bestått, inkl. fourball better-ball-aggregering (MIN av to nettoer, venter til begge partnere har registrert), poeng-caching ved tidlig avgjort match, og ADR-014-bryteren use_handicap=false. Fant og fikset underveis: tournaments.py sin SessionCreate manglet scoring_mode helt (økter kunne aldri opprettes i hole_result-modus via API-et) — lagt til. Bevisst utelatt/kjente begrensninger: en side som aldri når forventet deltakerantall (no-show) får aldri beregnet handicap og matchen kan da aldri avgjøres — ingen manuell overstyring bygget. Score-skriving er upsert (ingen avvisning ved duplikat) — ingen audit-trail på rettelser. Kapteins-only autorisasjon fortsatt ikke bygget (FEATURE_BACKLOG ); bar er «rostret på laget».
  • Match-lås ved avgjørelse (2026-07-16): submit_hole_score/ submit_hole_result avviser nå 409 hvis match.points_side_a IS NOT NULL (matchen er avgjort) — FØR upserten kjøres, både for nye hull og korrigering av allerede talte hull. Tetter en reell bug: uten dette kunne «spøkelses-hull» lagt inn etter avgjørelse endre en allerede cachet margin ved neste omregning. Automatisk, ingen ny autorisasjon involvert.
  • Ekte autentisering bygget og verifisert (2026-07-16): X-Debug-User-Id- stubben er HELT fjernet (ingen fallback). Magic-link + JWT-sesjon i app/routers/auth.py + app/auth.py (request-link/verify-link/ logout/me), ny migrasjon 004_auth.sql (magic_link_token-tabell + unik e-post-indeks på app_user). Token = secrets.token_urlsafe(32), kun SHA-256-hash lagres, atomisk forbruk (UPDATE ... RETURNING, ikke les-sjekk-skriv), generisk respons uansett om e-posten finnes (unngår enumerering), gamle uforbrukte lenker ugyldiggjøres når en ny utstedes, app_user opprettes FØRST ved vellykket verifisering (ikke ved forespørsel). Sesjons-JWT (PyJWT, algorithms=["HS256"] eksplisitt) i HttpOnly/SameSite=Lax/dynamisk-Secure-cookie, 30 dager, med et ekte eksistens-oppslag mot app_user på hver forespørsel (faktisk tilbakekalling — en slettet bruker kan ikke ri ut sesjonen). Alle 12 planlagte tester bestått. Fant og fikset underveis: ON CONFLICT (email) matchet ikke den nye PARTIELLE unike indeksen uten eksplisitt WHERE email IS NOT NULL (samme klasse feil som hole_scores partielle indekser i scoring-runden). Fant, IKKE fikset her (egen sak, se FEATURE_BACKLOG): organization- tabellens RLS-policy (org_self, og trolig ALLE org_isolation-policyer i 001/003) kaster en 500 i stedet for skjemaets lovede "trygg standard: se ingenting" når current_setting('app.current_org', true) returnerer TOMSTRENG (ikke NULL) — noe som kan skje på en gjenbrukt asyncpg-pool- tilkobling der en tidligere forespørsel satte GUC-en via SET LOCAL. Kun et problem for kode som spør org-scopede tabeller via plain_connection() (ingen org-kontekst) — /auth/me unngår det bevisst ved å ikke joine mot organization. Fiksen (NULLIF(current_setting(...), '')::uuid i alle 15 policyer) er reell, billig, og lav risiko, men berører selve isolasjonsgrunnmuren (ADR-003) og fortjener en egen, fokusert rettingsrunde med skikkelig testing — ikke en hastefiks boltet på noe annet.

Neste steg:

  1. Fiks RLS-tomstreng-buggen beskrevet over (egen liten runde, migrasjon 005) — reell, men isolert og lavrisiko.
  2. Containerisere TeeCup-API-et (Dockerfile + compose-tjeneste), koble mot teecup_db med teecup_app, rute via eksisterende Caddy til teecup.teeoff.no. (Under scratch-verifisering måtte hele /opt/teecup monteres, ikke bare app/, fordi handicap_engine.py er et toppnivå-søskenmodul til app-pakken — Dockerfilen må COPY begge inn med samme relative plassering.) Ekte SMTP-utsending av magic-link må også kobles inn før dette går live (i dag: dev-only logging bak TEECUP_DEV_LOG_MAGIC_LINKS).
  3. Deretter frontend (PWA, offline-first) og kommunikasjon (migrasjon 006, siden 004/005 nå er tatt av auth og RLS-fiksen).