# Story 3.2: Révocation API Key (User)

Status: done

## Story

En tant qu'**Utilisateur Pro**,
Je veux **révoquer ma propre clé API**,
de sorte que **je puisse sécuriser mon compte si la clé est compromise**.

## Acceptance Criteria

1. **Endpoint DELETE**: `DELETE /api/v1/api-keys/{key_id}` révoque la clé API spécifiée. (FR30)
2. **Propriété Vérifiée**: Seules les clés appartenant à l'utilisateur connecté peuvent être révoquées.
3. **Révocation Immédiate**: La clé est marquée `is_active=False` et les requêtes suivantes avec cette clé retournent 401 avec le code `API_KEY_REVOKED`.
4. **Réponse Confirmée**: Retourne 200 avec confirmation de révocation dans le format `{data: {...}, meta: {}}`.
5. **Clé Introuvable**: Si la clé n'existe pas ou n'appartient pas à l'utilisateur, retourne 404 avec `API_KEY_NOT_FOUND`.
6. **Restriction Tier**: Les utilisateurs Free reçoivent 403 avec le code `PRO_FEATURE_REQUIRED`.
7. **Authentification Requise**: Les utilisateurs non authentifiés reçoivent 401 avec `UNAUTHORIZED`.

## Tasks / Subtasks

- [x] **Task 1: Implémenter l'Endpoint DELETE** (AC: #1, #2, #3, #4)
  - [x] 1.1 Ajouter la route `DELETE /api/v1/api-keys/{key_id}` dans `routes/api_key_routes.py`
  - [x] 1.2 Vérifier que l'utilisateur est authentifié et Pro
  - [x] 1.3 Rechercher la clé par `id` ET `user_id` (sécurité propriété)
  - [x] 1.4 Si trouvée, définir `is_active=False` et sauvegarder
  - [x] 1.5 Retourner 200 avec confirmation `{data: {id, revoked: true}, meta: {}}`

- [x] **Task 2: Gérer les Cas d'Erreur** (AC: #5, #6, #7)
  - [x] 2.1 Retourner 404 si clé non trouvée ou n'appartient pas à l'utilisateur
  - [x] 2.2 Retourner 403 si utilisateur Free
  - [x] 2.3 Retourner 401 si non authentifié

