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>
12 KiB
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
- Création de prompt: Quand un utilisateur Pro POST sur
/api/v1/promptsavec{name, content}, un prompt template est créé et associé à son compte. (FR59) - Liste des prompts: GET
/api/v1/promptsretourne la liste des prompts de l'utilisateur. - Détail d'un prompt: GET
/api/v1/prompts/{id}retourne les détails d'un prompt spécifique. - Mise à jour: PATCH
/api/v1/prompts/{id}permet de mettre à jour le nom et/ou le contenu. - Suppression: DELETE
/api/v1/prompts/{id}supprime un prompt. - Restriction Pro: Les utilisateurs Free reçoivent 403 avec erreur
PRO_FEATURE_REQUIRED.
Tasks / Subtasks
-
Task 1: Créer le modèle CustomPrompt et la migration Alembic (AC: #1)
- 1.1 Ajouter le modèle
CustomPromptdansdatabase/models.py - 1.2 Générer la migration Alembic
alembic revision --autogenerate -m "add_custom_prompts_table" - 1.3 Appliquer la migration
alembic upgrade head - 1.4 Vérifier la table créée dans la DB
- 1.1 Ajouter le modèle
-
Task 2: Créer les schémas Pydantic (AC: Tous)
- 2.1 Créer
schemas/prompt_schemas.pyavec PromptCreate, PromptUpdate, PromptResponse, PromptListItem - 2.2 Définir les validators (content max 10000 chars, name max 255 chars)
- 2.3 Documenter les schémas pour OpenAPI
- 2.1 Créer
-
Task 3: Créer les routes CRUD (AC: Tous)
- 3.1 Créer
routes/prompt_routes.pyavec les endpoints CRUD - 3.2 Implémenter POST
/api/v1/prompts- Créer un prompt - 3.3 Implémenter GET
/api/v1/prompts- Lister les prompts (avec pagination) - 3.4 Implémenter GET
/api/v1/prompts/{id}- Détail d'un prompt - 3.5 Implémenter PATCH
/api/v1/prompts/{id}- Mettre à jour un prompt - 3.6 Implémenter DELETE
/api/v1/prompts/{id}- Supprimer un prompt - 3.7 Enregistrer le router dans
routes/api_v1_router.py
- 3.1 Créer
-
Task 4: Implémenter la restriction Pro (AC: #6)
- 4.1 Utiliser
require_pro_userdepuisroutes/deps.py - 4.2 Retourner 403 avec erreur
PRO_FEATURE_REQUIREDpour les utilisateurs Free
- 4.1 Utiliser
-
Task 5: Tests (AC: Tous)
- 5.1 Tests unitaires pour les schémas
- 5.2 Tests d'intégration pour les endpoints CRUD
- 5.3 Test de la restriction Pro (403 pour Free)
- 5.4 Test de la propriété utilisateur (un user ne peut pas voir les prompts d'un autre)
-
Task 6: Documentation OpenAPI (AC: Tous)
- 6.1 Documenter tous les endpoints avec exemples
- 6.2 Documenter les codes d'erreur
- 6.3 Vérifier sur
/docsque 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:
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èrescontent: 1-10000 caractères- Un utilisateur peut avoir plusieurs prompts
- Suppression en cascade si l'utilisateur est supprimé
📝 Schémas Pydantic
# 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:
-
POST - Créer un prompt:
- Utiliser
require_pro_userdependency - Valider les données avec Pydantic schema
- Créer l'entité en DB
- Logger l'action
- Retourner 201 avec
{data: {...}, meta: {}}
- Utiliser
-
GET list - Lister les prompts:
- Pagination avec
pageetper_pagequery 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
- Pagination avec
-
GET detail - Détail d'un prompt:
- Valider le format UUID
- Vérifier que le prompt appartient à l'utilisateur
- Retourner 404 avec
PROMPT_NOT_FOUNDsi introuvable
-
PATCH - Mettre à jour:
- Valider le format UUID
- Vérifier la propriété
- Permettre mise à jour partielle (uniquement les champs fournis)
- Mettre à jour
updated_at
-
DELETE - Supprimer:
- Valider le format UUID
- Vérifier la propriété
- Retourner 204 (No Content)
🚨 Points d'Attention - Anti-Patterns à Éviter
-
Ne PAS réinventer les dependencies - Utiliser
require_pro_userdepuisroutes/deps.py -
Ne PAS utiliser des modèles Pydantic inline - Créer les schémas dans
schemas/prompt_schemas.py -
Pagination obligatoire - La liste DOIT avoir
pageetper_pagepour éviter les N+1 queries -
Erreur 500 proscrite - Toujours catcher les exceptions DB et retourner des erreurs JSON structurées
-
Logger les actions - Utiliser
logger.info()pour create/update/delete -
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)
# 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
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.pyavec 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_userdependency - ✅ 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.pyroutes/prompt_routes.pytests/test_prompts.pyalembic/versions/5206607942a2_add_custom_prompts_table.py
Fichiers modifiés:
database/models.py- Ajout du modèle CustomPromptroutes/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