2026-07-16 08:21:57 +02:00
|
|
|
# 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)
|
2026-07-16 14:03:50 +02:00
|
|
|
- `ARCHITECTURE_DECISIONS.md` — hva som er bestemt og hvorfor (ADR-001…014). Fasit.
|
2026-07-16 08:21:57 +02:00
|
|
|
- `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).
|
2026-07-16 09:16:22 +02:00
|
|
|
- 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`.
|
2026-07-16 14:38:42 +02:00
|
|
|
- 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».
|
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
|
|
|
- **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_score`s 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.
|
2026-07-16 08:21:57 +02:00
|
|
|
|
|
|
|
|
Neste steg:
|
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
|
|
|
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
|
2026-07-16 08:21:57 +02:00
|
|
|
`teecup_db` med `teecup_app`, rute via eksisterende Caddy til
|
2026-07-16 14:38:42 +02:00
|
|
|
`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
|
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
|
|
|
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).
|