- [x] **Task 3: Vérifier l'Impact sur l'Authentification API** (AC: #3)
  - [x] 3.1 Vérifier que le middleware d'authentification API vérifie `is_active`
  - [x] 3.2 Si non vérifié, ajouter la vérification dans le middleware/fonction d'auth API

- [x] **Task 4: Ajouter les Tests** (AC: Tous)
  - [x] 4.1 Test révocation réussie pour utilisateur Pro
  - [x] 4.2 Test révocation échoue pour clé d'un autre utilisateur (404)
  - [x] 4.3 Test révocation échoue pour utilisateur Free (403)
  - [x] 4.4 Test révocation échoue sans authentification (401)
  - [x] 4.5 Test clé révoquée ne peut plus authentifier (401 avec API_KEY_REVOKED)

## Dev Notes

### Infrastructure Existante (Ne pas réimplémenter)

**Routeur API Keys** (`routes/api_key_routes.py`):
- POST `/api/v1/api-keys` - Création de clé ✅ Existant
- GET `/api/v1/api-keys` - Liste des clés ✅ Existant
- DELETE `/api/v1/api-keys/{key_id}` - **À implémenter** (cette story)

**Modèle ApiKey** (`database/models.py` lignes 208-257):
```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))
    key_prefix = Column(String(10))
    is_active = Column(Boolean, default=True)  # ⭐ Champ à modifier pour révocation
    scopes = Column(JSON, default=list)
    last_used_at = Column(DateTime)
    usage_count = Column(Integer, default=0)
    created_at = Column(DateTime)
    expires_at = Column(DateTime)
```

**Dépendance Auth Pro** (`routes/api_key_routes.py`):
```python
def _require_pro_user(credentials=Depends(security)):
    """Dependency that requires a valid Pro user JWT token"""
    # Vérifie JWT, récupère user, vérifie tier
    # Retourne user ou None
```

### Patterns à Suivre (depuis Story 3.1)

**Format de Réponse Succès**:
```json
{
  "data": {
    "id": "abc123",
    "revoked": true,
    "revoked_at": "2024-01-15T10:30:00Z"
  },
  "meta": {}
}
```

**Format de Réponse Erreur**:
```json
{
  "error": "API_KEY_NOT_FOUND",
  "message": "Clé API non trouvée ou n'appartient pas à l'utilisateur"
}
```

**Pattern de Query avec Propriété**:
```python
# Toujours filtrer par user_id pour sécurité
api_key = session.query(ApiKey).filter(
    ApiKey.id == key_id,
    ApiKey.user_id == user.id  # ⭐ Sécurité: seul propriétaire peut révoquer
).first()
```

### Structure de Fichiers

```
routes/
├── api_key_routes.py    # MODIFIER - Ajouter DELETE endpoint
├── auth_routes.py       # Existant - patterns à suivre
└── translate_routes.py  # Existant
```

### Project Structure Notes

- Le projet suit une structure plate (pas de dossier `backend/app/`)
- Les modèles sont dans `database/models.py`
- Les repositories sont dans `database/repositories.py`
- Les services sont dans `services/`
- Les tests sont dans `tests/`

### Références

- [Source: _bmad-output/planning-artifacts/epics.md#Story 3.2]
- [Source: _bmad-output/planning-artifacts/architecture.md#API Response Formats]
- [Source: database/models.py#ApiKey]
- [Source: routes/api_key_routes.py#_require_pro_user]

## Intelligence de la Story Précédente (3.1)

### Ce qui a été implémenté

1. **Routeur API Keys** avec POST et GET
2. **Génération sécurisée** avec `secrets.token_urlsafe(32)`
3. **Stockage haché** SHA256
4. **Dépendance `_require_pro_user`** pour vérification JWT + tier

### Patterns Établis à Réutiliser

```python
# Pattern authentification et vérification tier
@router.delete("/{key_id}")
async def revoke_api_key(
    key_id: str,
    user=Depends(_require_pro_user),
):
    if not user:
        return JSONResponse(status_code=401, content={"error": "UNAUTHORIZED", ...})
    
    tier = getattr(user, "tier", None) or (
        "pro" if user.plan.value in ("pro", "business", "enterprise") else "free"
    )
    
    if tier != "pro":
        return JSONResponse(status_code=403, content={"error": "PRO_FEATURE_REQUIRED", ...})
    
    # Logique de révocation...
```

### Points d'Attention Identifiés

1. **Ne pas utiliser `HTTPException`** - Utiliser `JSONResponse` pour format structuré
2. **Toujours snake_case** dans les réponses JSON
3. **Vérifier `is_active`** dans l'authentification API (middleware)

## Intelligence Git (Commits Récents)

Derniers commits analysés:
- `3d37ce4`: PostgreSQL database infrastructure
- `c4d6cae`: Redis sessions, security hardening
- `dfd45d9`: Admin login endpoint

**Patterns identifiés**:
- Utilisation de `JSONResponse` pour les réponses structurées
- Tests avec `pytest` et fixtures dans `conftest.py`
- Session DB avec `get_sync_session()` context manager

## Contexte Métier

### Epic 3: API & Automation (Pro)

Cette story est la **deuxième de l'Epic 3** qui permet aux utilisateurs Pro (Thomas) d'automatiser les traductions via:
1. ~~API Keys - Génération~~ (Story 3.1 ✅)
2. **API Keys - Révocation** (cette story)
3. Authentification X-API-Key (Story 3.4)
4. Webhooks (Stories 3.7-3.8)
5. Glossaires (Stories 3.9-3.10)
6. Custom Prompts (Stories 3.11-3.12)

### Valeur Business

La révocation est critique pour:
- Sécuriser un compte si la clé est compromise
- Gérer la rotation des clés
- Contrôler l'accès automatisé

### Dépendances

- **Story 3.1** (prérequis): Génération de clés API ✅
- **Story 3.4** (impact): L'authentification API doit vérifier `is_active`

## Guardrails Développeur

### ❌ À NE PAS FAIRE

1. **NE PAS** supprimer physiquement la clé de la DB (soft delete avec `is_active=False`)
2. **NE PAS** permettre la révocation d'une clé d'un autre utilisateur
3. **NE PAS** utiliser `HTTPException` avec `detail` string (utiliser JSONResponse structuré)
4. **NE PAS** oublier la vérification du tier Pro (403 pour Free)
5. **NE PAS** utiliser camelCase dans les réponses JSON (toujours snake_case)

### ✅ À FAIRE

1. **TOUJOURS** filtrer par `user_id` dans la query (sécurité propriété)
2. **TOUJOURS** utiliser soft delete (`is_active=False`)
3. **TOUJOURS** retourner 404 si clé non trouvée (pas 403 pour éviter énumération)
4. **TOUJOURS** suivre le format de réponse `{data: {...}, meta: {...}}`
5. **TOUJOURS** écrire des tests pour tous les cas (succès, erreur, edge cases)
6. **VÉRIFIER** que l'authentification API (Story 3.4) vérifie `is_active`

## Code Suggéré

### Endpoint DELETE à ajouter dans `routes/api_key_routes.py`

```python
from datetime import datetime, timezone

@router.delete("/{key_id}")
async def revoke_api_key(
    key_id: str,
    user=Depends(_require_pro_user),
):
    """
    Revoke an API key.

    Returns:
        200: API key revoked successfully
        401: Authentication required
        403: Pro subscription required
        404: API key not found
    """
    if not user:
        return JSONResponse(
            status_code=401,
            content={
                "error": "UNAUTHORIZED",
                "message": "Authentification requise",
            },
        )

    tier = getattr(user, "tier", None) or (
        "pro" if user.plan.value in ("pro", "business", "enterprise") else "free"
    )

    if tier != "pro":
        return JSONResponse(
            status_code=403,
            content={
                "error": "PRO_FEATURE_REQUIRED",
                "message": "Cette fonctionnalite necessite un abonnement Pro",
            },
        )

    with get_sync_session() as session:
        # ⭐ Sécurité: filtrer par user_id pour que seul le propriétaire puisse révoquer
        api_key = (
            session.query(ApiKey)
            .filter(
                ApiKey.id == key_id,
                ApiKey.user_id == user.id
            )
            .first()
        )

        if not api_key:
            return JSONResponse(
                status_code=404,
                content={
                    "error": "API_KEY_NOT_FOUND",
                    "message": "Clé API non trouvée ou n'appartient pas à l'utilisateur",
                },
            )

        # Soft delete - marquer comme inactive
        api_key.is_active = False
        session.commit()

        return JSONResponse(
            status_code=200,
            content={
                "data": {
                    "id": api_key.id,
                    "revoked": True,
                    "revoked_at": datetime.now(timezone.utc).isoformat(),
                },
                "meta": {},
            },
        )
```

### Vérification Auth API (pour Story 3.4)

L'authentification par clé API doit vérifier `is_active`:

```python
# Dans le middleware/fonction d'auth API
def verify_api_key(api_key: str) -> User:
    key_hash = hashlib.sha256(api_key.encode()).hexdigest()
    
    with get_sync_session() as session:
        key_record = session.query(ApiKey).filter(
            ApiKey.key_hash == key_hash,
            ApiKey.is_active == True  # ⭐ Vérifier que la clé est active
        ).first()
        
        if not key_record:
            raise InvalidAPIKeyError("API_KEY_REVOKED" if ... else "INVALID_API_KEY")
```

## Dev Agent Record

### Agent Model Used

Claude 3.5 Sonnet (claude-3-5-sonnet)

### Debug Log References

Aucun problème rencontré lors de l'implémentation.

### Completion Notes List

- ✅ Analyse exhaustive du contexte terminée - guide complet créé pour le développeur
- ✅ Code existant analysé (routes/api_key_routes.py, database/models.py)
- ✅ Patterns de la Story 3.1 réutilisables identifiés
- ✅ Impact sur Story 3.4 (auth API) documenté
- ✅ Endpoint DELETE implémenté dans routes/api_key_routes.py
- ✅ Fonction get_user_by_api_key ajoutée dans services/auth_service.py avec vérification is_active
- ✅ Gestion des erreurs API_KEY_REVOKED ajoutée dans routes/translate_routes.py
- ✅ 22 tests créés couvrant tous les AC

### Code Review Fixes Applied (2026-02-22)

Les issues suivantes ont été corrigées après la code review:

1. **HIGH - Champ `revoked_at` non persisté**: Ajout du champ `revoked_at` au modèle `ApiKey` dans `database/models.py`
2. **HIGH - Double révocation retourne 200**: Ajout du filtre `is_active=True` dans la query - une clé déjà révoquée retourne maintenant 404
3. **HIGH - UUID validation**: Ajout de la validation du format UUID pour `key_id` - retourne 400 avec `INVALID_KEY_ID` si invalide
4. **MEDIUM - Logging**: Ajout du logging pour les révocations (audit de sécurité)
5. **Migration DB**: Création de `alembic/versions/003_add_revoked_at_to_api_keys.py`
6. **Tests**: Mise à jour des tests pour refléter les corrections (double révocation = 404, validation UUID)

### File List

- `routes/api_key_routes.py` - MODIFIÉ - Ajout endpoint DELETE /{key_id} avec validation UUID et logging
- `services/auth_service.py` - MODIFIÉ - Ajout fonction get_user_by_api_key avec vérification is_active
- `routes/translate_routes.py` - MODIFIÉ - Gestion erreurs API_KEY_REVOKED et API_KEY_EXPIRED
- `database/models.py` - MODIFIÉ - Ajout champ revoked_at au modèle ApiKey
- `alembic/versions/003_add_revoked_at_to_api_keys.py` - CRÉÉ - Migration pour colonne revoked_at
- `tests/test_story_3_2_api_key_revocation.py` - MODIFIÉ - 22 tests couvrant tous les AC + edge cases

## Change Log

- 2026-02-22: Story créée avec contexte complet (patterns Story 3.1, code suggéré)
- 2026-02-22: Implémentation terminée - Endpoint DELETE, auth API vérification, tests

## Checklist de Validation

Avant de marquer cette story comme terminée, vérifier:

- [x] `DELETE /api/v1/api-keys/{key_id}` retourne 200 avec confirmation
- [x] La clé est marquée `is_active=False` (soft delete)
- [x] Un utilisateur ne peut pas révoquer la clé d'un autre utilisateur (404)
- [x] Les utilisateurs Free reçoivent 403 avec `PRO_FEATURE_REQUIRED`
- [x] Les utilisateurs non authentifiés reçoivent 401
- [x] Tous les tests passent (18/18)
- [x] L'authentification API vérifie `is_active` (implémenté dans get_user_by_api_key)
