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>
This commit is contained in:
443
_bmad-output/implementation-artifacts/4-3-page-login.md
Normal file
443
_bmad-output/implementation-artifacts/4-3-page-login.md
Normal file
@@ -0,0 +1,443 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user