Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
"""
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
Offentlig, uautentisert registrerings- og landingsside-API (ADR-017, ADR-018).
|
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
Bevisst egen fil med prefiks /public/..., atskilt fra /orgs/... -- gjør
|
|
|
|
|
sikkerhetsgrensen eksplisitt i koden: disse endepunktene krever VERKEN
|
|
|
|
|
innlogging (get_current_user) ELLER org-medlemskap (get_authorized_org) for
|
|
|
|
|
å svare i det hele tatt.
|
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
To SECURITY DEFINER-broer (public_tournament_org, public_org_by_slug) er den
|
|
|
|
|
ENESTE måten disse endepunktene får vite riktig RLS-kontekst FØR den er
|
|
|
|
|
kjent -- se ADR-017 Beslutning E / ADR-018 Beslutning F.
|
|
|
|
|
|
|
|
|
|
VIKTIG (ADR-018 Beslutning B): RLS (org_isolation) beskytter kun TENANT-
|
|
|
|
|
grenser (org A ser aldri org B), ikke INNHOLDS-synlighet innenfor riktig
|
|
|
|
|
org-kontekst. `tournament.visibility` håndheves derfor eksplisitt her i
|
|
|
|
|
applikasjonslaget, på hvert lese- OG skrive-endepunkt (registrering følger
|
|
|
|
|
samme grense som lesing, ADR-018 Beslutning D) -- RLS alene løser det ikke.
|
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from datetime import date, datetime, timezone
|
|
|
|
|
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
from fastapi import APIRouter, Depends
|
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
from pydantic import BaseModel, EmailStr, Field
|
|
|
|
|
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
from ..auth import CurrentUser, get_current_user_optional
|
2026-07-19 22:51:07 +02:00
|
|
|
from ..blind_draw import locked_team_ids
|
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
from ..db import org_connection, plain_connection
|
|
|
|
|
from ..errors import app_error, translate_db_errors
|
MinIO-runden er ferdig, verifisert og live — ADR-018 er nå helt komplett, ingenting utsatt igjen bortsett fra selve opplastings-skjermen i frontend.
Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd.
To reelle feil funnet under scratch-verifisering, aldri i produksjon:
pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker.
MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur.
Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet.
Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden.
Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført.
Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
2026-07-18 11:25:52 +02:00
|
|
|
from .. import storage
|
Update Todos
Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere
Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster)
Bygge HCP-historikk over tid
Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort:
Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder".
Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match).
Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
|
|
|
from ..team_authz import user_is_tournament_participant
|
2026-08-21 19:50:35 +02:00
|
|
|
from .individual_tournaments import LeaderboardEntry, RoundCellOut, _compute_individual_standings
|
2026-07-19 22:51:07 +02:00
|
|
|
from .matches import MatchOut, fetch_matches
|
|
|
|
|
from .scoring import Scorecard, fetch_scorecard
|
|
|
|
|
from .tournaments import Leaderboard, SessionOut, _fetch_sessions, fetch_leaderboard
|
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/public/tournaments", tags=["public-registration"])
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
org_router = APIRouter(prefix="/public/orgs", tags=["public-registration"])
|
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
|
|
|
|
|
|
2026-07-19 22:26:45 +02:00
|
|
|
async def resolve_org(tournament_id: str) -> str:
|
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
org_id = await conn.fetchval("SELECT public_tournament_org($1)", tournament_id)
|
|
|
|
|
if org_id is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
|
|
|
|
|
return str(org_id)
|
|
|
|
|
|
|
|
|
|
|
2026-07-19 22:26:45 +02:00
|
|
|
async def is_participant(conn, user_id: str, organization_id: str, tournament_id: str) -> bool:
|
Update Todos
Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere
Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster)
Bygge HCP-historikk over tid
Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort:
Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder".
Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match).
Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
|
|
|
"""Tynn wrapper -- selve sjekken flyttet til team_authz.py 2026-07-21
|
|
|
|
|
(`user_is_tournament_participant`) slik at org-scopede endepunkter
|
|
|
|
|
(matches.py/scoring.py/tournaments.py) kan gjenbruke den uten sirkulær
|
|
|
|
|
import. Beholder denne funksjonens navn/argumentrekkefølge uendret her
|
|
|
|
|
for å unngå å røre alle kallsteder i denne filen."""
|
|
|
|
|
return await user_is_tournament_participant(conn, organization_id, tournament_id, user_id)
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
|
|
|
|
|
|
2026-07-19 22:26:45 +02:00
|
|
|
def code_matches(actual_join_code: str, provided_code: str | None) -> bool:
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
return provided_code is not None and provided_code.strip().lower() == actual_join_code.lower()
|
|
|
|
|
|
|
|
|
|
|
2026-07-19 22:26:45 +02:00
|
|
|
async def check_visibility(
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
conn,
|
|
|
|
|
visibility: str,
|
|
|
|
|
organization_id: str,
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
user: CurrentUser | None,
|
|
|
|
|
) -> None:
|
|
|
|
|
if visibility == "public":
|
|
|
|
|
return
|
|
|
|
|
if user is not None:
|
|
|
|
|
is_member = await conn.fetchval(
|
|
|
|
|
"SELECT EXISTS (SELECT 1 FROM organization_membership WHERE user_id = $1 AND organization_id = $2)",
|
|
|
|
|
user.user_id,
|
|
|
|
|
organization_id,
|
|
|
|
|
)
|
|
|
|
|
if is_member:
|
|
|
|
|
return
|
Update Todos
Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro
Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting
Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere
Frontend: profil-seksjon i /account
Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx
Scratch-verifisere alt (15 sjekker bestått)
Typesjekket frontend-build
ADR-031 + .md-oppdatering
Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering:
Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes.
"Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted.
Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før.
Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg.
Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
|
|
|
# ADR-031: en faktisk DELTAKER skal aldri stenges ute av øktens
|
|
|
|
|
# egen turnering, uansett hvilken av de to ikke-offentlige tierene
|
|
|
|
|
# som er valgt -- visibility styrer eksponering mot UTENFORSTÅENDE,
|
|
|
|
|
# ikke mot spillere som faktisk er med. Utvidet fra kun
|
|
|
|
|
# visibility == "participants" (opprinnelig ADR-018 Beslutning C)
|
|
|
|
|
# da "Mine runder"-dashbordet gjorde det tydelig at 'org' (som er
|
|
|
|
|
# DEFAULT for enhver ny turnering) ellers ville stengt ute enhver
|
|
|
|
|
# spiller uten organisasjonsmedlemskap fra sin egen turnering.
|
|
|
|
|
if await is_participant(conn, user.user_id, organization_id, tournament_id):
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
return
|
|
|
|
|
raise app_error(403, "NOT_VISIBLE", "Du har ikke tilgang til denne turneringen.")
|
|
|
|
|
|
|
|
|
|
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
class JoinCodeResult(BaseModel):
|
|
|
|
|
tournament_id: str
|
|
|
|
|
organization_id: str
|
|
|
|
|
tournament_name: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/by-code/{code}", response_model=JoinCodeResult)
|
|
|
|
|
async def resolve_join_code(code: str) -> JoinCodeResult:
|
|
|
|
|
"""Løser en invitasjonskode til riktig turnering (ADR-020 Beslutning A) --
|
|
|
|
|
MÅ registreres FØR /{tournament_id} under, ellers ville "by-code" blitt
|
|
|
|
|
tolket som en tournament_id (ugyldig UUID -> 500, ikke 404).
|
|
|
|
|
|
2026-07-19 22:26:45 +02:00
|
|
|
Bevisst UAVHENGIG av tournament.visibility -- ingen check_visibility()-
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
kall her, i motsetning til alle andre endepunkter i denne filen. Koden
|
|
|
|
|
ER selve invitasjonen; å likevel kreve org-medlemskap/deltakerstatus
|
|
|
|
|
ville gjort den verdiløs nettopp for personer som mangler det."""
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
tournament_id = await conn.fetchval("SELECT public_tournament_by_code($1)", code)
|
|
|
|
|
if tournament_id is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Fant ingen turnering med denne koden.")
|
|
|
|
|
|
2026-07-19 22:26:45 +02:00
|
|
|
organization_id = await resolve_org(str(tournament_id))
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
name = await conn.fetchval("SELECT name FROM tournament WHERE id = $1", tournament_id)
|
|
|
|
|
return JoinCodeResult(
|
|
|
|
|
tournament_id=str(tournament_id), organization_id=organization_id, tournament_name=name
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
class SponsorOut(BaseModel):
|
|
|
|
|
id: str
|
|
|
|
|
name: str
|
|
|
|
|
url: str | None
|
MinIO-runden er ferdig, verifisert og live — ADR-018 er nå helt komplett, ingenting utsatt igjen bortsett fra selve opplastings-skjermen i frontend.
Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd.
To reelle feil funnet under scratch-verifisering, aldri i produksjon:
pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker.
MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur.
Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet.
Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden.
Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført.
Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
2026-07-18 11:25:52 +02:00
|
|
|
logo_url: str | None
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
|
|
|
|
|
|
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
class PublicTournamentInfo(BaseModel):
|
|
|
|
|
id: str
|
|
|
|
|
name: str
|
|
|
|
|
organization_name: str
|
|
|
|
|
status: str
|
2026-08-21 19:44:13 +02:00
|
|
|
# 2026-08-21: manglet her -- landingssiden/"Følg live"-lenken kunne
|
|
|
|
|
# derfor ikke skille individuell fra Cup-format offentlig, og rutet
|
|
|
|
|
# ALLTID til Cup-sidens /t/{id}/live (som feilet stille for en
|
|
|
|
|
# individuell turnering, se ARCHITECTURE_DECISIONS.md/CHANGELOG.md
|
|
|
|
|
# samme dag).
|
|
|
|
|
format_type: str
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
visibility: str
|
|
|
|
|
description: str | None
|
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
start_date: date | None
|
|
|
|
|
end_date: date | None
|
|
|
|
|
registration_open: bool
|
|
|
|
|
registration_deadline: datetime | None
|
|
|
|
|
registration_capacity: int | None
|
|
|
|
|
confirmed_count: int
|
MinIO-runden er ferdig, verifisert og live — ADR-018 er nå helt komplett, ingenting utsatt igjen bortsett fra selve opplastings-skjermen i frontend.
Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd.
To reelle feil funnet under scratch-verifisering, aldri i produksjon:
pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker.
MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur.
Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet.
Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden.
Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført.
Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
2026-07-18 11:25:52 +02:00
|
|
|
hero_image_url: str | None
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
sponsors: list[SponsorOut]
|
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/{tournament_id}", response_model=PublicTournamentInfo)
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
async def get_public_tournament(
|
|
|
|
|
tournament_id: str,
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
code: str | None = None,
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
user: CurrentUser | None = Depends(get_current_user_optional),
|
|
|
|
|
) -> PublicTournamentInfo:
|
2026-07-19 22:26:45 +02:00
|
|
|
organization_id = await resolve_org(tournament_id)
|
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
"""
|
|
|
|
|
SELECT t.id::text, t.name, o.name AS organization_name, t.status::text,
|
2026-08-21 19:44:13 +02:00
|
|
|
t.format_type::text, t.visibility, t.description, t.hero_image_key, t.join_code,
|
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
t.start_date, t.end_date, t.registration_deadline,
|
|
|
|
|
t.registration_capacity,
|
|
|
|
|
(SELECT count(*)::int FROM tournament_registration tr
|
|
|
|
|
WHERE tr.tournament_id = t.id AND tr.status IN ('confirmed', 'pending')
|
|
|
|
|
) AS confirmed_count
|
|
|
|
|
FROM tournament t
|
|
|
|
|
JOIN organization o ON o.id = t.organization_id
|
|
|
|
|
WHERE t.id = $1
|
|
|
|
|
""",
|
|
|
|
|
tournament_id,
|
|
|
|
|
)
|
|
|
|
|
if row is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
|
|
|
|
|
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
# ADR-020 Beslutning A: en matchende kode overstyrer visibility helt.
|
2026-07-19 22:26:45 +02:00
|
|
|
if not code_matches(row["join_code"], code):
|
|
|
|
|
await check_visibility(conn, row["visibility"], organization_id, tournament_id, user)
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
|
|
|
|
|
sponsor_rows = await conn.fetch(
|
MinIO-runden er ferdig, verifisert og live — ADR-018 er nå helt komplett, ingenting utsatt igjen bortsett fra selve opplastings-skjermen i frontend.
Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd.
To reelle feil funnet under scratch-verifisering, aldri i produksjon:
pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker.
MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur.
Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet.
Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden.
Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført.
Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
2026-07-18 11:25:52 +02:00
|
|
|
"SELECT id::text, name, url, logo_key FROM tournament_sponsor WHERE tournament_id = $1 ORDER BY created_at",
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
tournament_id,
|
|
|
|
|
)
|
|
|
|
|
|
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
deadline = row["registration_deadline"]
|
|
|
|
|
deadline_passed = deadline is not None and deadline < datetime.now(timezone.utc)
|
|
|
|
|
return PublicTournamentInfo(
|
|
|
|
|
id=row["id"],
|
|
|
|
|
name=row["name"],
|
|
|
|
|
organization_name=row["organization_name"],
|
|
|
|
|
status=row["status"],
|
2026-08-21 19:44:13 +02:00
|
|
|
format_type=row["format_type"],
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
visibility=row["visibility"],
|
|
|
|
|
description=row["description"],
|
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
start_date=row["start_date"],
|
|
|
|
|
end_date=row["end_date"],
|
|
|
|
|
registration_open=not deadline_passed,
|
|
|
|
|
registration_deadline=deadline,
|
|
|
|
|
registration_capacity=row["registration_capacity"],
|
|
|
|
|
confirmed_count=row["confirmed_count"],
|
MinIO-runden er ferdig, verifisert og live — ADR-018 er nå helt komplett, ingenting utsatt igjen bortsett fra selve opplastings-skjermen i frontend.
Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd.
To reelle feil funnet under scratch-verifisering, aldri i produksjon:
pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker.
MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur.
Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet.
Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden.
Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført.
Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
2026-07-18 11:25:52 +02:00
|
|
|
hero_image_url=storage.public_url(row["hero_image_key"]) if row["hero_image_key"] else None,
|
|
|
|
|
sponsors=[
|
|
|
|
|
SponsorOut(
|
|
|
|
|
id=r["id"],
|
|
|
|
|
name=r["name"],
|
|
|
|
|
url=r["url"],
|
|
|
|
|
logo_url=storage.public_url(r["logo_key"]) if r["logo_key"] else None,
|
|
|
|
|
)
|
|
|
|
|
for r in sponsor_rows
|
|
|
|
|
],
|
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
@router.get("/{tournament_id}/sessions", response_model=list[SessionOut])
|
|
|
|
|
async def get_public_sessions(
|
|
|
|
|
tournament_id: str,
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
code: str | None = None,
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
user: CurrentUser | None = Depends(get_current_user_optional),
|
|
|
|
|
) -> list[SessionOut]:
|
2026-07-19 22:26:45 +02:00
|
|
|
organization_id = await resolve_org(tournament_id)
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
async with org_connection(organization_id) as conn:
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
"SELECT visibility, join_code FROM tournament WHERE id = $1", tournament_id
|
|
|
|
|
)
|
|
|
|
|
if row is None:
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
|
2026-07-19 22:26:45 +02:00
|
|
|
if not code_matches(row["join_code"], code):
|
|
|
|
|
await check_visibility(conn, row["visibility"], organization_id, tournament_id, user)
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
return await _fetch_sessions(conn, tournament_id)
|
|
|
|
|
|
|
|
|
|
|
2026-08-21 19:44:13 +02:00
|
|
|
# --- Individuelle turneringer, offentlig "Følg live" (2026-08-21) ---------
|
|
|
|
|
#
|
|
|
|
|
# Cup-formatets tilskuer-rolle over har eksistert siden ADR-026 -- denne
|
|
|
|
|
# siden manglet helt for individuelle turneringer (bevisst utsatt,
|
|
|
|
|
# FEATURE_BACKLOG.md "Flaggturnering Del B", 2026-08-14: "egen, større
|
|
|
|
|
# oppgave hvis/når etterspurt"). Samme synlighets-/kode-mønster som resten
|
|
|
|
|
# av filen, ingen ny mekanisme.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PublicTournamentRound(BaseModel):
|
|
|
|
|
id: str
|
|
|
|
|
sequence: int
|
|
|
|
|
name: str
|
|
|
|
|
course_name: str
|
|
|
|
|
scheduled_at: datetime | None
|
|
|
|
|
# Utledet (ingen status-kolonne finnes på tournament_round selv) --
|
|
|
|
|
# samme "har noen påbegynt et hull ennå"-logikk som
|
|
|
|
|
# _attach_stroke_play_columns sin today_sequence-utregning bruker
|
|
|
|
|
# internt, generalisert til å gjelde HVER runde (ikke bare den seneste)
|
|
|
|
|
# og lagt til en "alle ferdig"-gren for "completed".
|
|
|
|
|
status: str # "not_started" | "in_progress" | "completed"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_HOLES_BY_CONFIG = {"full_18": 18, "front_9": 9, "back_9": 9}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/{tournament_id}/rounds", response_model=list[PublicTournamentRound])
|
|
|
|
|
async def get_public_rounds(
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
code: str | None = None,
|
|
|
|
|
user: CurrentUser | None = Depends(get_current_user_optional),
|
|
|
|
|
) -> list[PublicTournamentRound]:
|
|
|
|
|
organization_id = await resolve_org(tournament_id)
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
"SELECT visibility, join_code FROM tournament WHERE id = $1", tournament_id
|
|
|
|
|
)
|
|
|
|
|
if row is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
|
|
|
|
|
if not code_matches(row["join_code"], code):
|
|
|
|
|
await check_visibility(conn, row["visibility"], organization_id, tournament_id, user)
|
|
|
|
|
|
|
|
|
|
rounds = await conn.fetch(
|
|
|
|
|
"""
|
|
|
|
|
SELECT tr.id::text, tr.sequence, tr.name, tr.hole_config::text,
|
|
|
|
|
c.name AS course_name, tr.scheduled_at
|
|
|
|
|
FROM tournament_round tr
|
|
|
|
|
JOIN course c ON c.id = tr.course_id
|
|
|
|
|
WHERE tr.tournament_id = $1
|
|
|
|
|
ORDER BY tr.sequence
|
|
|
|
|
""",
|
|
|
|
|
tournament_id,
|
|
|
|
|
)
|
|
|
|
|
progress = await conn.fetch(
|
|
|
|
|
"""
|
|
|
|
|
SELECT trs.tournament_round_id::text,
|
|
|
|
|
count(*) FILTER (WHERE trs.holes_played > 0) AS started,
|
|
|
|
|
count(*) FILTER (WHERE trs.holes_played >= h.hole_count) AS completed,
|
|
|
|
|
count(*) AS total
|
|
|
|
|
FROM tournament_round_score trs
|
|
|
|
|
JOIN tournament_round tr ON tr.id = trs.tournament_round_id
|
|
|
|
|
JOIN LATERAL (SELECT count(*) AS hole_count FROM hole WHERE hole.course_id = tr.course_id) h ON true
|
|
|
|
|
WHERE tr.tournament_id = $1
|
|
|
|
|
GROUP BY trs.tournament_round_id
|
|
|
|
|
""",
|
|
|
|
|
tournament_id,
|
|
|
|
|
)
|
|
|
|
|
progress_by_round = {r["tournament_round_id"]: r for r in progress}
|
|
|
|
|
|
|
|
|
|
def status_for(round_id: str) -> str:
|
|
|
|
|
p = progress_by_round.get(round_id)
|
|
|
|
|
if p is None or p["started"] == 0:
|
|
|
|
|
return "not_started"
|
|
|
|
|
if p["completed"] == p["total"] and p["total"] > 0:
|
|
|
|
|
return "completed"
|
|
|
|
|
return "in_progress"
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
PublicTournamentRound(
|
|
|
|
|
id=r["id"],
|
|
|
|
|
sequence=r["sequence"],
|
|
|
|
|
name=r["name"] or f"Runde {r['sequence']}",
|
|
|
|
|
course_name=r["course_name"],
|
|
|
|
|
scheduled_at=r["scheduled_at"],
|
|
|
|
|
status=status_for(r["id"]),
|
|
|
|
|
)
|
|
|
|
|
for r in rounds
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PublicLeaderboardEntry(BaseModel):
|
|
|
|
|
tournament_participant_id: str
|
|
|
|
|
player_name: str
|
|
|
|
|
rounds_played: int
|
|
|
|
|
gross_total: int | None
|
|
|
|
|
net_total: int | None
|
|
|
|
|
stableford_total: int | None
|
|
|
|
|
copenhagen_total: int | None
|
|
|
|
|
bbb_total: int | None
|
|
|
|
|
class_id: str | None
|
|
|
|
|
class_name: str | None
|
|
|
|
|
position: str | None
|
|
|
|
|
is_leader: bool
|
|
|
|
|
today_label: str | None
|
|
|
|
|
thru_label: str | None
|
|
|
|
|
total_label: str | None
|
|
|
|
|
rank: int | None
|
|
|
|
|
cut: bool
|
|
|
|
|
status: str
|
|
|
|
|
eclectic_total: int | None
|
|
|
|
|
handicap_index: float | None
|
2026-08-21 19:50:35 +02:00
|
|
|
rounds: list[RoundCellOut]
|
2026-08-21 19:44:13 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"/{tournament_id}/individual-leaderboard", response_model=list[PublicLeaderboardEntry]
|
|
|
|
|
)
|
|
|
|
|
async def get_public_individual_leaderboard(
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
code: str | None = None,
|
|
|
|
|
user: CurrentUser | None = Depends(get_current_user_optional),
|
|
|
|
|
) -> list[PublicLeaderboardEntry]:
|
|
|
|
|
organization_id = await resolve_org(tournament_id)
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
"SELECT visibility, join_code FROM tournament WHERE id = $1", tournament_id
|
|
|
|
|
)
|
|
|
|
|
if row is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
|
|
|
|
|
if not code_matches(row["join_code"], code):
|
|
|
|
|
await check_visibility(conn, row["visibility"], organization_id, tournament_id, user)
|
|
|
|
|
|
|
|
|
|
_, entries = await _compute_individual_standings(conn, tournament_id)
|
|
|
|
|
|
|
|
|
|
# HCP-indeks hentes separat (bevisst valgt synlig her, jf. samme
|
|
|
|
|
# presedens som frittstående runders offentlige leaderboard) --
|
|
|
|
|
# LeaderboardEntry selv har den ikke, kun tournament_participant.
|
|
|
|
|
hcp_rows = await conn.fetch(
|
|
|
|
|
"SELECT id::text, handicap_index_snapshot FROM tournament_participant WHERE tournament_id = $1",
|
|
|
|
|
tournament_id,
|
|
|
|
|
)
|
|
|
|
|
hcp_by_participant = {r["id"]: r["handicap_index_snapshot"] for r in hcp_rows}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
PublicLeaderboardEntry(
|
|
|
|
|
tournament_participant_id=e.tournament_participant_id,
|
|
|
|
|
player_name=e.player_name,
|
|
|
|
|
rounds_played=e.rounds_played,
|
|
|
|
|
gross_total=e.gross_total,
|
|
|
|
|
net_total=e.net_total,
|
|
|
|
|
stableford_total=e.stableford_total,
|
|
|
|
|
copenhagen_total=e.copenhagen_total,
|
|
|
|
|
bbb_total=e.bbb_total,
|
|
|
|
|
class_id=e.class_id,
|
|
|
|
|
class_name=e.class_name,
|
|
|
|
|
position=e.position,
|
|
|
|
|
is_leader=e.is_leader,
|
|
|
|
|
today_label=e.today_label,
|
|
|
|
|
thru_label=e.thru_label,
|
|
|
|
|
total_label=e.total_label,
|
|
|
|
|
rank=e.rank,
|
|
|
|
|
cut=e.cut,
|
|
|
|
|
status=e.status,
|
|
|
|
|
eclectic_total=e.eclectic_total,
|
|
|
|
|
handicap_index=float(hcp_by_participant[e.tournament_participant_id])
|
|
|
|
|
if hcp_by_participant.get(e.tournament_participant_id) is not None
|
|
|
|
|
else None,
|
2026-08-21 19:50:35 +02:00
|
|
|
rounds=e.rounds,
|
2026-08-21 19:44:13 +02:00
|
|
|
)
|
|
|
|
|
for e in entries
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
2026-08-21 19:50:35 +02:00
|
|
|
class PublicRoundHole(BaseModel):
|
|
|
|
|
hole_number: int
|
|
|
|
|
par: int
|
|
|
|
|
gross_strokes: int | None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"/{tournament_id}/rounds/{round_id}/participants/{tournament_participant_id}/holes",
|
|
|
|
|
response_model=list[PublicRoundHole],
|
|
|
|
|
)
|
|
|
|
|
async def get_public_round_participant_holes(
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
round_id: str,
|
|
|
|
|
tournament_participant_id: str,
|
|
|
|
|
code: str | None = None,
|
|
|
|
|
user: CurrentUser | None = Depends(get_current_user_optional),
|
|
|
|
|
) -> list[PublicRoundHole]:
|
|
|
|
|
"""Hull-for-hull for ÉN spiller i ÉN runde -- brukt av den offentlige
|
|
|
|
|
leaderboard-radens utvidbare rutenett. Nøkkelen er bevisst
|
|
|
|
|
tournament_participant_id (samme id som i PublicLeaderboardEntry over),
|
|
|
|
|
IKKE tournament_round_participant_id (den er runde-spesifikk og aldri
|
|
|
|
|
eksponert offentlig) -- slår opp riktig runde-deltaker-rad internt."""
|
|
|
|
|
organization_id = await resolve_org(tournament_id)
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
"SELECT visibility, join_code FROM tournament WHERE id = $1", tournament_id
|
|
|
|
|
)
|
|
|
|
|
if row is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
|
|
|
|
|
if not code_matches(row["join_code"], code):
|
|
|
|
|
await check_visibility(conn, row["visibility"], organization_id, tournament_id, user)
|
|
|
|
|
|
|
|
|
|
round_row = await conn.fetchrow(
|
|
|
|
|
"SELECT course_id::text AS course_id FROM tournament_round WHERE id = $1 AND tournament_id = $2",
|
|
|
|
|
round_id,
|
|
|
|
|
tournament_id,
|
|
|
|
|
)
|
|
|
|
|
if round_row is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Runden finnes ikke i denne turneringen.")
|
|
|
|
|
|
|
|
|
|
round_participant_id = await conn.fetchval(
|
|
|
|
|
"SELECT id FROM tournament_round_participant WHERE tournament_round_id = $1 AND tournament_participant_id = $2",
|
|
|
|
|
round_id,
|
|
|
|
|
tournament_participant_id,
|
|
|
|
|
)
|
|
|
|
|
if round_participant_id is None:
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
holes = await conn.fetch(
|
|
|
|
|
"""
|
|
|
|
|
SELECT h.hole_number, h.par, trh.gross_strokes
|
|
|
|
|
FROM hole h
|
|
|
|
|
LEFT JOIN tournament_round_hole trh
|
|
|
|
|
ON trh.tournament_round_participant_id = $1 AND trh.hole_number = h.hole_number
|
|
|
|
|
WHERE h.course_id = $2
|
|
|
|
|
ORDER BY h.hole_number
|
|
|
|
|
""",
|
|
|
|
|
round_participant_id,
|
|
|
|
|
round_row["course_id"],
|
|
|
|
|
)
|
|
|
|
|
return [PublicRoundHole(**dict(h)) for h in holes]
|
|
|
|
|
|
|
|
|
|
|
2026-07-19 22:51:07 +02:00
|
|
|
# --- Tilskuer-rolle (ADR-026): leaderboard, matcher, scorekort -------------
|
|
|
|
|
#
|
|
|
|
|
# Samme synlighets-/kode-mønster som info-/sesjon-kallene over -- ingen ny
|
|
|
|
|
# mekanisme. Leaderboard har ingen spillernivå-data i det hele tatt (kun
|
|
|
|
|
# lag-id/-navn/-farge/poeng), så INGEN ekstra reveal-sjekk trengs der.
|
|
|
|
|
# Match-listen arver blind draw-skjulingen (ADR-013) automatisk via
|
|
|
|
|
# fetch_matches() sin egen own_team_ids()-håndtering (tom mengde for en
|
|
|
|
|
# anonym/uinnloggede leser -- ser bare avslørte matcher, akkurat som en
|
|
|
|
|
# tilfeldig org-medlem uten roster ville gjort). Scorekortet trenger en
|
|
|
|
|
# EKSPLISITT reveal-sjekk her (matchen kan i prinsippet ha registrerte hull
|
|
|
|
|
# før begge lag har låst -- ikke normal flyt, men ikke teknisk umulig).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/{tournament_id}/leaderboard", response_model=Leaderboard)
|
|
|
|
|
async def get_public_leaderboard(
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
code: str | None = None,
|
|
|
|
|
user: CurrentUser | None = Depends(get_current_user_optional),
|
|
|
|
|
) -> Leaderboard:
|
|
|
|
|
organization_id = await resolve_org(tournament_id)
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
"SELECT visibility, join_code FROM tournament WHERE id = $1", tournament_id
|
|
|
|
|
)
|
|
|
|
|
if row is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
|
|
|
|
|
if not code_matches(row["join_code"], code):
|
|
|
|
|
await check_visibility(conn, row["visibility"], organization_id, tournament_id, user)
|
|
|
|
|
return await fetch_leaderboard(conn, tournament_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"/{tournament_id}/sessions/{session_id}/matches", response_model=list[MatchOut]
|
|
|
|
|
)
|
|
|
|
|
async def get_public_matches(
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
session_id: str,
|
|
|
|
|
code: str | None = None,
|
|
|
|
|
user: CurrentUser | None = Depends(get_current_user_optional),
|
|
|
|
|
) -> list[MatchOut]:
|
|
|
|
|
organization_id = await resolve_org(tournament_id)
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
"SELECT visibility, join_code FROM tournament WHERE id = $1", tournament_id
|
|
|
|
|
)
|
|
|
|
|
if row is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
|
|
|
|
|
if not code_matches(row["join_code"], code):
|
|
|
|
|
await check_visibility(conn, row["visibility"], organization_id, tournament_id, user)
|
|
|
|
|
|
|
|
|
|
# Sikkerhetskritisk: session_id må faktisk høre til DENNE turneringen
|
|
|
|
|
# -- org_connection() setter kun tenant-grensen (RLS), ikke at stiens
|
|
|
|
|
# to id-er faktisk henger sammen. Uten denne sjekken kunne noen med
|
|
|
|
|
# kode/tilgang til én offentlig turnering lese en HVILKEN SOM HELST
|
|
|
|
|
# økt i samme organisasjon (inkl. en privat en) ved å gjette id-en.
|
|
|
|
|
session_tournament = await conn.fetchval(
|
|
|
|
|
"SELECT tournament_id::text FROM session WHERE id = $1", session_id
|
|
|
|
|
)
|
|
|
|
|
if session_tournament != tournament_id:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Økten finnes ikke i denne turneringen.")
|
|
|
|
|
|
|
|
|
|
user_id = user.user_id if user is not None else None
|
|
|
|
|
return await fetch_matches(conn, organization_id, session_id, user_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/{tournament_id}/matches/{match_id}/scorecard", response_model=Scorecard)
|
|
|
|
|
async def get_public_scorecard(
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
match_id: str,
|
|
|
|
|
code: str | None = None,
|
|
|
|
|
user: CurrentUser | None = Depends(get_current_user_optional),
|
|
|
|
|
) -> Scorecard:
|
|
|
|
|
organization_id = await resolve_org(tournament_id)
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
"SELECT visibility, join_code FROM tournament WHERE id = $1", tournament_id
|
|
|
|
|
)
|
|
|
|
|
if row is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
|
|
|
|
|
if not code_matches(row["join_code"], code):
|
|
|
|
|
await check_visibility(conn, row["visibility"], organization_id, tournament_id, user)
|
|
|
|
|
|
|
|
|
|
# Samme tenant-vs-sti-konsistenssjekk som matchlisten over, PLUSS en
|
|
|
|
|
# eksplisitt reveal-sjekk (blind draw, ADR-013) -- scorekortet skal
|
|
|
|
|
# aldri kunne leses offentlig før laget selv har avslørt oppstillingen.
|
|
|
|
|
match_row = await conn.fetchrow(
|
|
|
|
|
"""
|
|
|
|
|
SELECT s.tournament_id::text AS tournament_id, m.session_id::text AS session_id
|
|
|
|
|
FROM match m JOIN session s ON s.id = m.session_id
|
|
|
|
|
WHERE m.id = $1
|
|
|
|
|
""",
|
|
|
|
|
match_id,
|
|
|
|
|
)
|
|
|
|
|
if match_row is None or match_row["tournament_id"] != tournament_id:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Matchen finnes ikke i denne turneringen.")
|
|
|
|
|
|
|
|
|
|
locked = await locked_team_ids(conn, match_row["session_id"])
|
|
|
|
|
if len(locked) < 2:
|
|
|
|
|
raise app_error(403, "NOT_VISIBLE", "Denne matchen er ikke avslørt ennå.")
|
|
|
|
|
|
|
|
|
|
return await fetch_scorecard(conn, match_id)
|
|
|
|
|
|
|
|
|
|
|
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
class RegisterRequest(BaseModel):
|
|
|
|
|
display_name: str = Field(min_length=1, max_length=200)
|
|
|
|
|
email: EmailStr | None = None
|
|
|
|
|
mobile: str | None = None
|
|
|
|
|
birth_date: date | None = None
|
|
|
|
|
nickname: str | None = None
|
|
|
|
|
country: str | None = None
|
|
|
|
|
club: str | None = None
|
|
|
|
|
club_member_number: str | None = None
|
|
|
|
|
gender: str | None = Field(default=None, pattern="^[mfx]$")
|
|
|
|
|
handicap_index: float | None = None
|
|
|
|
|
# API-et krever eksplisitt True -- ingen implisitt samtykke (ADR-017).
|
|
|
|
|
consent: bool
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
# ADR-020 Beslutning A: en matchende invitasjonskode overstyrer visibility.
|
|
|
|
|
code: str | None = None
|
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class RegistrationResult(BaseModel):
|
|
|
|
|
id: str
|
|
|
|
|
status: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/{tournament_id}/register", response_model=RegistrationResult, status_code=201)
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
async def register_for_tournament(
|
|
|
|
|
tournament_id: str,
|
|
|
|
|
body: RegisterRequest,
|
|
|
|
|
user: CurrentUser | None = Depends(get_current_user_optional),
|
|
|
|
|
) -> RegistrationResult:
|
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
if not body.consent:
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Samtykke må gis for å melde seg på.")
|
|
|
|
|
|
2026-07-19 22:26:45 +02:00
|
|
|
organization_id = await resolve_org(tournament_id)
|
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
|
|
|
|
|
async with org_connection(organization_id) as conn, translate_db_errors():
|
|
|
|
|
# Lås turneringsraden: forhindrer at to samtidige påmeldinger begge
|
|
|
|
|
# leser "under kapasitet" og begge kommer inn (TOCTOU) -- samme
|
|
|
|
|
# mønster som to-lags-grensen i tournaments.py sin create_team.
|
|
|
|
|
tournament = await conn.fetchrow(
|
|
|
|
|
"""
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
SELECT visibility, join_code, registration_deadline, registration_capacity,
|
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
registration_overflow_policy, registration_requires_approval
|
|
|
|
|
FROM tournament WHERE id = $1 FOR UPDATE
|
|
|
|
|
""",
|
|
|
|
|
tournament_id,
|
|
|
|
|
)
|
|
|
|
|
if tournament is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Turneringen finnes ikke.")
|
|
|
|
|
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
# ADR-018 Beslutning D: registrering følger samme synlighetsgrense
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
# som selve landingssiden -- ingen særbehandling. ADR-020 Beslutning A:
|
|
|
|
|
# unntaket er en matchende invitasjonskode, som overstyrer dette helt.
|
2026-07-19 22:26:45 +02:00
|
|
|
if not code_matches(tournament["join_code"], body.code):
|
|
|
|
|
await check_visibility(conn, tournament["visibility"], organization_id, tournament_id, user)
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
|
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
deadline = tournament["registration_deadline"]
|
|
|
|
|
if deadline is not None and deadline < datetime.now(timezone.utc):
|
|
|
|
|
raise app_error(409, "REGISTRATION_CLOSED", "Påmeldingen er stengt.")
|
|
|
|
|
|
|
|
|
|
# E-post-matching (ADR-017 Beslutning B): finn en allerede
|
|
|
|
|
# organisator-opprettet spillerrad med samme e-post og fyll inn
|
|
|
|
|
# manglende felt der, i stedet for å opprette en duplikat.
|
|
|
|
|
player_id = None
|
|
|
|
|
if body.email:
|
|
|
|
|
existing = await conn.fetchrow(
|
|
|
|
|
"SELECT id FROM player WHERE organization_id = $1 AND lower(email) = lower($2)",
|
|
|
|
|
organization_id,
|
|
|
|
|
body.email,
|
|
|
|
|
)
|
|
|
|
|
if existing is not None:
|
|
|
|
|
player_id = existing["id"]
|
|
|
|
|
await conn.execute(
|
|
|
|
|
"""
|
|
|
|
|
UPDATE player SET
|
|
|
|
|
mobile = COALESCE(mobile, $2),
|
|
|
|
|
birth_date = COALESCE(birth_date, $3),
|
|
|
|
|
nickname = COALESCE(nickname, $4),
|
|
|
|
|
country = COALESCE(country, $5),
|
|
|
|
|
club = COALESCE(club, $6),
|
|
|
|
|
club_member_number = COALESCE(club_member_number, $7),
|
|
|
|
|
gender = COALESCE(gender, $8),
|
|
|
|
|
handicap_index = COALESCE(handicap_index, $9)
|
|
|
|
|
WHERE id = $1
|
|
|
|
|
""",
|
|
|
|
|
player_id,
|
|
|
|
|
body.mobile,
|
|
|
|
|
body.birth_date,
|
|
|
|
|
body.nickname,
|
|
|
|
|
body.country,
|
|
|
|
|
body.club,
|
|
|
|
|
body.club_member_number,
|
|
|
|
|
body.gender,
|
|
|
|
|
body.handicap_index,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if player_id is None:
|
|
|
|
|
player_row = await conn.fetchrow(
|
|
|
|
|
"""
|
|
|
|
|
INSERT INTO player
|
|
|
|
|
(organization_id, display_name, handicap_index, gender,
|
|
|
|
|
mobile, email, birth_date, nickname, country, club, club_member_number)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
|
|
|
RETURNING id
|
|
|
|
|
""",
|
|
|
|
|
organization_id,
|
|
|
|
|
body.display_name,
|
|
|
|
|
body.handicap_index,
|
|
|
|
|
body.gender,
|
|
|
|
|
body.mobile,
|
|
|
|
|
body.email.lower() if body.email else None,
|
|
|
|
|
body.birth_date,
|
|
|
|
|
body.nickname,
|
|
|
|
|
body.country,
|
|
|
|
|
body.club,
|
|
|
|
|
body.club_member_number,
|
|
|
|
|
)
|
|
|
|
|
player_id = player_row["id"]
|
|
|
|
|
|
|
|
|
|
already = await conn.fetchval(
|
|
|
|
|
"SELECT id FROM tournament_registration WHERE tournament_id = $1 AND player_id = $2",
|
|
|
|
|
tournament_id,
|
|
|
|
|
player_id,
|
|
|
|
|
)
|
|
|
|
|
if already is not None:
|
|
|
|
|
raise app_error(409, "DUPLICATE", "Denne spilleren er allerede påmeldt.")
|
|
|
|
|
|
|
|
|
|
# Rekkefølge fra ADR-017 Beslutning C: frist -> kapasitet -> godkjenning.
|
|
|
|
|
status = "confirmed"
|
|
|
|
|
if tournament["registration_capacity"] is not None:
|
|
|
|
|
active_count = await conn.fetchval(
|
|
|
|
|
"""
|
|
|
|
|
SELECT count(*) FROM tournament_registration
|
|
|
|
|
WHERE tournament_id = $1 AND status IN ('confirmed', 'pending')
|
|
|
|
|
""",
|
|
|
|
|
tournament_id,
|
|
|
|
|
)
|
|
|
|
|
if active_count >= tournament["registration_capacity"]:
|
|
|
|
|
if tournament["registration_overflow_policy"] == "waitlist":
|
|
|
|
|
status = "waitlisted"
|
|
|
|
|
else:
|
|
|
|
|
raise app_error(409, "LIMIT_REACHED", "Turneringen er full.")
|
|
|
|
|
|
|
|
|
|
if status != "waitlisted" and tournament["registration_requires_approval"]:
|
|
|
|
|
status = "pending"
|
|
|
|
|
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
"""
|
|
|
|
|
INSERT INTO tournament_registration
|
|
|
|
|
(organization_id, tournament_id, player_id, status, consent_given_at)
|
|
|
|
|
VALUES ($1, $2, $3, $4, now())
|
|
|
|
|
RETURNING id::text, status
|
|
|
|
|
""",
|
|
|
|
|
organization_id,
|
|
|
|
|
tournament_id,
|
|
|
|
|
player_id,
|
|
|
|
|
status,
|
|
|
|
|
)
|
|
|
|
|
return RegistrationResult(id=row["id"], status=row["status"])
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class PublicOrgTournament(BaseModel):
|
|
|
|
|
id: str
|
|
|
|
|
name: str
|
|
|
|
|
status: str
|
|
|
|
|
start_date: date | None
|
|
|
|
|
end_date: date | None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PublicOrgInfo(BaseModel):
|
|
|
|
|
name: str
|
|
|
|
|
tournaments: list[PublicOrgTournament]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@org_router.get("/{slug}", response_model=PublicOrgInfo)
|
|
|
|
|
async def get_public_org(slug: str) -> PublicOrgInfo:
|
|
|
|
|
async with plain_connection() as conn:
|
|
|
|
|
organization_id = await conn.fetchval("SELECT public_org_by_slug($1)", slug)
|
|
|
|
|
# NULL for BÅDE "finnes ikke" og "finnes, men er privat" -- samme
|
|
|
|
|
# anti-enumerering som magic-link (ADR-009), se ADR-018 Beslutning F.
|
|
|
|
|
if organization_id is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Organisasjonen finnes ikke.")
|
|
|
|
|
|
|
|
|
|
async with org_connection(str(organization_id)) as conn:
|
|
|
|
|
org_row = await conn.fetchrow("SELECT name FROM organization WHERE id = $1", organization_id)
|
|
|
|
|
tournament_rows = await conn.fetch(
|
|
|
|
|
"""
|
|
|
|
|
SELECT id::text, name, status::text, start_date, end_date
|
|
|
|
|
FROM tournament
|
|
|
|
|
WHERE visibility = 'public'
|
|
|
|
|
ORDER BY start_date DESC NULLS LAST, created_at DESC
|
|
|
|
|
""",
|
|
|
|
|
)
|
|
|
|
|
return PublicOrgInfo(
|
|
|
|
|
name=org_row["name"],
|
|
|
|
|
tournaments=[PublicOrgTournament(**dict(r)) for r in tournament_rows],
|
|
|
|
|
)
|