Update Todos
Utvid team_authz.py: org owner/admin får overstyre uten unntak Oppdater alle 5 kallsteder i matches.py/scoring.py Scratch-verifisering: org-admin uten roster kan låse/legge til deltakere/score, vanlig member kan ikke, rostret spiller uendret Rulle ut live + oppdatere statusdokumenter Live, verifisert, og dokumentert. Klar for å skrive V0-prompten for blind draw-skjermen når du vil — samme mønster som de forrige, og nå med bekreftet organisator-tilgang selv om ingen kaptein har logget inn ennå.
This commit is contained in:
parent
0b6e7e4219
commit
a46377e7bc
6 changed files with 88 additions and 14 deletions
|
|
@ -248,7 +248,9 @@
|
||||||
"Bash(python3 -)",
|
"Bash(python3 -)",
|
||||||
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(len\\(d\\), 'facilities'\\); print\\(d[:2]\\)\")",
|
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(len\\(d\\), 'facilities'\\); print\\(d[:2]\\)\")",
|
||||||
"Bash(rm -f /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch.env /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch_app_pw.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_user.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_pass.txt /tmp/scratch_cookies2.txt)",
|
"Bash(rm -f /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch.env /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch_app_pw.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_user.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_pass.txt /tmp/scratch_cookies2.txt)",
|
||||||
"Bash(curl -s \"https://teecup.teeoff.no/tournaments/73b6bf38-90cd-4ef7-9535-8b8e108eaa9a?org=test-org-id&name=Test\")"
|
"Bash(curl -s \"https://teecup.teeoff.no/tournaments/73b6bf38-90cd-4ef7-9535-8b8e108eaa9a?org=test-org-id&name=Test\")",
|
||||||
|
"Bash(python3 -m py_compile app/team_authz.py app/routers/matches.py app/routers/scoring.py)",
|
||||||
|
"Bash(rm -f /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch.env /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch_app_pw.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_user.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_pass.txt /tmp/owner_cookies.txt /tmp/member_cookies.txt /tmp/captain_cookies.txt)"
|
||||||
],
|
],
|
||||||
"additionalDirectories": [
|
"additionalDirectories": [
|
||||||
"/opt/teeoff/deploy",
|
"/opt/teeoff/deploy",
|
||||||
|
|
|
||||||
23
CLAUDE.md
23
CLAUDE.md
|
|
@ -776,6 +776,29 @@ Ferdig og verifisert:
|
||||||
ALDRI være et eget `<form>` -- bruk `<div>` + eksplisitt
|
ALDRI være et eget `<form>` -- bruk `<div>` + eksplisitt
|
||||||
klikk-/Enter-håndtering.
|
klikk-/Enter-håndtering.
|
||||||
|
|
||||||
|
- **Organisator-overstyring i `team_authz.py`, LIVE (2026-07-18):** reist av
|
||||||
|
brukeren rett før blind draw-skjermen skulle designes: `app/team_authz.py`
|
||||||
|
sin `user_may_act_for_team` krevde tidligere en `team_roster`-rad med
|
||||||
|
lenket bruker-konto — ingen vei for organisatoren til å låse/legge til
|
||||||
|
deltakere/føre score hvis ingen spiller hadde logget inn ennå (vanlig
|
||||||
|
tidlig i en turnering). Utvidet til også å godta org-eier/admin
|
||||||
|
(`organization_membership.role IN ('owner','admin')`), på ETHVERT lag,
|
||||||
|
ingen unntak for at organisatoren selv er rostret på motstanderlaget.
|
||||||
|
**Det unntaket ble bevisst vurdert og avvist** (brukeren spurte selv om
|
||||||
|
det, jeg anbefalte det opprinnelig, men vi kom sammen frem til at det ville
|
||||||
|
skapt en verre låsning: er organisatoren spillende på Lag A og
|
||||||
|
Lag B heller ikke har noen innlogget spiller, ville Lag B blitt helt
|
||||||
|
låst ute) — TeeCup er et tillitsbasert klubb-/vennegjeng-verktøy, ikke en
|
||||||
|
sikkerhetsgrense mot en fiendtlig organisator som uansett allerede ser
|
||||||
|
begge lags fulle troppe-liste (blind draw skjuler kun selve
|
||||||
|
kamp-paringen). Alle 5 kallsteder oppdatert (matches.py sin
|
||||||
|
add_participant/lock_lineup, scoring.py sin submit_hole_score/
|
||||||
|
submit_hole_result). **Verifisert grundig i scratch:** org-eier uten
|
||||||
|
roster kan nå låse BEGGE lag + føre score, en vanlig 'member'-rolle
|
||||||
|
fortsatt blokkert (uendret), en rostret spiller fungerer uendret
|
||||||
|
uavhengig av org-rolle. `test_isolation.sql` 12/12. Ingen migrasjon (ren
|
||||||
|
Python-endring) — kun `teecup_api` redeployet, `teeoff.no` upåvirket.
|
||||||
|
|
||||||
Neste steg:
|
Neste steg:
|
||||||
1. Blind draw-skjermen (neste i "bygg i rekkefølgen ting brukes", gjenopptatt
|
1. Blind draw-skjermen (neste i "bygg i rekkefølgen ting brukes", gjenopptatt
|
||||||
etter ADR-019-sidespranget) — deretter scorekort, leaderboard. Samme
|
etter ADR-019-sidespranget) — deretter scorekort, leaderboard. Samme
|
||||||
|
|
|
||||||
|
|
@ -309,11 +309,26 @@
|
||||||
bevisst IKKE «kun én kaptein per lag» — flere spillere kan i dag merkes
|
bevisst IKKE «kun én kaptein per lag» — flere spillere kan i dag merkes
|
||||||
kaptein samtidig på samme lag. Bør revurderes samtidig med resten av dette
|
kaptein samtidig på samme lag. Bør revurderes samtidig med resten av dette
|
||||||
punktet, ikke løses isolert i roster-endepunktet.
|
punktet, ikke løses isolert i roster-endepunktet.
|
||||||
|
- **Nytt 2026-07-18, ✅ BYGGET:** `app/team_authz.py` sin `user_may_act_for_team`
|
||||||
|
(brukt av lås/deltakere i matches.py OG score-skriving i scoring.py) gir nå
|
||||||
|
også org-eier/admin (`organization_membership.role IN ('owner','admin')`)
|
||||||
|
samme rettigheter som en rostret spiller, på ETHVERT lag — reist av
|
||||||
|
brukeren rett før blind draw-skjermen: uten dette kunne INGEN sette opp
|
||||||
|
eller låse et lag før minst én spiller hadde logget inn og blitt koblet.
|
||||||
|
Bevisst INGEN unntak for at organisatoren selv er rostret på
|
||||||
|
MOTSTANDERLAGET (vurdert og avvist — se team_authz.py sin docstring for
|
||||||
|
full begrunnelse: tillitsbasert verktøy, organisator ser uansett begge
|
||||||
|
rostre allerede, og et unntak ville skapt en reell låsning der ingen
|
||||||
|
kunne sette opp motstanderlaget om det heller ikke har en innlogget
|
||||||
|
spiller). Verifisert i scratch: org-eier uten roster kan nå låse begge
|
||||||
|
lag + føre score, vanlig 'member'-rolle fortsatt blokkert, rostret
|
||||||
|
spiller uendret. Rullet ut live samme dag.
|
||||||
|
|
||||||
### Scoring-autorisasjon: hvem fører, hvem korrigerer, hvem lukker (rejst 2026-07-16)
|
### Scoring-autorisasjon: hvem fører, hvem korrigerer, hvem lukker (rejst 2026-07-16)
|
||||||
- **Status:** ❓ trenger beslutning — direkte oppfølger av «Brukerroller» over.
|
- **Status:** ❓ trenger beslutning — direkte oppfølger av «Brukerroller» over.
|
||||||
- **Hvem fører score i dag:** alle med en `team_roster`-rad på laget (samme
|
- **Hvem fører score i dag:** alle med en `team_roster`-rad på laget, ELLER
|
||||||
minimale grense som deltaker/lås, se ADR-013-relatert kode). Ikke
|
org-eier/admin (utvidet 2026-07-18, se «Brukerroller» over) — samme
|
||||||
|
minimale grense som deltaker/lås, se ADR-013-relatert kode. Ikke
|
||||||
kaptein-only, ikke begrenset til de(n) som faktisk spiller matchen.
|
kaptein-only, ikke begrenset til de(n) som faktisk spiller matchen.
|
||||||
- **Hvem kan korrigere en ført score i dag:** akkurat de samme — skriving er en
|
- **Hvem kan korrigere en ført score i dag:** akkurat de samme — skriving er en
|
||||||
upsert (`ON CONFLICT ... DO UPDATE`), så en korrigering er ikke skilt fra en
|
upsert (`ON CONFLICT ... DO UPDATE`), så en korrigering er ikke skilt fra en
|
||||||
|
|
|
||||||
|
|
@ -267,7 +267,7 @@ async def add_participant(
|
||||||
if tee_course_id is None or tee_course_id != session["course_id"]:
|
if tee_course_id is None or tee_course_id != session["course_id"]:
|
||||||
raise app_error(400, "OUT_OF_SCOPE", "tee_id tilhører ikke øktens bane.")
|
raise app_error(400, "OUT_OF_SCOPE", "tee_id tilhører ikke øktens bane.")
|
||||||
|
|
||||||
if not await user_may_act_for_team(conn, expected_team_id, user.user_id):
|
if not await user_may_act_for_team(conn, organization_id, expected_team_id, user.user_id):
|
||||||
raise app_error(403, "NOT_ROSTERED_ON_TEAM", "Du er ikke rostret på dette laget.")
|
raise app_error(403, "NOT_ROSTERED_ON_TEAM", "Du er ikke rostret på dette laget.")
|
||||||
|
|
||||||
locked = await locked_team_ids(conn, match["session_id"])
|
locked = await locked_team_ids(conn, match["session_id"])
|
||||||
|
|
@ -338,7 +338,7 @@ async def lock_lineup(
|
||||||
if session is None:
|
if session is None:
|
||||||
raise app_error(404, "NOT_FOUND", "Økten finnes ikke.")
|
raise app_error(404, "NOT_FOUND", "Økten finnes ikke.")
|
||||||
|
|
||||||
if not await user_may_act_for_team(conn, body.team_id, user.user_id):
|
if not await user_may_act_for_team(conn, organization_id, body.team_id, user.user_id):
|
||||||
raise app_error(403, "NOT_ROSTERED_ON_TEAM", "Du er ikke rostret på dette laget.")
|
raise app_error(403, "NOT_ROSTERED_ON_TEAM", "Du er ikke rostret på dette laget.")
|
||||||
|
|
||||||
# UNIQUE(session_id, team_id) gir 409 via translate_db_errors ved
|
# UNIQUE(session_id, team_id) gir 409 via translate_db_errors ved
|
||||||
|
|
|
||||||
|
|
@ -272,7 +272,7 @@ async def submit_hole_score(
|
||||||
400, "MISMATCHED_SIDE", "match_participant_id tilhører ikke angitt side."
|
400, "MISMATCHED_SIDE", "match_participant_id tilhører ikke angitt side."
|
||||||
)
|
)
|
||||||
|
|
||||||
if not await user_may_act_for_team(conn, expected_team_id, user.user_id):
|
if not await user_may_act_for_team(conn, organization_id, expected_team_id, user.user_id):
|
||||||
raise app_error(403, "NOT_ROSTERED_ON_TEAM", "Du er ikke rostret på dette laget.")
|
raise app_error(403, "NOT_ROSTERED_ON_TEAM", "Du er ikke rostret på dette laget.")
|
||||||
|
|
||||||
if body.match_participant_id is not None:
|
if body.match_participant_id is not None:
|
||||||
|
|
@ -365,8 +365,8 @@ async def submit_hole_result(
|
||||||
# Begge lag kan rapportere et hull-resultat (hvilken som helst side kan
|
# Begge lag kan rapportere et hull-resultat (hvilken som helst side kan
|
||||||
# vinne/tape/dele) -- brukeren må være rostret på ETT av de to lagene.
|
# vinne/tape/dele) -- brukeren må være rostret på ETT av de to lagene.
|
||||||
if not (
|
if not (
|
||||||
await user_may_act_for_team(conn, match["team_a_id"], user.user_id)
|
await user_may_act_for_team(conn, organization_id, match["team_a_id"], user.user_id)
|
||||||
or await user_may_act_for_team(conn, match["team_b_id"], user.user_id)
|
or await user_may_act_for_team(conn, organization_id, match["team_b_id"], user.user_id)
|
||||||
):
|
):
|
||||||
raise app_error(
|
raise app_error(
|
||||||
403, "NOT_ROSTERED_ON_TEAM", "Du er ikke rostret på noen av lagene i denne matchen."
|
403, "NOT_ROSTERED_ON_TEAM", "Du er ikke rostret på noen av lagene i denne matchen."
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,33 @@
|
||||||
"""
|
"""
|
||||||
Delt autorisasjonssjekk: er brukeren rostret på et gitt lag?
|
Delt autorisasjonssjekk: kan brukeren handle på vegne av et gitt lag?
|
||||||
|
|
||||||
Brukt av både matches.py (deltaker/lås-skriving) og scoring.py (score-skriving)
|
Brukt av både matches.py (deltaker/lås-skriving) og scoring.py (score-skriving).
|
||||||
— se plan for begrunnelsen (bevisst IKKE kapteins-only ennå, FEATURE_BACKLOG).
|
|
||||||
|
To uavhengige veier inn:
|
||||||
|
1. Rostret på laget -- spiller med `player.user_id` koblet på nettopp DENNE
|
||||||
|
team_roster-raden (den opprinnelige, fortsatt gjeldende regelen).
|
||||||
|
2. Org-eier/admin i organisasjonen laget tilhører. Lagt til 2026-07-18: uten
|
||||||
|
dette kunne INGEN sette opp eller låse et lag før minst én spiller hadde
|
||||||
|
logget inn og blitt koblet -- vanlig tidlig i en turnering, og
|
||||||
|
organisatoren (som uansett allerede ser begge lags fulle troppe-liste,
|
||||||
|
blind draw skjuler kun selve kamp-paringen) satt fast.
|
||||||
|
|
||||||
|
Bevisst INGEN unntak for at organisatoren selv er rostret på
|
||||||
|
MOTSTANDERLAGET i samme turnering -- vurdert og avvist: TeeCup er et
|
||||||
|
tillitsbasert verktøy for klubber/vennegjenger, ikke en sikkerhets-
|
||||||
|
grense mot en fiendtlig organisator (som uansett har full administrativ
|
||||||
|
tilgang), og et slikt unntak ville skapt en reell låsning (ingen kan
|
||||||
|
sette opp MOTSTANDERLAGET om det heller ikke har noen innlogget spiller
|
||||||
|
-- verre enn problemet det skulle løse).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from asyncpg import Connection
|
from asyncpg import Connection
|
||||||
|
|
||||||
|
|
||||||
async def user_may_act_for_team(conn: Connection, team_id: str, user_id: str) -> bool:
|
async def user_may_act_for_team(
|
||||||
"""Brukeren har en team_roster-rad på laget."""
|
conn: Connection, organization_id: str, team_id: str, user_id: str
|
||||||
return await conn.fetchval(
|
) -> bool:
|
||||||
|
is_rostered = await conn.fetchval(
|
||||||
"""
|
"""
|
||||||
SELECT EXISTS (
|
SELECT EXISTS (
|
||||||
SELECT 1 FROM team_roster tr
|
SELECT 1 FROM team_roster tr
|
||||||
|
|
@ -21,3 +38,20 @@ async def user_may_act_for_team(conn: Connection, team_id: str, user_id: str) ->
|
||||||
team_id,
|
team_id,
|
||||||
user_id,
|
user_id,
|
||||||
)
|
)
|
||||||
|
if is_rostered:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue