- Modèles Organization + OrgMember, migration Alembic (SQLite compatible) - organization_id nullable sur Document, Decision, Mandate, VotingProtocol - Service, schéma, router /organizations + dependency get_active_org_id - Seed : Duniter G1 + Axiom Team ; tout le contenu seed attaché à Duniter G1 - Backend : list/create filtrés par header X-Organization - Frontend : store organizations, WorkspaceSelector réel, useApi injecte l'org - Fix critique : rate_limiter exclut les requêtes OPTIONS (CORS preflight) → résout le bug "Failed to fetch /auth/me" au reload (429 sur preflight) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
97 lines
3.6 KiB
Python
97 lines
3.6 KiB
Python
"""Organizations router: list, create, membership."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.database import get_db
|
|
from app.models.user import DuniterIdentity
|
|
from app.schemas.organization import OrgMemberOut, OrganizationCreate, OrganizationOut
|
|
from app.services.auth_service import get_current_identity
|
|
from app.services.org_service import (
|
|
add_member,
|
|
create_organization,
|
|
get_organization,
|
|
get_organization_by_slug,
|
|
is_member,
|
|
list_members,
|
|
list_organizations,
|
|
)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/", response_model=list[OrganizationOut])
|
|
async def get_organizations(db: AsyncSession = Depends(get_db)) -> list[OrganizationOut]:
|
|
"""List all organizations (public — transparent ones need no auth)."""
|
|
orgs = await list_organizations(db)
|
|
return [OrganizationOut.model_validate(o) for o in orgs]
|
|
|
|
|
|
@router.post("/", response_model=OrganizationOut, status_code=status.HTTP_201_CREATED)
|
|
async def post_organization(
|
|
payload: OrganizationCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
identity: DuniterIdentity = Depends(get_current_identity),
|
|
) -> OrganizationOut:
|
|
"""Create a new organization (authenticated users only)."""
|
|
existing = await get_organization_by_slug(db, payload.slug)
|
|
if existing:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail=f"Slug '{payload.slug}' déjà utilisé",
|
|
)
|
|
org = await create_organization(db, **payload.model_dump())
|
|
# Creator becomes admin
|
|
await add_member(db, org.id, identity.id, role="admin")
|
|
return OrganizationOut.model_validate(org)
|
|
|
|
|
|
@router.get("/{org_id}", response_model=OrganizationOut)
|
|
async def get_organization_detail(
|
|
org_id: uuid.UUID, db: AsyncSession = Depends(get_db)
|
|
) -> OrganizationOut:
|
|
org = await get_organization(db, org_id)
|
|
if not org:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Organisation introuvable")
|
|
return OrganizationOut.model_validate(org)
|
|
|
|
|
|
@router.get("/{org_id}/members", response_model=list[OrgMemberOut])
|
|
async def get_members(
|
|
org_id: uuid.UUID,
|
|
db: AsyncSession = Depends(get_db),
|
|
identity: DuniterIdentity = Depends(get_current_identity),
|
|
) -> list[OrgMemberOut]:
|
|
org = await get_organization(db, org_id)
|
|
if not org:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Organisation introuvable")
|
|
if not org.is_transparent and not await is_member(db, org_id, identity.id):
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Accès refusé")
|
|
members = await list_members(db, org_id)
|
|
return [OrgMemberOut.model_validate(m) for m in members]
|
|
|
|
|
|
@router.post("/{org_id}/join", response_model=OrgMemberOut, status_code=status.HTTP_201_CREATED)
|
|
async def join_organization(
|
|
org_id: uuid.UUID,
|
|
db: AsyncSession = Depends(get_db),
|
|
identity: DuniterIdentity = Depends(get_current_identity),
|
|
) -> OrgMemberOut:
|
|
"""Join a transparent organization."""
|
|
org = await get_organization(db, org_id)
|
|
if not org:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Organisation introuvable")
|
|
if not org.is_transparent:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Rejoindre cette organisation nécessite une invitation",
|
|
)
|
|
if await is_member(db, org_id, identity.id):
|
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Déjà membre")
|
|
member = await add_member(db, org_id, identity.id)
|
|
return OrgMemberOut.model_validate(member)
|