- Implement tests for database generator to ensure proper session handling. - Create tests for EXIF extraction and conversion functions. - Add tests for image-related endpoints, ensuring proper data retrieval and isolation between clients. - Develop tests for OCR functionality, including language detection and text extraction. - Introduce tests for the image processing pipeline, covering success and failure scenarios. - Validate rate limiting functionality and ensure independent counters for different clients. - Implement scraper tests to verify HTML content fetching and error handling. - Add unit tests for various services, including storage and filename generation. - Establish worker entry point for ARQ to handle background image processing tasks.
199 lines
7.5 KiB
Python
199 lines
7.5 KiB
Python
"""
|
|
Router — Auth : gestion des clients API (CRUD + rotation de clé)
|
|
"""
|
|
import logging
|
|
import secrets
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.database import get_db
|
|
from app.dependencies.auth import get_current_client, hash_api_key, require_scope
|
|
from app.models.client import APIClient
|
|
from app.schemas.auth import (
|
|
ClientCreate,
|
|
ClientCreateResponse,
|
|
ClientResponse,
|
|
ClientUpdate,
|
|
KeyRotateResponse,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/auth", tags=["Authentification"])
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# CRÉER UN CLIENT
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
@router.post(
|
|
"/clients",
|
|
response_model=ClientCreateResponse,
|
|
status_code=status.HTTP_201_CREATED,
|
|
summary="Créer un nouveau client API",
|
|
description=(
|
|
"Crée un client et retourne la clé API **en clair une seule fois**. "
|
|
"Stockez-la immédiatement — elle ne sera plus jamais affichée."
|
|
),
|
|
dependencies=[Depends(require_scope("admin"))],
|
|
)
|
|
async def create_client(
|
|
body: ClientCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> ClientCreateResponse:
|
|
# Génération de la clé API
|
|
raw_key = secrets.token_urlsafe(32)
|
|
key_hash = hash_api_key(raw_key)
|
|
|
|
client = APIClient(
|
|
name=body.name,
|
|
api_key_hash=key_hash,
|
|
scopes=body.scopes,
|
|
plan=body.plan,
|
|
)
|
|
db.add(client)
|
|
await db.flush()
|
|
await db.refresh(client)
|
|
|
|
logger.info("Client créé : %s (%s)", client.name, client.id)
|
|
|
|
return ClientCreateResponse(
|
|
id=client.id,
|
|
name=client.name,
|
|
scopes=client.scopes,
|
|
plan=client.plan,
|
|
is_active=client.is_active,
|
|
created_at=client.created_at,
|
|
updated_at=client.updated_at,
|
|
api_key=raw_key,
|
|
)
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# LISTER LES CLIENTS
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
@router.get(
|
|
"/clients",
|
|
response_model=list[ClientResponse],
|
|
summary="Lister tous les clients API",
|
|
dependencies=[Depends(require_scope("admin"))],
|
|
)
|
|
async def list_clients(
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> list[ClientResponse]:
|
|
result = await db.execute(select(APIClient).order_by(APIClient.created_at.desc()))
|
|
clients = result.scalars().all()
|
|
return [ClientResponse.model_validate(c) for c in clients]
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# DÉTAIL D'UN CLIENT
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
@router.get(
|
|
"/clients/{client_id}",
|
|
response_model=ClientResponse,
|
|
summary="Détail d'un client API",
|
|
dependencies=[Depends(require_scope("admin"))],
|
|
)
|
|
async def get_client(
|
|
client_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> ClientResponse:
|
|
result = await db.execute(select(APIClient).where(APIClient.id == client_id))
|
|
client = result.scalar_one_or_none()
|
|
if not client:
|
|
raise HTTPException(status_code=404, detail="Client introuvable")
|
|
return ClientResponse.model_validate(client)
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# MODIFIER UN CLIENT
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
@router.patch(
|
|
"/clients/{client_id}",
|
|
response_model=ClientResponse,
|
|
summary="Modifier un client API",
|
|
dependencies=[Depends(require_scope("admin"))],
|
|
)
|
|
async def update_client(
|
|
client_id: str,
|
|
body: ClientUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> ClientResponse:
|
|
result = await db.execute(select(APIClient).where(APIClient.id == client_id))
|
|
client = result.scalar_one_or_none()
|
|
if not client:
|
|
raise HTTPException(status_code=404, detail="Client introuvable")
|
|
|
|
update_data = body.model_dump(exclude_unset=True)
|
|
for field, value in update_data.items():
|
|
setattr(client, field, value)
|
|
|
|
await db.flush()
|
|
await db.refresh(client)
|
|
|
|
logger.info("Client mis à jour : %s (%s)", client.name, client.id)
|
|
return ClientResponse.model_validate(client)
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# ROTATION DE CLÉ
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
@router.post(
|
|
"/clients/{client_id}/rotate-key",
|
|
response_model=KeyRotateResponse,
|
|
summary="Régénérer la clé API d'un client",
|
|
description="Invalide l'ancienne clé et en génère une nouvelle.",
|
|
dependencies=[Depends(require_scope("admin"))],
|
|
)
|
|
async def rotate_key(
|
|
client_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> KeyRotateResponse:
|
|
result = await db.execute(select(APIClient).where(APIClient.id == client_id))
|
|
client = result.scalar_one_or_none()
|
|
if not client:
|
|
raise HTTPException(status_code=404, detail="Client introuvable")
|
|
|
|
raw_key = secrets.token_urlsafe(32)
|
|
client.api_key_hash = hash_api_key(raw_key)
|
|
await db.flush()
|
|
|
|
logger.info("Clé API rotée pour client : %s (%s)", client.name, client.id)
|
|
|
|
return KeyRotateResponse(id=client.id, api_key=raw_key)
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# DÉSACTIVER UN CLIENT (soft delete)
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
@router.delete(
|
|
"/clients/{client_id}",
|
|
response_model=ClientResponse,
|
|
summary="Désactiver un client API",
|
|
description="Soft delete — marque le client comme inactif sans supprimer les données.",
|
|
dependencies=[Depends(require_scope("admin"))],
|
|
)
|
|
async def delete_client(
|
|
client_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> ClientResponse:
|
|
result = await db.execute(select(APIClient).where(APIClient.id == client_id))
|
|
client = result.scalar_one_or_none()
|
|
if not client:
|
|
raise HTTPException(status_code=404, detail="Client introuvable")
|
|
|
|
client.is_active = False
|
|
await db.flush()
|
|
await db.refresh(client)
|
|
|
|
logger.info("Client désactivé : %s (%s)", client.name, client.id)
|
|
return ClientResponse.model_validate(client)
|