Update Todos
Legge til visuell hint på øktkort ("Sett opp flights") i program-skjermen
Avklare designbeslutninger for Brukerroller med bruker
Skrive team_authz.py på nytt (kaptein-only + match-deltaker-sjekk)
Oppdatere matches.py til å bruke user_is_team_captain
Oppdatere scoring.py til å bruke user_is_match_participant
Håndheve 'kun én kaptein per lag' i tournaments.py PATCH/POST roster
Sjekke ekte teecup_db for lag uten kaptein / med flere kapteiner (kun lesing)
Skrive ADR-023 for Brukerroller-beslutningene
Sette opp scratch MinIO + engangs API-container
Kjøre autorisasjonstester (kaptein/deltaker) mot scratch-API-et
Rydde opp scratch-infrastruktur
Ekte typesjekket frontend-build (øktkort-hint)
Deploy etter eksplisitt bekreftelse + oppdatere .md-filer
Alt er scratch-verifisert. Klar for utrulling — planen:
Ingen migrasjon (ren autorisasjonslogikk-endring, ingen skjema rørt).
docker compose up -d --build teecup_api teecup_frontend mot ekte teeoff_default-nettverk — bygger og bytter ut begge containere.
Endringen har reell konsekvens for ekte data akkurat nå: "De Unge" i "De Gamle er Eldst" har 0 kapteiner — de vil fortsette å kunne sette opp/føre score som før (fallback-regelen dekker dette), men fra nå av bør du utpeke en kaptein på det laget for at kaptein-rollen skal bety noe der òg.
Etter deploy: sjekke /health + /dashboard fortsatt 200, teeoff.no upåvirket (samme som alle tidligere runder).
119 lines
4.5 KiB
Python
119 lines
4.5 KiB
Python
"""
|
|
Delt autorisasjonssjekk: to distinkte spørsmål, to funksjoner.
|
|
|
|
1. `user_is_team_captain` -- "kan brukeren sette opp/fjerne/låse LAGETS
|
|
oppstilling?" Brukt av matches.py sin add_participant/remove_participant/
|
|
lock_lineup, FØR noen match_participant-rad i det hele tatt finnes for
|
|
brukeren selv (kylling-og-egg: man kan ikke kreve at brukeren ALLEREDE er
|
|
deltaker for å få lov til å LEGGE TIL deltakere).
|
|
|
|
2026-07-19 (Brukerroller-runden): byttet fra "hvem som helst rostret på
|
|
laget" til kaptein (`team_roster.is_captain = true`) ELLER org-eier/admin
|
|
-- kapteinen er nå en reell autorisasjonsrolle, ikke bare et visningsmerke.
|
|
Se ARCHITECTURE_DECISIONS.md for hele begrunnelsen.
|
|
|
|
Bevisst unntak: har laget INGEN utpekt kaptein ennå, godtas enhver
|
|
rostret spiller i stedet for å låse laget helt ute -- vanlig tidlig i en
|
|
turnering før noen har rukket å utpeke en kaptein (og en reell risiko
|
|
funnet i eksisterende produksjonsdata: langt fra alle roster-rader har en
|
|
kaptein i dag). Har laget FØRST fått en kaptein (`update_roster_entry`
|
|
håndhever nå "kun én kaptein per lag"), er det utelukkende den som
|
|
gjelder -- ingen andre rostrede spillere.
|
|
|
|
2. `user_is_match_participant` -- "kan brukeren føre/korrigere score for
|
|
DENNE spesifikke matchen?" Brukt av scoring.py. Krever en faktisk
|
|
match_participant-rad for brukeren i akkurat denne matchen (valgfritt
|
|
begrenset til én side via `team_side`) -- IKKE bare rostret på laget, og
|
|
UAVHENGIG av kapteinmerket (å være kaptein gir ikke i seg selv rett til å
|
|
føre score for en match man selv ikke spiller).
|
|
|
|
Begge har samme org-eier/admin-fallback, av samme grunn som tidligere
|
|
(2026-07-18-runden): uten den kunne INGEN sette opp eller føre noe før minst
|
|
én spiller hadde logget inn og blitt koblet til sin player-rad. Bevisst
|
|
INGEN unntak for at organisatoren selv er rostret på MOTSTANDERLAGET i samme
|
|
turnering -- vurdert og avvist tidligere: TeeCup er et tillitsbasert verktøy
|
|
for klubber/vennegjenger, ikke en sikkerhetsgrense mot en fiendtlig
|
|
organisator (som uansett har full administrativ tilgang), og et slikt
|
|
unntak ville skapt en reell låsning.
|
|
"""
|
|
|
|
from asyncpg import Connection
|
|
|
|
|
|
async def _is_org_admin(conn: Connection, organization_id: str, user_id: str) -> bool:
|
|
# organization_membership har ingen RLS-policy (se auth.get_authorized_org
|
|
# sin egen kommentar) -- filtrert eksplisitt på organization_id her, trygt
|
|
# på samme tilkobling uansett hvilken app.current_org som er satt.
|
|
is_org_admin = await conn.fetchval(
|
|
"""
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM organization_membership
|
|
WHERE organization_id = $1 AND user_id = $2 AND role IN ('owner', 'admin')
|
|
)
|
|
""",
|
|
organization_id,
|
|
user_id,
|
|
)
|
|
return bool(is_org_admin)
|
|
|
|
|
|
async def user_is_team_captain(
|
|
conn: Connection, organization_id: str, team_id: str, user_id: str
|
|
) -> bool:
|
|
has_captain = await conn.fetchval(
|
|
"SELECT EXISTS (SELECT 1 FROM team_roster WHERE team_id = $1 AND is_captain = true)",
|
|
team_id,
|
|
)
|
|
if has_captain:
|
|
may_act = await conn.fetchval(
|
|
"""
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM team_roster tr
|
|
JOIN player p ON p.id = tr.player_id
|
|
WHERE tr.team_id = $1 AND p.user_id = $2 AND tr.is_captain = true
|
|
)
|
|
""",
|
|
team_id,
|
|
user_id,
|
|
)
|
|
else:
|
|
may_act = await conn.fetchval(
|
|
"""
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM team_roster tr
|
|
JOIN player p ON p.id = tr.player_id
|
|
WHERE tr.team_id = $1 AND p.user_id = $2
|
|
)
|
|
""",
|
|
team_id,
|
|
user_id,
|
|
)
|
|
if may_act:
|
|
return True
|
|
return await _is_org_admin(conn, organization_id, user_id)
|
|
|
|
|
|
async def user_is_match_participant(
|
|
conn: Connection,
|
|
organization_id: str,
|
|
match_id: str,
|
|
user_id: str,
|
|
team_side: str | None = None,
|
|
) -> bool:
|
|
is_participant = await conn.fetchval(
|
|
"""
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM match_participant mp
|
|
JOIN team_roster tr ON tr.id = mp.team_roster_id
|
|
JOIN player p ON p.id = tr.player_id
|
|
WHERE mp.match_id = $1 AND p.user_id = $2
|
|
AND ($3::text IS NULL OR mp.team_side::text = $3)
|
|
)
|
|
""",
|
|
match_id,
|
|
user_id,
|
|
team_side,
|
|
)
|
|
if is_participant:
|
|
return True
|
|
return await _is_org_admin(conn, organization_id, user_id)
|