"""Public API router: read-only endpoints for external consumption. All endpoints are accessible without authentication. No mutations allowed -- strictly read-only. """ from __future__ import annotations import uuid from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.models.decision import Decision from app.models.document import Document, DocumentItem from app.models.sanctuary import SanctuaryEntry from app.models.vote import VoteSession from app.schemas.document import DocumentFullOut, DocumentItemOut, DocumentOut from app.schemas.sanctuary import SanctuaryEntryOut from app.services import document_service, sanctuary_service router = APIRouter() # ── Documents (public, read-only) ───────────────────────────────────────── @router.get( "/documents", response_model=list[DocumentOut], tags=["public-documents"], summary="Liste publique des documents actifs", ) async def list_documents( db: AsyncSession = Depends(get_db), doc_type: str | None = Query(default=None, description="Filtrer par type de document"), status_filter: str | None = Query(default=None, alias="status", description="Filtrer par statut"), skip: int = Query(default=0, ge=0), limit: int = Query(default=50, ge=1, le=200), ) -> list[DocumentOut]: """Liste les documents de reference avec leurs items (lecture seule).""" stmt = select(Document) if doc_type is not None: stmt = stmt.where(Document.doc_type == doc_type) if status_filter is not None: stmt = stmt.where(Document.status == status_filter) stmt = stmt.order_by(Document.created_at.desc()).offset(skip).limit(limit) result = await db.execute(stmt) documents = result.scalars().all() out = [] for doc in documents: count_result = await db.execute( select(func.count()).select_from(DocumentItem).where(DocumentItem.document_id == doc.id) ) items_count = count_result.scalar() or 0 doc_out = DocumentOut.model_validate(doc) doc_out.items_count = items_count out.append(doc_out) return out @router.get( "/documents/{slug}", response_model=DocumentFullOut, tags=["public-documents"], summary="Document complet avec ses items", ) async def get_document( slug: str, db: AsyncSession = Depends(get_db), ) -> DocumentFullOut: """Recupere un document avec tous ses items (texte complet, serialise).""" doc = await document_service.get_document_with_items(slug, db) if doc is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Document introuvable", ) return DocumentFullOut.model_validate(doc) # ── Sanctuary (public, read-only) ───────────────────────────────────────── @router.get( "/sanctuary/entries", response_model=list[SanctuaryEntryOut], tags=["public-sanctuary"], summary="Liste des entrees du sanctuaire", ) async def list_sanctuary_entries( db: AsyncSession = Depends(get_db), entry_type: str | None = Query(default=None, description="Filtrer par type (document, decision, vote_result)"), skip: int = Query(default=0, ge=0), limit: int = Query(default=50, ge=1, le=200), ) -> list[SanctuaryEntryOut]: """Liste les entrees du sanctuaire (archives verifiees).""" stmt = select(SanctuaryEntry) if entry_type is not None: stmt = stmt.where(SanctuaryEntry.entry_type == entry_type) stmt = stmt.order_by(SanctuaryEntry.created_at.desc()).offset(skip).limit(limit) result = await db.execute(stmt) entries = result.scalars().all() return [SanctuaryEntryOut.model_validate(e) for e in entries] @router.get( "/sanctuary/entries/{id}", response_model=SanctuaryEntryOut, tags=["public-sanctuary"], summary="Entree du sanctuaire par ID", ) async def get_sanctuary_entry( id: uuid.UUID, db: AsyncSession = Depends(get_db), ) -> SanctuaryEntryOut: """Recupere une entree du sanctuaire avec lien IPFS et ancrage on-chain.""" result = await db.execute( select(SanctuaryEntry).where(SanctuaryEntry.id == id) ) entry = result.scalar_one_or_none() if entry is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Entree sanctuaire introuvable", ) return SanctuaryEntryOut.model_validate(entry) @router.get( "/sanctuary/verify/{id}", tags=["public-sanctuary"], summary="Verification d'integrite d'une entree", ) async def verify_sanctuary_entry( id: uuid.UUID, db: AsyncSession = Depends(get_db), ) -> dict: """Verifie l'integrite d'une entree du sanctuaire (comparaison de hash).""" try: result = await sanctuary_service.verify_entry(id, db) except ValueError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(exc), ) return result # ── Votes (public, read-only) ───────────────────────────────────────────── @router.get( "/votes/sessions/{id}/result", tags=["public-votes"], summary="Resultat d'une session de vote", ) async def get_vote_result( id: uuid.UUID, db: AsyncSession = Depends(get_db), ) -> dict: """Recupere le resultat d'une session de vote (lecture seule, public).""" result = await db.execute( select(VoteSession).where(VoteSession.id == id) ) session = result.scalar_one_or_none() if session is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Session de vote introuvable", ) return { "session_id": str(session.id), "status": session.status, "votes_for": session.votes_for, "votes_against": session.votes_against, "votes_total": session.votes_total, "wot_size": session.wot_size, "smith_size": session.smith_size, "techcomm_size": session.techcomm_size, "threshold_required": session.threshold_required, "result": session.result, "starts_at": session.starts_at.isoformat() if session.starts_at else None, "ends_at": session.ends_at.isoformat() if session.ends_at else None, "chain_recorded": session.chain_recorded, "chain_tx_hash": session.chain_tx_hash, } # ── Platform status ─────────────────────────────────────────────────────── @router.get( "/status", tags=["public-status"], summary="Statut de la plateforme", ) async def platform_status( db: AsyncSession = Depends(get_db), ) -> dict: """Statut general de la plateforme: compteurs de documents, decisions, votes actifs.""" # Count documents doc_count_result = await db.execute( select(func.count()).select_from(Document) ) documents_count = doc_count_result.scalar() or 0 # Count decisions decision_count_result = await db.execute( select(func.count()).select_from(Decision) ) decisions_count = decision_count_result.scalar() or 0 # Count active vote sessions active_votes_result = await db.execute( select(func.count()).select_from(VoteSession).where(VoteSession.status == "open") ) active_votes_count = active_votes_result.scalar() or 0 # Count total vote sessions total_votes_result = await db.execute( select(func.count()).select_from(VoteSession) ) total_votes_count = total_votes_result.scalar() or 0 # Count sanctuary entries sanctuary_count_result = await db.execute( select(func.count()).select_from(SanctuaryEntry) ) sanctuary_count = sanctuary_count_result.scalar() or 0 return { "platform": "Glibredecision", "documents_count": documents_count, "decisions_count": decisions_count, "active_votes_count": active_votes_count, "total_votes_count": total_votes_count, "sanctuary_entries_count": sanctuary_count, }