teecup/FEATURE_BACKLOG.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

187 lines
10 KiB
Markdown

# TeeCup — Funksjons-backlog
> Formål: fange ALT som er ønsket for TeeCup på ett sted, slik at intet krav går
> tapt mellom økter, modeller eller samtaler. Kilder: de to opprinnelige
> Gemini-samtalene (funksjonalitet + Docker) og arbeidet gjort med Claude.
>
> Status-koder: ✅ ferdig · 🔨 pågår · 📋 planlagt/fanget · ❓ trenger beslutning
> · 🔀 endret fra opprinnelig råd · 💤 utsatt (bevisst)
>
> Sist oppdatert: 2026-07-16
---
## Fundament (bygget)
| Funksjon | Status | Notat |
|---|---|---|
| Handicap-/matchmotor (testet) | ✅ | 24 tester, R&A-verifisert. Erstatter Geminis løse JS-funksjon. |
| Formater: singles, fourball, foursome, greensome, scramble | ✅ | I motoren, allowance som konfig. |
| Brutto/netto + prosent-allowance (75 %, 90 %, 3/4) | ✅ | ADR-005. Prosentene bekreftet mot Golf GameBook (ADR-014). |
| 9-hulls (front/back) slagfordeling | ✅ | ADR-008, egen funksjon + test. |
| Miksede formater per turnering (økter) | ✅ | ADR-007. Ryder Cup-strukturen. |
| Spiller-pool m/ reserver, delvis deltakelse | ✅ | ADR-007. |
| Multi-tenant skjema + RLS | ✅ | ADR-001/003. Isolasjon bevist med oppførselstest. |
| Dedikert app-rolle (ingen superuser/BYPASSRLS) | ✅ | migrasjon 002. |
| API: oppsett (spillere/lag/roster/økter/matcher/blind draw) | ✅ | Verifisert for ekte mot scratch-db + engangscontainer. |
| API: scoring (hole_score/match_hole_result, matchstatus, handicap-beregning) | ✅ | ADR-012/014. Verifisert for ekte, inkl. fourball better-ball og race-sikker recompute. |
| Banedata fra teeoff via API | 🔀 | ADR-004. Endret fra Geminis «delt database direkte». |
| Konfigurerbar handicap-pipeline (4 brytere) | ✅ | ADR-014. Bygget i `app/handicap.py`, brukt av scoring-runden. |
| Ekte autentisering (magic-link + JWT-sesjon) | ✅ | ADR-009. `app/routers/auth.py` + migrasjon `004_auth.sql`. `X-Debug-User-Id`-stubben er helt fjernet. Ekte SMTP-utsending gjenstår (i dag: dev-only logging). |
---
### RLS-tomstreng-bug (funnet 2026-07-16, IKKE fikset ennå)
- **Status:** ❓ trenger egen rettingsrunde (lavrisiko, men berører ADR-003s
isolasjonsgrunnmur — fortjener fokusert testing, ikke en hastefiks).
- Alle RLS-policyer i 001/003 (`org_isolation` på 13 tabeller + `org_self`
`organization`) bruker `current_setting('app.current_org', true)::uuid`.
Denne håndterer NULL trygt (gir ingen rader, som tiltenkt), men IKKE
tomstreng — og en gjenbrukt asyncpg-pool-tilkobling der en TIDLIGERE
forespørsel satte GUC-en via `SET LOCAL` kan lese den tilbake som `''`
(tomstreng) i stedet for NULL etter at den transaksjonen er ferdig. Da
kaster casten en 500 (`invalid input syntax for type uuid: ""`) i stedet
for skjemaets lovede "trygg standard: se ingenting".
- **Oppdaget av:** `/auth/me` (ny denne runden) prøvde å joine mot
`organization`-tabellen via `plain_connection()` (ingen org-kontekst) for å
hente organisasjonsnavn til en multi-org-liste — det er FØRSTE gang noe
spør en org-scopet, RLS-beskyttet tabell via en tilkobling uten
org-kontekst. Mitigert MIDLERTIDIG i `/auth/me` ved rett og slett å ikke
joine mot `organization` (returnerer kun `organization_id` + `role`, ikke
navn) — unngår buggen, løser den ikke.
- **Fiks:** `NULLIF(current_setting('app.current_org', true), '')::uuid` i
stedet for `current_setting(...)::uuid`, i alle 15 policyer (`ALTER
POLICY`, egen migrasjon 005). NULLIF konverterer tomstreng til NULL FØR
cast, så den trygge "se ingenting"-oppførselen gjenopprettes uansett hvilken
tilstand GUC-en er i.
- **Følgeoppgave når fikset:** `/auth/me` kan da trygt joine mot
`organization` igjen og returnere organisasjonsnavn, ikke bare ID+rolle.
---
## Ønsket, men IKKE fanget før nå (fra Gemini-samtalene)
### Brukerroller (utover org-medlemskap)
- **Status:** ❓ trenger beslutning
- De opprinnelige samtalene beskriver: turneringsadmin, **lagkaptein**, spiller,
**tilskuer** (les-only, følger live uten skriverettigheter).
- Vi har i dag org-roller (owner/admin/member) + `is_captain` på roster.
- **Mangler:** «tilskuer» som begrep. Henger sammen med hvem som ser den
offentlige feeden (se Kommunikasjon). Kaptein-rollen bør kanskje gi spesifikke
rettigheter (sette oppstilling), ikke bare være et flagg.
### Scoring-autorisasjon: hvem fører, hvem korrigerer, hvem lukker (rejst 2026-07-16)
- **Status:** ❓ trenger beslutning — direkte oppfølger av «Brukerroller» over.
- **Hvem fører score i dag:** alle med en `team_roster`-rad på laget (samme
minimale grense som deltaker/lås, se ADR-013-relatert kode). Ikke
kaptein-only, ikke begrenset til de(n) som faktisk spiller matchen.
- **Hvem kan korrigere en ført score i dag:** akkurat de samme — skriving er en
upsert (`ON CONFLICT ... DO UPDATE`), så en korrigering er ikke skilt fra en
førstegangs-innføring. Ingen godkjenning fra motstanderen, ingen audit-trail
(forrige verdi overskrives sporløst).
- **Match-lås ved avgjørelse: ✅ FIKSET 2026-07-16.** `submit_hole_score` og
`submit_hole_result` (`app/routers/scoring.py`) avviser nå med 409 («Matchen
er avgjort og kan ikke lenger endres.») FØR upserten kjøres, hvis
`match.points_side_a IS NOT NULL` — dette er allerede et pålitelig,
entydig signal siden kolonnen kun settes når `compute_match_state` sier
matchen er avgjort. Gjelder BÅDE nye hull og korrigering av allerede talte
hull. Verifisert: hull etter avgjørelse avvist, korrigering av et
talt hull avvist, GET scorecard fortsatt leselig, ikke-avgjorte matcher
upåvirket. **Bevisst IKKE bygget:** ingen manuell «avslutt match før den
er avgjort»-handling (det grenser mot walkover under, fortsatt utsatt).
Turnering har et `status`-felt (draft/active/completed/archived) men
INGEN endepunkt endrer det ennå — organisator kan ikke markere en turnering
ferdig via API-et i dag (eget, senere punkt).
- **Manglende WO/konsesjon:** hvis en side aldri stiller nok spillere
(`match_participant`-antallet når aldri det økten krever), beregnes
handicap ALDRI (se `app/handicap.py`), og matchen kan derfor ALDRI få et
hull-resultat — den blir hengende uavgjort for alltid. Det finnes ingen
«gi bort hullet/matchen/turneringen»-mekanisme (walkover/konsesjon) i det
hele tatt ennå — verken datamodell eller endepunkt.
- **Avgjort 2026-07-16:**
- **Match-lås ved avgjørelse: ✅ bygget** — se eget punkt over. Automatisk
(ikke en handling noen utfører), så «hvem får låse» ble aldri et
spørsmål som trengte avklaring.
- **Walkover/konsesjon VENTER** til brukerroller (kaptein/organisator,
punktet over) er avgjort — å bygge den nå på dagens løse
«rostret på laget»-grense betyr sannsynligvis å bygge den om senere.
- **Fortsatt åpent:** (a) skal score-føring begrenses til faktiske
matchdeltakere (ikke bare «noen på laget»)? (b) skal korrigering kreve
motpartens godkjenning, eller er upsert-modellen god nok for v1? (c) skal
turnering-status (draft/active/completed/archived) kunne settes via API?
### Blind draw (skjult lagoppstilling)
- **Status:** ✅ skjema (migrasjon 003, `lineup_lock`) + API bygget og verifisert
(`app/routers/matches.py`: synlighetsfilter i SQL, ikke Python-filter — se ADR-013).
- Kapteinene låser oppstillingen skjult; matchene avsløres samtidig når begge er
ferdige.
- **Gjenstår:** hvem som FÅR låse et lag er i dag bare «rostret på laget», ikke
kaptein-spesifikt — se «Brukerroller» over.
### Forenklet scoreføring (uten slagtall)
- **Status:** ✅ skjema (migrasjon 003) + API bygget og verifisert
(`app/routers/scoring.py`: `hole-scores` for `stroke`-modus,
`hole-results` for `hole_result`-modus, begge mater samme `compute_match_state`).
### Bøtekasse (Kangaroo Court)
- **Status:** 📋 planlagt
- Meld inn overtramp med bøter («kastet kølla på hull 4»).
- Henger sammen med Kommunikasjon: bøter deles i feeden. Sannsynligvis en egen
post-type i meldingsmodellen.
### Flerårig statistikk / historikk / MVP
- **Status:** 📋 planlagt (datamodell støtter det allerede)
- Seiersprosent i singelmatcher, historisk MVP, statistikk over år.
- Datamodellen tillater dette fordi spillere lever på org-nivå og gjenbrukes på
tvers av turneringer, og handicap fryses per turnering (reproduserbart).
### Push-varsler
- **Status:** 📋 planlagt (infrastruktur)
- «BREAKING: X vant matchen». PWA push. Egen infrastruktur-bit.
### Sanntid (WebSockets)
- **Status:** ❓ trenger beslutning
- Live leaderboard og chat som oppdateres uten refresh. Vi har leaderboard-viewet,
men ikke sanntidsleveringen. Valg: WebSockets vs. polling.
### Knockout / cup-turnering (egen turneringstype)
- **Status:** 💤 utsatt (egen fremtidig type, ADR-011)
- Utslagsmatcher: 128 spillere → finale + bronsefinale. Rundene er *avhengige*
(vinner går videre), krever bracket/progresjon, seeding, fripass. IKKE i v1.
- v1 er låst til nøyaktig to lag (Ryder Cup-format). Match-modellen er generell, så
denne typen kan legges til senere uten dataomskriving — den legger bare til et
progresjons-lag.
---
## Kommunikasjon (under design)
| Del | Status | Notat |
|---|---|---|
| Lag-intern chat («det hemmelige rommet») | 🔨 | Bekreftet ønsket. Kanal m/ scope `team`. |
| Offentlig runde-feed («Banter Board») | ❓ | Synlighetsnivå ikke besluttet (deltakere/org/offentlig lenke). |
| Bilder i feed/chat | 📋 | v1. Objektlagring (MinIO), presigned opplasting. |
| Video | 💤 | Arkitekt for det, bygg senere (ADR-forslag). |
| 1-til-1 direktemeldinger | 💤 | Gemini frarådet for v1; ikke etterspurt av deg. |
| Moderering (for offentlig innhold) | ❓ | Kreves hvis «alle med lenken». Mønster finnes i teeoff. |
---
## UX / frontend (senere fase)
- 📋 Høy kontrast, dark/light, store +/- knapper, stor «Neste hull»-knapp
(banebruk i sollys/med solbriller).
- ✅ Offline-first (ADR-006) — prinsipp besluttet; implementasjon senere.
- 📋 PWA: manifest, service workers, «Legg til på hjemskjerm».
---
## Bevisst endret fra opprinnelige (Gemini-)råd
- 🔀 **Banedata:** API mot teeoff (ADR-004), IKKE direkte delt database. Direkte
DB-kobling ville låst TeeCup til teeoffs skjemaendringer.
- 🔀 **Handicap-motor:** egen testet Python-modul, ikke den innlimte JS-funksjonen
(som bl.a. ikke håndterte 9-hull eller konfigurerbare allowances korrekt).
- 🔀 **Tenant-modell:** organisasjon som tenant med RLS, ikke bare «turnering-ID».
- 🔀 **Sesjons-secret:** egne secrets for TeeCup, ikke fallback til teeoffs
(teeoff selv bruker en slik fallback — bevisst unngått her).