# Implementation Plan : Authentification ## Overview Ce plan découpe l'ajout de l'authentification en tâches de codage incrémentales, chacune s'appuyant sur les précédentes et se terminant par un câblage complet (pas de code orphelin). Le backend (`SchotzIadKmTracker.Api`, ASP.NET Core .NET 8, EF Core + SQLite) est construit couche par couche — modèle → options → hachage → JWT → email → service métier → controller → sécurité pipeline — puis le frontend Angular zoneless (signals) — service → interceptor/garde → écrans → routes — et enfin la configuration/déploiement. Les tests de propriétés (FsCheck.Xunit) et d'exemples/intégration (xUnit + WebApplicationFactory) sont des sous-tâches placées au plus près du code qu'elles valident. ## Tasks - [x] 1. Backend — Modèle de données et persistance - [x] 1.1 Créer les entités `Compte` et `ResetPasswordToken` - Créer `Models/Entities/Compte.cs` (singleton `Id = 1` non généré, `Email`, `MotDePasseHash`, `CreatedAt`, `UpdatedAt`) - Créer `Models/Entities/ResetPasswordToken.cs` (`Id`, `TokenHash`, `Expiration`, `UsedAt?`, `CreatedAt`) - _Requirements: 1.5, 6.1, 7.2, 8.1_ - [x] 1.2 Déclarer les DbSet et la configuration `OnModelCreating` - Ajouter `DbSet` et `DbSet` dans `Data/AppDbContext.cs` - Configurer `Compte` : `HasKey`, `Id` `ValueGeneratedNever()`, index unique `UX_Comptes_Email` sur `Email` - Configurer `ResetPasswordToken` : index unique `UX_ResetTokens_TokenHash`, index `IX_ResetTokens_Expiration` - _Requirements: 1.1, 1.2, 6.1_ - [x] 1.3 Générer la migration EF `AjoutAuthentification` - Exécuter `dotnet ef migrations add AjoutAuthentification` pour créer les tables `Comptes` et `ResetPasswordTokens` avec leurs index - Vérifier que la migration est appliquée par le `MigrateAsync()` existant de `Program.cs` (aucune modification du démarrage requise) - _Requirements: 1.1, 4.4_ - [x] 2. Backend — Options de configuration - [x] 2.1 Créer les classes d'options et les lier à la configuration - Créer `Configuration/JwtOptions.cs` (`SecretKey`, `Issuer`, `Audience`, `ExpirationMinutes = 720`, `SectionName = "Jwt"`) - Créer `Configuration/AuthOptions.cs` (`ResetTokenLifetimeMinutes = 60`, `FrontendBaseUrl`, `SectionName = "Auth"`) - Créer `Configuration/SmtpOptions.cs` (`Host`, `Port = 587`, `User`, `Password`, `From`, `SectionName = "Smtp"`) - Enregistrer les trois via `builder.Services.Configure<...>(...GetSection(...))` dans `Program.cs` - _Requirements: 5.5, 6.3, 6.7_ - [x]* 2.2 Écrire un test de liaison des options - Vérifier que `JwtOptions`, `AuthOptions`, `SmtpOptions` se lient correctement depuis la configuration (démarrage) - _Requirements: 5.5, 6.3, 6.7_ - [x] 3. Backend — Hachage des mots de passe - [x] 3.1 Intégrer `PasswordHasher` en DI - Enregistrer `IPasswordHasher` -> `PasswordHasher` en singleton dans `Program.cs` - _Requirements: 1.5, 8.1, 8.3_ - [x]* 3.2 Écrire le test de propriété du hachage - **Property 1 : Round-trip du hachage de mot de passe** - **Validates: Requirements 1.5, 8.1, 8.3** - [x] 4. Backend — Service JWT - [x] 4.1 Implémenter `IJwtService` / `JwtService` - Créer `Services/IJwtService.cs` et `Services/JwtService.cs` - `GenererJeton(Compte)` : émission HS256 avec claims `sub`, `email`, `jti`, expiration = maintenant + `JwtOptions.ExpirationMinutes` ; aucune donnée sensible (jamais le hash) - Enregistrer `IJwtService` -> `JwtService` (scoped) dans `Program.cs` - _Requirements: 2.1, 2.4, 5.1, 5.4, 5.5_ - [x]* 4.2 Écrire le test de propriété émission/validation et claims - **Property 2 : Émission/validation JWT et préservation des claims** - **Validates: Requirements 2.1, 2.4, 5.1, 5.4, 5.5** - [x]* 4.3 Écrire le test de propriété jeton expiré - **Property 3 : Un jeton JWT expiré est rejeté** - **Validates: Requirements 4.3, 5.2** - [x]* 4.4 Écrire le test de propriété signature invalide - **Property 4 : Une signature JWT invalide est rejetée** - **Validates: Requirements 5.3, 4.2** - [x] 5. Backend — Service email - [x] 5.1 Implémenter `IEmailService` et ses deux implémentations - Créer `Services/IEmailService.cs` (`EnvoyerLienResetAsync(destinataire, lienReset, ct)`) - Créer `Services/SmtpEmailService.cs` (MailKit, configuré par `SmtpOptions`, email HTML/texte contenant le lien) - Créer `Services/LoggingEmailService.cs` (repli : journalise le lien via `ILogger` niveau Information) - Ajouter la dépendance NuGet MailKit au projet API - _Requirements: 6.4, 6.5, 6.7_ - [x] 5.2 Câbler la sélection DI selon `Smtp__Host` - Dans `Program.cs`, enregistrer `LoggingEmailService` si `Smtp:Host` est vide, sinon `SmtpEmailService` - _Requirements: 6.4, 6.5_ - [x]* 5.3 Écrire le test de sélection DI de `IEmailService` - `Smtp__Host` vide ⇒ `LoggingEmailService` ; renseigné ⇒ `SmtpEmailService` - _Requirements: 6.4, 6.5_ - [x] 6. Backend — DTOs, validateurs et service d'authentification - [x] 6.1 Créer les DTOs d'authentification et les validateurs FluentValidation - Créer `Models/DTOs/AuthDtos.cs` (records `RegisterDto`, `LoginDto`, `LoginResultDto`, `ForgotPasswordDto`, `ResetPasswordDto`, `CompteExisteDto`) — aucun n'expose `MotDePasseHash` - Créer les validateurs FluentValidation : email syntaxiquement valide, `MotDePasse`/`NouveauMotDePasse` >= 8 caractères - _Requirements: 1.3, 1.4, 7.5, 8.2_ - [x] 6.2 Implémenter `IAuthService` / `AuthService` - Créer `Services/IAuthService.cs` et `Services/AuthService.cs` + exceptions métier (compte existant, identifiants invalides, lien invalide/expiré) - `CompteExisteAsync` ; `RegisterAsync` (compte unique, sinon exception -> 409, hachage du mdp) ; `LoginAsync` (vérifie via `VerifyHashedPassword`, retourne JWT, sinon exception générique -> 401) - `ForgotPasswordAsync` (réponse générique ; si email connu : crée un `ResetPasswordToken` haché avec expiration = now + `ResetTokenLifetimeMinutes`, construit le lien `{FrontendBaseUrl}/reset-password?token=...`, appelle `IEmailService`) ; `ResetPasswordAsync` (jeton valide/non expiré/non utilisé, remplace le mdp haché, marque `UsedAt`) - Enregistrer `IAuthService` -> `AuthService` (scoped) dans `Program.cs` - _Requirements: 1.1, 1.2, 2.1, 2.2, 6.1, 6.2, 6.4, 6.5, 6.6, 7.1, 7.2, 7.3, 7.4, 8.3, 9.1_ - [x]* 6.3 Écrire le test de propriété inscription crée le compte - **Property 5 : L'inscription crée le compte lorsqu'aucun compte n'existe** - **Validates: Requirements 1.1** - [x]* 6.4 Écrire le test de propriété inscription rejetée si compte existant - **Property 6 : L'inscription est rejetée lorsqu'un compte existe déjà** - **Validates: Requirements 1.2** - [x]* 6.5 Écrire le test de propriété mot de passe trop court à l'inscription - **Property 7 : Un mot de passe trop court est refusé à l'inscription** - **Validates: Requirements 1.3** - [x]* 6.6 Écrire le test de propriété connexion (émission/rejet) - **Property 8 : La connexion émet un jeton sur identifiants corrects et rejette les identifiants invalides** - **Validates: Requirements 2.1, 2.2** - [x]* 6.7 Écrire le test de propriété non-divulgation forgot-password - **Property 9 : Non-divulgation lors de la demande de réinitialisation** - **Validates: Requirements 6.6** - [x]* 6.8 Écrire le test de propriété expiration du jeton de reset - **Property 10 : Expiration du jeton de reset paramétrée** - **Validates: Requirements 6.1, 6.2, 6.3** - [x]* 6.9 Écrire le test de propriété réinitialisation valide remplace le mot de passe - **Property 11 : Une réinitialisation valide remplace le mot de passe** - **Validates: Requirements 7.1** - [x]* 6.10 Écrire le test de propriété jeton de reset à usage unique - **Property 12 : Un jeton de reset est à usage unique** - **Validates: Requirements 7.2, 7.4** - [x]* 6.11 Écrire le test de propriété jeton de reset expiré ou inconnu - **Property 13 : Un jeton de reset expiré ou inconnu est rejeté** - **Validates: Requirements 7.3, 7.4** - [x]* 6.12 Écrire le test de propriété nouveau mot de passe trop court à la réinitialisation - **Property 14 : Un nouveau mot de passe trop court est refusé à la réinitialisation** - **Validates: Requirements 7.5** - [x]* 6.13 Écrire les tests d'exemples de validation et d'exclusion du hash - Emails invalides (`"a@"`, `"abc"`, `"@x"`, chaîne vide) ⇒ 400 ; email valide accepté - Les corps de réponse d'auth ne contiennent aucun champ de hash - _Requirements: 1.4, 8.2_ - [x] 7. Backend — Controller d'authentification - [x] 7.1 Implémenter `AuthController` avec les 5 endpoints - Créer `Controllers/AuthController.cs` (`[ApiController]`, `[Route("api/auth")]`, `[AllowAnonymous]`) - `POST /register`, `POST /login`, `POST /forgot-password`, `POST /reset-password`, `GET /compte-existe` - Mapper les exceptions métier en codes HTTP via `ProblemDetails` : 409 (compte existant), 401 (identifiants génériques), 400 (mdp court / email invalide / lien invalide ou expiré), 200 générique (forgot-password) - _Requirements: 1.2, 1.3, 1.4, 2.1, 2.2, 6.6, 7.3, 7.4, 7.5, 9.1_ - [x] 8. Backend — Sécurité du pipeline - [x] 8.1 Configurer l'authentification JWT et l'autorisation globale - Ajouter `AddAuthentication(JwtBearer).AddJwtBearer(...)` avec `TokenValidationParameters` (issuer, audience, lifetime, signing key depuis `JwtOptions`, `ClockSkew` 30s) - Ajouter le filtre global `AuthorizeFilter(RequireAuthenticatedUser())` sur `AddControllers` - Insérer `app.UseAuthentication()` **avant** `app.UseAuthorization()` - Conserver `[AllowAnonymous]` sur `AuthController` et laisser `/health` public - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 5.1, 5.2, 5.3_ - [x]* 8.2 Écrire le test d'intégration de protection des routes - Via `WebApplicationFactory` : routes de données (`/api/trajets`, `/api/profil`, `/api/vehicules`, `/api/bareme`, `/api/resumes`, exports PDF, recalcul) ⇒ 401 sans jeton, 2xx avec jeton valide - `/api/auth/*` et `/health` accessibles sans jeton - _Requirements: 4.1, 4.2, 4.4, 4.5_ - [x]* 8.3 Écrire le test d'intégration bout en bout - register → login → appel protégé avec le token ; puis forgot → reset → login avec le nouveau mot de passe - _Requirements: 1.1, 2.1, 7.1_ - [x] 9. Checkpoint backend - Ensure all tests pass, ask the user if questions arise. - [x] 10. Frontend — Service, modèles, interceptor et garde - [x] 10.1 Créer les modèles TS et `AuthService` (signals + localStorage) - Créer les types `LoginDto`, `RegisterDto`, `LoginResult`, `ResetPasswordDto` - Créer `core/services/auth.service.ts` : signal `_token` initialisé depuis `localStorage`, `estAuthentifie` (computed), `login/register/forgotPassword/resetPassword/compteExiste/logout/getToken`, base `${environment.apiUrl}/auth` - _Requirements: 2.3, 3.1, 3.2, 9.1_ - [x] 10.2 Implémenter `authInterceptor` et l'enregistrer avant `timeoutInterceptor` - Créer `core/interceptors/auth.interceptor.ts` : ajoute `Authorization: Bearer` aux requêtes vers `environment.apiUrl` (hors endpoints publics d'auth et appels externes) ; sur 401, purge le token et redirige vers `/login` - Dans `app.config.ts` : `withInterceptors([authInterceptor, timeoutInterceptor])` - _Requirements: 4.7_ - [x] 10.3 Implémenter `authGuard` - Créer `core/guards/auth.guard.ts` (`CanActivateFn`) : si non authentifié -> `UrlTree` vers `/login`, sinon `true` - _Requirements: 4.6, 9.3_ - [x]* 10.4 Écrire les tests de service, interceptor et garde - `AuthService` stocke/supprime le token dans `localStorage` ; `authGuard` redirige un non-authentifié vers `/login` ; `authInterceptor` purge le token et redirige sur 401 - _Requirements: 2.3, 3.1, 4.6, 4.7, 9.3_ - [x] 11. Frontend — Écrans standalone (signals) - [x] 11.1 Implémenter `LoginComponent` (`/login`) - Formulaire email + mot de passe ; sur succès, stocke le token puis navigue vers la cible protégée ; état async via signals - _Requirements: 2.1, 2.3_ - [x] 11.2 Implémenter `RegisterComponent` (`/register`, Etape 1) - Formulaire email + mot de passe ; sur succès redirige vers `/login` ; état async via signals - _Requirements: 1.6, 9.1, 9.2_ - [x] 11.3 Implémenter `ForgotPasswordComponent` (`/forgot-password`) - Saisie email ; affiche un message générique quel que soit le résultat ; état async via signals - _Requirements: 6.6_ - [x] 11.4 Implémenter `ResetPasswordComponent` (`/reset-password`) - Lit le `token` depuis les query params de l'URL, saisit le nouveau mot de passe ; sur succès redirige vers `/login` ; état async via signals - _Requirements: 7.6_ - [x]* 11.5 Écrire le test de lecture du token par `ResetPasswordComponent` - Vérifie la lecture du `token` depuis l'URL - _Requirements: 7.6_ - [x] 12. Frontend — Routes et onboarding en deux étapes - [x] 12.1 Mettre à jour `app.routes.ts` - Ajouter les routes publiques `/login`, `/register`, `/forgot-password`, `/reset-password` (sans garde) - Protéger les routes de données par `[authGuard, profilConfigureGuard]` et `/onboarding` (Etape 2) + `/profil` + `/vehicules` par `[authGuard]` - Conserver l'`OnboardingComponent` existant comme écran de l'Etape 2 (comportement inchangé) - _Requirements: 4.6, 9.3, 9.4, 9.5, 9.6_ - [x] 13. Configuration et déploiement - [x] 13.1 Mettre à jour `.env.example` et `docker-compose.yml` - `.env.example` : documenter `JWT_SECRET_KEY`, `JWT_EXPIRATION_MINUTES=720`, `AUTH_RESET_TOKEN_LIFETIME_MINUTES=60`, `AUTH_FRONTEND_BASE_URL`, `SMTP_HOST/PORT/USER/PASSWORD/FROM` (sans valeurs secrètes réelles) - `docker-compose.yml` (service `backend`) : mapper `JWT__SecretKey` (`:?` requis), `JWT__ExpirationMinutes`, `Auth__ResetTokenLifetimeMinutes`, `Auth__FrontendBaseUrl`, `Smtp__*` - _Requirements: 5.5, 6.3, 6.7_ - [x] 13.2 Vérifier les builds - Exécuter `dotnet build` sur le backend et corriger les erreurs de compilation - Exécuter `ng build` sur le frontend `schotziad-km-tracker` et corriger les erreurs de compilation - _Requirements: 4.4, 9.6_ - [x] 14. Checkpoint final - Ensure all tests pass, ask the user if questions arise. ## Notes - Les tâches marquées `*` sont des tests optionnels et peuvent être ignorées pour un MVP plus rapide ; elles ne sont pas implémentées automatiquement. - Chaque tâche référence des exigences précises pour la traçabilité, et chaque test de propriété référence sa propriété de conception (P1–P14). - Les tests de propriétés (FsCheck.Xunit, ≥ 100 itérations) valident les propriétés universelles ; les tests d'exemples et d'intégration (xUnit + WebApplicationFactory) couvrent la validation d'entrée, la sélection DI et la protection des routes. - Les checkpoints assurent une validation incrémentale (backend d'abord, puis l'ensemble). - Le séquençage respecte le contexte projet : entités → migration EF appliquée au démarrage → options → services → controller → sécurité pipeline (ordre `UseAuthentication` avant `UseAuthorization`), puis frontend zoneless (signals) et déploiement Docker. ## Task Dependency Graph ```json { "waves": [ { "id": 0, "tasks": ["1.1", "2.1", "10.1"] }, { "id": 1, "tasks": ["1.2", "2.2", "3.1", "5.1", "10.2", "10.3", "11.1", "11.2", "11.3", "11.4"] }, { "id": 2, "tasks": ["1.3", "3.2", "4.1", "5.2", "6.1", "10.4", "11.5", "12.1"] }, { "id": 3, "tasks": ["4.2", "4.3", "4.4", "5.3", "6.2", "13.1"] }, { "id": 4, "tasks": ["6.3", "6.4", "6.5", "6.6", "6.7", "6.8", "6.9", "6.10", "6.11", "6.12", "6.13", "7.1"] }, { "id": 5, "tasks": ["8.1"] }, { "id": 6, "tasks": ["8.2", "8.3", "13.2"] } ] } ```