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>
328 lines
12 KiB
Markdown
328 lines
12 KiB
Markdown
# Story 3.11: Custom Prompts - Endpoint CRUD
|
|
|
|
Status: done
|
|
|
|
## Story
|
|
|
|
En tant qu'**utilisateur Pro**,
|
|
Je veux **créer et gérer des prompts système personnalisés via API**,
|
|
de sorte que **je puisse guider le contexte de traduction LLM**.
|
|
|
|
## Acceptance Criteria
|
|
|
|
1. **Création de prompt**: Quand un utilisateur Pro POST sur `/api/v1/prompts` avec `{name, content}`, un prompt template est créé et associé à son compte. (FR59)
|
|
2. **Liste des prompts**: GET `/api/v1/prompts` retourne la liste des prompts de l'utilisateur.
|
|
3. **Détail d'un prompt**: GET `/api/v1/prompts/{id}` retourne les détails d'un prompt spécifique.
|
|
4. **Mise à jour**: PATCH `/api/v1/prompts/{id}` permet de mettre à jour le nom et/ou le contenu.
|
|
5. **Suppression**: DELETE `/api/v1/prompts/{id}` supprime un prompt.
|
|
6. **Restriction Pro**: Les utilisateurs Free reçoivent 403 avec erreur `PRO_FEATURE_REQUIRED`.
|
|
|
|
## Tasks / Subtasks
|
|
|
|
- [x] **Task 1: Créer le modèle CustomPrompt et la migration Alembic** (AC: #1)
|
|
- [x] 1.1 Ajouter le modèle `CustomPrompt` dans `database/models.py`
|
|
- [x] 1.2 Générer la migration Alembic `alembic revision --autogenerate -m "add_custom_prompts_table"`
|
|
- [x] 1.3 Appliquer la migration `alembic upgrade head`
|
|
- [x] 1.4 Vérifier la table créée dans la DB
|
|
|
|
- [x] **Task 2: Créer les schémas Pydantic** (AC: Tous)
|
|
- [x] 2.1 Créer `schemas/prompt_schemas.py` avec PromptCreate, PromptUpdate, PromptResponse, PromptListItem
|
|
- [x] 2.2 Définir les validators (content max 10000 chars, name max 255 chars)
|
|
- [x] 2.3 Documenter les schémas pour OpenAPI
|
|
|
|
- [x] **Task 3: Créer les routes CRUD** (AC: Tous)
|
|
- [x] 3.1 Créer `routes/prompt_routes.py` avec les endpoints CRUD
|
|
- [x] 3.2 Implémenter POST `/api/v1/prompts` - Créer un prompt
|
|
- [x] 3.3 Implémenter GET `/api/v1/prompts` - Lister les prompts (avec pagination)
|
|
- [x] 3.4 Implémenter GET `/api/v1/prompts/{id}` - Détail d'un prompt
|
|
- [x] 3.5 Implémenter PATCH `/api/v1/prompts/{id}` - Mettre à jour un prompt
|
|
- [x] 3.6 Implémenter DELETE `/api/v1/prompts/{id}` - Supprimer un prompt
|
|
- [x] 3.7 Enregistrer le router dans `routes/api_v1_router.py`
|
|
|
|
- [x] **Task 4: Implémenter la restriction Pro** (AC: #6)
|
|
- [x] 4.1 Utiliser `require_pro_user` depuis `routes/deps.py`
|
|
- [x] 4.2 Retourner 403 avec erreur `PRO_FEATURE_REQUIRED` pour les utilisateurs Free
|
|
|
|
- [x] **Task 5: Tests** (AC: Tous)
|
|
- [x] 5.1 Tests unitaires pour les schémas
|
|
- [x] 5.2 Tests d'intégration pour les endpoints CRUD
|
|
- [x] 5.3 Test de la restriction Pro (403 pour Free)
|
|
- [x] 5.4 Test de la propriété utilisateur (un user ne peut pas voir les prompts d'un autre)
|
|
|
|
- [x] **Task 6: Documentation OpenAPI** (AC: Tous)
|
|
- [x] 6.1 Documenter tous les endpoints avec exemples
|
|
- [x] 6.2 Documenter les codes d'erreur
|
|
- [x] 6.3 Vérifier sur `/docs` que la documentation est complète
|
|
|
|
## Dev Notes
|
|
|
|
### 🏗️ Architecture - Structure du Module
|
|
|
|
**Fichiers à créer/modifier:**
|
|
|
|
```
|
|
database/
|
|
├── models.py # MODIFIÉ - Ajouter CustomPrompt
|
|
schemas/
|
|
├── prompt_schemas.py # CRÉÉ - Schémas Pydantic
|
|
routes/
|
|
├── prompt_routes.py # CRÉÉ - Endpoints CRUD
|
|
├── api_v1_router.py # MODIFIÉ - Enregistrement du router
|
|
alembic/versions/
|
|
└── xxx_add_custom_prompts_table.py # CRÉÉ - Migration DB
|
|
tests/
|
|
└── test_prompts.py # CRÉÉ - Tests
|
|
```
|
|
|
|
### 📊 Modèle de Données
|
|
|
|
**Table `custom_prompts`:**
|
|
|
|
```python
|
|
class CustomPrompt(Base):
|
|
"""User's custom prompts for LLM translation context.
|
|
Story 3.11: Custom Prompts - Endpoint CRUD
|
|
"""
|
|
|
|
__tablename__ = "custom_prompts"
|
|
|
|
id = Column(String(36), primary_key=True, default=generate_uuid)
|
|
user_id = Column(
|
|
String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
|
|
)
|
|
name = Column(String(255), nullable=False) # User-friendly name
|
|
content = Column(Text, nullable=False) # The actual prompt content (up to 10000 chars)
|
|
created_at = Column(DateTime, default=_utcnow)
|
|
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
|
|
|
|
# Indexes
|
|
__table_args__ = (Index("ix_custom_prompts_user_id", "user_id"),)
|
|
```
|
|
|
|
**Contraintes:**
|
|
- `name`: 1-255 caractères
|
|
- `content`: 1-10000 caractères
|
|
- Un utilisateur peut avoir plusieurs prompts
|
|
- Suppression en cascade si l'utilisateur est supprimé
|
|
|
|
### 📝 Schémas Pydantic
|
|
|
|
```python
|
|
# schemas/prompt_schemas.py
|
|
|
|
class PromptCreate(BaseModel):
|
|
"""Schema for creating a prompt."""
|
|
name: str = Field(..., min_length=1, max_length=255)
|
|
content: str = Field(..., min_length=1, max_length=10000)
|
|
|
|
@field_validator("name", "content")
|
|
@classmethod
|
|
def strip_whitespace(cls, v: str) -> str:
|
|
return v.strip()
|
|
|
|
|
|
class PromptUpdate(BaseModel):
|
|
"""Schema for updating a prompt (all fields optional)."""
|
|
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
|
content: Optional[str] = Field(None, min_length=1, max_length=10000)
|
|
|
|
|
|
class PromptResponse(BaseModel):
|
|
"""Schema for prompt in response."""
|
|
id: str
|
|
name: str
|
|
content: str
|
|
created_at: Optional[datetime] = None
|
|
updated_at: Optional[datetime] = None
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class PromptListItem(BaseModel):
|
|
"""Schema for prompt in list (lighter version)."""
|
|
id: str
|
|
name: str
|
|
content_preview: str = Field(..., description="First 100 chars of content")
|
|
created_at: Optional[datetime] = None
|
|
updated_at: Optional[datetime] = None
|
|
|
|
model_config = {"from_attributes": True}
|
|
```
|
|
|
|
### 🔧 Pattern CRUD à Suivre (basé sur glossary_routes.py)
|
|
|
|
**Structure des endpoints:**
|
|
|
|
1. **POST** - Créer un prompt:
|
|
- Utiliser `require_pro_user` dependency
|
|
- Valider les données avec Pydantic schema
|
|
- Créer l'entité en DB
|
|
- Logger l'action
|
|
- Retourner 201 avec `{data: {...}, meta: {}}`
|
|
|
|
2. **GET list** - Lister les prompts:
|
|
- Pagination avec `page` et `per_page` query params
|
|
- `DEFAULT_PAGE_SIZE = 50`, `MAX_PAGE_SIZE = 100`
|
|
- Retourner `{data: [...], meta: {total, page, per_page, total_pages}}`
|
|
- Utiliser `content_preview` (100 premiers caractères) pour alléger la réponse
|
|
|
|
3. **GET detail** - Détail d'un prompt:
|
|
- Valider le format UUID
|
|
- Vérifier que le prompt appartient à l'utilisateur
|
|
- Retourner 404 avec `PROMPT_NOT_FOUND` si introuvable
|
|
|
|
4. **PATCH** - Mettre à jour:
|
|
- Valider le format UUID
|
|
- Vérifier la propriété
|
|
- Permettre mise à jour partielle (uniquement les champs fournis)
|
|
- Mettre à jour `updated_at`
|
|
|
|
5. **DELETE** - Supprimer:
|
|
- Valider le format UUID
|
|
- Vérifier la propriété
|
|
- Retourner 204 (No Content)
|
|
|
|
### 🚨 Points d'Attention - Anti-Patterns à Éviter
|
|
|
|
1. **Ne PAS réinventer les dependencies** - Utiliser `require_pro_user` depuis `routes/deps.py`
|
|
|
|
2. **Ne PAS utiliser des modèles Pydantic inline** - Créer les schémas dans `schemas/prompt_schemas.py`
|
|
|
|
3. **Pagination obligatoire** - La liste DOIT avoir `page` et `per_page` pour éviter les N+1 queries
|
|
|
|
4. **Erreur 500 proscrite** - Toujours catcher les exceptions DB et retourner des erreurs JSON structurées
|
|
|
|
5. **Logger les actions** - Utiliser `logger.info()` pour create/update/delete
|
|
|
|
6. **UUID validation** - Toujours valider le format UUID avant de query la DB
|
|
|
|
### 📚 Références de Code Existant
|
|
|
|
**Pattern exact à suivre:** `routes/glossary_routes.py` (Story 3.9)
|
|
|
|
```python
|
|
# Structure du router
|
|
router = APIRouter(prefix="/api/v1/prompts", tags=["Prompts v1"])
|
|
|
|
# POST /api/v1/prompts
|
|
@router.post("", response_model=PromptDetailResponse, status_code=201)
|
|
async def create_prompt(body: PromptCreate, user: ProUser = Depends(require_pro_user)):
|
|
...
|
|
|
|
# GET /api/v1/prompts
|
|
@router.get("", response_model=PromptListResponse)
|
|
async def list_prompts(page: int = Query(1, ge=1), per_page: int = Query(50, ge=1, le=100), user: ProUser = Depends(require_pro_user)):
|
|
...
|
|
|
|
# GET /api/v1/prompts/{prompt_id}
|
|
@router.get("/{prompt_id}", response_model=PromptDetailResponse)
|
|
async def get_prompt(prompt_id: str, user: ProUser = Depends(require_pro_user)):
|
|
...
|
|
|
|
# PATCH /api/v1/prompts/{prompt_id}
|
|
@router.patch("/{prompt_id}", response_model=PromptDetailResponse)
|
|
async def update_prompt(prompt_id: str, body: PromptUpdate, user: ProUser = Depends(require_pro_user)):
|
|
...
|
|
|
|
# DELETE /api/v1/prompts/{prompt_id}
|
|
@router.delete("/{prompt_id}", status_code=204)
|
|
async def delete_prompt(prompt_id: str, user: ProUser = Depends(require_pro_user)):
|
|
...
|
|
```
|
|
|
|
### 🧪 Tests à Créer
|
|
|
|
**Fichier:** `tests/test_prompts.py`
|
|
|
|
```python
|
|
def test_create_prompt_success():
|
|
"""Pro user can create a prompt"""
|
|
|
|
def test_create_prompt_free_user_forbidden():
|
|
"""Free user receives 403 PRO_FEATURE_REQUIRED"""
|
|
|
|
def test_list_prompts_with_pagination():
|
|
"""List returns paginated results"""
|
|
|
|
def test_get_prompt_detail():
|
|
"""Get single prompt by ID"""
|
|
|
|
def test_get_prompt_not_found():
|
|
"""404 for non-existent or other user's prompt"""
|
|
|
|
def test_update_prompt():
|
|
"""PATCH updates name and/or content"""
|
|
|
|
def test_delete_prompt():
|
|
"""DELETE removes prompt"""
|
|
|
|
def test_prompt_ownership():
|
|
"""User cannot access another user's prompts"""
|
|
|
|
def test_content_max_length():
|
|
"""Content > 10000 chars returns 400"""
|
|
|
|
def test_name_max_length():
|
|
"""Name > 255 chars returns 400"""
|
|
```
|
|
|
|
### Project Structure Notes
|
|
|
|
- **Nouveau fichier:** `schemas/prompt_schemas.py` - Schémas Pydantic pour prompts
|
|
- **Nouveau fichier:** `routes/prompt_routes.py` - Endpoints CRUD
|
|
- **Nouveau fichier:** `tests/test_prompts.py` - Tests
|
|
- **Modification:** `database/models.py` - Ajout du modèle CustomPrompt
|
|
- **Modification:** `routes/api_v1_router.py` - Enregistrement du router
|
|
|
|
### References
|
|
|
|
- [Source: _bmad-output/planning-artifacts/epics.md#Story-3.11] - Story requirements (FR59)
|
|
- [Source: _bmad-output/planning-artifacts/architecture.md#API-Response-Formats] - Format API
|
|
- [Source: routes/glossary_routes.py] - Pattern CRUD à suivre (Story 3.9)
|
|
- [Source: routes/deps.py] - Dependencies `require_pro_user`
|
|
- [Source: schemas/glossary_schemas.py] - Pattern schémas Pydantic
|
|
- [Source: database/models.py] - Structure des modèles existants
|
|
|
|
## Dev Agent Record
|
|
|
|
### Agent Model Used
|
|
|
|
Claude 3.5 Sonnet (claude-3-5-sonnet)
|
|
|
|
### Debug Log References
|
|
|
|
Aucune erreur bloquante rencontrée. Migration Alembic a nécessité une adaptation pour SQLite (limitation ALTER COLUMN).
|
|
|
|
### Completion Notes List
|
|
|
|
- ✅ Modèle CustomPrompt ajouté à `database/models.py` avec index sur user_id
|
|
- ✅ Migration Alembic générée et appliquée (revision 5206607942a2)
|
|
- ✅ Schémas Pydantic créés: PromptCreate, PromptUpdate, PromptResponse, PromptListItem
|
|
- ✅ Endpoints CRUD complets: POST, GET (list), GET (detail), PATCH, DELETE
|
|
- ✅ Restriction Pro implémentée via `require_pro_user` dependency
|
|
- ✅ 19 tests d'intégration créés et passent tous
|
|
- ✅ Documentation OpenAPI complète avec descriptions et exemples
|
|
- ✅ Validation des contraintes (name: 1-255 chars, content: 1-10000 chars)
|
|
- ✅ Pagination implémentée sur l'endpoint de liste
|
|
- ✅ Propriété utilisateur vérifiée (404 si accès à un prompt d'un autre utilisateur)
|
|
- ✅ [Review Fix] Validation PATCH body vide ajoutée (erreur 400 NO_UPDATE_FIELDS)
|
|
- ✅ [Review Fix] Helper `_validate_uuid()` extraite pour éviter duplication
|
|
- ✅ [Review Fix] Méthode `has_updates()` ajoutée à PromptUpdate
|
|
|
|
### File List
|
|
|
|
**Nouveaux fichiers:**
|
|
- `schemas/prompt_schemas.py`
|
|
- `routes/prompt_routes.py`
|
|
- `tests/test_prompts.py`
|
|
- `alembic/versions/5206607942a2_add_custom_prompts_table.py`
|
|
|
|
**Fichiers modifiés:**
|
|
- `database/models.py` - Ajout du modèle CustomPrompt
|
|
- `routes/api_v1_router.py` - Enregistrement du router prompt_router
|
|
|
|
## Change Log
|
|
|
|
- 2026-02-22: Implémentation complète de la Story 3.11 - Custom Prompts CRUD endpoints
|
|
- 2026-02-22: Code review - Fix validation PATCH body vide, refactor UUID validation, ajout test
|