- T55: Setup APScheduler with AsyncIOScheduler and @scheduled_job decorator - T56: Implement hourly usage stats sync from OpenRouter API - T57: Implement daily API key validation job - T58: Implement weekly cleanup job for old usage stats - Add usage_stats_retention_days config option - Integrate scheduler with FastAPI lifespan events - Add 26 unit tests for scheduler, sync, and cleanup tasks - Add apscheduler to requirements.txt The background tasks now automatically: - Sync usage stats every hour from OpenRouter - Validate API keys daily at 2 AM UTC - Clean up old data weekly on Sunday at 3 AM UTC
676 lines
19 KiB
Markdown
676 lines
19 KiB
Markdown
# Prompt di Ingaggio: API Pubblica (T35-T40)
|
|
|
|
## 🎯 MISSIONE
|
|
|
|
Implementare la fase **API Pubblica** del progetto OpenRouter API Key Monitor seguendo rigorosamente TDD.
|
|
|
|
**Task da completare:** T35, T36, T37, T38, T39, T40
|
|
|
|
---
|
|
|
|
## 📋 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
|
|
- ✅ Gestione API Keys (T23-T29): 61 test
|
|
- ✅ Dashboard & Statistiche (T30-T34): 27 test
|
|
- 🎯 **Totale: 324+ test, ~98% coverage su moduli implementati**
|
|
|
|
**Servizi Pronti:**
|
|
- `EncryptionService` - Cifratura/decifratura
|
|
- `get_current_user()` - Autenticazione JWT
|
|
- `generate_api_token()`, `verify_api_token()` - Token API pubblica
|
|
- `get_dashboard_data()`, `get_usage_stats()` - Aggregazione dati
|
|
- `ApiKey`, `UsageStats`, `ApiToken` models
|
|
|
|
**Documentazione:**
|
|
- PRD: `/home/google/Sources/LucaSacchiNet/openrouter-watcher/prd.md` (sezione 2.4)
|
|
- Architecture: `/home/google/Sources/LucaSacchiNet/openrouter-watcher/export/architecture.md` (sezione 5.2.3)
|
|
|
|
---
|
|
|
|
## 🔧 TASK DA IMPLEMENTARE
|
|
|
|
### T35: Creare Pydantic Schemas per API Pubblica
|
|
|
|
**File:** `src/openrouter_monitor/schemas/public_api.py`
|
|
|
|
**Requisiti:**
|
|
- `PublicStatsResponse`: summary (requests, cost, tokens), period (start_date, end_date)
|
|
- `PublicUsageResponse`: items (list), pagination (page, limit, total, pages)
|
|
- `PublicKeyInfo`: id, name, is_active, stats (total_requests, total_cost)
|
|
- `PublicKeyListResponse`: items (list[PublicKeyInfo]), total
|
|
- `ApiTokenCreate`: name (str, 1-100 chars)
|
|
- `ApiTokenResponse`: id, name, created_at, last_used_at, is_active (NO token!)
|
|
- `ApiTokenCreateResponse`: id, name, token (plaintext, solo al momento creazione), created_at
|
|
|
|
**Implementazione:**
|
|
```python
|
|
from pydantic import BaseModel, Field
|
|
from datetime import date, datetime
|
|
from typing import List, Optional
|
|
from decimal import Decimal
|
|
|
|
class PeriodInfo(BaseModel):
|
|
start_date: date
|
|
end_date: date
|
|
days: int
|
|
|
|
class PublicStatsSummary(BaseModel):
|
|
total_requests: int
|
|
total_cost: Decimal
|
|
total_tokens_input: int
|
|
total_tokens_output: int
|
|
|
|
class PublicStatsResponse(BaseModel):
|
|
summary: PublicStatsSummary
|
|
period: PeriodInfo
|
|
|
|
class PublicUsageItem(BaseModel):
|
|
date: date
|
|
model: str
|
|
requests_count: int
|
|
tokens_input: int
|
|
tokens_output: int
|
|
cost: Decimal
|
|
|
|
class PaginationInfo(BaseModel):
|
|
page: int
|
|
limit: int
|
|
total: int
|
|
pages: int
|
|
|
|
class PublicUsageResponse(BaseModel):
|
|
items: List[PublicUsageItem]
|
|
pagination: PaginationInfo
|
|
|
|
class PublicKeyStats(BaseModel):
|
|
total_requests: int
|
|
total_cost: Decimal
|
|
|
|
class PublicKeyInfo(BaseModel):
|
|
id: int
|
|
name: str
|
|
is_active: bool
|
|
stats: PublicKeyStats
|
|
|
|
class PublicKeyListResponse(BaseModel):
|
|
items: List[PublicKeyInfo]
|
|
total: int
|
|
|
|
class ApiTokenCreate(BaseModel):
|
|
name: str = Field(..., min_length=1, max_length=100)
|
|
|
|
class ApiTokenResponse(BaseModel):
|
|
id: int
|
|
name: str
|
|
created_at: datetime
|
|
last_used_at: Optional[datetime]
|
|
is_active: bool
|
|
|
|
class ApiTokenCreateResponse(BaseModel):
|
|
id: int
|
|
name: str
|
|
token: str # PLAINTEXT - shown only once!
|
|
created_at: datetime
|
|
```
|
|
|
|
**Test:** `tests/unit/schemas/test_public_api_schemas.py` (10+ test)
|
|
|
|
---
|
|
|
|
### T36: Implementare Endpoint GET /api/v1/stats (API Pubblica)
|
|
|
|
**File:** `src/openrouter_monitor/routers/public_api.py`
|
|
|
|
**Requisiti:**
|
|
- Endpoint: `GET /api/v1/stats`
|
|
- Auth: API Token (non JWT!) - `get_current_user_from_api_token()`
|
|
- Query params:
|
|
- start_date (optional, default 30 giorni fa)
|
|
- end_date (optional, default oggi)
|
|
- Verifica token valido e attivo
|
|
- Aggiorna `last_used_at` del token
|
|
- Ritorna: `PublicStatsResponse`
|
|
- Solo lettura, nessuna modifica
|
|
|
|
**Implementazione:**
|
|
```python
|
|
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
|
from sqlalchemy.orm import Session
|
|
from datetime import date, timedelta
|
|
|
|
from openrouter_monitor.database import get_db
|
|
from openrouter_monitor.dependencies import get_current_user_from_api_token
|
|
from openrouter_monitor.models import User
|
|
from openrouter_monitor.schemas import PublicStatsResponse
|
|
from openrouter_monitor.services.stats import get_public_stats
|
|
|
|
router = APIRouter(prefix="/api/v1", tags=["public-api"])
|
|
|
|
@router.get("/stats", response_model=PublicStatsResponse)
|
|
async def get_public_stats_endpoint(
|
|
start_date: Optional[date] = Query(default=None),
|
|
end_date: Optional[date] = Query(default=None),
|
|
current_user: User = Depends(get_current_user_from_api_token),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Get usage statistics via API token authentication.
|
|
|
|
Authentication: Bearer <api_token>
|
|
Returns aggregated statistics for the authenticated user's API keys.
|
|
"""
|
|
# Default to last 30 days if dates not provided
|
|
if not end_date:
|
|
end_date = date.today()
|
|
if not start_date:
|
|
start_date = end_date - timedelta(days=29)
|
|
|
|
# Get stats using existing service
|
|
stats = await get_public_stats(db, current_user.id, start_date, end_date)
|
|
|
|
return PublicStatsResponse(
|
|
summary=stats,
|
|
period=PeriodInfo(
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
days=(end_date - start_date).days + 1
|
|
)
|
|
)
|
|
```
|
|
|
|
**Test:**
|
|
- Test con token valido (200)
|
|
- Test con token invalido (401)
|
|
- Test con token scaduto/revocado (401)
|
|
- Test date default (30 giorni)
|
|
- Test date custom
|
|
- Test aggiornamento last_used_at
|
|
|
|
---
|
|
|
|
### T37: Implementare Endpoint GET /api/v1/usage (API Pubblica)
|
|
|
|
**File:** `src/openrouter_monitor/routers/public_api.py`
|
|
|
|
**Requisiti:**
|
|
- Endpoint: `GET /api/v1/usage`
|
|
- Auth: API Token
|
|
- Query params:
|
|
- start_date (required)
|
|
- end_date (required)
|
|
- page (default 1)
|
|
- limit (default 100, max 1000)
|
|
- Paginazione con offset/limit
|
|
- Ritorna: `PublicUsageResponse`
|
|
|
|
**Implementazione:**
|
|
```python
|
|
@router.get("/usage", response_model=PublicUsageResponse)
|
|
async def get_public_usage_endpoint(
|
|
start_date: date,
|
|
end_date: date,
|
|
page: int = Query(default=1, ge=1),
|
|
limit: int = Query(default=100, ge=1, le=1000),
|
|
current_user: User = Depends(get_current_user_from_api_token),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Get detailed usage data via API token authentication.
|
|
|
|
Returns paginated usage records aggregated by date and model.
|
|
"""
|
|
skip = (page - 1) * limit
|
|
|
|
# Get usage data
|
|
items, total = await get_public_usage(
|
|
db, current_user.id, start_date, end_date, skip, limit
|
|
)
|
|
|
|
pages = (total + limit - 1) // limit
|
|
|
|
return PublicUsageResponse(
|
|
items=items,
|
|
pagination=PaginationInfo(
|
|
page=page,
|
|
limit=limit,
|
|
total=total,
|
|
pages=pages
|
|
)
|
|
)
|
|
```
|
|
|
|
**Test:**
|
|
- Test con filtri date (200)
|
|
- Test paginazione
|
|
- Test limit max 1000
|
|
- Test senza token (401)
|
|
- Test token scaduto (401)
|
|
|
|
---
|
|
|
|
### T38: Implementare Endpoint GET /api/v1/keys (API Pubblica)
|
|
|
|
**File:** `src/openrouter_monitor/routers/public_api.py`
|
|
|
|
**Requisiti:**
|
|
- Endpoint: `GET /api/v1/keys`
|
|
- Auth: API Token
|
|
- Ritorna: lista API keys con statistiche aggregate
|
|
- NO key values (cifrate comunque)
|
|
- Solo: id, name, is_active, stats (totali)
|
|
|
|
**Implementazione:**
|
|
```python
|
|
@router.get("/keys", response_model=PublicKeyListResponse)
|
|
async def get_public_keys_endpoint(
|
|
current_user: User = Depends(get_current_user_from_api_token),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Get API keys list with aggregated statistics.
|
|
|
|
Returns non-sensitive key information with usage stats.
|
|
Key values are never exposed.
|
|
"""
|
|
from sqlalchemy import func
|
|
|
|
# Query API keys with aggregated stats
|
|
results = db.query(
|
|
ApiKey.id,
|
|
ApiKey.name,
|
|
ApiKey.is_active,
|
|
func.coalesce(func.sum(UsageStats.requests_count), 0).label('total_requests'),
|
|
func.coalesce(func.sum(UsageStats.cost), 0).label('total_cost')
|
|
).outerjoin(UsageStats).filter(
|
|
ApiKey.user_id == current_user.id
|
|
).group_by(ApiKey.id).all()
|
|
|
|
items = [
|
|
PublicKeyInfo(
|
|
id=r.id,
|
|
name=r.name,
|
|
is_active=r.is_active,
|
|
stats=PublicKeyStats(
|
|
total_requests=r.total_requests,
|
|
total_cost=Decimal(str(r.total_cost))
|
|
)
|
|
)
|
|
for r in results
|
|
]
|
|
|
|
return PublicKeyListResponse(items=items, total=len(items))
|
|
```
|
|
|
|
**Test:**
|
|
- Test lista keys con stats (200)
|
|
- Test NO key values in risposta
|
|
- Test senza token (401)
|
|
|
|
---
|
|
|
|
### T39: Implementare Rate Limiting su API Pubblica
|
|
|
|
**File:** `src/openrouter_monitor/middleware/rate_limit.py` o `src/openrouter_monitor/dependencies/rate_limit.py`
|
|
|
|
**Requisiti:**
|
|
- Rate limit per API token: 100 richieste/ora (default)
|
|
- Rate limit per IP: 30 richieste/minuto (fallback)
|
|
- Memorizzare contatori in memory (per MVP, Redis in futuro)
|
|
- Header nelle risposte: X-RateLimit-Limit, X-RateLimit-Remaining
|
|
- Ritorna 429 Too Many Requests quando limite raggiunto
|
|
|
|
**Implementazione:**
|
|
```python
|
|
from fastapi import HTTPException, status, Request
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, Tuple
|
|
import time
|
|
|
|
# Simple in-memory rate limiting (use Redis in production)
|
|
class RateLimiter:
|
|
def __init__(self):
|
|
self._storage: Dict[str, Tuple[int, float]] = {} # key: (count, reset_time)
|
|
|
|
def is_allowed(self, key: str, limit: int, window_seconds: int) -> Tuple[bool, int, int]:
|
|
"""Check if request is allowed. Returns (allowed, remaining, limit)."""
|
|
now = time.time()
|
|
reset_time = now + window_seconds
|
|
|
|
if key not in self._storage:
|
|
self._storage[key] = (1, reset_time)
|
|
return True, limit - 1, limit
|
|
|
|
count, current_reset = self._storage[key]
|
|
|
|
# Reset window if expired
|
|
if now > current_reset:
|
|
self._storage[key] = (1, reset_time)
|
|
return True, limit - 1, limit
|
|
|
|
# Check limit
|
|
if count >= limit:
|
|
return False, 0, limit
|
|
|
|
self._storage[key] = (count + 1, current_reset)
|
|
return True, limit - count - 1, limit
|
|
|
|
rate_limiter = RateLimiter()
|
|
|
|
async def rate_limit_by_token(
|
|
credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer(auto_error=False)),
|
|
request: Request = None
|
|
) -> None:
|
|
"""Rate limiting dependency for API endpoints."""
|
|
from openrouter_monitor.config import get_settings
|
|
|
|
settings = get_settings()
|
|
|
|
# Use token as key if available, otherwise IP
|
|
if credentials:
|
|
key = f"token:{credentials.credentials}"
|
|
limit = settings.rate_limit_requests # 100/hour
|
|
window = settings.rate_limit_window # 3600 seconds
|
|
else:
|
|
key = f"ip:{request.client.host}"
|
|
limit = 30 # 30/minute for IP
|
|
window = 60
|
|
|
|
allowed, remaining, limit_total = rate_limiter.is_allowed(key, limit, window)
|
|
|
|
if not allowed:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
|
detail="Rate limit exceeded. Try again later.",
|
|
headers={"Retry-After": str(window)}
|
|
)
|
|
|
|
# Add rate limit headers to response (will be added by middleware)
|
|
request.state.rate_limit_remaining = remaining
|
|
request.state.rate_limit_limit = limit_total
|
|
|
|
class RateLimitHeadersMiddleware:
|
|
def __init__(self, app):
|
|
self.app = app
|
|
|
|
async def __call__(self, scope, receive, send):
|
|
if scope["type"] == "http":
|
|
request = Request(scope, receive)
|
|
|
|
async def send_with_headers(message):
|
|
if message["type"] == "http.response.start":
|
|
headers = message.get("headers", [])
|
|
|
|
# Add rate limit headers if available
|
|
if hasattr(request.state, 'rate_limit_remaining'):
|
|
headers.append(
|
|
(b"x-ratelimit-remaining",
|
|
str(request.state.rate_limit_remaining).encode())
|
|
)
|
|
headers.append(
|
|
(b"x-ratelimit-limit",
|
|
str(request.state.rate_limit_limit).encode())
|
|
)
|
|
|
|
message["headers"] = headers
|
|
|
|
await send(message)
|
|
|
|
await self.app(scope, receive, send_with_headers)
|
|
else:
|
|
await self.app(scope, receive, send)
|
|
```
|
|
|
|
**Aggiungere ai router:**
|
|
```python
|
|
from openrouter_monitor.dependencies.rate_limit import rate_limit_by_token
|
|
|
|
@router.get("/stats", response_model=PublicStatsResponse, dependencies=[Depends(rate_limit_by_token)])
|
|
async def get_public_stats_endpoint(...):
|
|
...
|
|
```
|
|
|
|
**Test:**
|
|
- Test rate limit token (100/ora)
|
|
- Test rate limit IP (30/minuto)
|
|
- Test 429 quando limite raggiunto
|
|
- Test headers X-RateLimit-* presenti
|
|
- Test reset dopo window
|
|
|
|
---
|
|
|
|
### T40: Scrivere Test per API Pubblica
|
|
|
|
**File:** `tests/unit/routers/test_public_api.py`
|
|
|
|
**Requisiti:**
|
|
- Test integrazione per tutti gli endpoint API pubblica
|
|
- Mock/generare API token validi per test
|
|
- Test rate limiting
|
|
- Test sicurezza (token invalido, scaduto)
|
|
- Coverage >= 90%
|
|
|
|
**Test da implementare:**
|
|
- **Stats Tests:**
|
|
- GET /api/v1/stats con token valido (200)
|
|
- GET /api/v1/stats date default (30 giorni)
|
|
- GET /api/v1/stats date custom
|
|
- GET /api/v1/stats token invalido (401)
|
|
- GET /api/v1/stats token scaduto (401)
|
|
- GET /api/v1/stats aggiorna last_used_at
|
|
|
|
- **Usage Tests:**
|
|
- GET /api/v1/usage con filtri (200)
|
|
- GET /api/v1/usage paginazione
|
|
- GET /api/v1/usage senza token (401)
|
|
|
|
- **Keys Tests:**
|
|
- GET /api/v1/keys lista (200)
|
|
- GET /api/v1/keys NO key values in risposta
|
|
|
|
- **Rate Limit Tests:**
|
|
- Test 100 richieste/ora
|
|
- Test 429 dopo limite
|
|
- Test headers rate limit
|
|
|
|
- **Security Tests:**
|
|
- User A non vede dati di user B con token di A
|
|
- Token JWT non funziona su API pubblica (401)
|
|
|
|
---
|
|
|
|
## 🔄 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
|
|
|
|
```
|
|
src/openrouter_monitor/
|
|
├── schemas/
|
|
│ ├── __init__.py # Aggiungi export public_api
|
|
│ └── public_api.py # T35
|
|
├── routers/
|
|
│ ├── __init__.py # Aggiungi export public_api
|
|
│ └── public_api.py # T36, T37, T38
|
|
├── dependencies/
|
|
│ ├── __init__.py # Aggiungi export
|
|
│ ├── auth.py # Aggiungi get_current_user_from_api_token
|
|
│ └── rate_limit.py # T39
|
|
├── middleware/
|
|
│ └── rate_limit.py # T39 (opzionale)
|
|
└── main.py # Registra public_api router + middleware
|
|
|
|
tests/unit/
|
|
├── schemas/
|
|
│ └── test_public_api_schemas.py # T35 + T40
|
|
├── dependencies/
|
|
│ └── test_rate_limit.py # T39 + T40
|
|
└── routers/
|
|
└── test_public_api.py # T36-T38 + T40
|
|
```
|
|
|
|
---
|
|
|
|
## 🧪 ESEMPI TEST
|
|
|
|
### Test Dependency API Token
|
|
```python
|
|
@pytest.mark.asyncio
|
|
async def test_get_current_user_from_api_token_valid_returns_user(db_session, test_user):
|
|
# Arrange
|
|
token, token_hash = generate_api_token()
|
|
api_token = ApiToken(user_id=test_user.id, token_hash=token_hash, name="Test")
|
|
db_session.add(api_token)
|
|
db_session.commit()
|
|
|
|
# Act
|
|
user = await get_current_user_from_api_token(token, db_session)
|
|
|
|
# Assert
|
|
assert user.id == test_user.id
|
|
```
|
|
|
|
### Test Endpoint Stats
|
|
```python
|
|
def test_public_stats_with_valid_token_returns_200(client, api_token):
|
|
response = client.get(
|
|
"/api/v1/stats",
|
|
headers={"Authorization": f"Bearer {api_token}"}
|
|
)
|
|
assert response.status_code == 200
|
|
assert "summary" in response.json()
|
|
```
|
|
|
|
### Test Rate Limiting
|
|
```python
|
|
def test_rate_limit_429_after_100_requests(client, api_token):
|
|
# Make 100 requests
|
|
for _ in range(100):
|
|
response = client.get("/api/v1/stats", headers={"Authorization": f"Bearer {api_token}"})
|
|
assert response.status_code == 200
|
|
|
|
# 101st request should fail
|
|
response = client.get("/api/v1/stats", headers={"Authorization": f"Bearer {api_token}"})
|
|
assert response.status_code == 429
|
|
```
|
|
|
|
---
|
|
|
|
## ✅ CRITERI DI ACCETTAZIONE
|
|
|
|
- [ ] T35: Schemas API pubblica con validazione
|
|
- [ ] T36: Endpoint /api/v1/stats con auth API token
|
|
- [ ] T37: Endpoint /api/v1/usage con paginazione
|
|
- [ ] T38: Endpoint /api/v1/keys con stats aggregate
|
|
- [ ] T39: Rate limiting implementato (100/ora, 429)
|
|
- [ ] T40: Test completi coverage >= 90%
|
|
- [ ] `get_current_user_from_api_token()` dependency funzionante
|
|
- [ ] Headers X-RateLimit-* presenti nelle risposte
|
|
- [ ] Token JWT non funziona su API pubblica
|
|
- [ ] 6 commit atomici con conventional commits
|
|
- [ ] progress.md aggiornato
|
|
|
|
---
|
|
|
|
## 📝 COMMIT MESSAGES
|
|
|
|
```
|
|
feat(schemas): T35 add Pydantic public API schemas
|
|
|
|
feat(auth): add get_current_user_from_api_token dependency
|
|
|
|
feat(public-api): T36 implement GET /api/v1/stats endpoint
|
|
|
|
feat(public-api): T37 implement GET /api/v1/usage endpoint with pagination
|
|
|
|
feat(public-api): T38 implement GET /api/v1/keys endpoint
|
|
|
|
feat(rate-limit): T39 implement rate limiting for public API
|
|
|
|
test(public-api): T40 add comprehensive public API endpoint tests
|
|
```
|
|
|
|
---
|
|
|
|
## 🚀 VERIFICA FINALE
|
|
|
|
```bash
|
|
cd /home/google/Sources/LucaSacchiNet/openrouter-watcher
|
|
|
|
# Test schemas
|
|
pytest tests/unit/schemas/test_public_api_schemas.py -v
|
|
|
|
# Test dependencies
|
|
pytest tests/unit/dependencies/test_rate_limit.py -v
|
|
|
|
# Test routers
|
|
pytest tests/unit/routers/test_public_api.py -v --cov=src/openrouter_monitor/routers
|
|
|
|
# Test completo
|
|
pytest tests/unit/ -v --cov=src/openrouter_monitor
|
|
|
|
# Verifica endpoint manualmente
|
|
curl -H "Authorization: Bearer or_api_xxxxx" http://localhost:8000/api/v1/stats
|
|
```
|
|
|
|
---
|
|
|
|
## 📋 DIFFERENZE CHIAVE: API Pubblica vs Web API
|
|
|
|
| Feature | Web API (/api/auth, /api/keys) | API Pubblica (/api/v1/*) |
|
|
|---------|--------------------------------|--------------------------|
|
|
| **Auth** | JWT Bearer | API Token Bearer |
|
|
| **Scopo** | Gestione (CRUD) | Lettura dati |
|
|
| **Rate Limit** | No (o diverso) | Sì (100/ora) |
|
|
| **Audience** | Frontend web | Integrazioni esterne |
|
|
| **Token TTL** | 24 ore | Illimitato (fino a revoca) |
|
|
|
|
---
|
|
|
|
## 🔒 CONSIDERAZIONI SICUREZZA
|
|
|
|
### Do's ✅
|
|
- Verificare sempre API token con hash in database
|
|
- Aggiornare `last_used_at` ad ogni richiesta
|
|
- Rate limiting per prevenire abusi
|
|
- Non esporre mai API key values (cifrate)
|
|
- Validare date (max range 365 giorni)
|
|
|
|
### Don'ts ❌
|
|
- MAI accettare JWT su API pubblica
|
|
- MAI loggare API token in plaintext
|
|
- MAI ritornare dati di altri utenti
|
|
- MAI bypassare rate limiting
|
|
- MAI permettere range date > 365 giorni
|
|
|
|
---
|
|
|
|
## 📝 NOTE IMPORTANTI
|
|
|
|
- **Path assoluti**: Usa sempre `/home/google/Sources/LucaSacchiNet/openrouter-watcher/`
|
|
- **Dependency**: Crea `get_current_user_from_api_token()` separata da `get_current_user()`
|
|
- **Rate limiting**: In-memory per MVP, Redis per produzione
|
|
- **Token format**: API token inizia con `or_api_`, JWT no
|
|
- **last_used_at**: Aggiornare ad ogni chiamata API pubblica
|
|
|
|
---
|
|
|
|
**AGENTE:** @tdd-developer
|
|
|
|
**INIZIA CON:** T35 - Pydantic public API schemas
|
|
|
|
**QUANDO FINITO:** Conferma completamento, coverage >= 90%, aggiorna progress.md
|