""" 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 is_org_member(conn: Connection, organization_id: str, user_id: str) -> bool: """ETHVERT medlemskap (ikke bare owner/admin, se is_org_admin over) -- brukt som den ene halvparten av OR-et som (2026-07-21, deltaker-tilgang- runden) erstattet den tidligere blanke get_authorized_org-sperren på read-endepunkter som selv ikke har noen mer finkornet sjekk (list_teams, list_sessions, get_scorecard) -- bevarer eksisterende org-medlemmers tilgang uendret, samtidig som en ikke-medlem deltaker slipper gjennom via user_is_tournament_participant under.""" return bool( await conn.fetchval( "SELECT EXISTS (SELECT 1 FROM organization_membership WHERE organization_id = $1 AND user_id = $2)", organization_id, user_id, ) ) async def user_is_org_player(conn: Connection, organization_id: str, user_id: str) -> bool: """Er brukeren koblet til NOEN spillerprofil i denne organisasjonen (ikke nødvendigvis en gitt turnering) -- brukt for lavsensitiv data som bane-/hull-info (courses.py sin list_holes), der en full turnering-deltaker-sjekk ville vært unødvendig presis for hva som faktisk beskyttes (par/stroke-index, ikke spillerdata).""" return bool( await conn.fetchval( "SELECT EXISTS (SELECT 1 FROM player WHERE organization_id = $1 AND user_id = $2)", organization_id, user_id, ) ) async def user_is_tournament_participant( conn: Connection, organization_id: str, tournament_id: str, user_id: str ) -> bool: """Har brukerens koblede spillerprofil (ADR-017 Beslutning B) en registrering ELLER en rostret plass i NØYAKTIG denne turneringen? Flyttet hit fra registration.py 2026-07-21 (het `is_participant` der) -- org-scopede endepunkter (matches.py/scoring.py/tournaments.py) kan ikke importere fra registration.py uten sirkulær import (registration.py importerer FRA disse tre for øvrig), men alle tre importerer allerede fritt fra denne (dependency-frie) modulen. registration.py importerer nå denne i stedet for sin egen kopi.""" return bool( await conn.fetchval( """ SELECT EXISTS ( SELECT 1 FROM player p WHERE p.organization_id = $1 AND p.user_id = $2 AND ( EXISTS ( SELECT 1 FROM tournament_registration tr WHERE tr.player_id = p.id AND tr.tournament_id = $3 ) OR EXISTS ( SELECT 1 FROM team_roster tro JOIN team t ON t.id = tro.team_id WHERE tro.player_id = p.id AND t.tournament_id = $3 ) ) ) """, organization_id, user_id, tournament_id, ) ) async def user_is_own_tournament_participant( conn: Connection, organization_id: str, tournament_participant_id: str, user_id: str ) -> bool: """Individuelle turneringer (ADR-037): er brukeren SELV spilleren bak denne `tournament_participant`-raden (via `player.user_id`)? Mirror av `user_is_match_participant`, men uten `team_side` -- en individuell turnering har ingen lag å begrense siden til. Brukt av `individual_tournaments.py` sin `update_hole` (2026-07-30): før denne kunne ETHVERT org-medlem skrive score for EN HVILKEN SOM HELST deltaker, ikke bare sin egen -- samme klasse hull `user_is_match_ participant` lukket for lagturneringer i ADR-023. Kun scoring er strammet inn her; runde-/deltaker-oppsett (opprett runde, legg til/ fjern turnering-/rundedeltaker) forblir bevisst på org-medlemsnivå, samme presedens som `session`-opprettelse og `team_roster`-tilføyelse i tournaments.py -- ingen av dem er captain-/admin-gatet heller, kun selve VALGET AV HVEM SOM SPILLER EN GITT MATCH/RUNDE og selve scoreregistreringen er det.""" is_self = await conn.fetchval( """ SELECT EXISTS ( SELECT 1 FROM tournament_participant tp JOIN player p ON p.id = tp.player_id WHERE tp.id = $1 AND p.user_id = $2 ) """, tournament_participant_id, user_id, ) if is_self: 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, ) )