# Prompt di Ingaggio: Gestione API Keys (T23-T29) ## 🎯 MISSIONE Implementare la fase **Gestione API Keys** del progetto OpenRouter API Key Monitor seguendo rigorosamente TDD. **Task da completare:** T23, T24, T25, T26, T27, T28, T29 --- ## πŸ“‹ CONTESTO **AGENTE:** @tdd-developer **Repository:** `/home/google/Sources/LucaSacchiNet/openrouter-watcher` **Stato Attuale:** - βœ… Setup (T01-T05): 59 test - βœ… Database & Models (T06-T11): 73 test - βœ… Security Services (T12-T16): 70 test - βœ… User Authentication (T17-T22): 34 test - 🎯 **Totale: 236 test passanti, 98.23% coverage** **Servizi Pronti da utilizzare:** - `EncryptionService` - Cifratura/decifratura API keys - `hash_password()`, `verify_password()` - Autenticazione - `create_access_token()`, `decode_access_token()` - JWT - `get_current_user()` - Dependency injection - `generate_api_token()` - Token API pubblica **Modelli Pronti:** - `User`, `ApiKey`, `UsageStats`, `ApiToken` - SQLAlchemy models - `get_db()` - Database session **Documentazione:** - PRD: `/home/google/Sources/LucaSacchiNet/openrouter-watcher/prd.md` - Architecture: `/home/google/Sources/LucaSacchiNet/openrouter-watcher/export/architecture.md` - Kanban: `/home/google/Sources/LucaSacchiNet/openrouter-watcher/export/kanban.md` --- ## πŸ”§ TASK DA IMPLEMENTARE ### T23: Creare Pydantic Schemas per API Keys **File:** `src/openrouter_monitor/schemas/api_key.py` **Requisiti:** - `ApiKeyCreate`: name (str, min 1, max 100), key (str) - OpenRouter API key - `ApiKeyUpdate`: name (optional), is_active (optional) - `ApiKeyResponse`: id, name, is_active, created_at, last_used_at (orm_mode=True) - `ApiKeyListResponse`: items (list[ApiKeyResponse]), total (int) - Validazione: key deve iniziare con "sk-or-v1-" (formato OpenRouter) **Implementazione:** ```python from pydantic import BaseModel, Field, validator from datetime import datetime from typing import Optional, List class ApiKeyCreate(BaseModel): name: str = Field(..., min_length=1, max_length=100) key: str = Field(..., min_length=20) @validator('key') def validate_openrouter_key_format(cls, v): if not v.startswith('sk-or-v1-'): raise ValueError('Invalid OpenRouter API key format') return v class ApiKeyUpdate(BaseModel): name: Optional[str] = Field(None, min_length=1, max_length=100) is_active: Optional[bool] = None class ApiKeyResponse(BaseModel): id: int name: str is_active: bool created_at: datetime last_used_at: Optional[datetime] = None class Config: from_attributes = True # Pydantic v2 class ApiKeyListResponse(BaseModel): items: List[ApiKeyResponse] total: int ``` **Test:** `tests/unit/schemas/test_api_key_schemas.py` (8+ test) --- ### T24: Implementare POST /api/keys (Create API Key) **File:** `src/openrouter_monitor/routers/api_keys.py` **Requisiti:** - Endpoint: `POST /api/keys` - Auth: Richiede `current_user: User = Depends(get_current_user)` - Riceve: `ApiKeyCreate` schema - Verifica limite API keys per utente (`MAX_API_KEYS_PER_USER`) - Cifra API key con `EncryptionService` - Salva nel DB: `ApiKey(user_id=current_user.id, name=..., key_encrypted=...)` - Ritorna: `ApiKeyResponse`, status 201 - Errori: limite raggiunto (400), formato key invalido (422) **Implementazione:** ```python from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from sqlalchemy import func router = APIRouter(prefix="/api/keys", tags=["api-keys"]) @router.post("", response_model=ApiKeyResponse, status_code=status.HTTP_201_CREATED) async def create_api_key( key_data: ApiKeyCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): # Verifica limite API keys current_count = db.query(func.count(ApiKey.id)).filter( ApiKey.user_id == current_user.id, ApiKey.is_active == True ).scalar() if current_count >= settings.max_api_keys_per_user: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Maximum {settings.max_api_keys_per_user} API keys allowed" ) # Cifra API key encryption_service = EncryptionService(settings.encryption_key) encrypted_key = encryption_service.encrypt(key_data.key) # Crea API key api_key = ApiKey( user_id=current_user.id, name=key_data.name, key_encrypted=encrypted_key, is_active=True ) db.add(api_key) db.commit() db.refresh(api_key) return api_key ``` **Test:** `tests/unit/routers/test_api_keys.py` - Test creazione successo (201) - Test limite massimo raggiunto (400) - Test formato key invalido (422) - Test utente non autenticato (401) --- ### T25: Implementare GET /api/keys (List API Keys) **File:** `src/openrouter_monitor/routers/api_keys.py` **Requisiti:** - Endpoint: `GET /api/keys` - Auth: Richiede `current_user` - Query params: skip (default 0), limit (default 10, max 100) - Ritorna: solo API keys dell'utente corrente - Ordinamento: created_at DESC (piΓΉ recenti prima) - Ritorna: `ApiKeyListResponse` **Implementazione:** ```python @router.get("", response_model=ApiKeyListResponse) async def list_api_keys( skip: int = 0, limit: int = 10, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): query = db.query(ApiKey).filter(ApiKey.user_id == current_user.id) total = query.count() api_keys = query.order_by(ApiKey.created_at.desc()).offset(skip).limit(limit).all() return ApiKeyListResponse( items=api_keys, total=total ) ``` **Test:** - Test lista vuota (utente senza keys) - Test lista con API keys - Test paginazione (skip, limit) - Test ordinamento (piΓΉ recenti prima) - Test utente vede solo proprie keys --- ### T26: Implementare PUT /api/keys/{id} (Update API Key) **File:** `src/openrouter_monitor/routers/api_keys.py` **Requisiti:** - Endpoint: `PUT /api/keys/{key_id}` - Auth: Richiede `current_user` - Riceve: `ApiKeyUpdate` schema - Verifica: API key esiste e appartiene all'utente corrente - Aggiorna: solo campi forniti (name, is_active) - Ritorna: `ApiKeyResponse` - Errori: key non trovata (404), non autorizzato (403) **Implementazione:** ```python @router.put("/{key_id}", response_model=ApiKeyResponse) async def update_api_key( key_id: int, key_data: ApiKeyUpdate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): api_key = db.query(ApiKey).filter(ApiKey.id == key_id).first() if not api_key: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="API key not found" ) if api_key.user_id != current_user.id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized to update this API key" ) # Aggiorna solo campi forniti if key_data.name is not None: api_key.name = key_data.name if key_data.is_active is not None: api_key.is_active = key_data.is_active db.commit() db.refresh(api_key) return api_key ``` **Test:** - Test aggiornamento nome successo - Test aggiornamento is_active successo - Test key non esistente (404) - Test key di altro utente (403) --- ### T27: Implementare DELETE /api/keys/{id} (Delete API Key) **File:** `src/openrouter_monitor/routers/api_keys.py` **Requisiti:** - Endpoint: `DELETE /api/keys/{key_id}` - Auth: Richiede `current_user` - Verifica: API key esiste e appartiene all'utente corrente - Elimina: record dal DB (cascade elimina anche usage_stats) - Ritorna: status 204 (No Content) - Errori: key non trovata (404), non autorizzato (403) **Implementazione:** ```python @router.delete("/{key_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_api_key( key_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): api_key = db.query(ApiKey).filter(ApiKey.id == key_id).first() if not api_key: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="API key not found" ) if api_key.user_id != current_user.id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized to delete this API key" ) db.delete(api_key) db.commit() return None ``` **Test:** - Test eliminazione successo (204) - Test key non esistente (404) - Test key di altro utente (403) --- ### T28: Implementare Validazione API Key con OpenRouter **File:** `src/openrouter_monitor/services/openrouter.py` **Requisiti:** - Funzione: `validate_api_key(key: str) -> bool` - Chiama endpoint OpenRouter: `GET https://openrouter.ai/api/v1/auth/key` - Header: `Authorization: Bearer {key}` - Ritorna: True se valida (200), False se invalida (401/403) - Usa `httpx` per richieste HTTP - Timeout: 10 secondi **Implementazione:** ```python import httpx from openrouter_monitor.config import get_settings settings = get_settings() async def validate_api_key(key: str) -> bool: """Validate OpenRouter API key by calling their API.""" async with httpx.AsyncClient(timeout=10.0) as client: try: response = await client.get( f"{settings.openrouter_api_url}/auth/key", headers={"Authorization": f"Bearer {key}"} ) return response.status_code == 200 except httpx.RequestError: return False async def get_key_info(key: str) -> dict | None: """Get API key info from OpenRouter.""" async with httpx.AsyncClient(timeout=10.0) as client: try: response = await client.get( f"{settings.openrouter_api_url}/auth/key", headers={"Authorization": f"Bearer {key}"} ) if response.status_code == 200: return response.json() return None except httpx.RequestError: return None ``` **Test:** `tests/unit/services/test_openrouter.py` - Test key valida ritorna True - Test key invalida ritorna False - Test timeout ritorna False - Test network error gestito --- ### T29: Scrivere Test per API Keys Endpoints **File:** `tests/unit/routers/test_api_keys.py` **Requisiti:** - Test integrazione completo per tutti gli endpoint - Usare TestClient con FastAPI - Mock EncryptionService per test veloci - Mock chiamate OpenRouter per T28 - Coverage >= 90% **Test da implementare:** - **Create Tests (T24):** - POST /api/keys successo (201) - POST /api/keys limite raggiunto (400) - POST /api/keys formato invalido (422) - POST /api/keys senza auth (401) - **List Tests (T25):** - GET /api/keys lista vuota - GET /api/keys con dati - GET /api/keys paginazione - GET /api/keys senza auth (401) - **Update Tests (T26):** - PUT /api/keys/{id} aggiorna nome - PUT /api/keys/{id} aggiorna is_active - PUT /api/keys/{id} key non esiste (404) - PUT /api/keys/{id} key di altro utente (403) - **Delete Tests (T27):** - DELETE /api/keys/{id} successo (204) - DELETE /api/keys/{id} key non esiste (404) - DELETE /api/keys/{id} key di altro utente (403) - **Security Tests:** - Utente A non vede keys di utente B - Utente A non modifica keys di utente B - Utente A non elimina keys di utente B --- ## πŸ”„ WORKFLOW TDD Per **OGNI** task: 1. **RED**: Scrivi test che fallisce (prima del codice!) 2. **GREEN**: Implementa codice minimo per passare il test 3. **REFACTOR**: Migliora codice, test rimangono verdi --- ## πŸ“ STRUTTURA FILE DA CREARE/MODIFICARE ``` src/openrouter_monitor/ β”œβ”€β”€ schemas/ β”‚ β”œβ”€β”€ __init__.py # Aggiungi export ApiKey schemas β”‚ └── api_key.py # T23 β”œβ”€β”€ routers/ β”‚ β”œβ”€β”€ __init__.py # Aggiungi api_keys router β”‚ β”œβ”€β”€ auth.py # Esistente β”‚ └── api_keys.py # T24, T25, T26, T27 β”œβ”€β”€ services/ β”‚ β”œβ”€β”€ __init__.py # Aggiungi export openrouter β”‚ └── openrouter.py # T28 └── main.py # Registra api_keys router tests/unit/ β”œβ”€β”€ schemas/ β”‚ └── test_api_key_schemas.py # T23 + T29 β”œβ”€β”€ routers/ β”‚ └── test_api_keys.py # T24-T27 + T29 └── services/ └── test_openrouter.py # T28 + T29 ``` --- ## πŸ§ͺ ESEMPI TEST ### Test Schema ```python def test_api_key_create_valid_data_passes_validation(): data = ApiKeyCreate( name="Production Key", key="sk-or-v1-abc123..." ) assert data.name == "Production Key" assert data.key.startswith("sk-or-v1-") ``` ### Test Endpoint Create ```python @pytest.mark.asyncio async def test_create_api_key_success_returns_201(client, auth_token, db_session): response = client.post( "/api/keys", json={"name": "Test Key", "key": "sk-or-v1-validkey123"}, headers={"Authorization": f"Bearer {auth_token}"} ) assert response.status_code == 201 assert response.json()["name"] == "Test Key" assert "id" in response.json() ``` ### Test Sicurezza ```python def test_user_cannot_see_other_user_api_keys(client, auth_token_user_a, api_key_user_b): response = client.get( "/api/keys", headers={"Authorization": f"Bearer {auth_token_user_a}"} ) assert response.status_code == 200 # Verifica che key di user_b non sia nella lista key_ids = [k["id"] for k in response.json()["items"]] assert api_key_user_b.id not in key_ids ``` --- ## βœ… CRITERI DI ACCETTAZIONE - [ ] T23: Schemas API keys con validazione formato OpenRouter - [ ] T24: POST /api/keys con cifratura e limite keys - [ ] T25: GET /api/keys con paginazione e filtri - [ ] T26: PUT /api/keys/{id} aggiornamento - [ ] T27: DELETE /api/keys/{id} eliminazione - [ ] T28: Validazione key con OpenRouter API - [ ] T29: Test completi coverage >= 90% - [ ] Tutti i test passano: `pytest tests/unit/ -v` - [ ] API keys cifrate nel database (mai plaintext) - [ ] Utenti vedono/modificano solo proprie keys - [ ] 7 commit atomici con conventional commits - [ ] progress.md aggiornato --- ## πŸ“ COMMIT MESSAGES ``` feat(schemas): T23 add Pydantic API key schemas feat(api-keys): T24 implement create API key endpoint with encryption feat(api-keys): T25 implement list API keys endpoint with pagination feat(api-keys): T26 implement update API key endpoint feat(api-keys): T27 implement delete API key endpoint feat(openrouter): T28 implement API key validation service test(api-keys): T29 add comprehensive API keys endpoint tests ``` --- ## πŸš€ VERIFICA FINALE ```bash cd /home/google/Sources/LucaSacchiNet/openrouter-watcher # Test schemas pytest tests/unit/schemas/test_api_key_schemas.py -v # Test routers pytest tests/unit/routers/test_api_keys.py -v --cov=src/openrouter_monitor/routers # Test services pytest tests/unit/services/test_openrouter.py -v # Test completo pytest tests/unit/ -v --cov=src/openrouter_monitor # Verifica coverage >= 90% ``` --- ## πŸ”’ CONSIDERAZIONI SICUREZZA ### Do's βœ… - Cifrare sempre API keys prima di salvare nel DB - Verificare ownership (user_id) per ogni operazione - Validare formato key OpenRouter prima di salvare - Usare transactions per operazioni DB - Loggare operazioni (non i dati sensibili) ### Don'ts ❌ - MAI salvare API key in plaintext - MAI loggare API key complete - MAI permettere a utente di vedere key di altri - MAI ritornare key cifrate nelle response - MAI ignorare errori di decrittazione --- ## ⚠️ NOTE IMPORTANTI - **Path assoluti**: Usa sempre `/home/google/Sources/LucaSacchiNet/openrouter-watcher/` - **EncryptionService**: Riutilizza da `services/encryption.py` - **Formato Key**: OpenRouter keys iniziano con "sk-or-v1-" - **Limite Keys**: Configurabile via `MAX_API_KEYS_PER_USER` (default 10) - **Cascade Delete**: Eliminando ApiKey si eliminano anche UsageStats - **Ordinamento**: Lista keys ordinata per created_at DESC --- **AGENTE:** @tdd-developer **INIZIA CON:** T23 - Pydantic API key schemas **QUANDO FINITO:** Conferma completamento, coverage >= 90%, aggiorna progress.md