From 8e9c600aa36bba486cde19ca6b323c96fe0aa36d Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Tue, 23 Jun 2026 15:35:35 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20admin=20voit=20toutes=20les=20images,=20?= =?UTF-8?q?ajout=20images:delete=20par=20d=C3=A9faut,=20filtre=20client=5F?= =?UTF-8?q?id=20pour=20admin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routers/images.py | 45 ++++++++++++++----------- app/schemas/auth.py | 2 +- imago-admin/src/pages/admin/Clients.tsx | 2 +- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/app/routers/images.py b/app/routers/images.py index b8333d9..467f125 100644 --- a/app/routers/images.py +++ b/app/routers/images.py @@ -40,18 +40,17 @@ router = APIRouter(prefix="/images", tags=["Images"]) # ───────────────────────────────────────────────────────────── async def get_image_or_404( - image_id: int, client_id: str, db: AsyncSession + image_id: int, client: APIClient, db: AsyncSession ) -> Image: """ - Récupère une image par ID en vérifiant qu'elle appartient au client. + Récupère une image par ID. L'admin peut accéder à toutes les images. + Les autres clients sont limités à leurs propres images. Lève HTTP 404 si introuvable ou si elle n'appartient pas au client. """ - result = await db.execute( - select(Image).where( - Image.id == image_id, - Image.client_id == client_id, - ) - ) + query = select(Image).where(Image.id == image_id) + if not client.has_scope("admin"): + query = query.where(Image.client_id == client.id) + result = await db.execute(query) image = result.scalar_one_or_none() if not image: raise HTTPException(status_code=404, detail="Image introuvable") @@ -159,11 +158,19 @@ async def list_images( tag: Optional[str] = Query(None, description="Filtrer par tag AI"), status_filter: Optional[ProcessingStatus] = Query(None, alias="status"), search: Optional[str] = Query(None, description="Recherche dans description et OCR"), + client_id: Optional[str] = Query(None, description="Filtrer par client (admin seulement)"), db: AsyncSession = Depends(get_db), client: APIClient = Depends(get_current_client), ): - # Filtre d'isolation par client - query = select(Image).where(Image.client_id == client.id) + # Admin : voit toutes les images, ou filtrées par client_id + if client.has_scope("admin"): + if client_id: + query = select(Image).where(Image.client_id == client_id) + else: + query = select(Image) + else: + # Filtre d'isolation par client + query = select(Image).where(Image.client_id == client.id) if status_filter: query = query.where(Image.processing_status == status_filter) @@ -242,7 +249,7 @@ async def get_image( db: AsyncSession = Depends(get_db), client: APIClient = Depends(get_current_client), ): - image = await get_image_or_404(image_id, client.id, db) + image = await get_image_or_404(image_id, client, db) return ImageDetail.from_orm_full(image) @@ -262,7 +269,7 @@ async def get_status( db: AsyncSession = Depends(get_db), client: APIClient = Depends(get_current_client), ): - image = await get_image_or_404(image_id, client.id, db) + image = await get_image_or_404(image_id, client, db) return StatusResponse( id=image.id, @@ -288,7 +295,7 @@ async def get_exif( db: AsyncSession = Depends(get_db), client: APIClient = Depends(get_current_client), ): - image = await get_image_or_404(image_id, client.id, db) + image = await get_image_or_404(image_id, client, db) return { "id": image.id, @@ -333,7 +340,7 @@ async def get_ocr( db: AsyncSession = Depends(get_db), client: APIClient = Depends(get_current_client), ): - image = await get_image_or_404(image_id, client.id, db) + image = await get_image_or_404(image_id, client, db) return { "id": image.id, @@ -358,7 +365,7 @@ async def get_ai( db: AsyncSession = Depends(get_db), client: APIClient = Depends(get_current_client), ): - image = await get_image_or_404(image_id, client.id, db) + image = await get_image_or_404(image_id, client, db) return { "id": image.id, @@ -425,7 +432,7 @@ async def reprocess_image( db: AsyncSession = Depends(get_db), client: APIClient = Depends(get_current_client), ): - image = await get_image_or_404(image_id, client.id, db) + image = await get_image_or_404(image_id, client, db) # Reset du statut image.processing_status = ProcessingStatus.PENDING @@ -487,7 +494,7 @@ async def delete_image( db: AsyncSession = Depends(get_db), client: APIClient = Depends(get_current_client), ): - image = await get_image_or_404(image_id, client.id, db) + image = await get_image_or_404(image_id, client, db) # Décrémentation du quota file_size = image.file_size or 0 @@ -524,7 +531,7 @@ async def get_download_url( ): from app.services.storage_backend import get_storage_backend - image = await get_image_or_404(image_id, client.id, db) + image = await get_image_or_404(image_id, client, db) backend = get_storage_backend() url = await backend.get_signed_url(image.file_path, expires_in=expires_in) @@ -545,7 +552,7 @@ async def get_thumbnail_url( ): from app.services.storage_backend import get_storage_backend - image = await get_image_or_404(image_id, client.id, db) + image = await get_image_or_404(image_id, client, db) if not image.thumbnail_path: raise HTTPException(status_code=404, detail="Thumbnail non disponible") diff --git a/app/schemas/auth.py b/app/schemas/auth.py index e6350bc..ba1a887 100644 --- a/app/schemas/auth.py +++ b/app/schemas/auth.py @@ -24,7 +24,7 @@ class ClientCreate(BaseModel): """Créer un nouveau client API.""" name: str = Field(..., min_length=1, max_length=256, description="Nom de l'application cliente") scopes: List[str] = Field( - default=["images:read", "images:write"], + default=["images:read", "images:write", "images:delete"], description="Permissions accordées", ) plan: ClientPlan = Field(default=ClientPlan.FREE, description="Plan tarifaire") diff --git a/imago-admin/src/pages/admin/Clients.tsx b/imago-admin/src/pages/admin/Clients.tsx index 660320d..aeda1ee 100644 --- a/imago-admin/src/pages/admin/Clients.tsx +++ b/imago-admin/src/pages/admin/Clients.tsx @@ -33,7 +33,7 @@ export default function Clients() { // Form state const [formName, setFormName] = useState(''); const [formPlan, setFormPlan] = useState('free'); - const [formScopes, setFormScopes] = useState(['images:read', 'images:write']); + const [formScopes, setFormScopes] = useState(['images:read', 'images:write', 'images:delete']); const filtered = clients?.filter((c) => { if (search && !c.name.toLowerCase().includes(search.toLowerCase())) return false;