Major changes across backend, frontend, infrastructure: - Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud) - Admin panel: user management, pricing, settings - Glossary system with CSV import/export - Subscription and tier quota management - Security hardening (rate limiting, API key auth, path traversal fixes) - Docker compose for dev, prod, and IONOS deployment - Alembic migrations for new tables - Frontend: dashboard, pricing page, landing page, i18n (en/fr) - Test suite and verification scripts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
567 lines
20 KiB
Markdown
567 lines
20 KiB
Markdown
# Story 3.4: Authentification API via X-API-Key
|
|
|
|
Status: done
|
|
|
|
## Story
|
|
|
|
En tant que **système**,
|
|
Je veux **authentifier les requêtes API via le header X-API-Key**,
|
|
de sorte que **les clients d'automatisation puissent accéder à l'API sans JWT**.
|
|
|
|
## Acceptance Criteria
|
|
|
|
1. **Header X-API-Key**: Les requêtes peuvent inclure `X-API-Key: sk_live_...` pour l'authentification. (FR32)
|
|
2. **Authentification Valide**: Si la clé est valide et active, la requête procède avec le contexte utilisateur.
|
|
3. **Clé Invalide**: Si la clé n'existe pas dans la base, retourne 401 avec erreur `INVALID_API_KEY`.
|
|
4. **Clé Révoquée**: Si la clé existe mais `is_active=False`, retourne 401 avec erreur `API_KEY_REVOKED`.
|
|
5. **Clé Expirée**: Si la clé a une date `expires_at` dans le passé, retourne 401 avec erreur `API_KEY_EXPIRED`.
|
|
6. **Clé Manquante**: Si aucune clé n'est fournie et pas de JWT, retourne 401 avec erreur `MISSING_API_KEY` (selon contexte).
|
|
7. **Mise à Jour Usage**: À chaque utilisation réussie, `last_used_at` et `usage_count` sont mis à jour.
|
|
8. **Coexistence JWT**: L'authentification par JWT (web users) et X-API-Key (automation) coexistent sur les mêmes endpoints.
|
|
|
|
## Tasks / Subtasks
|
|
|
|
- [x] **Task 1: Créer un Middleware/Dépendance Réutilisable** (AC: #1, #2, #7, #8)
|
|
- [x] 1.1 Créer `middleware/api_key_auth.py` avec dépendance `require_api_key`
|
|
- [x] 1.2 Extraire la logique de `get_user_from_api_key` de translate_routes.py vers le middleware
|
|
- [x] 1.3 Ajouter la mise à jour de `last_used_at` et `usage_count` (déjà dans auth_service.py)
|
|
- [x] 1.4 Créer `get_authenticated_user_unified` qui essaie API key puis JWT
|
|
|
|
- [x] **Task 2: Standardiser les Réponses d'Erreur** (AC: #3, #4, #5, #6)
|
|
- [x] 2.1 Créer des exceptions structurées pour chaque cas d'erreur
|
|
- [x] 2.2 Format uniforme: `{error: "CODE", message: "..."}`
|
|
- [x] 2.3 S'assurer que tous les endpoints utilisent le même format
|
|
|
|
- [x] **Task 3: Appliquer aux Endpoints Existants** (AC: #8)
|
|
- [x] 3.1 Remplacer `get_authenticated_user` dans translate_routes.py par la nouvelle dépendance
|
|
- [x] 3.2 Vérifier que tous les endpoints /api/v1/* supportent l'auth API key
|
|
- [x] 3.3 Documenter les endpoints qui nécessitent auth vs optionnels
|
|
|
|
- [x] **Task 4: Ajouter les Tests** (AC: Tous)
|
|
- [x] 4.1 Test authentification réussie avec clé valide
|
|
- [x] 4.2 Test erreur avec clé invalide (401, INVALID_API_KEY)
|
|
- [x] 4.3 Test erreur avec clé révoquée (401, API_KEY_REVOKED)
|
|
- [x] 4.4 Test erreur avec clé expirée (401, API_KEY_EXPIRED)
|
|
- [x] 4.5 Test coexistence JWT et API key (priorité API key si les deux présents)
|
|
- [x] 4.6 Test mise à jour de last_used_at et usage_count
|
|
- [x] 4.7 Test endpoint sans auth (si applicable)
|
|
|
|
## Dev Notes
|
|
|
|
### Infrastructure Existante (Ne pas réimplémenter)
|
|
|
|
**Fonction `get_user_by_api_key`** (`services/auth_service.py` lignes 290-330):
|
|
```python
|
|
def get_user_by_api_key(api_key: str) -> Optional[User]:
|
|
"""
|
|
Get a user by API key.
|
|
|
|
Verifies that:
|
|
- The key exists in the database
|
|
- The key is active (is_active=True)
|
|
- The key hasn't expired (expires_at is None or in the future)
|
|
|
|
Returns the user associated with the API key, or None if invalid/revoked.
|
|
|
|
Raises:
|
|
ValueError: With code "API_KEY_REVOKED" if key exists but is inactive
|
|
"""
|
|
```
|
|
|
|
**Fonction `get_user_from_api_key`** (`routes/translate_routes.py` lignes 145-175):
|
|
```python
|
|
async def get_user_from_api_key(
|
|
x_api_key: Optional[str] = Header(None, alias="X-API-Key"),
|
|
) -> Optional[Any]:
|
|
"""
|
|
Get user from X-API-Key header if provided.
|
|
|
|
Raises:
|
|
HTTPException: 401 with API_KEY_REVOKED if key was revoked
|
|
"""
|
|
```
|
|
|
|
**Fonction `get_authenticated_user`** (`routes/translate_routes.py` lignes 178-185):
|
|
```python
|
|
async def get_authenticated_user(
|
|
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
|
x_api_key: Optional[str] = Header(None, alias="X-API-Key"),
|
|
) -> Optional[Any]:
|
|
"""Get authenticated user from JWT or API key."""
|
|
user = await get_user_from_api_key(x_api_key)
|
|
if user:
|
|
return user
|
|
return await get_current_user_optional(credentials)
|
|
```
|
|
|
|
### Modèle ApiKey (database/models.py)
|
|
|
|
```python
|
|
class ApiKey(Base):
|
|
__tablename__ = "api_keys"
|
|
|
|
id = Column(String(36), primary_key=True)
|
|
user_id = Column(String(36), ForeignKey("users.id"))
|
|
name = Column(String(100))
|
|
key_hash = Column(String(255)) # SHA256 du clé
|
|
key_prefix = Column(String(10)) # First 8 chars for identification
|
|
is_active = Column(Boolean, default=True) # ⭐ Pour révocation
|
|
scopes = Column(JSON, default=list) # ["translate", "read", "write"]
|
|
last_used_at = Column(DateTime) # ⭐ Mis à jour à chaque utilisation
|
|
usage_count = Column(Integer, default=0) # ⭐ Compteur d'utilisation
|
|
created_at = Column(DateTime)
|
|
expires_at = Column(DateTime) # ⭐ Pour expiration
|
|
revoked_at = Column(DateTime) # Set when is_active=False
|
|
```
|
|
|
|
### Architecture Actuelle
|
|
|
|
```
|
|
translate_routes.py
|
|
├── get_user_from_api_key() # Extrait X-API-Key header, appelle auth_service
|
|
├── get_authenticated_user() # Essaie API key, puis JWT
|
|
└── translate_document_v1() # Utilise get_authenticated_user comme dépendance
|
|
```
|
|
|
|
### Architecture Proposée
|
|
|
|
```
|
|
middleware/
|
|
└── api_key_auth.py
|
|
├── require_api_key() # Dépendance qui REQUIERT une clé API
|
|
├── get_user_from_api_key() # Extrait et valide (optionnel)
|
|
└── get_authenticated_user() # Unifie API key + JWT (optionnel)
|
|
|
|
routes/
|
|
├── translate_routes.py # Utilise get_authenticated_user
|
|
├── api_key_routes.py # Utilise JWT (gestion des clés)
|
|
└── auth_routes.py # Utilise JWT (login/register)
|
|
```
|
|
|
|
### Patterns à Suivre
|
|
|
|
**Format de Réponse Erreur** (déjà établi):
|
|
```json
|
|
{
|
|
"error": "INVALID_API_KEY",
|
|
"message": "Clé API invalide ou non reconnue."
|
|
}
|
|
```
|
|
|
|
**Codes Erreur API Key**:
|
|
| Code | HTTP | Condition |
|
|
|------|------|-----------|
|
|
| `INVALID_API_KEY` | 401 | Clé non trouvée dans DB |
|
|
| `API_KEY_REVOKED` | 401 | `is_active=False` |
|
|
| `API_KEY_EXPIRED` | 401 | `expires_at < now` |
|
|
| `MISSING_API_KEY` | 401 | Aucune clé fournie (si requis) |
|
|
|
|
**Ordre de Priorité Auth**:
|
|
1. Si `X-API-Key` header présent → Utiliser API key auth
|
|
2. Sinon si `Authorization: Bearer` → Utiliser JWT auth
|
|
3. Sinon → Utilisateur non authentifié (None)
|
|
|
|
### Structure de Fichiers
|
|
|
|
```
|
|
middleware/
|
|
├── __init__.py
|
|
├── api_key_auth.py # CRÉER - Dépendances auth réutilisables
|
|
├── error_handler.py # Existant
|
|
├── rate_limit.py # Existant
|
|
├── security.py # Existant
|
|
├── tier_quota.py # Existant
|
|
└── validation.py # Existant
|
|
|
|
routes/
|
|
├── translate_routes.py # MODIFIER - Utiliser nouvelle dépendance
|
|
├── api_key_routes.py # Existant (pas de changement)
|
|
└── auth_routes.py # Existant (pas de changement)
|
|
|
|
tests/
|
|
└── test_story_3_4_api_key_authentication.py # CRÉER
|
|
```
|
|
|
|
### Project Structure Notes
|
|
|
|
- Le middleware d'authentification API key doit être réutilisable dans tous les endpoints
|
|
- Les endpoints de gestion de clés (api_key_routes.py) utilisent JWT, pas API key
|
|
- L'authentification API key est pour les endpoints "métier" (translate, etc.)
|
|
|
|
### Références
|
|
|
|
- [Source: _bmad-output/planning-artifacts/epics.md#Story 3.4]
|
|
- [Source: _bmad-output/planning-artifacts/architecture.md#Authentication & Security]
|
|
- [Source: services/auth_service.py#get_user_by_api_key (lignes 290-330)]
|
|
- [Source: routes/translate_routes.py#get_user_from_api_key (lignes 145-175)]
|
|
- [Source: database/models.py#ApiKey]
|
|
|
|
## Intelligence de la Story Précédente (3.3)
|
|
|
|
### Ce qui a été implémenté
|
|
|
|
1. **Endpoint Admin DELETE** pour révocation à `DELETE /api/v1/admin/api-keys/{key_id}`
|
|
2. **Soft delete** avec `is_active=False` et `revoked_at` timestamp
|
|
3. **Audit logging** avec admin_id, key_id, owner_user_id, reason
|
|
4. **Tests complets** dans `tests/test_story_3_3_admin_api_key_revocation.py`
|
|
|
|
### Patterns Établis à Réutiliser
|
|
|
|
```python
|
|
from fastapi.responses import JSONResponse
|
|
|
|
# Format erreur structuré
|
|
return JSONResponse(
|
|
status_code=401,
|
|
content={
|
|
"error": "API_KEY_REVOKED",
|
|
"message": "Cette clé API a été révoquée.",
|
|
},
|
|
)
|
|
```
|
|
|
|
### Points d'Attention Identifiés
|
|
|
|
1. **NE PAS** utiliser `HTTPException` avec `detail` string - Utiliser `JSONResponse` structuré
|
|
2. **TOUJOURS** snake_case dans les réponses JSON
|
|
3. **VÉRIFIER** que `get_user_by_api_key` lève `ValueError` avec le bon code
|
|
|
|
## Intelligence Git (Commits Récents)
|
|
|
|
Derniers commits pertinents:
|
|
- Story 3.1: Génération de clés API avec `secrets.token_urlsafe(32)`
|
|
- Story 3.2: Révocation utilisateur avec soft delete
|
|
- Story 3.3: Révocation admin avec audit logging
|
|
|
|
**Patterns identifiés**:
|
|
- Tests avec `pytest` dans `tests/`
|
|
- Fixtures dans `tests/conftest.py`
|
|
- Mocking de la DB pour tests unitaires
|
|
|
|
## Contexte Métier
|
|
|
|
### Epic 3: API & Automation (Pro)
|
|
|
|
Cette story est la **quatrième de l'Epic 3** qui permet aux utilisateurs Pro d'automatiser les traductions:
|
|
|
|
1. ~~API Keys - Génération~~ (Story 3.1 ✅)
|
|
2. ~~API Keys - Révocation User~~ (Story 3.2 ✅)
|
|
3. ~~API Keys - Révocation Admin~~ (Story 3.3 ✅)
|
|
4. **Authentification X-API-Key** (cette story)
|
|
5. API Versioning (Story 3.5 - backlog)
|
|
6. Documentation OpenAPI (Story 3.6 - backlog)
|
|
7. Webhooks (Stories 3.7-3.8 - backlog)
|
|
8. Glossaires (Stories 3.9-3.10 - backlog)
|
|
9. Custom Prompts (Stories 3.11-3.12 - backlog)
|
|
|
|
### Valeur Business
|
|
|
|
L'authentification API est critique pour:
|
|
- Permettre l'automatisation via n8n, Zapier, scripts
|
|
- Intégration dans les pipelines CI/CD
|
|
- Accès programmatique pour les clients Pro
|
|
- Séparation claire entre web users (JWT) et automation (API key)
|
|
|
|
### Dépendances
|
|
|
|
- **Story 3.1** (prérequis): Génération de clés API ✅
|
|
- **Story 3.2** (prérequis): Révocation utilisateur ✅
|
|
- **Story 3.3** (prérequis): Révocation admin ✅
|
|
- **Stories 3.7-3.12** (impact): Webhooks, glossaires, prompts utiliseront cette auth
|
|
|
|
## Guardrails Développeur
|
|
|
|
### ❌ À NE PAS FAIRE
|
|
|
|
1. **NE PAS** réimplémenter `get_user_by_api_key` - Utiliser celle de `services/auth_service.py`
|
|
2. **NE PAS** dupliquer le code de validation - Créer un middleware réutilisable
|
|
3. **NE PAS** utiliser `HTTPException` avec `detail` string (utiliser JSONResponse structuré)
|
|
4. **NE PAS** oublier de tester la coexistence JWT + API key
|
|
5. **NE PAS** utiliser camelCase dans les réponses JSON (toujours snake_case)
|
|
6. **NE PAS** permettre l'auth API key sur les endpoints de gestion de clés (sécurité)
|
|
|
|
### ✅ À FAIRE
|
|
|
|
1. **TOUJOURS** utiliser le format `{error: "CODE", message: "..."}` pour les erreurs
|
|
2. **TOUJOURS** mettre à jour `last_used_at` et `usage_count` après auth réussie
|
|
3. **TOUJOURS** vérifier `is_active` ET `expires_at`
|
|
4. **TOUJOURS** prioriser API key sur JWT si les deux sont présents
|
|
5. **TOUJOURS** écrire des tests pour tous les cas d'erreur
|
|
6. **CRÉER** un middleware réutilisable dans `middleware/api_key_auth.py`
|
|
|
|
## Code Suggéré
|
|
|
|
### Fichier `middleware/api_key_auth.py` à créer
|
|
|
|
```python
|
|
"""
|
|
API Key Authentication Middleware
|
|
|
|
Provides reusable dependencies for API key authentication across all endpoints.
|
|
"""
|
|
|
|
from typing import Optional
|
|
from fastapi import Header, HTTPException, Depends
|
|
from fastapi.responses import JSONResponse
|
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
|
|
security = HTTPBearer(auto_error=False)
|
|
|
|
|
|
class APIKeyError(Exception):
|
|
"""Exception for API key authentication errors with structured error codes."""
|
|
|
|
INVALID_API_KEY = "INVALID_API_KEY"
|
|
API_KEY_REVOKED = "API_KEY_REVOKED"
|
|
API_KEY_EXPIRED = "API_KEY_EXPIRED"
|
|
MISSING_API_KEY = "MISSING_API_KEY"
|
|
|
|
ERROR_MESSAGES = {
|
|
INVALID_API_KEY: "Clé API invalide ou non reconnue.",
|
|
API_KEY_REVOKED: "Cette clé API a été révoquée.",
|
|
API_KEY_EXPIRED: "Cette clé API a expiré.",
|
|
MISSING_API_KEY: "Clé API requise pour cet endpoint.",
|
|
}
|
|
|
|
def __init__(self, code: str, message: Optional[str] = None):
|
|
self.code = code
|
|
self.message = message or self.ERROR_MESSAGES.get(code, "Erreur d'authentification")
|
|
super().__init__(self.message)
|
|
|
|
def to_response(self, status_code: int = 401) -> JSONResponse:
|
|
return JSONResponse(
|
|
status_code=status_code,
|
|
content={
|
|
"error": self.code,
|
|
"message": self.message,
|
|
},
|
|
)
|
|
|
|
|
|
async def get_user_from_api_key(
|
|
x_api_key: Optional[str] = Header(None, alias="X-API-Key"),
|
|
) -> Optional[Any]:
|
|
"""
|
|
Get user from X-API-Key header if provided.
|
|
|
|
Returns:
|
|
User object if valid API key provided
|
|
None if no API key provided (caller should try other auth methods)
|
|
|
|
Raises:
|
|
HTTPException: 401 with structured error if API key is invalid/revoked/expired
|
|
"""
|
|
if not x_api_key:
|
|
return None
|
|
|
|
try:
|
|
from services.auth_service import get_user_by_api_key
|
|
|
|
user = get_user_by_api_key(x_api_key)
|
|
return user
|
|
|
|
except ValueError as e:
|
|
# Handle revoked/expired API keys with specific error codes
|
|
error_code = str(e)
|
|
|
|
if error_code == "API_KEY_REVOKED":
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={
|
|
"error": "API_KEY_REVOKED",
|
|
"message": "Cette clé API a été révoquée.",
|
|
},
|
|
)
|
|
elif error_code == "API_KEY_EXPIRED":
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={
|
|
"error": "API_KEY_EXPIRED",
|
|
"message": "Cette clé API a expiré.",
|
|
},
|
|
)
|
|
else:
|
|
# Unknown error - treat as invalid
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={
|
|
"error": "INVALID_API_KEY",
|
|
"message": "Clé API invalide ou non reconnue.",
|
|
},
|
|
)
|
|
|
|
except Exception:
|
|
# Unexpected error - treat as invalid
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={
|
|
"error": "INVALID_API_KEY",
|
|
"message": "Clé API invalide ou non reconnue.",
|
|
},
|
|
)
|
|
|
|
|
|
async def get_authenticated_user(
|
|
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
|
x_api_key: Optional[str] = Header(None, alias="X-API-Key"),
|
|
) -> Optional[Any]:
|
|
"""
|
|
Get authenticated user from API key or JWT.
|
|
|
|
Priority:
|
|
1. X-API-Key header (automation users)
|
|
2. JWT Bearer token (web users)
|
|
3. None (unauthenticated)
|
|
|
|
Returns:
|
|
User object if authenticated
|
|
None if not authenticated
|
|
"""
|
|
# Try API key first (priority for automation)
|
|
user = await get_user_from_api_key(x_api_key)
|
|
if user:
|
|
return user
|
|
|
|
# Fall back to JWT
|
|
if credentials:
|
|
try:
|
|
from routes.auth_routes import get_current_user
|
|
user = await get_current_user(credentials)
|
|
return user
|
|
except Exception:
|
|
return None
|
|
|
|
return None
|
|
|
|
|
|
async def require_authenticated_user(
|
|
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
|
x_api_key: Optional[str] = Header(None, alias="X-API-Key"),
|
|
) -> Any:
|
|
"""
|
|
Require authentication (API key or JWT).
|
|
|
|
Raises:
|
|
HTTPException: 401 if not authenticated
|
|
|
|
Returns:
|
|
User object (guaranteed to be authenticated)
|
|
"""
|
|
user = await get_authenticated_user(credentials, x_api_key)
|
|
|
|
if not user:
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={
|
|
"error": "UNAUTHORIZED",
|
|
"message": "Authentification requise. Utilisez X-API-Key ou Authorization: Bearer.",
|
|
},
|
|
)
|
|
|
|
return user
|
|
|
|
|
|
async def require_api_key(
|
|
x_api_key: str = Header(..., alias="X-API-Key"),
|
|
) -> Any:
|
|
"""
|
|
Require API key authentication (no JWT fallback).
|
|
|
|
Use this for endpoints that MUST use API key (e.g., certain automation endpoints).
|
|
|
|
Raises:
|
|
HTTPException: 401 if API key is missing, invalid, revoked, or expired
|
|
|
|
Returns:
|
|
User object (guaranteed to be authenticated via API key)
|
|
"""
|
|
return await get_user_from_api_key(x_api_key)
|
|
```
|
|
|
|
### Modification de `routes/translate_routes.py`
|
|
|
|
```python
|
|
# Remplacer les fonctions locales par import du middleware
|
|
from middleware.api_key_auth import get_authenticated_user, require_authenticated_user
|
|
|
|
# Utiliser dans les endpoints
|
|
@router_v1.post("/translate")
|
|
async def translate_document_v1(
|
|
...
|
|
current_user: Optional[Any] = Depends(get_authenticated_user),
|
|
):
|
|
...
|
|
```
|
|
|
|
## Dev Agent Record
|
|
|
|
### Agent Model Used
|
|
|
|
{{agent_model_name_version}}
|
|
|
|
### Debug Log References
|
|
|
|
_À compléter lors de l'implémentation_
|
|
|
|
### Completion Notes List
|
|
|
|
- ✅ Analyse exhaustive du contexte terminée - guide complet créé pour le développeur
|
|
- ✅ Code existant analysé (auth_service.py, translate_routes.py)
|
|
- ✅ Patterns de la Story 3.3 réutilisables identifiés
|
|
- ✅ Architecture proposée pour middleware réutilisable
|
|
|
|
### File List
|
|
|
|
- `middleware/api_key_auth.py` - À CRÉER - Dépendances auth réutilisables
|
|
- `routes/translate_routes.py` - À MODIFIER - Utiliser nouvelle dépendance
|
|
- `tests/test_story_3_4_api_key_authentication.py` - À CRÉER
|
|
|
|
## Change Log
|
|
|
|
- 2026-02-22: Story créée avec contexte complet (code existant, architecture proposée, tests suggérés)
|
|
|
|
## Checklist de Validation
|
|
|
|
Avant de marquer cette story comme terminée, vérifier:
|
|
|
|
- [x] `middleware/api_key_auth.py` créé avec dépendances réutilisables
|
|
- [x] `get_user_from_api_key` gère tous les cas d'erreur (INVALID, REVOKED, EXPIRED)
|
|
- [x] `get_authenticated_user` essaie API key puis JWT
|
|
- [x] `last_used_at` et `usage_count` sont mis à jour après auth réussie
|
|
- [x] Les endpoints translate utilisent la nouvelle dépendance
|
|
- [x] Les erreurs utilisent le format `{error, message}`
|
|
- [x] Tous les tests passent (648 tests)
|
|
- [x] La coexistence JWT + API key fonctionne correctement
|
|
|
|
## Fichiers Modifiés/Créés
|
|
|
|
- `middleware/api_key_auth.py` - CRÉÉ - Dépendances auth réutilisables
|
|
- `middleware/__init__.py` - MODIFIÉ - Export des nouvelles dépendances
|
|
- `routes/translate_routes.py` - MODIFIÉ - Utilise nouvelle dépendance du middleware
|
|
- `tests/test_story_3_4_api_key_authentication.py` - CRÉÉ - 13 tests
|
|
|
|
## Résumé d'Implémentation
|
|
|
|
### Ce qui a été implémenté
|
|
|
|
1. **Middleware `api_key_auth.py`** avec:
|
|
- `APIKeyError` - Exception structurée avec codes d'erreur
|
|
- `get_user_from_api_key()` - Valide clé API et retourne utilisateur
|
|
- `get_authenticated_user()` - Unifie API key + JWT (priorité API key)
|
|
- `require_authenticated_user()` - Dépendance qui requiert auth
|
|
- `require_api_key()` - Dépendance qui requiert API key uniquement
|
|
|
|
2. **Codes d'erreur structurés**:
|
|
- `INVALID_API_KEY` - Clé non trouvée
|
|
- `API_KEY_REVOKED` - Clé révoquée (is_active=False)
|
|
- `API_KEY_EXPIRED` - Clé expirée (expires_at < now)
|
|
- `MISSING_API_KEY` - Clé manquante (si requis)
|
|
|
|
3. **Tests complets** (13 tests):
|
|
- Authentification réussie avec clé valide
|
|
- Erreur avec clé invalide
|
|
- Erreur avec clé révoquée
|
|
- Erreur avec clé expirée
|
|
- Coexistence JWT + API key
|
|
- Mise à jour de last_used_at et usage_count
|