# Story 3.3: Admin - Révocation API Key (Any User)

Status: done

## Story

En tant qu'**Admin**,
Je veux **révoquer la clé API de n'importe quel utilisateur**,
de sorte que **je puisse gérer la sécurité et prévenir les abus**.

## Acceptance Criteria

1. **Endpoint Admin DELETE**: `DELETE /api/v1/admin/api-keys/{key_id}` révoque la clé API spécifiée. (FR31)
2. **Accès Admin Requis**: Seul un administrateur authentifié peut accéder à cet endpoint.
3. **Révocation Universelle**: L'admin peut révoquer N'IMPORTE quelle clé, peu importe le propriétaire.
4. **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`.
5. **Réponse Confirmée**: Retourne 200 avec confirmation de révocation dans le format `{data: {...}, meta: {}}`.
6. **Audit Logging**: L'action est journalisée avec `admin_id` et `reason` (paramètre optionnel).
7. **Clé Introuvable**: Si la clé n'existe pas, retourne 404 avec `API_KEY_NOT_FOUND`.
8. **Paramètre Raison**: L'admin peut optionnellement fournir une raison pour la révocation (body parameter).

## Tasks / Subtasks

- [x] **Task 1: Implémenter l'Endpoint Admin DELETE** (AC: #1, #2, #3, #4, #5, #8)
  - [x] 1.1 Ajouter la route `DELETE /api/v1/admin/api-keys/{key_id}` dans `main.py`
  - [x] 1.2 Utiliser la dépendance `require_admin` existante pour l'authentification
  - [x] 1.3 Ajouter paramètre optionnel `reason` dans le body de la requête
  - [x] 1.4 Rechercher la clé par `id` uniquement (PAS de filtre user_id)
  - [x] 1.5 Si trouvée, définir `is_active=False` et sauvegarder
  - [x] 1.6 Retourner 200 avec confirmation `{data: {id, revoked: true, revoked_at, reason?}, meta: {}}`

- [x] **Task 2: Implémenter l'Audit Logging** (AC: #6)
  - [x] 2.1 Logger l'action avec structlog ou logging standard
  - [x] 2.2 Inclure: timestamp, admin identifier, key_id, key owner user_id, reason
  - [x] 2.3 Niveau de log: INFO pour révocation réussie

- [x] **Task 3: Gérer les Cas d'Erreur** (AC: #7)
  - [x] 3.1 Retourner 404 si clé non trouvée
  - [x] 3.2 Retourner 401 si non authentifié admin (déjà géré par require_admin)
  - [x] 3.3 Vérifier que la clé existe avant de tenter la révocation

- [x] **Task 4: Ajouter les Tests** (AC: Tous)
  - [x] 4.1 Test révocation réussie par admin
  - [x] 4.2 Test révocation avec raison fournie
  - [x] 4.3 Test révocation échoue sans authentification admin (401)
  - [x] 4.4 Test révocation de clé inexistante (404)
  - [x] 4.5 Test clé révoquée ne peut plus authentifier (vérifier via get_user_by_api_key)
  - [x] 4.6 Test vérifier que l'audit logging fonctionne

## Dev Notes

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

**Fonction Admin Auth** (`main.py` lignes 215-236):
```python
async def require_admin(authorization: Optional[str] = Header(None)) -> bool:
    """Dependency to require admin authentication"""
    # Vérifie le token admin (stocké en mémoire ou via JWT)
    # Retourne True si valide, sinon lève HTTPException
```

**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"))  # Propriétaire de la clé
    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
    # ... autres champs
```

**Pattern Admin Routes** (`main.py`):
- Toutes les routes admin sont directement dans `main.py` avec préfixe `/api/v1/admin/`
- Utilisent `is_admin: bool = Depends(require_admin)` comme dépendance

### Différences Critiques avec Story 3.2

| Aspect | Story 3.2 (User) | Story 3.3 (Admin) |
|--------|------------------|-------------------|
| Endpoint | `DELETE /api/v1/api-keys/{key_id}` | `DELETE /api/v1/admin/api-keys/{key_id}` |
| Auth | `_require_pro_user` (Pro tier) | `require_admin` (Admin) |
| Filtre Query | `user_id == user.id` (propriété) | Aucun (accès universel) |
| Logging | Non requis | **Obligatoire** avec admin_id + reason |
| Raison | Non applicable | Paramètre optionnel |

### Patterns à Suivre

**Format de Réponse Succès**:
```json
{
  "data": {
    "id": "abc123",
    "revoked": true,
    "revoked_at": "2024-01-15T10:30:00Z",
    "owner_user_id": "user-uuid-here",
    "reason": "Violation des conditions d'utilisation"
  },
  "meta": {}
}
```

**Format de Réponse Erreur**:
```json
{
  "error": "API_KEY_NOT_FOUND",
  "message": "Clé API non trouvée"
}
```

**Pattern de Query Admin** (SANS filtre user_id):
```python
# Admin peut révoquer N'IMPORTE quelle clé
api_key = session.query(ApiKey).filter(
    ApiKey.id == key_id
).first()
```

### Structure de Fichiers

```
main.py                      # MODIFIER - Ajouter DELETE /api/v1/admin/api-keys/{key_id}
routes/api_key_routes.py     # Existant - Story 3.2 (ne pas modifier)
services/auth_service.py     # Existant - get_user_by_api_key vérifie déjà is_active
database/models.py           # Existant - ApiKey model
tests/test_story_3_3_admin_api_key_revocation.py  # CRÉER
```

### Project Structure Notes

- Les routes admin sont centralisées dans `main.py` (pas de fichier routes/admin_routes.py)
- Pattern établi par les autres endpoints admin existants
- Le logging utilise `structlog` ou le module `logging` standard de Python

### Références

- [Source: _bmad-output/planning-artifacts/epics.md#Story 3.3]
- [Source: _bmad-output/planning-artifacts/architecture.md#Admin Dashboard]
- [Source: _bmad-output/planning-artifacts/architecture.md#API Response Formats]
- [Source: main.py#require_admin (lignes 215-236)]
- [Source: routes/api_key_routes.py#revoke_api_key (lignes 257-308)]

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

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

1. **Endpoint DELETE** pour révocation utilisateur à `DELETE /api/v1/api-keys/{key_id}`
2. **Soft delete** avec `is_active=False`
3. **Fonction `get_user_by_api_key`** dans `services/auth_service.py` vérifie `is_active`
4. **18 tests** complets dans `tests/test_story_3_2_api_key_revocation.py`

### Patterns Établis à Réutiliser

```python
from datetime import datetime, timezone
from fastapi.responses import JSONResponse

# Soft delete pattern
api_key.is_active = False
session.commit()

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

### 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` vérifie déjà `is_active` (✅ confirmé)

## Intelligence Git (Commits Récents)

Derniers commits pertinents:
- `dfd45d9`: Admin login endpoint - patterns d'authentification admin
- `80318a8`: Admin dashboard - structure des routes admin

**Patterns identifiés**:
- Routes admin directement dans `main.py`
- Dépendance `require_admin` pour l'auth
- Tests avec `pytest` dans `tests/`

## Contexte Métier

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

Cette story est la **troisiè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** (cette story)
4. Authentification X-API-Key (Story 3.4 - backlog)
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

La révocation admin est critique pour:
- Gérer les abus (surutilisation, comportement suspect)
- Répondre aux demandes de support (compte compromis)
- Appliquer les conditions d'utilisation
- Contrôle de sécurité centralisé

### Dépendances

- **Story 3.1** (prérequis): Génération de clés API ✅
- **Story 3.2** (prérequis): Révocation utilisateur ✅
- **Story 3.4** (impact): L'authentification API vérifie `is_active` ✅ (déjà implémenté)

## 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** filtrer par `user_id` dans la query (admin a accès universel)
3. **NE PAS** utiliser `HTTPException` avec `detail` string (utiliser JSONResponse structuré)
4. **NE PAS** oublier l'audit logging (obligatoire pour cette story)
5. **NE PAS** utiliser camelCase dans les réponses JSON (toujours snake_case)
6. **NE PAS** créer un fichier `routes/admin_routes.py` - les routes admin restent dans `main.py`

### ✅ À FAIRE

1. **TOUJOURS** utiliser `require_admin` dependency pour l'authentification
2. **TOUJOURS** soft delete (`is_active=False`)
3. **TOUJOURS** logger l'action avec admin_id, key_id, owner_user_id, et reason
4. **TOUJOURS** retourner 404 si clé non trouvée
5. **TOUJOURS** suivre le format de réponse `{data: {...}, meta: {...}}`
6. **TOUJOURS** écrire des tests pour tous les cas
7. **INCLURE** le `owner_user_id` dans la réponse pour traçabilité

## 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
- ✅ Patterns de la Story 3.2 réutilisables identifiés
- ✅ Code existant analysé (main.py, routes/api_key_routes.py)
- ✅ Différences critiques avec Story 3.2 documentées
- ✅ Endpoint DELETE /api/v1/admin/api-keys/{key_id} implémenté dans main.py
- ✅ AdminRevokeApiKeyRequest model ajouté pour le body parameter optionnel `reason`
- ✅ Soft delete avec is_active=False et revoked_at timestamp
- ✅ Audit logging avec logger.info incluant admin_id, key_id, owner_user_id, reason, timestamp
- ✅ 15 tests créés et tous passent (tests/test_story_3_3_admin_api_key_revocation.py)
- ✅ Migration alembic 003_add_revoked_at_to_api_keys.py appliquée
- ✅ Tests de régression passent (Story 3.2 et admin tier change)

### File List

- `main.py` - MODIFIÉ - Ajout endpoint DELETE /api/v1/admin/api-keys/{key_id}, AdminRevokeApiKeyRequest model, require_admin retourne admin_id
- `tests/test_story_3_3_admin_api_key_revocation.py` - CRÉÉ - 15 tests couvrant tous les AC

## Change Log

- 2026-02-22: Story créée avec contexte complet (patterns Story 3.2, code suggéré, différences critiques)
- 2026-02-22: Implémentation terminée - Endpoint DELETE, audit logging, tests
- 2026-02-22: Code review - Fix dead code (lignes dupliquées supprimées), fix AC6 (admin_id ajouté au log), fix tests (vérification admin_id ajoutée)

## Checklist de Validation

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

- [x] `DELETE /api/v1/admin/api-keys/{key_id}` retourne 200 avec confirmation
- [x] La clé est marquée `is_active=False` (soft delete)
- [x] L'admin peut révoquer n'importe quelle clé (pas de filtre user_id)
- [x] L'action est journalisée avec admin_id, key_id, owner_user_id, reason
- [x] Le paramètre `reason` optionnel fonctionne correctement
- [x] Les utilisateurs non-admin reçoivent 401
- [x] Les clés inexistantes retournent 404
- [x] Tous les tests passent (15/15)
- [x] La clé révoquée ne peut plus authentifier (vérifier via test)
