Files
openrouter-watcher/prompt/prompt-ingaggio-api-keys.md
Luca Sacchi Ricciardi 2e4c1bb1e5 feat(schemas): T23 add Pydantic API key schemas
- 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
2026-04-07 14:28:03 +02:00

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 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:

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:

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: 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:

@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 httpx per 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:

  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

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