Multi-tenancy : espaces de travail + fix auth reload (rate limiter OPTIONS)
- 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>
This commit is contained in:
96
backend/app/routers/organizations.py
Normal file
96
backend/app/routers/organizations.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user