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>
363 lines
16 KiB
Markdown
363 lines
16 KiB
Markdown
# Story 4.4: Page Register
|
|
|
|
Status: done
|
|
|
|
## Story
|
|
|
|
En tant que **nouvel utilisateur**,
|
|
Je veux **créer un compte via un formulaire clair et moderne**,
|
|
de sorte que **je puisse commencer à traduire des documents**.
|
|
|
|
## Acceptance Criteria
|
|
|
|
1. **Navigation**: Page accessible via `/auth/register`
|
|
2. **Formulaire**: Email + mot de passe + confirmation avec validation en temps réel
|
|
3. **API Integration**: Utilise `apiClient` pour appeler `POST /api/v1/auth/register`
|
|
4. **Auto-login**: Après inscription réussie → appel automatique à `POST /api/v1/auth/login` pour obtenir les tokens
|
|
5. **Success**: Tokens stockés dans localStorage → redirection vers `/dashboard`
|
|
6. **Error Handling**: Erreurs API affichées inline (EMAIL_EXISTS, INVALID_EMAIL, etc.)
|
|
7. **Branding**: Logo "Office Translator" avec icône `Languages` de Lucide (PAS "文A" ni "Translate Co.")
|
|
8. **TanStack Query**: Utilise `useMutation` pour les requêtes (PAS useState+fetch directement)
|
|
9. **UX**: Loading state, validation visuelle inline, toggle password visibility, password strength indicator
|
|
10. **Colocation**: Composants, hooks et types dans `frontend/src/app/auth/register/`
|
|
|
|
## Tasks / Subtasks
|
|
|
|
- [x] **Task 1: Créer les types TypeScript** (AC: #3, #4)
|
|
- [x] 1.1 Créer `types.ts` dans `frontend/src/app/auth/register/`
|
|
- [x] 1.2 Définir `RegisterRequest`, `RegisterResponse`, `RegisterFormData`
|
|
|
|
- [x] **Task 2: Créer le hook useRegister** (AC: #3, #4, #5, #8)
|
|
- [x] 2.1 Créer `useRegister.ts` dans `frontend/src/app/auth/register/`
|
|
- [x] 2.2 Utiliser `useMutation` pour `POST /api/v1/auth/register`
|
|
- [x] 2.3 Après succès inscription → appeler `apiClient.post('/api/v1/auth/login', {email, password})`
|
|
- [x] 2.4 Stocker tokens dans localStorage: `token`, `refresh_token`, `user`
|
|
- [x] 2.5 Rediriger vers `/dashboard` (ou `?redirect=` param)
|
|
|
|
- [x] **Task 3: Créer RegisterForm.tsx** (AC: #2, #6, #7, #9)
|
|
- [x] 3.1 Créer `RegisterForm.tsx` dans `frontend/src/app/auth/register/`
|
|
- [x] 3.2 Champs: email, password, confirmPassword (optionnel: name)
|
|
- [x] 3.3 Validation inline: email format, password ≥ 8 chars, confirmation match
|
|
- [x] 3.4 Password strength indicator (barres visuelles)
|
|
- [x] 3.5 Toggle visibility pour les deux champs password
|
|
- [x] 3.6 Afficher erreur API inline (pas de state `error` séparé — utiliser `mutation.isError`)
|
|
- [x] 3.7 Branding: `Languages` icône + "Office Translator"
|
|
|
|
- [x] **Task 4: Mettre à jour page.tsx** (AC: #1, #10)
|
|
- [x] 4.1 Remplacer le contenu de `frontend/src/app/auth/register/page.tsx`
|
|
- [x] 4.2 Utiliser `RegisterForm` colocated avec `Suspense`
|
|
- [x] 4.3 Conserver le fond dégradé (copier le pattern de `auth/login/page.tsx`)
|
|
|
|
- [x] **Task 5: Vérifier le build et tester** (AC: Tous)
|
|
- [x] 5.1 `npm run build` dans `frontend/` → 0 erreurs TypeScript ✓ Compilé avec succès
|
|
- [x] 5.2 Tester inscription avec email valide → redirection /dashboard
|
|
- [x] 5.3 Tester email déjà existant → message d'erreur "Un compte existe déjà..."
|
|
- [x] 5.4 Tester validation inline (email invalide, password trop court, confirmation mismatch)
|
|
|
|
## Dev Notes
|
|
|
|
### 🏗️ Stack Technique
|
|
|
|
| Technologie | Version |
|
|
|-------------|---------|
|
|
| Next.js | 16.0.6 (App Router) |
|
|
| React | 19.2.0 |
|
|
| TanStack Query | v5.90.21 |
|
|
| Tailwind CSS | configuré |
|
|
| Lucide React | disponible |
|
|
| shadcn/ui | Button, Input, Label, Card via `@/components/ui/` |
|
|
|
|
### 📁 Structure Cible (Colocation Pattern)
|
|
|
|
```
|
|
frontend/src/app/auth/register/
|
|
├── page.tsx # ✅ Déjà existant - À REMPLACER (garde structure Suspense)
|
|
├── RegisterForm.tsx # 🆕 Composant formulaire colocated
|
|
├── useRegister.ts # 🆕 Hook mutation colocated
|
|
└── types.ts # 🆕 Types TypeScript colocated
|
|
```
|
|
|
|
**Règle absolue (architecture.md):**
|
|
```
|
|
🚨 FICHIERS SPÉCIAUX: page.tsx, layout.tsx → TOUJOURS minuscules
|
|
🚨 COLOCATION: Components/hooks/types dans le dossier de leur page
|
|
```
|
|
|
|
### ⚠️ CRITIQUE: Comportement du Backend
|
|
|
|
**POST `/api/v1/auth/register`** — retourne **PAS de tokens**:
|
|
```json
|
|
// Succès (201)
|
|
{
|
|
"data": { "id": "usr_xxx", "email": "user@example.com", "tier": "free" },
|
|
"meta": {}
|
|
}
|
|
```
|
|
|
|
**DONC: Auto-login obligatoire après inscription**. Le hook doit chaîner:
|
|
1. `apiClient.post('/api/v1/auth/register', {email, password, name?})`
|
|
2. Si succès → `apiClient.post<LoginResponse>('/api/v1/auth/login', {email, password})`
|
|
3. Stocker les tokens → rediriger
|
|
|
|
**POST `/api/v1/auth/login`** — retourne:
|
|
```json
|
|
{
|
|
"data": { "access_token": "eyJ...", "refresh_token": "eyJ...", "token_type": "bearer" },
|
|
"meta": {}
|
|
}
|
|
```
|
|
|
|
**Codes d'erreur registration:**
|
|
| Code erreur | HTTP | Message |
|
|
|-------------|------|---------|
|
|
| `EMAIL_EXISTS` | 400 | "Un compte existe déjà avec cette adresse email" |
|
|
| `INVALID_EMAIL` | 400 | "Format d'email invalide" |
|
|
| `INVALID_REQUEST` | 400 | "Données d'inscription invalides" |
|
|
|
|
### 🔧 Types TypeScript à Créer
|
|
|
|
```typescript
|
|
// frontend/src/app/auth/register/types.ts
|
|
|
|
export interface RegisterRequest {
|
|
email: string;
|
|
password: string;
|
|
name?: string;
|
|
}
|
|
|
|
export interface RegisterResponse {
|
|
id: string;
|
|
email: string;
|
|
tier: 'free' | 'pro';
|
|
}
|
|
```
|
|
|
|
**Note:** Réutiliser `LoginResponse` depuis `../login/types` pour l'auto-login:
|
|
```typescript
|
|
import type { LoginResponse } from '../login/types';
|
|
```
|
|
|
|
### 🔧 Hook useRegister à Créer
|
|
|
|
```typescript
|
|
// frontend/src/app/auth/register/useRegister.ts
|
|
'use client';
|
|
|
|
import { useMutation } from '@tanstack/react-query';
|
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|
import { apiClient } from '@/lib/apiClient';
|
|
import type { RegisterRequest, RegisterResponse } from './types';
|
|
import type { LoginResponse } from '../login/types';
|
|
|
|
export function useRegister() {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const redirect = searchParams.get('redirect') || '/dashboard';
|
|
|
|
return useMutation({
|
|
mutationFn: async (data: RegisterRequest) => {
|
|
// Étape 1: Inscription
|
|
await apiClient.post<RegisterResponse>('/api/v1/auth/register', data);
|
|
|
|
// Étape 2: Auto-login pour obtenir les tokens
|
|
const loginResponse = await apiClient.post<LoginResponse>(
|
|
'/api/v1/auth/login',
|
|
{ email: data.email, password: data.password }
|
|
);
|
|
return loginResponse.data;
|
|
},
|
|
onSuccess: (data) => {
|
|
// Même pattern que useLogin
|
|
localStorage.setItem('token', data.access_token);
|
|
localStorage.setItem('refresh_token', data.refresh_token);
|
|
// Note: register ne retourne pas user complet, construire depuis loginResponse
|
|
router.push(redirect);
|
|
},
|
|
});
|
|
}
|
|
```
|
|
|
|
### 🔧 RegisterForm.tsx à Créer
|
|
|
|
**Pattern à suivre: `frontend/src/app/auth/login/LoginForm.tsx`** (déjà refactorisé)
|
|
|
|
**Différences par rapport à LoginForm:**
|
|
- Champ `email` + `password` + `confirmPassword` (+ optionnel `name`)
|
|
- Validation inline: format email, longueur password ≥ 8, passwords match
|
|
- Password strength indicator (barres visuelles — code existant dans l'ancien register)
|
|
- Bouton: `UserPlus` + "Create Account" → `ArrowRight`
|
|
- Lien bas: "Vous avez déjà un compte?" → `/auth/login`
|
|
- Après soumission: utiliser `mutation.isError` et `mutation.error?.message`
|
|
|
|
**Composants UI disponibles dans `@/components/ui/`:**
|
|
- `button.tsx` — variants: `default`, `premium`, `outline`; sizes: `default`, `lg`
|
|
- `input.tsx` — supporte `leftIcon`, `rightIcon` props (voir usage dans ancien register)
|
|
- `label.tsx`
|
|
- `card.tsx` — variants: `elevated`, `outline`; prop `hover={false}` (pattern login)
|
|
- `badge.tsx`
|
|
|
|
**⚠️ Anti-pattern à éviter avec Input:**
|
|
```typescript
|
|
// ❌ MAUVAIS - l'ancien register utilise leftIcon/rightIcon props
|
|
// Vérifier si ces props existent dans frontend/src/components/ui/input.tsx
|
|
// Si NON → utiliser div relative + positionnement absolu (comme LoginForm)
|
|
|
|
// ✅ Pattern LoginForm (sûr):
|
|
<div className="relative">
|
|
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input className="pl-10" ... />
|
|
</div>
|
|
```
|
|
|
|
### 📁 Fichier Existant à Remplacer
|
|
|
|
`frontend/src/app/auth/register/page.tsx` (605 lignes) contient:
|
|
- ❌ Branding "Translate Co." + logo "文A" → **À remplacer par "Office Translator" + `Languages`**
|
|
- ❌ `fetch("http://localhost:8000/api/auth/register")` hardcodé → **À remplacer par `apiClient`**
|
|
- ❌ Tout inline (pas de colocation) → **À extraire dans composants colocated**
|
|
- ❌ `useState` loading manual → **À remplacer par `useMutation`**
|
|
- ✅ Conserver: fond dégradé avec effets flottants, structure Suspense
|
|
- ✅ Conserver: password strength indicator (réutiliser la logique)
|
|
- ✅ Conserver: validation inline avec `CheckCircle`/`AlertTriangle`
|
|
|
|
### 🚨 Anti-Patterns à Éviter
|
|
|
|
1. **NE PAS hardcoder l'URL**: ~~`fetch("http://localhost:8000/api/auth/register")`~~ → utiliser `apiClient.post()`
|
|
2. **NE PAS utiliser l'ancienne API**: ~~`/api/auth/register`~~ → utiliser `/api/v1/auth/register`
|
|
3. **NE PAS utiliser `useState` + `fetch` direct** → utiliser `useMutation`
|
|
4. **NE PAS appeler login endpoint avec `fetch` direct** → chaîner dans `mutationFn` avec `apiClient`
|
|
5. **NE PAS oublier le Suspense** — `useSearchParams()` nécessite Suspense boundary
|
|
6. **NE PAS créer de logique dupliquée** — réutiliser `LoginResponse` type depuis `../login/types`
|
|
7. **NE PAS créer un fichier `Page.tsx`** — app router exige `page.tsx` en minuscules
|
|
8. **NE PAS mettre `name` comme champ obligatoire** — le backend l'accepte mais ne l'exige pas (vérifier: champ optionnel)
|
|
|
|
### 📊 Token Storage (même pattern que login)
|
|
|
|
```typescript
|
|
// Dans onSuccess après auto-login:
|
|
localStorage.setItem('token', data.access_token);
|
|
localStorage.setItem('refresh_token', data.refresh_token);
|
|
// Note: login v1 ne retourne pas user dans data → ne pas stocker user si absent
|
|
```
|
|
|
|
**Pourquoi localStorage et pas httpOnly cookies:**
|
|
- `apiClient.ts` lit depuis `localStorage.getItem('token')` (ligne 35)
|
|
- Cohérent avec `useLogin.ts` existant
|
|
- Migration vers httpOnly cookies = modification backend (hors scope)
|
|
|
|
### 🔍 Composants Réutilisables Existants
|
|
|
|
| Composant | Chemin | Usage |
|
|
|-----------|--------|-------|
|
|
| `apiClient` | `@/lib/apiClient` | Toutes requêtes HTTP |
|
|
| `ApiClientError` | `@/lib/apiClient` | Type pour `mutation.error` |
|
|
| `Button` | `@/components/ui/button` | variant="premium" ou default |
|
|
| `Input` | `@/components/ui/input` | Champs formulaire |
|
|
| `Label` | `@/components/ui/label` | Labels formulaire |
|
|
| `Card` | `@/components/ui/card` | Container formulaire |
|
|
| `LoginResponse` | `../login/types` | Type réponse auto-login |
|
|
|
|
### 🔗 Cohérence Cross-Story (Context Continuité)
|
|
|
|
**Story 4.3 (Page Login — terminée)** a établi ces patterns:
|
|
- Route: `app/auth/login/` (sans route group `(auth)`)
|
|
- Pattern colocation: `LoginForm.tsx`, `useLogin.ts`, `types.ts` dans `app/auth/login/`
|
|
- Page wrapper: `Suspense` + fond dégradé avec effets flottants
|
|
- Hook: `useMutation` + `onSuccess` qui stocke dans localStorage + redirect
|
|
- Branding: `Languages` icône + "Office Translator"
|
|
|
|
**Register DOIT suivre le même pattern** pour cohérence dans `app/auth/register/`.
|
|
|
|
**Lien de navigation inter-pages:**
|
|
- Login → Register: `/auth/register` (ajouter lien si absent)
|
|
- Register → Login: `/auth/login` (existe déjà dans ancien page.tsx)
|
|
|
|
### 🧪 Tests à Effectuer
|
|
|
|
1. **Build**: `npm run build` dans `frontend/` → 0 erreurs TypeScript/ESLint
|
|
2. **Inscription succès**: Email valide + password ≥ 8 chars → redirection /dashboard avec tokens
|
|
3. **Email existant**: Réponse 400 EMAIL_EXISTS → message "Un compte existe déjà..."
|
|
4. **Email invalide**: Format incorrect → validation inline immédiate
|
|
5. **Password mismatch**: Confirmation ≠ password → erreur inline
|
|
6. **Password trop court**: < 8 chars → erreur inline
|
|
7. **Token storage**: Vérifier localStorage après succès (`token`, `refresh_token`)
|
|
8. **Branding**: Vérifier "Office Translator" et icône `Languages` (pas "Translate Co." / "文A")
|
|
|
|
### Project Structure Notes
|
|
|
|
**Fichiers à modifier:**
|
|
- `frontend/src/app/auth/register/page.tsx` — Simplifier, utiliser `RegisterForm` colocated
|
|
|
|
**Fichiers à créer:**
|
|
- `frontend/src/app/auth/register/RegisterForm.tsx` — Composant formulaire
|
|
- `frontend/src/app/auth/register/useRegister.ts` — Hook mutation avec auto-login
|
|
- `frontend/src/app/auth/register/types.ts` — `RegisterRequest`, `RegisterResponse`
|
|
|
|
**Aucun fichier global à créer** — tout est colocated dans le dossier de la route.
|
|
|
|
### References
|
|
|
|
- [Source: _bmad-output/planning-artifacts/epics.md#Story-4.4] — Story requirements
|
|
- [Source: _bmad-output/planning-artifacts/architecture.md#Frontend-Architecture] — TanStack Query + colocation
|
|
- [Source: _bmad-output/planning-artifacts/architecture.md#Naming-Patterns] — Conventions nommage
|
|
- [Source: _bmad-output/planning-artifacts/architecture.md#API-Response-Formats] — Format réponse API
|
|
- [Source: routes/auth_routes.py#register_v1] — Endpoint register (201, sans tokens)
|
|
- [Source: routes/auth_routes.py#login_v1] — Endpoint login (tokens dans data)
|
|
- [Source: _bmad-output/implementation-artifacts/4-3-page-login.md] — Pattern login (colocation, hook, branding)
|
|
- [Source: frontend/src/app/auth/login/LoginForm.tsx] — Modèle composant formulaire
|
|
- [Source: frontend/src/app/auth/login/useLogin.ts] — Modèle hook useMutation
|
|
- [Source: frontend/src/app/auth/login/types.ts] — LoginResponse réutilisable
|
|
- [Source: frontend/src/app/auth/register/page.tsx] — Fichier actuel à refactorer (conserve: fond, Suspense)
|
|
- [Source: frontend/src/lib/apiClient.ts] — Client API centralisé (lecture token: localStorage)
|
|
- [Source: frontend/src/components/ui/] — Composants shadcn/ui disponibles
|
|
|
|
## Dev Agent Record
|
|
|
|
### Agent Model Used
|
|
|
|
openrouter/anthropic/claude-sonnet-4.6
|
|
|
|
### Debug Log References
|
|
|
|
### Completion Notes List
|
|
|
|
- Créé `types.ts` : `RegisterRequest`, `RegisterResponse` (optionnel: name; réutilise `LoginResponse` depuis `../login/types`)
|
|
- Créé `useRegister.ts` : `useMutation` qui chaîne register → auto-login → localStorage → redirect. Endpoint `/api/v1/auth/register` (sans tokens) + `/api/v1/auth/login` (tokens). Pattern identique à `useLogin.ts`.
|
|
- Créé `RegisterForm.tsx` : formulaire email + password + confirmPassword. Validation inline avec état `touched`, password strength indicator (4 barres), toggle visibility (Eye/EyeOff), branding "Office Translator" + icône `Languages`, erreur API via `mutation.isError` / `mutation.error?.message`.
|
|
- Remplacé `page.tsx` (605 lignes) par version simplifiée : fond dégradé conservé, `Suspense` boundary pour `useSearchParams()`, `RegisterForm` colocated.
|
|
- Build Next.js 16.0.6 : **✓ Compilé sans erreurs TypeScript**. Route `/auth/register` présente dans l'output.
|
|
|
|
### File List
|
|
|
|
- `frontend/src/app/auth/register/types.ts` — Créé
|
|
- `frontend/src/app/auth/register/useRegister.ts` — Créé
|
|
- `frontend/src/app/auth/register/RegisterForm.tsx` — Créé
|
|
- `frontend/src/app/auth/register/page.tsx` — Modifié (remplacé 605 lignes par version colocation)
|
|
- `frontend/src/app/auth/login/types.ts` — Modifié (correction type LoginResponse: supprimé `user`, ajouté `token_type`)
|
|
|
|
## Senior Developer Review (AI)
|
|
|
|
**Date:** 2026-02-23
|
|
**Reviewer:** Code Review Workflow
|
|
|
|
### Issues Found & Fixed
|
|
|
|
| # | Sévérité | Issue | Action |
|
|
|---|----------|-------|--------|
|
|
| 1 | 🔴 CRITICAL | Type `LoginResponse` incorrect — contenait `user: User` mais l'API v1 login ne retourne pas user | Corrigé: supprimé `user`, ajouté `token_type` |
|
|
| 2 | 🟡 MEDIUM | Pas de `onError` handler dans useMutation | Ajouté callback avec logging console |
|
|
| 3 | 🟡 MEDIUM | Liens morts `/terms` et `/privacy` | Remplacé par texte simple |
|
|
| 4 | 🟡 MEDIUM | Duplication logique password visibility | Refactorisé en composant `PasswordToggleIcon` |
|
|
| 5 | 🟢 LOW | Couleurs `bg-warning`/`bg-success` non standard Tailwind | Remplacé par `bg-yellow-500`/`bg-green-500` |
|
|
|
|
### Files Modified by Review
|
|
|
|
- `frontend/src/app/auth/login/types.ts`
|
|
- `frontend/src/app/auth/login/useLogin.ts`
|
|
- `frontend/src/app/auth/register/useRegister.ts`
|
|
- `frontend/src/app/auth/register/RegisterForm.tsx`
|
|
|
|
### Verdict
|
|
|
|
✅ **APPROVED** — Tous les AC implémentés, issues corrigées.
|