2026-07-16 20:41:34 +02:00
|
|
|
"""
|
|
|
|
|
Organisasjon-bootstrap (opprett en ny organisasjon og bli owner).
|
|
|
|
|
|
|
|
|
|
Eneste stedet i API-et som setter inn en `organization`-rad. Bruker
|
|
|
|
|
`get_current_user` (IKKE `get_authorized_org`, som forutsetter en ALLEREDE
|
|
|
|
|
eksisterende org-medlemskap -- sirkulært for akkurat denne handlingen, samme
|
|
|
|
|
resonnement som lag-/roster-opprettelse i oppsett-runden).
|
|
|
|
|
|
|
|
|
|
Selvrefererende RLS-bootstrap: `org_self`-policyen (etter migrasjon 005) er
|
|
|
|
|
`USING (id = app_current_org())` uten eksplisitt WITH CHECK, som Postgres
|
|
|
|
|
speiler dynamisk til INSERT-sjekken. Genererer derfor org-ens uuid FØR
|
|
|
|
|
innsetting og setter `app.current_org` til NØYAKTIG den verdien via den
|
|
|
|
|
allerede eksisterende `org_connection()` -- da er WITH CHECK trivielt sann
|
|
|
|
|
(id == current_org), og `teecup_app` (NOSUPERUSER/NOBYPASSRLS) kan sette inn
|
|
|
|
|
sin egen FØRSTE organisasjonsrad uten noen egen privilegert tilkobling.
|
|
|
|
|
Verifisert med en negativ kontroll (mismatchende id blir avvist) i
|
|
|
|
|
scratch-testingen -- se plan.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends
|
|
|
|
|
from pydantic import BaseModel, 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_authorized_org, get_current_user
|
2026-07-16 20:41:34 +02:00
|
|
|
from ..db import org_connection
|
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 ..errors import app_error, translate_db_errors
|
2026-07-16 20:41:34 +02:00
|
|
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OrganizationCreate(BaseModel):
|
|
|
|
|
name: str = Field(min_length=1, max_length=200)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OrganizationOut(BaseModel):
|
|
|
|
|
id: str
|
|
|
|
|
name: str
|
|
|
|
|
role: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/orgs", response_model=OrganizationOut, status_code=201)
|
|
|
|
|
async def create_organization(
|
|
|
|
|
body: OrganizationCreate,
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
) -> OrganizationOut:
|
|
|
|
|
new_id = str(uuid.uuid4())
|
|
|
|
|
|
|
|
|
|
async with org_connection(new_id) as conn, translate_db_errors():
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
"INSERT INTO organization (id, name) VALUES ($1, $2) RETURNING id::text, name",
|
|
|
|
|
new_id,
|
|
|
|
|
body.name,
|
|
|
|
|
)
|
|
|
|
|
await conn.execute(
|
|
|
|
|
"INSERT INTO organization_membership (organization_id, user_id, role) VALUES ($1, $2, 'owner')",
|
|
|
|
|
new_id,
|
|
|
|
|
user.user_id,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return OrganizationOut(id=row["id"], name=row["name"], role="owner")
|
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 OrganizationUpdate(BaseModel):
|
|
|
|
|
"""Alle felt valgfrie -- kun de som faktisk sendes med blir endret
|
|
|
|
|
(exclude_unset, se update_organization). Foreløpig kun de to feltene
|
|
|
|
|
landingssiden trenger (ADR-018) -- ikke en generell org-innstillings-
|
|
|
|
|
endepunkt."""
|
|
|
|
|
|
|
|
|
|
public_profile: bool | None = None
|
|
|
|
|
slug: str | None = Field(default=None, pattern="^[a-z0-9]+(-[a-z0-9]+)*$")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OrganizationSettings(BaseModel):
|
|
|
|
|
id: str
|
|
|
|
|
name: str
|
|
|
|
|
public_profile: bool
|
|
|
|
|
slug: str | None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch("/orgs/{organization_id}", response_model=OrganizationSettings)
|
|
|
|
|
async def update_organization(
|
|
|
|
|
body: OrganizationUpdate,
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
) -> OrganizationSettings:
|
|
|
|
|
updates = body.model_dump(exclude_unset=True)
|
|
|
|
|
if not updates:
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Ingen felt å oppdatere.")
|
|
|
|
|
|
|
|
|
|
# Trygt mot injeksjon: nøklene kommer KUN fra OrganizationUpdate sine
|
|
|
|
|
# egne feltnavn (fast, kjent hviteliste).
|
|
|
|
|
set_clauses = [f"{key} = ${i}" for i, key in enumerate(updates, start=1)]
|
|
|
|
|
values = list(updates.values())
|
|
|
|
|
values.append(organization_id)
|
|
|
|
|
|
|
|
|
|
async with org_connection(organization_id) as conn, translate_db_errors():
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
f"""
|
|
|
|
|
UPDATE organization SET {', '.join(set_clauses)}
|
|
|
|
|
WHERE id = ${len(values)}
|
|
|
|
|
RETURNING id::text, name, public_profile, slug
|
|
|
|
|
""",
|
|
|
|
|
*values,
|
|
|
|
|
)
|
|
|
|
|
if row is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Organisasjonen finnes ikke.")
|
|
|
|
|
return OrganizationSettings(**dict(row))
|