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>
11 KiB
Story 1.3: Login Utilisateur (JWT)
Status: done
Story
As a registered user, I want to login with email and password, so that I can access my dashboard and translation features.
Acceptance Criteria
- AC1: Login Success - When correct email + password submitted, receive
access_token(15min expiry) andrefresh_token(7 days expiry) - AC2: JWT Signing - Tokens are JWT signed with
SECRET_KEYfrom environment variable - AC3: Invalid Password - Returns 401 with
{error: "INVALID_CREDENTIALS", message: "..."} - AC4: User Not Found - Returns 401 with
{error: "INVALID_CREDENTIALS", message: "..."}(security: same error as wrong password to prevent email enumeration) - AC5: API Versioning - Endpoint at
/api/v1/auth/login(not legacy/api/auth/login)
Tasks / Subtasks
-
Task 1: Create Login Endpoint (AC: 1, 2, 5)
- 1.1 Create
POST /api/v1/auth/loginendpoint inroutes/auth_routes.py - 1.2 Accept
UserLoginschema withemailandpasswordfields - 1.3 Return
TokenResponsewithaccess_token,refresh_token, andtoken_type: "bearer" - 1.4 Generate access token with 15min expiry using PyJWT
- 1.5 Generate refresh token with 7 days expiry using PyJWT
- 1.1 Create
-
Task 2: Implement JWT Token Generation (AC: 2)
- 2.1 Create or update
create_access_token()function inservices/auth_service.py - 2.2 Create or update
create_refresh_token()function inservices/auth_service.py - 2.3 Use
SECRET_KEYfromcore/config.py(loaded from env) - 2.4 Include
sub(user_id) andexpclaims in tokens - 2.5 Optionally include
tierclaim for quick access
- 2.1 Create or update
-
Task 3: Implement Credential Validation (AC: 1, 3, 4)
- 3.1 Fetch user by email using existing
get_user_by_email()fromservices/auth_service.py - 3.2 Verify password using existing
verify_password()function (passlib[bcrypt]) - 3.3 Return standardized error for user not found: 401
INVALID_CREDENTIALS(security: same as wrong password to prevent email enumeration) - 3.4 Return standardized error for wrong password: 401
INVALID_CREDENTIALS - 3.5 Use same error message pattern as Story 1.2 (French messages)
- 3.1 Fetch user by email using existing
-
Task 4: Standardize Error Responses (AC: 3, 4)
- 4.1 User not found → 401 with
{error: "INVALID_CREDENTIALS", message: "Email ou mot de passe incorrect"}(security: prevents email enumeration) - 4.2 Invalid password → 401 with
{error: "INVALID_CREDENTIALS", message: "Mot de passe incorrect"} - 4.3 Invalid email format → 400 with
{error: "INVALID_EMAIL", message: "Format d'email invalide"}(Pydantic validation) - 4.4 Use
JSONResponsepattern from Story 1.2 for error responses
- 4.1 User not found → 401 with
-
Task 5: Add Tests (AC: 1-5)
- 5.1 Create
tests/test_auth_login.py - 5.2 Test: successful login returns 200 + tokens with correct expiry
- 5.3 Test: invalid password returns 401 INVALID_CREDENTIALS
- 5.4 Test: non-existent user returns 401 INVALID_CREDENTIALS (security: same error as wrong password)
- 5.5 Test: invalid email format returns 400 INVALID_EMAIL
- 5.6 Test: tokens contain correct user_id in
subclaim - 5.7 Test: tokens are signed and verifiable with SECRET_KEY
- 5.1 Create
Dev Notes
🚨 BROWNFIELD CONTEXT - CRITICAL
This is REFACTORING, not greenfield. Existing code to leverage:
| File | Current State | Action |
|---|---|---|
routes/auth_routes.py |
Has /api/auth/login (legacy) + /api/v1/auth/register |
Add /api/v1/auth/login |
services/auth_service.py |
Has verify_password(), get_user_by_email(), hash_password() |
Reuse functions |
core/config.py |
Should have SECRET_KEY |
Verify/ensure exists |
database/models.py |
User model with id, email, hashed_password, tier |
Already complete (Story 1.1) |
Architecture Compliance
API Response Format (from architecture.md):
Success (200):
{
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer"
},
"meta": {}
}
Error (401):
{
"error": "INVALID_CREDENTIALS",
"message": "Mot de passe incorrect"
}
⚠️ NO data field in error responses!
Token Structure (from architecture.md:183-184):
Access Token: 15min expiry
Refresh Token: 7 days expiry
Algorithm: HS256 (PyJWT default)
Claims: sub (user_id), exp (expiry), optionally tier
Naming (from architecture.md:276-289):
- DB/API: snake_case (
user_id,access_token) - Python: snake_case variables/functions, PascalCase classes
- Endpoint:
/api/v1/auth/login
Existing Code Patterns to Follow (from Story 1.2)
Router pattern (established in Story 1.2):
# routes/auth_routes.py
router_v1 = APIRouter(prefix="/api/v1/auth")
@router_v1.post("/login")
async def login_v1(user_login: UserLogin):
# Implementation
Error response pattern (from Story 1.2):
from fastapi.responses import JSONResponse
# For errors, use JSONResponse directly (not HTTPException)
return JSONResponse(
status_code=401,
content={
"error": "INVALID_CREDENTIALS",
"message": "Mot de passe incorrect"
}
)
Password verification (services/auth_service.py):
def verify_password(plain_password: str, hashed_password: str) -> bool:
if PASSLIB_AVAILABLE:
return pwd_context.verify(plain_password, hashed_password)
else:
# Fallback SHA256 (dev only)
User lookup (services/auth_service.py:180-200):
def get_user_by_email(email: str) -> Optional[User]:
# Already exists, reuse
Dependencies (Already Installed)
| Package | Version | Purpose |
|---|---|---|
| PyJWT | 2.8.0 | JWT token generation |
| passlib[bcrypt] | 1.7.4 | Password verification |
| pydantic | 2.x | Request/response schemas |
| pytest | 7.x | Testing |
Files to Modify
| File | Action | Notes |
|---|---|---|
routes/auth_routes.py |
MODIFY | Add /api/v1/auth/login endpoint |
services/auth_service.py |
MODIFY | Add token generation functions if not present |
schemas/auth_schemas.py or inline |
CREATE/MODIFY | UserLogin schema |
core/config.py |
VERIFY | Ensure SECRET_KEY setting exists |
tests/test_auth_login.py |
CREATE | New test file |
Files NOT to Touch
database/models.py- User model already complete (Story 1.1)database/connection.py- Already setupalembic/- No schema changes needed- Frontend files - Not in scope
Token Generation Code Reference
import jwt
from datetime import datetime, timedelta, timezone
from core.config import settings
def create_access_token(user_id: str, tier: str = "free") -> str:
payload = {
"sub": user_id,
"tier": tier,
"exp": datetime.now(timezone.utc) + timedelta(minutes=15),
"type": "access"
}
return jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256")
def create_refresh_token(user_id: str) -> str:
payload = {
"sub": user_id,
"exp": datetime.now(timezone.utc) + timedelta(days=7),
"type": "refresh"
}
return jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256")
Testing Commands
# Run tests
pytest tests/test_auth_login.py -v
# Test endpoint manually
curl -X POST http://localhost:8000/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "test@example.com", "password": "Password123!"}'
Security Considerations (from NFR6)
- Access token expires in 15 minutes
- Refresh token expires in 7 days
- SECRET_KEY must come from environment (never hardcoded)
- Password verification uses bcrypt (not plaintext comparison)
- Error messages should not reveal whether email exists vs password is wrong (but AC4 requires separate error - use same timing to prevent enumeration)
Project Structure Notes
- Backend uses snake_case for files, variables, functions
- PascalCase for classes
- Follow existing patterns in
routes/andservices/ - Tests in
tests/directory following pytest conventions
References
- [Source: _bmad-output/planning-artifacts/epics.md#Story 1.3]
- [Source: _bmad-output/planning-artifacts/architecture.md#Authentication & Security]
- [Source: _bmad-output/planning-artifacts/architecture.md#API Response Formats]
- [Source: _bmad-output/planning-artifacts/prd.md#FR18]
- [Source: _bmad-output/planning-artifacts/prd.md#NFR6]
- [Source: _bmad-output/implementation-artifacts/1-2-inscription-utilisateur.md - Previous story patterns]
Dev Agent Record
Agent Model Used
claude-4.6-sonnet-medium-thinking (Cursor)
Debug Log References
Aucun blocage rencontré. Implémentation directe suivant les patterns établis en Story 1.2.
Completion Notes List
- Endpoint créé :
POST /api/v1/auth/logindansrouter_v1deroutes/auth_routes.py, suivant exactement le pattern deregister_v1(parse JSON manuel + PydanticValidationError catch). - Token generation :
create_access_token()appelé avecexpires_delta=timedelta(minutes=15);create_refresh_token()mis à jour pour accepter un paramètreexpires_deltaoptionnel, appelé avectimedelta(days=7). Les endpoints legacy continuent d'utiliser leurs valeurs par défaut (24h / 30j) sans impact. - Gestion d'erreurs :
INVALID_CREDENTIALS(401) pour email inconnu ET mot de passe incorrect (sécurité : évite l'énumération d'emails),INVALID_EMAIL(400) si format email invalide (Pydantic),INVALID_REQUEST(400) pour JSON malformé ou champs manquants. Tous les messages sont en français conformément à la Story 1.2. - Format de réponse :
{"data": {"access_token": ..., "refresh_token": ..., "token_type": "bearer"}, "meta": {}}conforme à l'architecture. - Token tier claim :
create_access_token()inclut le claimtierpour accès rapide au plan utilisateur. - Tests : 24 tests dans
tests/test_auth_login.pycouvrant AC1–AC5 + tier claim + algorithme HS256. - Cycle RED-GREEN-REFACTOR : tests écrits et confirmés échouants (404) avant l'implémentation.
File List
routes/auth_routes.py— modifié : ajout importtimedeltaetverify_password, ajout endpointlogin_v1, passage dutieràcreate_access_token(), fix sécurité anti-énumération emailservices/auth_service.py— modifié : paramètreexpires_deltaajouté àcreate_refresh_token(), paramètretierajouté àcreate_access_token()tests/test_auth_login.py— créé : 24 tests couvrant AC1–AC5, tier claim, algorithme HS256
Change Log
- 2026-02-20 : Code review — fix tier claim manquant, fix anti-énumération email (USER_NOT_FOUND → INVALID_CREDENTIALS), ajout tests tier claim et HS256
- 2026-02-20 : Implémentation Story 1.3 — endpoint
POST /api/v1/auth/loginavec JWT (15min/7j), validation des credentials, gestion d'erreurs standardisée, 24 tests ajoutés.