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.
10 KiB
10 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 ekteteecup_dbuten 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 somteeoff_admin/superuser i runtime.
Arkitektur-invarianter (ikke bryt uten en ny ADR)
- Tenant = organisasjon.
organization_idpå alle domenetabeller, håndhevet av RLS. App-koden setterapp.current_orgmedSET LOCALper 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+ roller002+ scoring/blind draw003. Isolasjon bevist medtest_isolation.sql(RLS-oppførsel, ikke bare at skjemaet kjører). - 002 hadde en reell bug (psql interpolerer ikke
:'var'inne iDO $$...$$) — 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 medFOR UPDATE-lås),matches.py(matcher/ deltakere/blind draw-lås, ADR-013-synlighet push-down i SQL). Delt feiloversettelse iapp/errors.py, delte synlighetsspørringer iapp/blind_draw.py.main.pyer 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 viaparse_allowance_config, handicap beregnes icompute_and_store_side_handicapsrett 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 medFOR UPDATE-lås mot race og SAMMENHENGENDE-prefiks-regel for uferdige hull).app/team_authz.pyskilt ut framatches.py(delt medscoring.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-bryterenuse_handicap=false. Fant og fikset underveis:tournaments.pysinSessionCreatemangletscoring_modehelt (økter kunne aldri opprettes ihole_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_resultavviser nå 409 hvismatch.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 iapp/routers/auth.py+app/auth.py(request-link/verify-link/logout/me), ny migrasjon004_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_useropprettes 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 motapp_userpå 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 eksplisittWHERE email IS NOT NULL(samme klasse feil somhole_scores partielle indekser i scoring-runden). Fant, IKKE fikset i denne runden (egen runde rett etterpå — se under):organization-tabellens RLS-policy kastet en 500 i stedet for skjemaets lovede "trygg standard: se ingenting" ved tomstreng-GUC. - RLS-tomstreng-bug FIKSET (2026-07-16): ny migrasjon
005_rls_null_guard.sql— deltSTABLESQL-funksjonapp_current_org()gjørNULLIF(current_setting('app.current_org', true), '')::uuidi stedet for det rå uttrykket, brukt av alle 15 RLS-policyer (ALTER POLICY, 14org_isolation+org_self). Verifisert med 3 nye regresjonstester itest_isolation.sql(Test 10-12) OG ved faktisk å gjenskape original- buggen mot en ekte container (pool-størrelse 1, varm opp medorg_connection(), deretter/auth/mepå samme gjenbrukte tilkobling — gikk fra 500 til 200). Viktig presisering fra denne runden: fiksen gjør IKKE at/auth/mekan joineorganizationdirekte viaplain_connection()— det var en feilaktig antakelse i forrige runde.org_selfkrever fortsatt en MATCHENDEapp.current_orgfor å vise en rad (riktig RLS-design, ikke noe fiksen skulle endre), og en bruker kan tilhøre flere organisasjoner samtidig, så det finnes ingen ÉN kontekst å sette for en tverr-org-spørring./auth/meslår derfor opp hvert org-navn ett om gangen viaorg_connection()(N+1, N = antall org-er brukeren tilhører) — dette er riktig løsning, ikke en omvei. - Organisasjon-bootstrap bygget og verifisert (2026-07-16): nytt
POST /orgs(app/routers/organizations.py) — det ENESTE stedet i API-et som setter inn enorganization-rad. Fant under statusgjennomgang at dette manglet helt (alle tidligere org-er var seedet med superbruker-SQL). Ingen ny migrasjon. Selvrefererende RLS-bootstrap bekreftet å fungere: generer org-ens uuid i Python, settapp.current_orgtil nøyaktig den via eksisterendeorg_connection(), sett innorganization-raden med samme id —org_selfs implisitteWITH CHECKblir da trivielt sann, ingen privilegert tilkobling nødvendig (i motsetning til hva 002s kommentar antydet). Verifisert med 5 tester inkl. en negativ kontroll (mismatchende id avvist medinsufficient_privilege) og full kryss-org-isolasjon mellom to uavhengig opprettede organisasjoner. - Ekte SMTP-utsending bygget og verifisert (2026-07-16): ny
app/email.py(send_magic_link_email,smtplibviaasyncio.to_thread, håndterer både implisitt TLS/port 465 og STARTTLS dynamisk). Brukeren la egne SMTP-credentials i.env(TEECUP_SMTP_*,TEECUP_FROM_EMAIL— ADR-009, ikke delt med teeoff); jeg leste kun nøkkelnavnene for å bekrefte de fantes, aldri verdiene.app/config.pysinSMTP_CONFIGUREDer valgfri (ikke_required) — dev-only logging (TEECUP_DEV_LOG_MAGIC_LINKS) fortsatt fungerer uendret når SMTP ikke er satt opp. Driftsfeil i utsendingen lekker aldri til klientresponsen (bevarer anti-enumerering). Verifisert med faktisk levering: sendte én ekte test-e-post til en adresse brukeren oppga — brukeren bekreftet mottak. Første gang noe i prosjektet er bevist ved ekte levering, ikke bare curl/scratch.
Neste steg:
- Containerisere TeeCup-API-et (Dockerfile + compose-tjeneste), koble mot
teecup_dbmedteecup_app, rute via eksisterende Caddy tilteecup.teeoff.no. (Under scratch-verifisering måtte hele/opt/teecupmonteres, ikke bareapp/, fordihandicap_engine.pyer et toppnivå-søskenmodul tilapp-pakken — Dockerfilen måCOPYbegge inn med samme relative plassering.) - Deretter frontend (PWA, offline-first) og kommunikasjon (migrasjon 006, siden 004/005 nå er tatt av auth og RLS-fiksen). Frontend er fortsatt IKKE startet (⬜ i utviklingsplanen i ARCHITECTURE_DECISIONS.md) — API-et alene er ikke en brukbar nettside.