diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 3614851..86dd4c1 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -13,7 +13,9 @@ "Bash(docker cp *)", "Bash(tee /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/schema_run.log)", "Bash(echo \"EXIT:$?\")", - "Read(//tmp/**)" + "Read(//tmp/**)", + "Bash(git add *)", + "Bash(git commit *)" ] } } diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..414f966 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,58 @@ +# 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…013). 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). +- API-skjelett i `app/` (org-kontekst via SET LOCAL, medlemskapssjekk). Kun + syntaks-verifisert — ikke kjørt ennå (mangler container med avhengigheter). + +Neste steg: +1. Containerisere TeeCup-API-et (Dockerfile + compose-tjeneste), koble mot + `teecup_db` med `teecup_app`, rute via eksisterende Caddy til + `teecup.teeoff.no`. +2. API-endepunkter: oppsett (turnering/lag/økter/roster/blind draw) og scoring + (slag eller hull-resultat → matchstatus fra motoren). +3. Deretter frontend (PWA, offline-first) og kommunikasjon (migrasjon 004). diff --git a/__pycache__/handicap_engine.cpython-312.pyc b/__pycache__/handicap_engine.cpython-312.pyc index 025aacc..f263e6b 100644 Binary files a/__pycache__/handicap_engine.cpython-312.pyc and b/__pycache__/handicap_engine.cpython-312.pyc differ diff --git a/test_isolation.sql b/test_isolation.sql index 1ccb925..3cb6999 100644 --- a/test_isolation.sql +++ b/test_isolation.sql @@ -5,7 +5,9 @@ -- sett fra runtime-rollen teecup_app (som IKKE er superuser/BYPASSRLS). -- -- Kjør som admin/superbruker mot en scratch- eller test-database der --- 001_initial_schema.sql og 002_roles_and_grants.sql allerede er kjørt: +-- 001_initial_schema.sql, 002_roles_and_grants.sql og +-- 003_scoring_and_blinddraw.sql allerede er kjørt (003 kreves for +-- match_hole_result/lineup_lock, testet her i tillegg til grunnskjemaet): -- psql -d teecup_scratch -f test_isolation.sql -- -- Alt kjøres i én transaksjon som RULLES TILBAKE til slutt — ingen testdata @@ -26,6 +28,52 @@ INSERT INTO tournament (organization_id, name) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'A-Cup'), ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'B-Cup'); +-- --- Seed nok struktur til å teste match_hole_result og lineup_lock --- +-- (ADR-012/013, migrasjon 003). Fortsatt privilegert rolle, RLS omgås her. +INSERT INTO course (id, organization_id, name) VALUES + ('c1111111-1111-1111-1111-111111111111', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'A-banen'), + ('c2222222-2222-2222-2222-222222222222', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'B-banen'); + +INSERT INTO team (id, organization_id, tournament_id, name) + SELECT 'de111111-1111-1111-1111-111111111111', t.organization_id, t.id, 'A rødt' + FROM tournament t WHERE t.organization_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'; +INSERT INTO team (id, organization_id, tournament_id, name) + SELECT 'de222222-2222-2222-2222-222222222222', t.organization_id, t.id, 'A blått' + FROM tournament t WHERE t.organization_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'; +INSERT INTO team (id, organization_id, tournament_id, name) + SELECT 'df111111-1111-1111-1111-111111111111', t.organization_id, t.id, 'B rødt' + FROM tournament t WHERE t.organization_id = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'; +INSERT INTO team (id, organization_id, tournament_id, name) + SELECT 'df222222-2222-2222-2222-222222222222', t.organization_id, t.id, 'B blått' + FROM tournament t WHERE t.organization_id = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'; + +INSERT INTO session (id, organization_id, tournament_id, sequence, format, course_id) + SELECT 'e5111111-1111-1111-1111-111111111111', t.organization_id, t.id, 1, 'singles', + 'c1111111-1111-1111-1111-111111111111' + FROM tournament t WHERE t.organization_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'; +INSERT INTO session (id, organization_id, tournament_id, sequence, format, course_id) + SELECT 'e5222222-2222-2222-2222-222222222222', t.organization_id, t.id, 1, 'singles', + 'c2222222-2222-2222-2222-222222222222' + FROM tournament t WHERE t.organization_id = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'; + +INSERT INTO match (id, organization_id, session_id, sequence, team_a_id, team_b_id) VALUES + ('fa111111-1111-1111-1111-111111111111', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + 'e5111111-1111-1111-1111-111111111111', 1, + 'de111111-1111-1111-1111-111111111111', 'de222222-2222-2222-2222-222222222222'), + ('fa222222-2222-2222-2222-222222222222', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', + 'e5222222-2222-2222-2222-222222222222', 1, + 'df111111-1111-1111-1111-111111111111', 'df222222-2222-2222-2222-222222222222'); + +INSERT INTO match_hole_result (organization_id, match_id, hole_number, winning_side) VALUES + ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'fa111111-1111-1111-1111-111111111111', 1, 'a'), + ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'fa222222-2222-2222-2222-222222222222', 1, 'b'); + +INSERT INTO lineup_lock (organization_id, session_id, team_id) VALUES + ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'e5111111-1111-1111-1111-111111111111', + 'de111111-1111-1111-1111-111111111111'), + ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'e5222222-2222-2222-2222-222222222222', + 'df111111-1111-1111-1111-111111111111'); + -- --- Bytt til runtime-rollen: nå SKAL RLS gjelde --------------------- SET ROLE teecup_app; SELECT set_config('app.current_org', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', false); @@ -79,11 +127,88 @@ BEGIN RAISE NOTICE 'OK Test 4: kontekstbytte gir riktig organisasjons data (%).', nm; END $$; +-- --- Tilbake til Org A-kontekst for de nye tabellene (ADR-012/013) ---- +SELECT set_config('app.current_org', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', false); + +-- Test 5: match_hole_result — ser bare Org A sin rad (1, ikke 2). +DO $$ +DECLARE n int; +BEGIN + SELECT count(*) INTO n FROM match_hole_result; + IF n <> 1 THEN + RAISE EXCEPTION 'ISOLASJON FEILET (match_hole_result lesing): ser % rader, forventet 1', n; + END IF; + RAISE NOTICE 'OK Test 5: match_hole_result — ser kun egen organisasjons rad (%).', n; +END $$; + +-- Test 6: match_hole_result — kan IKKE sette inn en rad merket med annen org, +-- selv om match_id-en (fra org B) i seg selv er FK-gyldig. +DO $$ +BEGIN + BEGIN + INSERT INTO match_hole_result (organization_id, match_id, hole_number, winning_side) + VALUES ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', + 'fa222222-2222-2222-2222-222222222222', 2, 'b'); + RAISE EXCEPTION 'ISOLASJON FEILET (match_hole_result skriving): fikk sette inn data for annen org'; + EXCEPTION + WHEN insufficient_privilege THEN + RAISE NOTICE 'OK Test 6: match_hole_result — WITH CHECK blokkerte kryss-org innsetting.'; + END; +END $$; + +-- Test 7: lineup_lock — ser bare Org A sin rad (1, ikke 2). +DO $$ +DECLARE n int; +BEGIN + SELECT count(*) INTO n FROM lineup_lock; + IF n <> 1 THEN + RAISE EXCEPTION 'ISOLASJON FEILET (lineup_lock lesing): ser % rader, forventet 1', n; + END IF; + RAISE NOTICE 'OK Test 7: lineup_lock — ser kun egen organisasjons rad (%).', n; +END $$; + +-- Test 8: lineup_lock — kan IKKE sette inn en rad merket med annen org, +-- selv om session_id/team_id (fra org B) i seg selv er FK-gyldige. +DO $$ +BEGIN + BEGIN + INSERT INTO lineup_lock (organization_id, session_id, team_id) + VALUES ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', + 'e5222222-2222-2222-2222-222222222222', + 'df222222-2222-2222-2222-222222222222'); + RAISE EXCEPTION 'ISOLASJON FEILET (lineup_lock skriving): fikk sette inn data for annen org'; + EXCEPTION + WHEN insufficient_privilege THEN + RAISE NOTICE 'OK Test 8: lineup_lock — WITH CHECK blokkerte kryss-org innsetting.'; + END; +END $$; + +-- Test 9: kontekstbytte til Org B gir Org Bs rader for begge nye tabeller. +SELECT set_config('app.current_org', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', false); +DO $$ +DECLARE ws team_side; +DECLARE tid uuid; +BEGIN + SELECT winning_side INTO ws FROM match_hole_result; + IF ws <> 'b' THEN + RAISE EXCEPTION 'ISOLASJON FEILET (match_hole_result kontekstbytte): forventet b, fikk %', ws; + END IF; + RAISE NOTICE 'OK Test 9a: match_hole_result — kontekstbytte gir riktig organisasjons data (%).', ws; + + SELECT team_id INTO tid FROM lineup_lock; + IF tid <> 'df111111-1111-1111-1111-111111111111' THEN + RAISE EXCEPTION 'ISOLASJON FEILET (lineup_lock kontekstbytte): forventet Org Bs lag, fikk %', tid; + END IF; + RAISE NOTICE 'OK Test 9b: lineup_lock — kontekstbytte gir riktig organisasjons data (%).', tid; +END $$; + RESET ROLE; RESET app.current_org; ROLLBACK; -- ingen testdata blir liggende igjen \echo '============================================' -\echo ' Alle isolasjonstester bestått (se OK-linjer)' +\echo ' Alle 9 isolasjonstester bestått (se OK-linjer)' +\echo ' Dekker: tournament/organization (001) samt' +\echo ' match_hole_result/lineup_lock (003, ADR-012/013)' \echo '============================================'