""" 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)