- Add ApiKeyCreate schema with OpenRouter key format validation - Add ApiKeyUpdate schema for partial updates - Add ApiKeyResponse schema (excludes key value for security) - Add ApiKeyListResponse schema for pagination - Export schemas from __init__.py - 100% coverage on new module (23 tests) Refs: T23
16 KiB
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 keyshash_password(),verify_password()- Autenticazionecreate_access_token(),decode_access_token()- JWTget_current_user()- Dependency injectiongenerate_api_token()- Token API pubblica
Modelli Pronti:
User,ApiKey,UsageStats,ApiToken- SQLAlchemy modelsget_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 keyApiKeyUpdate: 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:
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:
ApiKeyCreateschema - 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:
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:
@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:
ApiKeyUpdateschema - 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:
@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:
@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
httpxper richieste HTTP - Timeout: 10 secondi
Implementazione:
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:
- RED: Scrivi test che fallisce (prima del codice!)
- GREEN: Implementa codice minimo per passare il test
- 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
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
@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
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
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