Files
office_translator/_bmad-output/implementation-artifacts/1-3-login-utilisateur-jwt.md
Sepehr Ramezani 26bd096a06 feat: production deployment - full update with providers, admin, glossaries, pricing, tests
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>
2026-04-25 15:01:47 +02:00

267 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
1. **AC1: Login Success** - When correct email + password submitted, receive `access_token` (15min expiry) and `refresh_token` (7 days expiry)
2. **AC2: JWT Signing** - Tokens are JWT signed with `SECRET_KEY` from environment variable
3. **AC3: Invalid Password** - Returns 401 with `{error: "INVALID_CREDENTIALS", message: "..."}`
4. **AC4: User Not Found** - Returns 401 with `{error: "INVALID_CREDENTIALS", message: "..."}` (security: same error as wrong password to prevent email enumeration)
5. **AC5: API Versioning** - Endpoint at `/api/v1/auth/login` (not legacy `/api/auth/login`)
## Tasks / Subtasks
- [x] **Task 1: Create Login Endpoint** (AC: 1, 2, 5)
- [x] 1.1 Create `POST /api/v1/auth/login` endpoint in `routes/auth_routes.py`
- [x] 1.2 Accept `UserLogin` schema with `email` and `password` fields
- [x] 1.3 Return `TokenResponse` with `access_token`, `refresh_token`, and `token_type: "bearer"`
- [x] 1.4 Generate access token with 15min expiry using PyJWT
- [x] 1.5 Generate refresh token with 7 days expiry using PyJWT
- [x] **Task 2: Implement JWT Token Generation** (AC: 2)
- [x] 2.1 Create or update `create_access_token()` function in `services/auth_service.py`
- [x] 2.2 Create or update `create_refresh_token()` function in `services/auth_service.py`
- [x] 2.3 Use `SECRET_KEY` from `core/config.py` (loaded from env)
- [x] 2.4 Include `sub` (user_id) and `exp` claims in tokens
- [x] 2.5 Optionally include `tier` claim for quick access
- [x] **Task 3: Implement Credential Validation** (AC: 1, 3, 4)
- [x] 3.1 Fetch user by email using existing `get_user_by_email()` from `services/auth_service.py`
- [x] 3.2 Verify password using existing `verify_password()` function (passlib[bcrypt])
- [x] 3.3 Return standardized error for user not found: 401 `INVALID_CREDENTIALS` (security: same as wrong password to prevent email enumeration)
- [x] 3.4 Return standardized error for wrong password: 401 `INVALID_CREDENTIALS`
- [x] 3.5 Use same error message pattern as Story 1.2 (French messages)
- [x] **Task 4: Standardize Error Responses** (AC: 3, 4)
- [x] 4.1 User not found → 401 with `{error: "INVALID_CREDENTIALS", message: "Email ou mot de passe incorrect"}` (security: prevents email enumeration)
- [x] 4.2 Invalid password → 401 with `{error: "INVALID_CREDENTIALS", message: "Mot de passe incorrect"}`
- [x] 4.3 Invalid email format → 400 with `{error: "INVALID_EMAIL", message: "Format d'email invalide"}` (Pydantic validation)
- [x] 4.4 Use `JSONResponse` pattern from Story 1.2 for error responses
- [x] **Task 5: Add Tests** (AC: 1-5)
- [x] 5.1 Create `tests/test_auth_login.py`
- [x] 5.2 Test: successful login returns 200 + tokens with correct expiry
- [x] 5.3 Test: invalid password returns 401 INVALID_CREDENTIALS
- [x] 5.4 Test: non-existent user returns 401 INVALID_CREDENTIALS (security: same error as wrong password)
- [x] 5.5 Test: invalid email format returns 400 INVALID_EMAIL
- [x] 5.6 Test: tokens contain correct user_id in `sub` claim
- [x] 5.7 Test: tokens are signed and verifiable with SECRET_KEY
## 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):
```json
{
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer"
},
"meta": {}
}
```
Error (401):
```json
{
"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):
```python
# 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):
```python
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`):
```python
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`):
```python
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 setup
- `alembic/` - No schema changes needed
- Frontend files - Not in scope
### Token Generation Code Reference
```python
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
```bash
# 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/` and `services/`
- 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/login` dans `router_v1` de `routes/auth_routes.py`, suivant exactement le pattern de `register_v1` (parse JSON manuel + PydanticValidationError catch).
- **Token generation** : `create_access_token()` appelé avec `expires_delta=timedelta(minutes=15)` ; `create_refresh_token()` mis à jour pour accepter un paramètre `expires_delta` optionnel, appelé avec `timedelta(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 claim `tier` pour accès rapide au plan utilisateur.
- **Tests** : 24 tests dans `tests/test_auth_login.py` couvrant AC1AC5 + 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 import `timedelta` et `verify_password`, ajout endpoint `login_v1`, passage du `tier` à `create_access_token()`, fix sécurité anti-énumération email
- `services/auth_service.py` — modifié : paramètre `expires_delta` ajouté à `create_refresh_token()`, paramètre `tier` ajouté à `create_access_token()`
- `tests/test_auth_login.py` — créé : 24 tests couvrant AC1AC5, 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/login` avec JWT (15min/7j), validation des credentials, gestion d'erreurs standardisée, 24 tests ajoutés.