7
0
forked from yvv/decision

Compartimentation : isolation stricte des données par espace de travail
ci/woodpecker/push/woodpecker Pipeline was successful

- Ajout clause else IS NULL sur tous les endpoints list (protocols, decisions,
  mandates, documents, groups, votes) — sans X-Organization → données globales
  seulement, jamais le contenu d'un autre espace
- _get_protocol/_get_decision/_get_mandate : org_id propagé à tous les
  endpoints GET/PUT/DELETE/advance/assign/revoke/steps → 404 si UUID d'un
  autre espace
- votes.py : list_vote_sessions filtre via JOIN VotingProtocol.organization_id
- groups.py : suppression _org_id_from_header() mort, create_group assigne
  organization_id correctement

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-04-26 18:52:16 +02:00
parent f56d84e76b
commit 59fff64f9e
6 changed files with 96 additions and 45 deletions
+25 -10
View File
@@ -31,13 +31,20 @@ router = APIRouter()
# ── Helpers ───────────────────────────────────────────────────────────────── # ── Helpers ─────────────────────────────────────────────────────────────────
async def _get_decision(db: AsyncSession, decision_id: uuid.UUID) -> Decision: async def _get_decision(
"""Fetch a decision by ID with its steps eagerly loaded, or raise 404.""" db: AsyncSession, decision_id: uuid.UUID, org_id: uuid.UUID | None = None
result = await db.execute( ) -> Decision:
"""Fetch a decision by ID within the active org scope, or raise 404."""
stmt = (
select(Decision) select(Decision)
.options(selectinload(Decision.steps)) .options(selectinload(Decision.steps))
.where(Decision.id == decision_id) .where(Decision.id == decision_id)
) )
if org_id is not None:
stmt = stmt.where(Decision.organization_id == org_id)
else:
stmt = stmt.where(Decision.organization_id.is_(None))
result = await db.execute(stmt)
decision = result.scalar_one_or_none() decision = result.scalar_one_or_none()
if decision is None: if decision is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Decision introuvable") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Decision introuvable")
@@ -61,6 +68,8 @@ async def list_decisions(
if org_id is not None: if org_id is not None:
stmt = stmt.where(Decision.organization_id == org_id) stmt = stmt.where(Decision.organization_id == org_id)
else:
stmt = stmt.where(Decision.organization_id.is_(None))
if decision_type is not None: if decision_type is not None:
stmt = stmt.where(Decision.decision_type == decision_type) stmt = stmt.where(Decision.decision_type == decision_type)
if status_filter is not None: if status_filter is not None:
@@ -91,7 +100,7 @@ async def create_decision(
await db.refresh(decision) await db.refresh(decision)
# Reload with steps (empty at creation) # Reload with steps (empty at creation)
decision = await _get_decision(db, decision.id) decision = await _get_decision(db, decision.id, org_id)
return DecisionOut.model_validate(decision) return DecisionOut.model_validate(decision)
@@ -99,9 +108,10 @@ async def create_decision(
async def get_decision( async def get_decision(
id: uuid.UUID, id: uuid.UUID,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
org_id: uuid.UUID | None = Depends(get_active_org_id),
) -> DecisionOut: ) -> DecisionOut:
"""Get a single decision with all its steps.""" """Get a single decision with all its steps."""
decision = await _get_decision(db, id) decision = await _get_decision(db, id, org_id)
return DecisionOut.model_validate(decision) return DecisionOut.model_validate(decision)
@@ -111,9 +121,10 @@ async def update_decision(
payload: DecisionUpdate, payload: DecisionUpdate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
identity: DuniterIdentity = Depends(get_current_identity), identity: DuniterIdentity = Depends(get_current_identity),
org_id: uuid.UUID | None = Depends(get_active_org_id),
) -> DecisionOut: ) -> DecisionOut:
"""Update a decision's metadata (title, description, status, protocol).""" """Update a decision's metadata (title, description, status, protocol)."""
decision = await _get_decision(db, id) decision = await _get_decision(db, id, org_id)
update_data = payload.model_dump(exclude_unset=True) update_data = payload.model_dump(exclude_unset=True)
for field, value in update_data.items(): for field, value in update_data.items():
@@ -123,7 +134,7 @@ async def update_decision(
await db.refresh(decision) await db.refresh(decision)
# Reload with steps # Reload with steps
decision = await _get_decision(db, decision.id) decision = await _get_decision(db, decision.id, org_id)
return DecisionOut.model_validate(decision) return DecisionOut.model_validate(decision)
@@ -136,10 +147,11 @@ async def add_step(
payload: DecisionStepCreate, payload: DecisionStepCreate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
identity: DuniterIdentity = Depends(get_current_identity), identity: DuniterIdentity = Depends(get_current_identity),
org_id: uuid.UUID | None = Depends(get_active_org_id),
) -> DecisionStepOut: ) -> DecisionStepOut:
"""Add a step to a decision process.""" """Add a step to a decision process."""
# Verify decision exists # Verify decision exists
decision = await _get_decision(db, id) decision = await _get_decision(db, id, org_id)
step = DecisionStep( step = DecisionStep(
decision_id=decision.id, decision_id=decision.id,
@@ -160,6 +172,7 @@ async def advance_decision_endpoint(
id: uuid.UUID, id: uuid.UUID,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
identity: DuniterIdentity = Depends(get_current_identity), identity: DuniterIdentity = Depends(get_current_identity),
org_id: uuid.UUID | None = Depends(get_active_org_id),
) -> DecisionAdvanceOut: ) -> DecisionAdvanceOut:
"""Advance a decision to its next step or status.""" """Advance a decision to its next step or status."""
try: try:
@@ -168,7 +181,7 @@ async def advance_decision_endpoint(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
# Reload with steps for complete output # Reload with steps for complete output
decision = await _get_decision(db, decision.id) decision = await _get_decision(db, decision.id, org_id)
data = DecisionOut.model_validate(decision).model_dump() data = DecisionOut.model_validate(decision).model_dump()
data["message"] = f"Decision avancee au statut : {decision.status}" data["message"] = f"Decision avancee au statut : {decision.status}"
return DecisionAdvanceOut(**data) return DecisionAdvanceOut(**data)
@@ -184,6 +197,7 @@ async def create_vote_session_for_step_endpoint(
step_id: uuid.UUID, step_id: uuid.UUID,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
identity: DuniterIdentity = Depends(get_current_identity), identity: DuniterIdentity = Depends(get_current_identity),
org_id: uuid.UUID | None = Depends(get_active_org_id),
) -> VoteSessionOut: ) -> VoteSessionOut:
"""Create a vote session linked to a decision step.""" """Create a vote session linked to a decision step."""
try: try:
@@ -199,9 +213,10 @@ async def delete_decision(
id: uuid.UUID, id: uuid.UUID,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
identity: DuniterIdentity = Depends(get_current_identity), identity: DuniterIdentity = Depends(get_current_identity),
org_id: uuid.UUID | None = Depends(get_active_org_id),
) -> None: ) -> None:
"""Delete a decision (only if in draft status).""" """Delete a decision (only if in draft status)."""
decision = await _get_decision(db, id) decision = await _get_decision(db, id, org_id)
if decision.status != "draft": if decision.status != "draft":
raise HTTPException( raise HTTPException(
+2
View File
@@ -77,6 +77,8 @@ async def list_documents(
if org_id is not None: if org_id is not None:
stmt = stmt.where(Document.organization_id == org_id) stmt = stmt.where(Document.organization_id == org_id)
else:
stmt = stmt.where(Document.organization_id.is_(None))
if doc_type is not None: if doc_type is not None:
stmt = stmt.where(Document.doc_type == doc_type) stmt = stmt.where(Document.doc_type == doc_type)
if status_filter is not None: if status_filter is not None:
+11 -15
View File
@@ -11,6 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from app.database import get_db from app.database import get_db
from app.dependencies.org import get_active_org_id
from app.models.group import Group, GroupMember from app.models.group import Group, GroupMember
from app.schemas.group import GroupCreate, GroupMemberCreate, GroupMemberOut, GroupOut, GroupSummary from app.schemas.group import GroupCreate, GroupMemberCreate, GroupMemberOut, GroupOut, GroupSummary
from app.services.auth_service import get_current_identity from app.services.auth_service import get_current_identity
@@ -18,24 +19,18 @@ from app.services.auth_service import get_current_identity
router = APIRouter() router = APIRouter()
def _org_id_from_header(request_headers) -> uuid.UUID | None:
raw = request_headers.get("x-organization")
if not raw:
return None
try:
return uuid.UUID(raw)
except ValueError:
return None
@router.get("/", response_model=list[GroupSummary]) @router.get("/", response_model=list[GroupSummary])
async def list_groups( async def list_groups(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
org_id: uuid.UUID | None = Depends(get_active_org_id),
) -> list[GroupSummary]: ) -> list[GroupSummary]:
"""List all groups. No auth required — groups are public within the workspace.""" """List groups within the active workspace."""
result = await db.execute( stmt = select(Group).options(selectinload(Group.members)).order_by(Group.name)
select(Group).options(selectinload(Group.members)).order_by(Group.name) if org_id is not None:
) stmt = stmt.where(Group.organization_id == org_id)
else:
stmt = stmt.where(Group.organization_id.is_(None))
result = await db.execute(stmt)
groups = result.scalars().all() groups = result.scalars().all()
return [ return [
GroupSummary( GroupSummary(
@@ -54,8 +49,9 @@ async def create_group(
payload: GroupCreate, payload: GroupCreate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
_identity=Depends(get_current_identity), _identity=Depends(get_current_identity),
org_id: uuid.UUID | None = Depends(get_active_org_id),
) -> GroupOut: ) -> GroupOut:
group = Group(name=payload.name, description=payload.description) group = Group(name=payload.name, description=payload.description, organization_id=org_id)
db.add(group) db.add(group)
await db.commit() await db.commit()
await db.refresh(group) await db.refresh(group)
+30 -12
View File
@@ -37,8 +37,10 @@ router = APIRouter()
# ── Helpers ───────────────────────────────────────────────────────────────── # ── Helpers ─────────────────────────────────────────────────────────────────
async def _get_mandate(db: AsyncSession, mandate_id: uuid.UUID) -> Mandate: async def _get_mandate(
result = await db.execute( db: AsyncSession, mandate_id: uuid.UUID, org_id: uuid.UUID | None = None
) -> Mandate:
stmt = (
select(Mandate) select(Mandate)
.options( .options(
selectinload(Mandate.steps), selectinload(Mandate.steps),
@@ -47,6 +49,11 @@ async def _get_mandate(db: AsyncSession, mandate_id: uuid.UUID) -> Mandate:
) )
.where(Mandate.id == mandate_id) .where(Mandate.id == mandate_id)
) )
if org_id is not None:
stmt = stmt.where(Mandate.organization_id == org_id)
else:
stmt = stmt.where(Mandate.organization_id.is_(None))
result = await db.execute(stmt)
mandate = result.scalar_one_or_none() mandate = result.scalar_one_or_none()
if mandate is None: if mandate is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Mandat introuvable") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Mandat introuvable")
@@ -80,6 +87,8 @@ async def list_mandates(
if org_id is not None: if org_id is not None:
stmt = stmt.where(Mandate.organization_id == org_id) stmt = stmt.where(Mandate.organization_id == org_id)
else:
stmt = stmt.where(Mandate.organization_id.is_(None))
if mandate_type is not None: if mandate_type is not None:
stmt = stmt.where(Mandate.mandate_type == mandate_type) stmt = stmt.where(Mandate.mandate_type == mandate_type)
if status_filter is not None: if status_filter is not None:
@@ -111,7 +120,7 @@ async def create_mandate(
await db.commit() await db.commit()
await db.refresh(mandate) await db.refresh(mandate)
mandate = await _get_mandate(db, mandate.id) mandate = await _get_mandate(db, mandate.id, org_id)
return _mandate_out(mandate) return _mandate_out(mandate)
@@ -119,8 +128,9 @@ async def create_mandate(
async def get_mandate( async def get_mandate(
id: uuid.UUID, id: uuid.UUID,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
org_id: uuid.UUID | None = Depends(get_active_org_id),
) -> MandateOut: ) -> MandateOut:
mandate = await _get_mandate(db, id) mandate = await _get_mandate(db, id, org_id)
return _mandate_out(mandate) return _mandate_out(mandate)
@@ -130,14 +140,15 @@ async def update_mandate(
payload: MandateUpdate, payload: MandateUpdate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
identity: DuniterIdentity = Depends(get_current_identity), identity: DuniterIdentity = Depends(get_current_identity),
org_id: uuid.UUID | None = Depends(get_active_org_id),
) -> MandateOut: ) -> MandateOut:
mandate = await _get_mandate(db, id) mandate = await _get_mandate(db, id, org_id)
for field, value in payload.model_dump(exclude_unset=True).items(): for field, value in payload.model_dump(exclude_unset=True).items():
setattr(mandate, field, value) setattr(mandate, field, value)
await db.commit() await db.commit()
mandate = await _get_mandate(db, mandate.id) mandate = await _get_mandate(db, mandate.id, org_id)
return _mandate_out(mandate) return _mandate_out(mandate)
@@ -146,8 +157,9 @@ async def delete_mandate(
id: uuid.UUID, id: uuid.UUID,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
identity: DuniterIdentity = Depends(get_current_identity), identity: DuniterIdentity = Depends(get_current_identity),
org_id: uuid.UUID | None = Depends(get_active_org_id),
) -> None: ) -> None:
mandate = await _get_mandate(db, id) mandate = await _get_mandate(db, id, org_id)
if mandate.status != "draft": if mandate.status != "draft":
raise HTTPException( raise HTTPException(
@@ -168,8 +180,9 @@ async def add_step(
payload: MandateStepCreate, payload: MandateStepCreate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
identity: DuniterIdentity = Depends(get_current_identity), identity: DuniterIdentity = Depends(get_current_identity),
org_id: uuid.UUID | None = Depends(get_active_org_id),
) -> MandateStepOut: ) -> MandateStepOut:
mandate = await _get_mandate(db, id) mandate = await _get_mandate(db, id, org_id)
step = MandateStep(mandate_id=mandate.id, **payload.model_dump()) step = MandateStep(mandate_id=mandate.id, **payload.model_dump())
db.add(step) db.add(step)
@@ -183,8 +196,9 @@ async def add_step(
async def list_steps( async def list_steps(
id: uuid.UUID, id: uuid.UUID,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
org_id: uuid.UUID | None = Depends(get_active_org_id),
) -> list[MandateStepOut]: ) -> list[MandateStepOut]:
mandate = await _get_mandate(db, id) mandate = await _get_mandate(db, id, org_id)
return [MandateStepOut.model_validate(s) for s in mandate.steps] return [MandateStepOut.model_validate(s) for s in mandate.steps]
@@ -196,13 +210,14 @@ async def advance_mandate_endpoint(
id: uuid.UUID, id: uuid.UUID,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
identity: DuniterIdentity = Depends(get_current_identity), identity: DuniterIdentity = Depends(get_current_identity),
org_id: uuid.UUID | None = Depends(get_active_org_id),
) -> MandateAdvanceOut: ) -> MandateAdvanceOut:
try: try:
mandate = await advance_mandate(id, db) mandate = await advance_mandate(id, db)
except ValueError as exc: except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
mandate = await _get_mandate(db, mandate.id) mandate = await _get_mandate(db, mandate.id, org_id)
out = _mandate_out(mandate) out = _mandate_out(mandate)
return MandateAdvanceOut( return MandateAdvanceOut(
**out.model_dump(), **out.model_dump(),
@@ -216,13 +231,14 @@ async def assign_mandatee_endpoint(
payload: MandateAssignRequest, payload: MandateAssignRequest,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
identity: DuniterIdentity = Depends(get_current_identity), identity: DuniterIdentity = Depends(get_current_identity),
org_id: uuid.UUID | None = Depends(get_active_org_id),
) -> MandateOut: ) -> MandateOut:
try: try:
mandate = await assign_mandatee(id, payload.mandatee_id, db) mandate = await assign_mandatee(id, payload.mandatee_id, db)
except ValueError as exc: except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
mandate = await _get_mandate(db, mandate.id) mandate = await _get_mandate(db, mandate.id, org_id)
return _mandate_out(mandate) return _mandate_out(mandate)
@@ -231,13 +247,14 @@ async def revoke_mandate_endpoint(
id: uuid.UUID, id: uuid.UUID,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
identity: DuniterIdentity = Depends(get_current_identity), identity: DuniterIdentity = Depends(get_current_identity),
org_id: uuid.UUID | None = Depends(get_active_org_id),
) -> MandateOut: ) -> MandateOut:
try: try:
mandate = await revoke_mandate(id, db) mandate = await revoke_mandate(id, db)
except ValueError as exc: except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
mandate = await _get_mandate(db, mandate.id) mandate = await _get_mandate(db, mandate.id, org_id)
return _mandate_out(mandate) return _mandate_out(mandate)
@@ -251,6 +268,7 @@ async def create_vote_session_for_step_endpoint(
step_id: uuid.UUID, step_id: uuid.UUID,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
identity: DuniterIdentity = Depends(get_current_identity), identity: DuniterIdentity = Depends(get_current_identity),
org_id: uuid.UUID | None = Depends(get_active_org_id),
) -> VoteSessionOut: ) -> VoteSessionOut:
try: try:
session = await create_vote_session_for_step(id, step_id, db) session = await create_vote_session_for_step(id, step_id, db)
+18 -7
View File
@@ -34,13 +34,20 @@ router = APIRouter()
# ── Helpers ───────────────────────────────────────────────────────────────── # ── Helpers ─────────────────────────────────────────────────────────────────
async def _get_protocol(db: AsyncSession, protocol_id: uuid.UUID) -> VotingProtocol: async def _get_protocol(
"""Fetch a voting protocol by ID with its formula config, or raise 404.""" db: AsyncSession, protocol_id: uuid.UUID, org_id: uuid.UUID | None = None
result = await db.execute( ) -> VotingProtocol:
"""Fetch a voting protocol by ID within the active org scope, or raise 404."""
stmt = (
select(VotingProtocol) select(VotingProtocol)
.options(selectinload(VotingProtocol.formula_config)) .options(selectinload(VotingProtocol.formula_config))
.where(VotingProtocol.id == protocol_id) .where(VotingProtocol.id == protocol_id)
) )
if org_id is not None:
stmt = stmt.where(VotingProtocol.organization_id == org_id)
else:
stmt = stmt.where(VotingProtocol.organization_id.is_(None))
result = await db.execute(stmt)
protocol = result.scalar_one_or_none() protocol = result.scalar_one_or_none()
if protocol is None: if protocol is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Protocole introuvable") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Protocole introuvable")
@@ -74,6 +81,8 @@ async def list_protocols(
if org_id is not None: if org_id is not None:
stmt = stmt.where(VotingProtocol.organization_id == org_id) stmt = stmt.where(VotingProtocol.organization_id == org_id)
else:
stmt = stmt.where(VotingProtocol.organization_id.is_(None))
if vote_type is not None: if vote_type is not None:
stmt = stmt.where(VotingProtocol.vote_type == vote_type) stmt = stmt.where(VotingProtocol.vote_type == vote_type)
@@ -111,7 +120,7 @@ async def create_protocol(
await db.refresh(protocol) await db.refresh(protocol)
# Reload with formula config # Reload with formula config
protocol = await _get_protocol(db, protocol.id) protocol = await _get_protocol(db, protocol.id, org_id)
return VotingProtocolOut.model_validate(protocol) return VotingProtocolOut.model_validate(protocol)
@@ -119,9 +128,10 @@ async def create_protocol(
async def get_protocol( async def get_protocol(
id: uuid.UUID, id: uuid.UUID,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
org_id: uuid.UUID | None = Depends(get_active_org_id),
) -> VotingProtocolOut: ) -> VotingProtocolOut:
"""Get a single voting protocol with its formula configuration.""" """Get a single voting protocol with its formula configuration."""
protocol = await _get_protocol(db, id) protocol = await _get_protocol(db, id, org_id)
return VotingProtocolOut.model_validate(protocol) return VotingProtocolOut.model_validate(protocol)
@@ -131,12 +141,13 @@ async def update_protocol(
payload: VotingProtocolUpdate, payload: VotingProtocolUpdate,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
identity: DuniterIdentity = Depends(get_current_identity), identity: DuniterIdentity = Depends(get_current_identity),
org_id: uuid.UUID | None = Depends(get_active_org_id),
) -> VotingProtocolOut: ) -> VotingProtocolOut:
"""Update a voting protocol (meta-governance). """Update a voting protocol (meta-governance).
Only provided fields will be updated. Requires authentication. Only provided fields will be updated. Requires authentication.
""" """
protocol = await _get_protocol(db, id) protocol = await _get_protocol(db, id, org_id)
update_data = payload.model_dump(exclude_unset=True) update_data = payload.model_dump(exclude_unset=True)
for field, value in update_data.items(): for field, value in update_data.items():
@@ -145,7 +156,7 @@ async def update_protocol(
await db.commit() await db.commit()
# Reload with formula config # Reload with formula config
protocol = await _get_protocol(db, protocol.id) protocol = await _get_protocol(db, protocol.id, org_id)
return VotingProtocolOut.model_validate(protocol) return VotingProtocolOut.model_validate(protocol)
+10 -1
View File
@@ -25,6 +25,7 @@ from app.schemas.vote import (
VoteSessionListOut, VoteSessionListOut,
VoteSessionOut, VoteSessionOut,
) )
from app.dependencies.org import get_active_org_id
from app.services.auth_service import get_current_identity from app.services.auth_service import get_current_identity
from app.services.vote_service import ( from app.services.vote_service import (
close_session as svc_close_session, close_session as svc_close_session,
@@ -170,14 +171,22 @@ def _compute_result(
@router.get("/sessions", response_model=list[VoteSessionListOut]) @router.get("/sessions", response_model=list[VoteSessionListOut])
async def list_vote_sessions( async def list_vote_sessions(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
org_id: uuid.UUID | None = Depends(get_active_org_id),
session_status: str | None = Query(default=None, alias="status", description="Filtrer par statut (open, closed, tallied)"), session_status: str | None = Query(default=None, alias="status", description="Filtrer par statut (open, closed, tallied)"),
decision_id: uuid.UUID | None = Query(default=None, description="Filtrer par decision_id"), decision_id: uuid.UUID | None = Query(default=None, description="Filtrer par decision_id"),
skip: int = Query(default=0, ge=0), skip: int = Query(default=0, ge=0),
limit: int = Query(default=50, ge=1, le=200), limit: int = Query(default=50, ge=1, le=200),
) -> list[VoteSessionListOut]: ) -> list[VoteSessionListOut]:
"""List all vote sessions with optional filters by status and decision_id.""" """List all vote sessions with optional filters by status and decision_id."""
stmt = select(VoteSession) stmt = (
select(VoteSession)
.join(VotingProtocol, VoteSession.voting_protocol_id == VotingProtocol.id)
)
if org_id is not None:
stmt = stmt.where(VotingProtocol.organization_id == org_id)
else:
stmt = stmt.where(VotingProtocol.organization_id.is_(None))
if session_status is not None: if session_status is not None:
stmt = stmt.where(VoteSession.status == session_status) stmt = stmt.where(VoteSession.status == session_status)
if decision_id is not None: if decision_id is not None: