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

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

  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

  • Task 1: Refactor LoginForm avec apiClient et TanStack Query (AC: #3, #5, #8)

    • 1.1 Extraire LoginForm.tsx en composant colocated dans app/auth/login/
    • 1.2 Remplacer fetch() par apiClient.post<LoginResponse>()
    • 1.3 Utiliser useMutation de TanStack Query pour le login
    • 1.4 Gérer les erreurs avec ApiClientError et afficher toast
  • Task 2: Créer le hook useLogin (AC: #3, #4, #5, #6, #8)

    • 2.1 Créer useLogin.ts dans 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 /dashboard ou redirect param
  • Task 3: Créer les types TypeScript (AC: #3)

    • 3.1 Créer types.ts dans app/auth/login/
    • 3.2 Définir LoginRequest, LoginResponse, User
  • Task 4: Mettre à jour le branding (AC: #7)

    • 4.1 Remplacer "Translate Co." par "Office Translator"
    • 4.2 Remplacer logo "文A" par icône Languages de 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.tsx ou créer useToast)
    • 5.2 Afficher error.message de l'API en cas d'échec
  • Task 6: Tester l'intégration complète (AC: Tous)

    • 6.1 Vérifier build: npm run build dans frontend/
    • 6.2 Tester login avec backend (credentials valides/invalides)
    • 6.3 Vérifier redirection vers /dashboard
    • 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:

{
  "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&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

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