Migrasjon 008 er live på ekte teecup_db, backend er redeployet, alt verifisert (teeoff.no upåvirket gjennom hele prosessen).

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.
This commit is contained in:
Erol Haagenrud 2026-07-18 09:03:37 +02:00
parent 05a451ff75
commit bb3b19560c
5 changed files with 70 additions and 11 deletions

View file

@ -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",

View file

@ -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;

View file

@ -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

View file

@ -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. |
---

View file

@ -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