ADR-017 er skrevet inn, og migrasjonen er ferdig og verifisert:
007_registration_and_player_fields.sql — kjørt rent gjennom hele kjeden 001→007 på en fersk scratch-database, test_isolation.sql fortsatt 12/12. Ett reelt arkitekturproblem løst underveis, ikke bare skjema: et offentlig påmeldingskall kjenner en turnering-id, men ingen org-kontekst — og uten den slipper RLS ingen rader gjennom, heller ikke oppslaget for å finne riktig org. Løst med en snever SECURITY DEFINER-funksjon (public_tournament_org) som kun eksponerer koblingen turnering→org, ingenting annet. Testet presist: kalt som teecup_app-rollen med ingen org-kontekst satt — funksjonen fant riktig org, ga NULL (ikke feil) for en ukjent turnering, og et rått SELECT på samme tilkobling/rolle ga fortsatt 0 rader — beviser at RLS ikke er brutt generelt, bare dette ene smale unntaket finnes. Ikke gjort ennå (bevisst, dette var kun migrasjonssteget): Migrasjonen er ikke kjørt mot ekte teecup_db. Ingen API-endepunkter (offentlig registrerings-router, utvidet players.py).
This commit is contained in:
parent
90bb02e6de
commit
e1ddf13cce
5 changed files with 236 additions and 5 deletions
|
|
@ -187,7 +187,8 @@
|
|||
"Bash(break)",
|
||||
"Bash(rm -f /opt/teecup/.env.scratch /tmp/roster_cookies.txt)",
|
||||
"Bash(curl -s -o /dev/null -w 'dashboard: %{http_code}\\\\n' https://teecup.teeoff.no/dashboard)",
|
||||
"Bash(curl -s -o /dev/null -w 'health: %{http_code}\\\\n' https://teecup.teeoff.no/health)"
|
||||
"Bash(curl -s -o /dev/null -w 'health: %{http_code}\\\\n' https://teecup.teeoff.no/health)",
|
||||
"Bash(docker rm -f teecup_scratch_api >/dev/null 2>&1 *)"
|
||||
],
|
||||
"additionalDirectories": [
|
||||
"/opt/teeoff/deploy",
|
||||
|
|
|
|||
116
007_registration_and_player_fields.sql
Normal file
116
007_registration_and_player_fields.sql
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
-- =====================================================================
|
||||
-- TeeCup — migrasjon 007
|
||||
-- Selvregistrering + utvidet spillerprofil (ADR-017)
|
||||
-- =====================================================================
|
||||
-- Kjøres etter 001-006. Alt additivt (nullable eller default) bortsett fra
|
||||
-- den nye tabellen, som er helt ny.
|
||||
-- =====================================================================
|
||||
|
||||
\set ON_ERROR_STOP on
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 1. Utvidet spillerprofil (ADR-017 Beslutning D)
|
||||
-- ---------------------------------------------------------------------
|
||||
ALTER TABLE player ADD COLUMN mobile text;
|
||||
ALTER TABLE player ADD COLUMN email text;
|
||||
ALTER TABLE player ADD COLUMN birth_date date;
|
||||
ALTER TABLE player ADD COLUMN nickname text;
|
||||
ALTER TABLE player ADD COLUMN country text;
|
||||
ALTER TABLE player ADD COLUMN club text;
|
||||
ALTER TABLE player ADD COLUMN club_member_number text;
|
||||
|
||||
-- Bevisst IKKE en UNIQUE-constraint på e-post: familier deler av og til
|
||||
-- e-post (forelder melder på barn), og en hard unik-regel ville krasje
|
||||
-- akkurat den vanlige situasjonen. E-postmatching ved påmelding/innlogging
|
||||
-- (ADR-017 Beslutning B) er derfor et mykt, applikasjonslags-oppslag, ikke
|
||||
-- en databasegaranti.
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 2. Påmeldingsinnstillinger på turnering (ADR-017 Beslutning C)
|
||||
-- ---------------------------------------------------------------------
|
||||
ALTER TABLE tournament ADD COLUMN registration_deadline timestamptz;
|
||||
|
||||
ALTER TABLE tournament ADD COLUMN registration_capacity integer
|
||||
CHECK (registration_capacity IS NULL OR registration_capacity > 0);
|
||||
|
||||
ALTER TABLE tournament ADD COLUMN registration_overflow_policy text
|
||||
NOT NULL DEFAULT 'closed'
|
||||
CHECK (registration_overflow_policy IN ('waitlist', 'closed'));
|
||||
|
||||
ALTER TABLE tournament ADD COLUMN registration_requires_approval boolean
|
||||
NOT NULL DEFAULT false;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 3. tournament_registration (ADR-017 Beslutning C)
|
||||
--
|
||||
-- Bevisst ATSKILT fra team_roster: en registrering betyr "vil kanskje
|
||||
-- spille", team_roster betyr "committed til et bestemt lag". Organisator
|
||||
-- forfremmer en registrering til roster via de eksisterende
|
||||
-- team-/roster-endepunktene -- denne tabellen påvirker ALDRI team_roster
|
||||
-- automatisk.
|
||||
-- ---------------------------------------------------------------------
|
||||
CREATE TABLE tournament_registration (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
organization_id uuid NOT NULL,
|
||||
tournament_id uuid NOT NULL,
|
||||
player_id uuid NOT NULL,
|
||||
status text NOT NULL DEFAULT 'pending'
|
||||
CHECK (status IN ('pending', 'confirmed', 'waitlisted', 'declined', 'withdrawn')),
|
||||
registered_at timestamptz NOT NULL DEFAULT now(),
|
||||
-- Bevis for at samtykke faktisk ble gitt ved innsending, ikke bare
|
||||
-- antatt (ADR-017). API-laget skal avvise innsending uten samtykke --
|
||||
-- NOT NULL her er den siste linjen av forsvar, ikke den eneste.
|
||||
consent_given_at timestamptz NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
FOREIGN KEY (organization_id, tournament_id)
|
||||
REFERENCES tournament(organization_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (organization_id, player_id)
|
||||
REFERENCES player(organization_id, id) ON DELETE RESTRICT,
|
||||
UNIQUE (organization_id, id), -- for sammensatte FK-er
|
||||
UNIQUE (tournament_id, player_id) -- én registrering per spiller per turnering
|
||||
);
|
||||
|
||||
CREATE INDEX ON tournament_registration (organization_id, tournament_id);
|
||||
|
||||
-- RLS: samme org-isolasjon som resten (ADR-003). Skrevet direkte med
|
||||
-- app_current_org() siden denne tabellen kommer etter 005-fiksen -- ingen
|
||||
-- grunn til å reprodusere tomstreng-buggen bare for å rette den igjen.
|
||||
ALTER TABLE tournament_registration ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE tournament_registration FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY org_isolation ON tournament_registration
|
||||
USING (organization_id = app_current_org())
|
||||
WITH CHECK (organization_id = app_current_org());
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON tournament_registration TO teecup_app;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 4. public_tournament_org() -- den ENESTE broen fra en uautentisert
|
||||
-- forespørsel (ingen app.current_org satt ennå) til riktig org-kontekst
|
||||
-- (ADR-017 Beslutning A).
|
||||
--
|
||||
-- SECURITY DEFINER: kjører med skaperens rettigheter (teeoff_admin, som
|
||||
-- har BYPASSRLS) slik at oppslaget kan lese `tournament` FØR RLS-
|
||||
-- konteksten er kjent -- det er selve poenget, ikke et hull. Eksponerer
|
||||
-- KUN organization_id for en gitt turnering-id, ingenting annet fra
|
||||
-- tournament-raden. `SET search_path = public` hindrer search_path-
|
||||
-- kapring (standard herding for SECURITY DEFINER-funksjoner).
|
||||
--
|
||||
-- Bruksmønster i app-laget: slå opp org_id via denne FØRST, åpne deretter
|
||||
-- en vanlig org_connection(org_id) og fortsett med normal RLS-håndhevelse
|
||||
-- for alt det faktiske arbeidet (sjekk frist/kapasitet, sett inn
|
||||
-- registrering). Samme "løs kontekst-problemet FØR RLS kan håndheve
|
||||
-- noe"-mønster som selvrefererende org-bootstrap (app/routers/
|
||||
-- organizations.py), nå for et lese-oppslag i stedet for en innsetting.
|
||||
-- ---------------------------------------------------------------------
|
||||
CREATE FUNCTION public_tournament_org(p_tournament_id uuid)
|
||||
RETURNS uuid
|
||||
LANGUAGE sql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
STABLE
|
||||
AS $$
|
||||
SELECT organization_id FROM tournament WHERE id = p_tournament_id;
|
||||
$$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public_tournament_org(uuid) TO teecup_app;
|
||||
|
|
@ -405,6 +405,92 @@ sin port er ikke lenger tenkt nåbar direkte utenfra i prod.
|
|||
|
||||
---
|
||||
|
||||
## ADR-017 — Selvregistrering og utvidet spillerprofil
|
||||
|
||||
**Kontekst:** Reist av brukeren rett etter at lag/roster-skjermen var live.
|
||||
Dagens modell antar at organisator kjenner og legger inn alle spillere selv
|
||||
— i praksis vet organisator ofte ikke hvem som faktisk blir med før de
|
||||
melder seg på selv.
|
||||
|
||||
**Beslutning A — Påmelding er offentlig, krever IKKE innlogging.** En
|
||||
delbar lenke (`/register/{tournament_id}` — turneringens UUID er allerede
|
||||
uforutsigelig nok, ingen ny token-mekanisme) viser et minimalt skjema. Ingen
|
||||
magic-link, ingen konto kreves for å melde seg på.
|
||||
|
||||
**Begrunnelse:** Å kreve innlogging FØR man kan melde seg på er unødvendig
|
||||
friksjon for "jeg blir med lørdag"-bruksmønsteret. Kontosammenkobling skjer
|
||||
gratis senere (Beslutning B), ikke som et eget steg i selve påmeldingen.
|
||||
|
||||
**Beslutning B — E-post er sammenkoblingsnøkkelen** mellom en organisator-
|
||||
forhåndsopprettet `player`-rad og en spiller som senere melder seg selv på
|
||||
eller logger inn. Finnes det en `player`-rad i org-en med samme e-post ved
|
||||
påmelding, fylles manglende felt inn på DEN raden i stedet for å opprette en
|
||||
duplikat. `player.user_id` kobles først når noen med matchende e-post
|
||||
faktisk logger inn via magic-link — `verify_magic_link` utvides til også å
|
||||
slå opp org-scopede `player`-rader på e-post, ikke bare `app_user`.
|
||||
|
||||
**Konsekvens:** `player.email` er bevisst IKKE en `UNIQUE`-constraint —
|
||||
familier deler av og til e-post (forelder melder på barn), en hard unik-
|
||||
regel ville krasje akkurat den vanlige situasjonen. Matching er et mykt,
|
||||
applikasjonslags-oppslag.
|
||||
|
||||
**Beslutning C — Påmelding er et eget, lettvekts steg, atskilt fra
|
||||
`team_roster`**, med konfigurerbar godkjenning, kapasitet og samtykke. Ny
|
||||
tabell `tournament_registration` (status: `pending`/`confirmed`/
|
||||
`waitlisted`/`declined`/`withdrawn`). Tre nye felt på `tournament`:
|
||||
`registration_capacity` (nullable — organisators valg om det i det hele
|
||||
tatt skal være en grense), `registration_overflow_policy`
|
||||
(`waitlist`/`closed`, kun relevant når kapasitet er satt),
|
||||
`registration_requires_approval` (boolean). Rekkefølge ved en ny
|
||||
påmelding: (1) er fristen passert → avvis; (2) er kapasitet nådd →
|
||||
`waitlisted` eller avvis, avhengig av policy; (3) ellers `pending` eller
|
||||
`confirmed`, avhengig av godkjenningsbryteren.
|
||||
|
||||
**Begrunnelse:** `team_roster` betyr i dag "committed til et bestemt lag".
|
||||
Å blande "vil kanskje spille" med "spiller garantert" i samme tabell ville
|
||||
gjort det umulig å skille en påmeldt-men-ikke-plukket spiller fra en som
|
||||
aldri var interessert. Kapasitet og godkjenning er to reelle, uavhengige
|
||||
organisator-beslutninger — å låse ett svar for alle turneringer ville vært
|
||||
feil for minst noen av dem (brukeren bekreftet eksplisitt: begge skal være
|
||||
konfigurerbare valg, ikke faste regler).
|
||||
|
||||
**Beslutning D — Utvidet spillerprofil + samtykke.** Nye felt på `player`:
|
||||
`mobile`, `email`, `birth_date` (IKKE alder — alder blir feil neste år,
|
||||
fødselsdato er ikke det), `nickname`, `country`, `club`,
|
||||
`club_member_number`. Samtykke er obligatorisk ved påmelding — API-et
|
||||
avviser innsending (400) uten `consent: true`, og
|
||||
`tournament_registration.consent_given_at` er beviset på at det faktisk ble
|
||||
gitt, ikke bare antatt. Samtykket lever på REGISTRERINGEN, ikke på
|
||||
`player`, fordi det er selve påmeldingshandlingen for DENNE turneringen
|
||||
samtykket knytter seg til.
|
||||
|
||||
**Ikke et nytt felt:** "utslagssted for anledningen" er sannsynligvis
|
||||
allerede dekket av `match_participant.tee_id` (per match, ikke per
|
||||
spillerprofil, siden det kan variere fra runde til runde).
|
||||
|
||||
**Beslutning E — `public_tournament_org()`: den eneste broen fra en
|
||||
uautentisert forespørsel til riktig RLS-kontekst.** Et offentlig
|
||||
påmeldingskall kjenner en turnering-id, men ikke organisasjonen den hører
|
||||
til — og uten `app.current_org` satt slipper RLS ingen rader gjennom
|
||||
(heller ikke selve oppslaget for å FINNE riktig org). Løst med en snever
|
||||
`SECURITY DEFINER`-SQL-funksjon (`p_tournament_id -> organization_id`, kjørt
|
||||
med skaperens BYPASSRLS-rettigheter, `SET search_path = public` mot
|
||||
kapring) — samme "løs kontekst-problemet FØR RLS kan håndheve noe"-mønster
|
||||
som den selvrefererende org-bootstrapen (`app/routers/organizations.py`),
|
||||
nå for et lese-oppslag i stedet for en innsetting.
|
||||
|
||||
**Konsekvens:** App-laget slår opp org-id via denne funksjonen FØRST, åpner
|
||||
deretter en vanlig `org_connection(org_id)` og fortsetter med normal
|
||||
RLS-håndhevelse for alt det faktiske arbeidet. Funksjonen eksponerer KUN en
|
||||
uuid->uuid-kobling, ingenting annet fra `tournament`-raden — et bevisst
|
||||
smalt unntak, ikke en generell RLS-omgåelse. Ethvert fremtidig offentlig
|
||||
(uautentisert) endepunkt som trenger å slå opp org-kontekst fra en kjent
|
||||
ressurs-id bør gjenbruke akkurat dette mønsteret, ikke finne opp et nytt.
|
||||
|
||||
**Migrasjon:** `007_registration_and_player_fields.sql`.
|
||||
|
||||
---
|
||||
|
||||
## Åpne spørsmål (ikke besluttet ennå)
|
||||
|
||||
Disse må avklares før eller under de relevante fasene:
|
||||
|
|
|
|||
35
CLAUDE.md
35
CLAUDE.md
|
|
@ -3,7 +3,7 @@
|
|||
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…016). Fasit.
|
||||
- `ARCHITECTURE_DECISIONS.md` — hva som er bestemt og hvorfor (ADR-001…017). 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.
|
||||
|
|
@ -367,14 +367,41 @@ Ferdig og verifisert:
|
|||
→ 200, `teeoff.no` upåvirket. Selve skrive-flyten på `/tournaments/[id]`
|
||||
(opprett lag/roster) ikke testet med ekte data i denne runden — venter på
|
||||
brukeren, samme mønster som dashboard-rundens skrive-test.
|
||||
- **ADR-017 + migrasjon 007 (2026-07-18):** brukeren reiste selvregistrering
|
||||
rett etter at roster-skrive-flyten var bekreftet. Full ADR skrevet
|
||||
(5 beslutninger: offentlig påmelding uten innlogging, e-post som
|
||||
sammenkoblingsnøkkel mot forhåndsopprettede spillere, egen
|
||||
`tournament_registration`-tabell atskilt fra `team_roster` med
|
||||
konfigurerbar godkjenning/kapasitet/venteliste, utvidet spillerprofil +
|
||||
obligatorisk samtykke, og `public_tournament_org()`). Migrasjon
|
||||
`007_registration_and_player_fields.sql` skrevet og scratch-verifisert
|
||||
(001→007 kjører rent, `test_isolation.sql` fortsatt 12/12).
|
||||
**Reelt arkitekturproblem løst underveis, ikke bare skjema:** et
|
||||
offentlig (uautentisert) påmeldingskall kjenner en turnering-id, men
|
||||
ingen org-kontekst — og uten `app.current_org` slipper RLS ingen rader
|
||||
gjennom, heller ikke oppslaget for å FINNE riktig org. Løst med en snever
|
||||
`SECURITY DEFINER`-funksjon (`public_tournament_org`) som KUN eksponerer
|
||||
uuid→uuid-koblingen. **Verifisert presist, ikke bare "kjørte uten feil":**
|
||||
kalt funksjonen som `teecup_app`-rollen med ingen `app.current_org` satt
|
||||
— ga korrekt org-id for en kjent turnering, `NULL` (ikke feil) for en
|
||||
ukjent — OG et RÅTT `SELECT` på `tournament` på SAMME tilkobling/rolle ga
|
||||
fortsatt 0 rader, som beviser RLS ikke er brutt generelt, bare dette ene
|
||||
smale unntaket eksisterer.
|
||||
**Kun migrasjon + ADR i denne runden** — API-endepunkter (offentlig
|
||||
registrerings-router, utvidede `players.py`-felt) og frontend
|
||||
(påmeldingsskjema) er IKKE bygget ennå, og migrasjonen er IKKE kjørt mot
|
||||
ekte `teecup_db`.
|
||||
|
||||
Neste steg:
|
||||
1. Flere V0-skjermer (økt/program, blind draw, scorekort, leaderboard) —
|
||||
1. ADR-017: kjøre migrasjon 007 mot ekte `teecup_db` (venter på brukerens
|
||||
eksplisitte bekreftelse), bygge API-et (offentlig registrerings-router,
|
||||
utvidet `players.py`), så et påmeldingsskjema i frontend.
|
||||
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
|
||||
pakkes ut over eksisterende filer, og sjekk om V0-skjermen bygger inn
|
||||
handlinger backend ikke støtter ennå FØR integrering (se
|
||||
dashboard-/roster-rundene over for hvorfor begge er nødvendige hver
|
||||
gang).
|
||||
2. PWA-egenskaper (manifest, service worker, offline-cache) — ikke startet.
|
||||
3. Kommunikasjon (chat/feed) — ikke startet.
|
||||
3. PWA-egenskaper (manifest, service worker, offline-cache) — ikke startet.
|
||||
4. Kommunikasjon (chat/feed) — ikke startet.
|
||||
|
|
|
|||
|
|
@ -38,6 +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 | 🔨 | ADR-017. Migrasjon 007 skrevet + scratch-verifisert. API/frontend gjenstår. |
|
||||
| Roster: endre kaptein / fjern spiller (`PATCH`/`DELETE`) | ✅ | `app/routers/tournaments.py`. Bevisst ingen "kun én kaptein"-håndhevelse ennå — se «Brukerroller»-punktet under. |
|
||||
|
||||
---
|
||||
|
|
|
|||
Loading…
Reference in a new issue