fix: admin voit toutes les images, ajout images:delete par défaut, filtre client_id pour admin
CI / Lint & Format (push) Failing after 9s
CI / Tests (push) Has been skipped
CI / Security Scan (push) Failing after 7s
CI / Docker Build (push) Has been skipped

This commit is contained in:
2026-06-23 15:35:42 -04:00
parent 262f29f013
commit 8e9c600aa3
3 changed files with 28 additions and 21 deletions
+26 -19
View File
@@ -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")
+1 -1
View File
@@ -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")
+1 -1
View File
@@ -33,7 +33,7 @@ export default function Clients() {
// Form state
const [formName, setFormName] = useState('');
const [formPlan, setFormPlan] = useState<Plan>('free');
const [formScopes, setFormScopes] = useState<string[]>(['images:read', 'images:write']);
const [formScopes, setFormScopes] = useState<string[]>(['images:read', 'images:write', 'images:delete']);
const filtered = clients?.filter((c) => {
if (search && !c.name.toLowerCase().includes(search.toLowerCase())) return false;