""" API Key management routes for Pro users Story 3.1: Modèle API Key & Génération """ import hashlib import logging import secrets from datetime import datetime, timezone from typing import Optional logger = logging.getLogger(__name__) from fastapi import APIRouter, Depends, Request from fastapi.responses import JSONResponse from pydantic import BaseModel, Field from routes.deps import ProUser, require_pro_user from database.connection import get_sync_session from database.models import ApiKey router = APIRouter(prefix="/api/v1/api-keys", tags=["API Keys v1"]) MAX_API_KEYS_PER_USER = 10 class ApiKeyCreateRequest(BaseModel): name: Optional[str] = Field(default="Default API Key", max_length=100) class ApiKeyResponse(BaseModel): id: str key: str name: str key_prefix: str created_at: str def _generate_api_key() -> tuple[str, str, str]: """ Generate a secure API key with sk_live_ prefix. Returns: tuple: (raw_key, key_hash, key_prefix) """ raw_random = secrets.token_urlsafe(32) raw_key = f"sk_live_{raw_random}" key_hash = hashlib.sha256(raw_key.encode()).hexdigest() key_prefix = raw_key[:8] return raw_key, key_hash, key_prefix @router.post("") async def create_api_key( request: Request, body: Optional[ApiKeyCreateRequest] = None, user: ProUser = Depends(require_pro_user), ): """ Create a new API key for the authenticated Pro user. Returns: 201: API key created successfully (key shown ONCE) 401: Authentication required 403: Pro subscription required 429: Maximum API keys reached """ key_name = body.name if body and body.name else "Default API Key" raw_key, key_hash, key_prefix = _generate_api_key() with get_sync_session() as session: existing_count = ( session.query(ApiKey) .filter( ApiKey.user_id == user.id, ApiKey.is_active == True, ) .count() ) if existing_count >= MAX_API_KEYS_PER_USER: return JSONResponse( status_code=429, content={ "error": "API_KEY_LIMIT_REACHED", "message": f"Maximum de {MAX_API_KEYS_PER_USER} clés API atteint. Supprimez une clé existante.", }, ) api_key = ApiKey( user_id=user.id, name=key_name, key_hash=key_hash, key_prefix=key_prefix, is_active=True, scopes=["translate"], created_at=datetime.now(timezone.utc), ) session.add(api_key) session.commit() session.refresh(api_key) return JSONResponse( status_code=201, content={ "data": { "id": api_key.id, "key": raw_key, "name": api_key.name, "key_prefix": api_key.key_prefix, "created_at": api_key.created_at.isoformat() if api_key.created_at else None, }, "meta": {}, }, ) @router.get("") async def list_api_keys( request: Request, user: ProUser = Depends(require_pro_user), ): """ List all API keys for the authenticated Pro user. Note: Keys are returned without the secret (only prefix visible). Returns: 200: List of API keys 401: Authentication required 403: Pro subscription required """ with get_sync_session() as session: keys = ( session.query(ApiKey) .filter(ApiKey.user_id == user.id) .order_by(ApiKey.created_at.desc()) .all() ) return JSONResponse( status_code=200, content={ "data": [ { "id": key.id, "name": key.name, "key_prefix": key.key_prefix, "is_active": key.is_active, "last_used_at": key.last_used_at.isoformat() if key.last_used_at else None, "usage_count": key.usage_count, "created_at": key.created_at.isoformat() if key.created_at else None, } for key in keys ], "meta": {"total": len(keys)}, }, ) @router.delete("/{key_id}") async def revoke_api_key( key_id: str, user: ProUser = Depends(require_pro_user), ): """ Revoke an API key for the authenticated Pro user. This performs a soft delete by setting is_active=False. Only the owner of the key can revoke it. Only active keys can be revoked. Returns: 200: API key revoked successfully 401: Authentication required 403: Pro subscription required 404: API key not found or already revoked """ # Validate key_id format (UUID) try: import uuid as uuid_module uuid_module.UUID(key_id) except ValueError: return JSONResponse( status_code=400, content={ "error": "INVALID_KEY_ID", "message": "Format d'identifiant de clé API invalide.", }, ) with get_sync_session() as session: # Security: Filter by user_id AND is_active so only the owner can revoke active keys api_key = ( session.query(ApiKey) .filter( ApiKey.id == key_id, ApiKey.user_id == user.id, ApiKey.is_active == True, # Only active keys can be revoked ) .first() ) if not api_key: return JSONResponse( status_code=404, content={ "error": "API_KEY_NOT_FOUND", "message": "Clé API non trouvée, n'appartient pas à l'utilisateur ou déjà révoquée.", }, ) # Soft delete - mark as inactive and record revocation timestamp revoked_at = datetime.now(timezone.utc) api_key.is_active = False api_key.revoked_at = revoked_at session.commit() logger.info(f"API key {key_id} revoked by user {user.id}") return JSONResponse( status_code=200, content={ "data": { "id": api_key.id, "revoked": True, "revoked_at": revoked_at.isoformat(), }, "meta": {}, }, )