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>
444 lines
15 KiB
Markdown
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'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
|