""" 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 from ..auth import CurrentUser, get_authorized_org, get_current_user from ..db import org_connection from ..errors import app_error, translate_db_errors 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") 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))