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>
15 KiB
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
- Navigation: Page accessible via
/auth/loginou/login - Formulaire: Email + mot de passe avec validation en temps réel
- API Integration: Utilise
apiClientpour appelerPOST /api/v1/auth/login - Success: Si connexion réussie → redirection vers
/dashboard(ou URL dans?redirect=) - Error Handling: Si échec → toast d'erreur avec message de l'API (
{error, message}) - Token Storage: Stocke
access_tokenetrefresh_tokendans localStorage (note: httpOnly cookies nécessite backend) - Branding: Logo et nom "Office Translator" (pas "Translate Co.")
- TanStack Query: Utilise
useMutationpour la requête de login - UX: Loading state, validation visuelle, toggle password visibility
Tasks / Subtasks
-
Task 1: Refactor LoginForm avec apiClient et TanStack Query (AC: #3, #5, #8)
- 1.1 Extraire
LoginForm.tsxen composant colocated dansapp/auth/login/ - 1.2 Remplacer
fetch()parapiClient.post<LoginResponse>() - 1.3 Utiliser
useMutationde TanStack Query pour le login - 1.4 Gérer les erreurs avec
ApiClientErroret afficher toast
- 1.1 Extraire
-
Task 2: Créer le hook useLogin (AC: #3, #4, #5, #6, #8)
- 2.1 Créer
useLogin.tsdans le même dossier (colocation) - 2.2 Encapsuler la logique de mutation avec
useMutation - 2.3 Stocker tokens dans localStorage après succès
- 2.4 Rediriger vers
/dashboardouredirectparam
- 2.1 Créer
-
Task 3: Créer les types TypeScript (AC: #3)
- 3.1 Créer
types.tsdansapp/auth/login/ - 3.2 Définir
LoginRequest,LoginResponse,User
- 3.1 Créer
-
Task 4: Mettre à jour le branding (AC: #7)
- 4.1 Remplacer "Translate Co." par "Office Translator"
- 4.2 Remplacer logo "文A" par icône
Languagesde Lucide - 4.3 Aligner le design avec la landing page (Story 4.2)
-
Task 5: Intégrer le toast pour les erreurs (AC: #5)
- 5.1 Utiliser le système de toast existant (
notification.tsxou créeruseToast) - 5.2 Afficher
error.messagede l'API en cas d'échec
- 5.1 Utiliser le système de toast existant (
-
Task 6: Tester l'intégration complète (AC: Tous)
- 6.1 Vérifier build:
npm run builddans frontend/ - 6.2 Tester login avec backend (credentials valides/invalides)
- 6.3 Vérifier redirection vers /dashboard
- 6.4 Vérifier persistance des tokens
- 6.1 Vérifier build:
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:
{
"email": "user@example.com",
"password": "password123"
}
Response (succès):
{
"data": {
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"user": {
"id": "uuid",
"email": "user@example.com",
"tier": "free"
}
},
"meta": {}
}
Response (erreur):
{
"error": "INVALID_CREDENTIALS",
"message": "Email ou mot de passe incorrect",
"details": {}
}
🔧 Types TypeScript à Créer
// 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
// 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é
// 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:
- Déplacé vers
frontend/src/app/(auth)/login/page.tsx(route group) - Simplifié pour utiliser les composants colocated
- Le composant LoginForm extrait dans son propre fichier
🚨 Anti-Patterns à Éviter
-
NE PAS hardcoder l'URL API - Utiliser
apiClientqui litNEXT_PUBLIC_API_URL -
NE PAS utiliser fetch() direct - Utiliser
apiClient.post()pour la gestion automatique des erreurs -
NE PAS utiliser useEffect pour le login - Utiliser
useMutationde TanStack Query -
NE PAS utiliser "Translate Co." - Branding doit être "Office Translator"
-
NE PAS utiliser l'icône "文A" - Utiliser
Languagesde Lucide (comme sur la landing page) -
NE PAS oublier le Suspense -
useSearchParams()nécessite Suspense boundary
📊 Token Storage Note
Actuel: localStorage
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/uicard.tsx- Card avec variants (elevated, outline)input.tsx- Input shadcn/uilabel.tsx- Label shadcn/uinotification.tsx- Pour les toasts
🧪 Tests à Effectuer
- Build sans erreurs:
npm run builddans frontend/ - Login succès: Credentials valides → redirection /dashboard
- Login échec: Credentials invalides → message d'erreur affiché
- Token storage: Vérifier localStorage après login
- Redirect param:
/auth/login?redirect=/dashboard/api-keys→ redirige vers /dashboard/api-keys - 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.tsavec LoginRequest, LoginResponse, User interfaces - ✅ Créé
useLogin.tshook avec useMutation de TanStack Query - ✅ Créé
LoginForm.tsxcomposant avec branding Office Translator - ✅ Refactoré
page.tsxpour 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
/logincomme alias vers/auth/login
File List
Nouveaux fichiers:
frontend/src/app/auth/login/types.ts- Types TypeScript pour loginfrontend/src/app/auth/login/useLogin.ts- Hook useMutation pour loginfrontend/src/app/auth/login/LoginForm.tsx- Composant formulaire loginfrontend/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