Files
office_translator/_bmad-output/implementation-artifacts/4-3-page-login.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

444 lines
15 KiB
Markdown

# Story 4.3: Page Login
Status: done
## Story
En tant qu'**utilisateur**,
Je veux **me connecter via un formulaire clair et moderne**,
de sorte que **je puisse accéder à mon dashboard**.
## Acceptance Criteria
1. **Navigation**: Page accessible via `/auth/login` ou `/login`
2. **Formulaire**: Email + mot de passe avec validation en temps réel
3. **API Integration**: Utilise `apiClient` pour appeler `POST /api/v1/auth/login`
4. **Success**: Si connexion réussie → redirection vers `/dashboard` (ou URL dans `?redirect=`)
5. **Error Handling**: Si échec → toast d'erreur avec message de l'API (`{error, message}`)
6. **Token Storage**: Stocke `access_token` et `refresh_token` dans localStorage (note: httpOnly cookies nécessite backend)
7. **Branding**: Logo et nom "Office Translator" (pas "Translate Co.")
8. **TanStack Query**: Utilise `useMutation` pour la requête de login
9. **UX**: Loading state, validation visuelle, toggle password visibility
## Tasks / Subtasks
- [x] **Task 1: Refactor LoginForm avec apiClient et TanStack Query** (AC: #3, #5, #8)
- [x] 1.1 Extraire `LoginForm.tsx` en composant colocated dans `app/auth/login/`
- [x] 1.2 Remplacer `fetch()` par `apiClient.post<LoginResponse>()`
- [x] 1.3 Utiliser `useMutation` de TanStack Query pour le login
- [x] 1.4 Gérer les erreurs avec `ApiClientError` et afficher toast
- [x] **Task 2: Créer le hook useLogin** (AC: #3, #4, #5, #6, #8)
- [x] 2.1 Créer `useLogin.ts` dans le même dossier (colocation)
- [x] 2.2 Encapsuler la logique de mutation avec `useMutation`
- [x] 2.3 Stocker tokens dans localStorage après succès
- [x] 2.4 Rediriger vers `/dashboard` ou `redirect` param
- [x] **Task 3: Créer les types TypeScript** (AC: #3)
- [x] 3.1 Créer `types.ts` dans `app/auth/login/`
- [x] 3.2 Définir `LoginRequest`, `LoginResponse`, `User`
- [x] **Task 4: Mettre à jour le branding** (AC: #7)
- [x] 4.1 Remplacer "Translate Co." par "Office Translator"
- [x] 4.2 Remplacer logo "文A" par icône `Languages` de Lucide
- [x] 4.3 Aligner le design avec la landing page (Story 4.2)
- [x] **Task 5: Intégrer le toast pour les erreurs** (AC: #5)
- [x] 5.1 Utiliser le système de toast existant (`notification.tsx` ou créer `useToast`)
- [x] 5.2 Afficher `error.message` de l'API en cas d'échec
- [x] **Task 6: Tester l'intégration complète** (AC: Tous)
- [x] 6.1 Vérifier build: `npm run build` dans frontend/
- [x] 6.2 Tester login avec backend (credentials valides/invalides)
- [x] 6.3 Vérifier redirection vers /dashboard
- [x] 6.4 Vérifier persistance des tokens
## Dev Notes
### 🏗️ Architecture - Stack Frontend
**Technologies:**
- Next.js: 16.0.6 (App Router)
- React: 19.2.0
- TanStack Query: v5.90.21
- Tailwind CSS: configuré
- Lucide React: icônes
**⚠️ Règles Critiques App Router:**
```
🚨 FICHIERS SPÉCIAUX: page.tsx, layout.tsx → TOUJOURS minuscules
→ JAMAIS Page.tsx, Layout.tsx (le framework retourne 404)
🚨 COLOCATION: Components/hooks/types dans le dossier de leur page
```
### 📁 Structure Actuelle vs Cible
**Actuel (à refactorer):**
```
frontend/src/app/auth/login/
└── page.tsx # 386 lignes avec tout inline
```
**Cible (colocation pattern):**
```
frontend/src/app/(auth)/login/
├── page.tsx # Page wrapper avec Suspense
├── LoginForm.tsx # Composant formulaire (colocated)
├── useLogin.ts # Hook mutation (colocated)
└── types.ts # Types LoginRequest, LoginResponse (colocated)
```
### 🔧 Backend API Endpoint
**Endpoint:** `POST /api/v1/auth/login`
**Request:**
```json
{
"email": "user@example.com",
"password": "password123"
}
```
**Response (succès):**
```json
{
"data": {
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"user": {
"id": "uuid",
"email": "user@example.com",
"tier": "free"
}
},
"meta": {}
}
```
**Response (erreur):**
```json
{
"error": "INVALID_CREDENTIALS",
"message": "Email ou mot de passe incorrect",
"details": {}
}
```
### 🔧 Types TypeScript à Créer
```typescript
// app/(auth)/login/types.ts
export interface LoginRequest {
email: string;
password: string;
}
export interface User {
id: string;
email: string;
tier: 'free' | 'pro';
}
export interface LoginResponse {
access_token: string;
refresh_token: string;
user: User;
}
```
### 🔧 Hook useLogin à Créer
```typescript
// app/(auth)/login/useLogin.ts
'use client';
import { useMutation } from '@tanstack/react-query';
import { useRouter, useSearchParams } from 'next/navigation';
import { apiClient } from '@/lib/apiClient';
import type { LoginRequest, LoginResponse } from './types';
export function useLogin() {
const router = useRouter();
const searchParams = useSearchParams();
const redirect = searchParams.get('redirect') || '/dashboard';
return useMutation({
mutationFn: async (credentials: LoginRequest) => {
const response = await apiClient.post<LoginResponse>(
'/api/v1/auth/login',
credentials
);
return response.data;
},
onSuccess: (data) => {
// Stocker tokens dans localStorage
localStorage.setItem('token', data.access_token);
localStorage.setItem('refresh_token', data.refresh_token);
localStorage.setItem('user', JSON.stringify(data.user));
// Rediriger
router.push(redirect);
},
});
}
```
### 🔧 LoginForm.tsx Refactorisé
```typescript
// app/(auth)/login/LoginForm.tsx
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { Eye, EyeOff, Mail, Lock, ArrowRight, Loader2, Languages } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { useLogin } from './useLogin';
import { cn } from '@/lib/utils';
export function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const loginMutation = useLogin();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
loginMutation.mutate({ email, password });
};
return (
<Card variant="elevated" className="w-full max-w-md mx-auto">
<CardHeader className="text-center pb-6">
{/* Logo - Office Translator */}
<Link href="/" className="inline-flex items-center gap-3 mb-6 group">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary text-primary-foreground shadow-lg">
<Languages className="h-6 w-6" />
</div>
<span className="text-2xl font-semibold text-foreground">
Office Translator
</span>
</Link>
<CardTitle className="text-2xl font-bold">
Welcome back
</CardTitle>
<CardDescription>
Sign in to continue translating
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Error Message */}
{loginMutation.isError && (
<div className="rounded-lg bg-destructive/10 border border-destructive/30 p-4">
<p className="text-destructive text-sm">
{loginMutation.error?.message || 'Login failed'}
</p>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
{/* Email Field */}
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="email"
type="email"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="pl-10"
required
/>
</div>
</div>
{/* Password Field */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<Label htmlFor="password">Password</Label>
<Link href="/auth/forgot-password" className="text-sm text-primary hover:underline">
Forgot password?
</Link>
</div>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="password"
type={showPassword ? 'text' : 'password'}
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="pl-10 pr-10"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
</div>
{/* Submit Button */}
<Button
type="submit"
disabled={loginMutation.isPending || !email || !password}
className="w-full"
>
{loginMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Signing in...
</>
) : (
<>
Sign In
<ArrowRight className="ml-2 h-4 w-4" />
</>
)}
</Button>
</form>
{/* Sign up link */}
<p className="text-center text-sm text-muted-foreground">
Don&apos;t have an account?{' '}
<Link href="/auth/register" className="text-primary hover:underline">
Sign up for free
</Link>
</p>
</CardContent>
</Card>
);
}
```
### 📁 Fichier Actuel à Modifier
Le fichier actuel `frontend/src/app/auth/login/page.tsx` (386 lignes) doit être:
1. Déplacé vers `frontend/src/app/(auth)/login/page.tsx` (route group)
2. Simplifié pour utiliser les composants colocated
3. Le composant LoginForm extrait dans son propre fichier
### 🚨 Anti-Patterns à Éviter
1. **NE PAS hardcoder l'URL API** - Utiliser `apiClient` qui lit `NEXT_PUBLIC_API_URL`
2. **NE PAS utiliser fetch() direct** - Utiliser `apiClient.post()` pour la gestion automatique des erreurs
3. **NE PAS utiliser useEffect pour le login** - Utiliser `useMutation` de TanStack Query
4. **NE PAS utiliser "Translate Co."** - Branding doit être "Office Translator"
5. **NE PAS utiliser l'icône "文A"** - Utiliser `Languages` de Lucide (comme sur la landing page)
6. **NE PAS oublier le Suspense** - `useSearchParams()` nécessite Suspense boundary
### 📊 Token Storage Note
**Actuel:** localStorage
```typescript
localStorage.setItem('token', data.access_token);
localStorage.setItem('refresh_token', data.refresh_token);
```
**Future (sécurisé):** httpOnly cookies
- Nécessite modification backend: set-cookie dans response
- Non prévu dans cette story (backend déjà utilise localStorage pattern)
- L'apiClient actuel lit depuis localStorage
### 🔍 Composants UI Disponibles
Depuis `frontend/src/components/ui/`:
- `button.tsx` - Bouton shadcn/ui
- `card.tsx` - Card avec variants (elevated, outline)
- `input.tsx` - Input shadcn/ui
- `label.tsx` - Label shadcn/ui
- `notification.tsx` - Pour les toasts
### 🧪 Tests à Effectuer
1. **Build sans erreurs**: `npm run build` dans frontend/
2. **Login succès**: Credentials valides → redirection /dashboard
3. **Login échec**: Credentials invalides → message d'erreur affiché
4. **Token storage**: Vérifier localStorage après login
5. **Redirect param**: `/auth/login?redirect=/dashboard/api-keys` → redirige vers /dashboard/api-keys
6. **Branding**: Vérifier "Office Translator" et icône Languages
### Project Structure Notes
- **Modification:** `src/app/auth/login/page.tsx` → Simplifier et utiliser composants colocated
- **Optionnel:** Déplacer vers `src/app/(auth)/login/` pour route group cohérent
- **Création:** `src/app/(auth)/login/LoginForm.tsx` - Composant formulaire
- **Création:** `src/app/(auth)/login/useLogin.ts` - Hook mutation
- **Création:** `src/app/(auth)/login/types.ts` - Types TypeScript
**Note:** Le pattern colocation recommande de placer les composants, hooks et types spécifiques à la page dans le même dossier que `page.tsx`.
### References
- [Source: _bmad-output/planning-artifacts/epics.md#Story-4.3] - Story requirements
- [Source: _bmad-output/planning-artifacts/architecture.md#Frontend-Architecture] - TanStack Query decision
- [Source: _bmad-output/planning-artifacts/architecture.md#API-Response-Formats] - Format API
- [Source: _bmad-output/planning-artifacts/architecture.md#Naming-Patterns] - Conventions naming
- [Source: _bmad-output/planning-artifacts/architecture.md#Organisation-Frontend] - Structure colocation
- [Source: _bmad-output/implementation-artifacts/4-1-setup-tanstack-query-api-client.md] - apiClient setup
- [Source: _bmad-output/implementation-artifacts/4-2-landing-page.md] - Landing page (branding, styling)
- [Source: frontend/src/app/auth/login/page.tsx] - Page actuelle à refactorer
- [Source: frontend/src/lib/apiClient.ts] - Client API centralisé
- [Source: frontend/src/components/layout/site-header.tsx] - Branding Office Translator + icône Languages
## Dev Agent Record
### Agent Model Used
Claude 3.5 Sonnet (claude-3-5-sonnet)
### Debug Log References
Aucun problème rencontré lors de l'implémentation.
### Completion Notes List
- ✅ Créé `types.ts` avec LoginRequest, LoginResponse, User interfaces
- ✅ Créé `useLogin.ts` hook avec useMutation de TanStack Query
- ✅ Créé `LoginForm.tsx` composant avec branding Office Translator
- ✅ Refactoré `page.tsx` pour utiliser les composants colocalisés
- ✅ Remplacé fetch() direct par apiClient.post()
- ✅ Branding mis à jour: "Office Translator" + icône Languages
- ✅ Build Next.js réussi sans erreurs
- ✅ 11 tests existants passent
- ✅ Lint OK sur les nouveaux fichiers
- ✅ [Review] Ajouté NotificationProvider dans layout.tsx
- ✅ [Review] LoginForm utilise useNotification() pour les erreurs (toast)
- ✅ [Review] useLogin typé avec ApiClientError pour type safety
- ✅ [Review] Ajouté route `/login` comme alias vers `/auth/login`
### File List
**Nouveaux fichiers:**
- `frontend/src/app/auth/login/types.ts` - Types TypeScript pour login
- `frontend/src/app/auth/login/useLogin.ts` - Hook useMutation pour login
- `frontend/src/app/auth/login/LoginForm.tsx` - Composant formulaire login
- `frontend/src/app/login/page.tsx` - Route alias `/login``/auth/login`
**Fichiers modifiés:**
- `frontend/src/app/auth/login/page.tsx` - Refactoré pour utiliser composants colocalisés (386 → 41 lignes)
- `frontend/src/app/layout.tsx` - Ajouté NotificationProvider pour toasts
## Change Log
- 2026-02-23: Implémentation complète de la Story 4.3 - Page Login avec colocation pattern
- 2026-02-23: [Code Review] Fix 6 issues: NotificationProvider, toast system, error typing, /login alias