1. Migrasjon — kjør 013_messaging.sql mot ekte teecup_db (ny message-tabell, RLS, ingen endring av eksisterende data). 2. Backend + frontend — docker compose up -d --build teecup_api teecup_frontend. 3. Caddy — legger til /ws/*-ruten i /opt/teeoff/deploy/Caddyfile (allerede skrevet og syntaks-validert). Som i alle tidligere runder som har rørt denne filen: en graceful reload plukker historisk IKKE opp endringen (stale bind-mount-inode), så det trengs en full docker restart teeoff_caddy — det gir noen sekunders nedetid for teeoff.no også, ikke bare teecup.teeoff.no. Etter alt dette: sjekke /health + /dashboard → 200, en reell WebSocket-tilkobling fungerer over wss://teecup.teeoff.no/ws/..., og teeoff.no er tilbake på 200.
139 lines
5.2 KiB
Python
139 lines
5.2 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.
|
|
result = 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(result)
|
|
|
|
|
|
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)
|
|
|
|
|
|
async def user_is_rostered_on_team(conn: Connection, team_id: str, user_id: str) -> bool:
|
|
"""Kun for lag-chat (ADR-025 Beslutning C) -- BEVISST INGEN org-admin-
|
|
fallback, ulikt de to funksjonene over. «Det hemmelige rommet» er ekte
|
|
privat: kun spillere faktisk rostret på laget, aldri organisatoren, uansett
|
|
org-rolle. Ikke gjenbruk denne for autorisasjon utenfor chat."""
|
|
return bool(
|
|
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,
|
|
)
|
|
)
|