From bb3b19560c10e6b712288e251755797a4656a6ca Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Sat, 18 Jul 2026 09:03:37 +0200 Subject: [PATCH] =?UTF-8?q?Migrasjon=20008=20er=20live=20p=C3=A5=20ekte=20?= =?UTF-8?q?teecup=5Fdb,=20backend=20er=20redeployet,=20alt=20verifisert=20?= =?UTF-8?q?(teeoff.no=20up=C3=A5virket=20gjennom=20hele=20prosessen).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-017 sin backend er nå komplett: registrerings-API-et og kontosammenkoblingen fungerer sammen — en spiller organisator la inn på forhånd, i én eller flere klubber, kobles automatisk til riktig konto første gang hen logger inn, uten å bli medlem av noe hun ikke ba om. --- .claude/settings.local.json | 3 ++- 008_link_player_by_email.sql | 35 +++++++++++++++++++++++++++++++++++ CLAUDE.md | 34 +++++++++++++++++++++++++--------- FEATURE_BACKLOG.md | 2 +- app/routers/auth.py | 7 +++++++ 5 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 008_link_player_by_email.sql diff --git a/.claude/settings.local.json b/.claude/settings.local.json index d8624b4..f74955d 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -193,7 +193,8 @@ "Bash(sed -i \"s#^TEECUP_DB_NAME=.*#TEECUP_DB_NAME=teecup_scratch#\" .env.scratch)", "Bash(python3 -c \"import sys,json;print\\(json.load\\(sys.stdin\\)['confirmed_count']\\)\")", "Bash(rm -f /opt/teecup/.env.scratch /tmp/owner_cookies.txt)", - "Bash(curl -s https://teecup.teeoff.no/public/tournaments/00000000-0000-0000-0000-000000000000)" + "Bash(curl -s https://teecup.teeoff.no/public/tournaments/00000000-0000-0000-0000-000000000000)", + "Bash(docker exec -i teeoff_db psql -U teeoff_admin -d teecup_scratch -v ON_ERROR_STOP=1 < /opt/teecup/test_isolation.sql 2>&1 | tail -8 *)" ], "additionalDirectories": [ "/opt/teeoff/deploy", diff --git a/008_link_player_by_email.sql b/008_link_player_by_email.sql new file mode 100644 index 0000000..f679a74 --- /dev/null +++ b/008_link_player_by_email.sql @@ -0,0 +1,35 @@ +-- ===================================================================== +-- TeeCup — migrasjon 008 +-- E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B) +-- ===================================================================== +-- Kjøres etter 001-007. +-- +-- Kontekst: en spiller kan ha `player`-rader (organisator-opprettet eller +-- selvregistrert) i FLERE organisasjoner, uten å være MEDLEM av noen av +-- dem. Ved innlogging (magic-link -- e-postkontroll er allerede bevist) +-- skal enhver `player`-rad med matchende e-post og `user_id IS NULL` +-- kobles til den nå innloggede kontoen, uansett hvilken org den tilhører. +-- +-- Samme "løs kontekst-problemet FØR RLS kan håndheve noe"-mønster som +-- `public_tournament_org()` (migrasjon 007): en vanlig org-scopet +-- UPDATE via org_connection() kan ikke krysse organisasjonsgrenser -- +-- brukeren er jo nettopp IKKE nødvendigvis medlem av noen av dem. +-- SECURITY DEFINER kjører derfor med skaperens BYPASSRLS-rettigheter, +-- akkurat som i 007. `SET search_path = public` hindrer search_path- +-- kapring (samme herding). +-- ===================================================================== + +\set ON_ERROR_STOP on + +CREATE FUNCTION link_player_by_email(p_user_id uuid, p_email text) +RETURNS void +LANGUAGE sql +SECURITY DEFINER +SET search_path = public +AS $$ + UPDATE player + SET user_id = p_user_id + WHERE user_id IS NULL AND lower(email) = lower(p_email); +$$; + +GRANT EXECUTE ON FUNCTION link_player_by_email(uuid, text) TO teecup_app; diff --git a/CLAUDE.md b/CLAUDE.md index 19f854d..8473beb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -421,17 +421,33 @@ Ferdig og verifisert: offentlige endepunktet bekreftet nåbart over ekte https (ukjent turnering-id ga korrekt `404`/`NOT_FOUND`, ikke-destruktiv sjekk — selve påmeldingsflyten med ekte data ikke testet mot prod i denne runden). - **Gjenstår:** e-post-basert `player.user_id`-kobling ved innlogging - (ADR-017 Beslutning B sin andre halvdel — krever en egen - `SECURITY DEFINER`-funksjon på tvers av org-er, altså en ny migrasjon - 008, siden 007 allerede er kjørt mot prod). Frontend-påmeldingsskjema og - landingssider er egen, senere ADR-runde (se `FEATURE_BACKLOG.md`). + **E-post-basert kontosammenkobling LIVE, samme dag:** ny migrasjon + `008_link_player_by_email.sql` — `link_player_by_email(user_id, email)`, + samme `SECURITY DEFINER`-mønster som `public_tournament_org()` (007), + denne gangen for en tverr-org UPDATE i stedet for et lese-oppslag. + `verify_magic_link` (`app/routers/auth.py`) kaller den på HVER + innlogging (idempotent — funksjonens `WHERE user_id IS NULL` gjør + gjentatte kall til en no-op), ikke bare ved førstegangsopprettelse. + **Verifisert presist:** to separate org-er, hver med sin egen + organisator-opprettede "Kari"-rad (samme e-post, ulik store/små + bokstaver for å teste case-insensitivitet også) — ved Karis FØRSTE + innlogging ble BEGGE radene koblet til kontoen hennes, i to org-er hun + aldri har vært medlem av. Eksplisitt bekreftet: `organization_membership` + har NULL rader for henne etterpå — ren identitetskobling, ingen + privilegie-eskalering (å ha `player.user_id` satt gir ingen ny tilgang + gjennom `get_authorized_org`, som fortsatt krever ekte org-medlemskap + uavhengig av dette). Andre innlogging idempotent, ingen feil. + `test_isolation.sql` fortsatt 12/12. **Kjørt mot ekte `teecup_db` + 2026-07-18**, bruker bekreftet eksplisitt, backend redeployet, live + sjekker OK, `teeoff.no` upåvirket. + **ADR-017s backend er dermed komplett** (registrering + kontokobling). + Gjenstående: frontend-påmeldingsskjema og landingssider — egen, senere + ADR-runde (se `FEATURE_BACKLOG.md`). Neste steg: -1. ADR-017: e-post-basert kontosammenkobling ved innlogging (migrasjon 008 - + `verify_magic_link`-utvidelse), så landingssider (turnering/org) med - påmeldingsskjema i - frontend — se brukerens landingsside-spørsmål, samme økt. +1. Landingssider (turnering/org) med påmeldingsskjema i frontend — egen + ADR-runde (synlighetsvalg offentlig/org/deltakere, slug-URL for org, + MinIO for bilder — se `FEATURE_BACKLOG.md`). 2. Flere V0-skjermer (økt/program, blind draw, scorekort, leaderboard) — samme mønster: design i V0 (fortsett i samme prosjekt), FORVENT en full re-eksport hver gang — diff mot live-treet i et scratch-område før noe diff --git a/FEATURE_BACKLOG.md b/FEATURE_BACKLOG.md index 3d45556..d64e908 100644 --- a/FEATURE_BACKLOG.md +++ b/FEATURE_BACKLOG.md @@ -38,7 +38,7 @@ | Frontend: innlogging + verifisering, LIVE | ✅ | ADR-016. Next.js på `teecup.teeoff.no`, ekte magic-link-flyt bevist med reell e-post. Se egen seksjon under. | | Frontend: dashboard (org-bytter/-opprettelse + turneringsliste), LIVE | ✅ | `/dashboard`. Kablet mot `/auth/me`, `/orgs`, `/orgs/{id}/tournaments`. Skrive-flyt bekreftet med ekte data (org "Tjøme Gents" + turnering opprettet av bruker). | | Frontend: lag/roster-skjerm, LIVE | ✅ | `/tournaments/[id]`. To nye backend-endepunkter bygget samtidig (`PATCH`/`DELETE` roster). Skrive-flyt ikke testet med ekte data ennå. | -| Selvregistrering + utvidet spillerprofil (API) | ✅ | ADR-017. `app/routers/registration.py` (offentlig, uautentisert), utvidet `players.py`. Live. Frontend-påmeldingsskjema/landingssider gjenstår (egen ADR-runde). | +| Selvregistrering + utvidet spillerprofil (API) | ✅ | ADR-017. `app/routers/registration.py` (offentlig, uautentisert), utvidet `players.py`, e-post-basert `player.user_id`-kobling ved innlogging. Backend komplett og live. Frontend-påmeldingsskjema/landingssider gjenstår (egen ADR-runde). | | Roster: endre kaptein / fjern spiller (`PATCH`/`DELETE`) | ✅ | `app/routers/tournaments.py`. Bevisst ingen "kun én kaptein"-håndhevelse ennå — se «Brukerroller»-punktet under. | --- diff --git a/app/routers/auth.py b/app/routers/auth.py index fa2231c..2246dcb 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -161,6 +161,13 @@ async def verify_magic_link( # her, kun ved førstegangsopprettelse (se modul-docstring). user_id = await conn.fetchval("SELECT id FROM app_user WHERE email = $1", email) + # Koble enhver player-rad (i HVILKEN SOM HELST org, uansett + # medlemskap) med matchende e-post til denne nå-innloggede kontoen + # (ADR-017 Beslutning B). Trygt å kjøre på HVER innlogging, ikke + # bare førstegangsopprettelse -- funksjonens WHERE-ledd berører kun + # rader som ennå ikke er koblet, så gjentatte kall er en no-op. + await conn.execute("SELECT link_player_by_email($1, $2)", user_id, email) + user_row = await conn.fetchrow( """ SELECT id::text AS id, email::text AS email, display_name, preferred_locale