Ajout de l'authentification (compte unique, JWT, reset par email)

Application mono-utilisateur : inscription unique, connexion par JWT (12 h),
protection de toutes les routes de données, réinitialisation de mot de passe
par email avec repli de journalisation, onboarding réorganisé en deux étapes
(inscription → connexion → profil + véhicule).

Backend (SchotzIadKmTracker.Api) :
- Entités Compte (singleton) et ResetPasswordToken + migration AjoutAuthentification.
- Hachage PasswordHasher, JwtService (HS256, durée configurable), IEmailService
  (MailKit + repli LoggingEmailService selon Smtp:Host).
- AuthService (register 409 si compte existant, login 401 générique, forgot-password
  non-divulgation, reset à usage unique + expiration), AuthController (5 endpoints
  publics), DTOs + validateurs FluentValidation.
- Sécurité pipeline : JwtBearer + filtre d'autorisation global, UseAuthentication
  avant UseAuthorization ; /health et /api/auth/* publics.

Frontend (schotziad-km-tracker, zoneless + signals) :
- AuthService (localStorage), authInterceptor (Bearer + purge/redirection 401),
  authGuard, écrans Login/Register/ForgotPassword/ResetPassword, routes publiques
  et protection [authGuard, profilConfigureGuard].

Configuration : .env.example et docker-compose.yml (JWT__SecretKey, durées, Smtp__*).

Tests : 54 tests backend (propriétés FsCheck P1–P14, exemples, intégration
WebApplicationFactory) + tests frontend (AuthService, authGuard, authInterceptor,
ResetPasswordComponent). Suite complète au vert.

Spec : .kiro/specs/authentification (requirements, design, tasks).
This commit is contained in:
2026-09-14 09:07:08 +02:00
parent 0e6a97ca61
commit dd36bbc425
66 changed files with 6282 additions and 27 deletions
@@ -0,0 +1 @@
{"specId": "c444de76-b758-4bf8-9de8-2f233790bc08", "workflowType": "requirements-first", "specType": "feature"}
+694
View File
@@ -0,0 +1,694 @@
# Document de conception — Authentification
## Overview
Cette conception ajoute une couche d'authentification à l'application mono-utilisateur **SchotzIAD KM Tracker** (frontend Angular `schotziad-km-tracker` + backend `SchotzIadKmTracker.Api`). Elle introduit :
- un **Compte** unique (Email + mot de passe haché à sel) ;
- une **session par Jeton_JWT** émis à la connexion (durée 12 h par défaut, configurable) ;
- la **protection de toutes les routes de données existantes** (trajets, profil, véhicules, carnet d'adresses, barème, résumés, montants réglés, exports PDF, recalcul) ;
- un **mécanisme de réinitialisation par email** avec repli de journalisation si aucun SMTP n'est configuré ;
- la **réorganisation de l'onboarding en deux étapes** : d'abord inscription puis connexion, ensuite saisie du profil et du premier véhicule (comportement actuel inchangé).
La conception s'insère dans l'architecture existante sans la bouleverser : controllers MVC classiques, EF Core + SQLite avec migrations appliquées au démarrage via `dbContext.Database.MigrateAsync()`, DTOs en `record` dans `Models/DTOs`, validation FluentValidation auto‑validée, services enregistrés en DI dans `Program.cs`, `ProblemDetails` (RFC 7807) pour les erreurs. Côté frontend : Angular standalone **zoneless** (état async via **signals**), `provideHttpClient(withInterceptors(...))`, gardes `CanActivateFn`, `environment.apiUrl`.
### Décisions structurantes (résumé)
| Sujet | Décision | Justification |
|-------|----------|---------------|
| Hachage | ASP.NET Core Identity `PasswordHasher<T>` | Standard .NET, à sel, itératif (PBKDF2), aucune dépendance externe supplémentaire, `VerifyHashedPassword` sans reconstitution du clair (Exigence 8). |
| Jeton de reset | Entité dédiée `ResetPasswordToken` (TokenHash, Expiration, UsedAt) | Sépare le cycle de vie du reset du Compte, permet l'usage unique et l'expiration proprement, et le hachage du jeton en base (Exigences 6, 7). |
| JWT | HMAC‑SHA256 (`HS256`) via `AddJwtBearer` | Mono‑service, clé symétrique secrète configurable ; validation native ASP.NET Core (Exigences 4, 5). |
| SMTP | `IEmailService` + implémentation MailKit + repli `ILogger` | MailKit est robuste et recommandé vs `SmtpClient` obsolète ; repli qui journalise le lien si SMTP absent (Exigence 6). |
| Stockage JWT (front) | `localStorage` | Session persistante 12 h sur poste personnel mono‑utilisateur ; risque XSS discuté §Frontend. |
## Architecture
### Vue d'ensemble backend
```mermaid
graph TD
subgraph Frontend Angular
LC[LoginComponent]
RC[RegisterComponent]
FPC[ForgotPasswordComponent]
RPC[ResetPasswordComponent]
AS[AuthService signals]
AI[authInterceptor]
AG[authGuard]
end
subgraph Backend SchotzIadKmTracker.Api
AC[AuthController AllowAnonymous]
SA[Service_Auth : IAuthService]
PH[PasswordHasher Compte]
JS[IJwtService]
ES[IEmailService MailKit / Log]
DB[(AppDbContext SQLite)]
PROT[Controllers protégés Authorize<br/>Trajets, Profil, Vehicules,<br/>AdressesNommees, Bareme, Resumes,<br/>Remboursement, ExportsPdf, Navigation]
end
LC --> AS
RC --> AS
FPC --> AS
RPC --> AS
AS -->|HTTP| AC
AI -.->|Bearer + gestion 401| PROT
AC --> SA
SA --> PH
SA --> JS
SA --> ES
SA --> DB
PROT -->|JwtBearer valide le token| JS
```
`AuthController` (routes publiques `[AllowAnonymous]`) délègue à `Service_Auth` (`IAuthService`), qui orchestre `PasswordHasher<Compte>`, `IJwtService`, `IEmailService` et `AppDbContext`. Les controllers de données existants deviennent des `Route_Protegee` via `[Authorize]` (Exigence 4).
### Insertion dans le pipeline `Program.cs`
L'ordre du middleware est critique : `UseAuthentication()` doit être ajouté **avant** `UseAuthorization()` (déjà présent). Ajouts prévus :
```csharp
// --- Options ---
builder.Services.Configure<JwtOptions>(builder.Configuration.GetSection(JwtOptions.SectionName));
builder.Services.Configure<AuthOptions>(builder.Configuration.GetSection(AuthOptions.SectionName));
builder.Services.Configure<SmtpOptions>(builder.Configuration.GetSection(SmtpOptions.SectionName));
// --- Services d'auth ---
builder.Services.AddSingleton<IPasswordHasher<Compte>, PasswordHasher<Compte>>();
builder.Services.AddScoped<IJwtService, JwtService>();
builder.Services.AddScoped<IAuthService, AuthService>();
// --- Sélection de l'implémentation Email selon la présence de config SMTP ---
var smtpOptions = builder.Configuration.GetSection(SmtpOptions.SectionName).Get<SmtpOptions>() ?? new SmtpOptions();
if (string.IsNullOrWhiteSpace(smtpOptions.Host))
builder.Services.AddScoped<IEmailService, LoggingEmailService>(); // repli journalisation (Exigence 6.5)
else
builder.Services.AddScoped<IEmailService, SmtpEmailService>(); // MailKit (Exigence 6.4)
// --- Authentification JWT ---
var jwtOptions = builder.Configuration.GetSection(JwtOptions.SectionName).Get<JwtOptions>() ?? new JwtOptions();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true, // rejette les jetons expirés (Exigence 5.2)
ValidateIssuerSigningKey = true, // rejette les signatures invalides (Exigence 5.3)
ValidIssuer = jwtOptions.Issuer,
ValidAudience = jwtOptions.Audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SecretKey)),
ClockSkew = TimeSpan.FromSeconds(30) // tolérance d'horloge réduite
};
});
```
Pipeline HTTP (extrait, ordre imposé) :
```csharp
app.UseCors(CorsPolicyName);
app.UseAuthentication(); // AJOUT — doit précéder UseAuthorization
app.UseAuthorization(); // existant
app.MapControllers();
```
## Components and Interfaces
### Service d'authentification
```csharp
namespace SchotzIadKmTracker.Api.Services;
public interface IAuthService
{
Task<bool> CompteExisteAsync(CancellationToken ct); // Exigence 1.2, 9.1 (GET /compte-existe)
Task RegisterAsync(RegisterDto dto, CancellationToken ct); // Exigence 1 — lève CompteDejaExistantException si compte présent
Task<string> LoginAsync(LoginDto dto, CancellationToken ct); // Exigence 2 — retourne le JWT ; lève IdentifiantsInvalidesException
Task ForgotPasswordAsync(ForgotPasswordDto dto, string resetBaseUrl, CancellationToken ct); // Exigence 6 — toujours succès générique
Task ResetPasswordAsync(ResetPasswordDto dto, CancellationToken ct); // Exigence 7 — lève LienInvalideException / LienExpireException
}
```
### Service JWT
```csharp
public interface IJwtService
{
/// <summary>Émet un Jeton_JWT signé HS256 dont l'expiration = maintenant + JwtOptions.ExpirationMinutes.</summary>
string GenererJeton(Compte compte); // Exigences 2.1, 2.4, 5.4
}
```
Claims émis : `sub` = `compte.Id`, `email` = `compte.Email`, `jti` = GUID (identifiant unique du jeton), `iat`/`exp` gérés par la librairie. Aucune donnée sensible (jamais le hash).
### Service email
```csharp
public interface IEmailService
{
Task EnvoyerLienResetAsync(string destinataire, string lienReset, CancellationToken ct); // Exigence 6.4 / 6.5
}
```
- `SmtpEmailService` (MailKit `SmtpClient`) : envoie un email HTML/texte contenant `lienReset`, configuré par `SmtpOptions`.
- `LoggingEmailService` : journalise le lien via `ILogger` au niveau `Information` (repli lorsque `Smtp__Host` est vide).
La sélection se fait en DI (voir `Program.cs` ci‑dessus). Le service appelant n'a pas connaissance de l'implémentation retenue (transparence, Exigence 6).
### Controller d'authentification
`AuthController` (`[ApiController]`, `[Route("api/auth")]`, `[AllowAnonymous]`) :
| Méthode | Route | Rôle | Codes |
|---------|-------|------|-------|
| POST | `/api/auth/register` | Inscription du compte unique (Exigence 1) | 200 / 400 / 409 |
| POST | `/api/auth/login` | Connexion, retourne `{ token, expiration }` (Exigence 2) | 200 / 401 |
| POST | `/api/auth/forgot-password` | Demande de reset, réponse générique (Exigence 6) | 200 |
| POST | `/api/auth/reset-password` | Réinitialisation via Jeton_Reset (Exigence 7) | 200 / 400 |
| GET | `/api/auth/compte-existe` | `{ existe: bool }` pour orienter le frontend (inscription vs connexion) | 200 |
La construction de l'URL de reset se fait côté serveur à partir d'une base configurable (`Auth__FrontendBaseUrl`) : `"{FrontendBaseUrl}/reset-password?token={token}"`. La déconnexion (Exigence 3) est entièrement côté client (suppression du token) et ne nécessite pas d'endpoint.
### Protection des controllers existants (Exigence 4)
Deux options :
1. **Filtre d'autorisation global** (retenu) — impose l'authentification par défaut sur tout le pipeline MVC, puis on ouvre explicitement l'auth via `[AllowAnonymous]` sur `AuthController` et `/health`. C'est le plus sûr (aucune route de données oubliée) :
```csharp
builder.Services.AddControllers(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy)); // toutes les routes protégées par défaut
});
```
`/health` reste public (endpoint minimal `MapHealthChecks`, non couvert par les filtres MVC). Les controllers de données (`TrajetsController`, `ProfilController`, `VehiculesController`, `AdressesController`, `AdressesNommeesController`, `BaremeController`, `ResumesController`, `RemboursementController`, `ExportsPdfController`, `NavigationController`) sont ainsi protégés sans modification individuelle — ce qui couvre trajets, profil, véhicules, carnet, barème, résumés, montants, exports PDF et recalcul (Exigence 4.4).
2. Alternative documentée : annoter chaque controller de données avec `[Authorize]`. Plus verbeux et sujet à l'oubli ; non retenu.
## Data Models
### Entité `Compte`
```csharp
namespace SchotzIadKmTracker.Api.Models.Entities;
/// <summary>Compte unique de l'application mono-utilisateur.</summary>
public class Compte
{
public int Id { get; set; } = 1; // singleton, comme Profil
[MaxLength(255)] public string Email { get; set; } = string.Empty;
public string MotDePasseHash { get; set; } = string.Empty; // jamais exposé (Exigence 8.2)
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
```
### Entité `ResetPasswordToken`
```csharp
/// <summary>Jeton de réinitialisation à usage unique et à durée limitée.</summary>
public class ResetPasswordToken
{
public int Id { get; set; }
public string TokenHash { get; set; } = string.Empty; // SHA-256 du jeton, jamais le jeton en clair
public DateTime Expiration { get; set; } // Exigence 6.2 (défaut +60 min)
public DateTime? UsedAt { get; set; } // null = non utilisé ; renseigné à l'usage (Exigence 7.2)
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
```
**Justification du choix d'entité dédiée** : bien que l'application soit mono‑utilisateur, séparer le jeton du `Compte` évite de polluer l'entité Compte de colonnes transitoires, permet d'exprimer clairement l'usage unique (`UsedAt`) et l'expiration, et de purger/historiser sans toucher au Compte. Le jeton est **haché** en base (`TokenHash`) : seul le jeton en clair transmis par email permet la réinitialisation, ce qui limite l'impact d'une fuite de la base.
### Déclarations `AppDbContext` (ajouts)
```csharp
public DbSet<Compte> Comptes => Set<Compte>();
public DbSet<ResetPasswordToken> ResetPasswordTokens => Set<ResetPasswordToken>();
// OnModelCreating
modelBuilder.Entity<Compte>(e =>
{
e.ToTable("Comptes");
e.HasKey(c => c.Id);
e.Property(c => c.Id).ValueGeneratedNever(); // singleton Id = 1
e.HasIndex(c => c.Email, "UX_Comptes_Email").IsUnique(); // Email unique
});
modelBuilder.Entity<ResetPasswordToken>(e =>
{
e.ToTable("ResetPasswordTokens");
e.HasIndex(t => t.TokenHash, "UX_ResetTokens_TokenHash").IsUnique();
e.HasIndex(t => t.Expiration, "IX_ResetTokens_Expiration");
});
```
### Migration EF
Une nouvelle migration `AjoutAuthentification` crée les tables `Comptes` et `ResetPasswordTokens` avec leurs index. Elle est appliquée automatiquement au démarrage par le `MigrateAsync()` existant de `Program.cs`, y compris dans le conteneur Docker (volume `trajets-data`). Aucune donnée existante n'est impactée.
### DTOs (records, `Models/DTOs/AuthDtos.cs`)
```csharp
namespace SchotzIadKmTracker.Api.Models.DTOs;
public record RegisterDto(string Email, string MotDePasse);
public record LoginDto(string Email, string MotDePasse);
public record LoginResultDto(string Token, DateTime Expiration);
public record ForgotPasswordDto(string Email);
public record ResetPasswordDto(string Token, string NouveauMotDePasse);
public record CompteExisteDto(bool Existe);
```
Aucun DTO n'expose `MotDePasseHash` (Exigence 8.2).
### Configuration (Options)
```csharp
public class JwtOptions { public const string SectionName = "Jwt";
public string SecretKey { get; set; } = ""; public string Issuer { get; set; } = "SchotzIadKmTracker";
public string Audience { get; set; } = "SchotzIadKmTracker"; public int ExpirationMinutes { get; set; } = 720; } // Exigence 5.5
public class AuthOptions { public const string SectionName = "Auth";
public int ResetTokenLifetimeMinutes { get; set; } = 60; // Exigence 6.3
public string FrontendBaseUrl { get; set; } = "http://localhost:8080"; }
public class SmtpOptions { public const string SectionName = "Smtp";
public string Host { get; set; } = ""; public int Port { get; set; } = 587;
public string User { get; set; } = ""; public string Password { get; set; } = ""; public string From { get; set; } = ""; }
```
## Correctness Properties
*Une propriété est une caractéristique ou un comportement qui doit rester vrai pour toutes les exécutions valides du système — une affirmation formelle de ce que le système doit faire. Les propriétés font le pont entre les spécifications lisibles par l'humain et des garanties de correction vérifiables par la machine.*
Ces propriétés découlent de la prework d'analyse des critères d'acceptation. Elles seront implémentées avec **FsCheck.Xunit** (déjà présent dans le projet de tests), à raison d'au moins **100 itérations** par propriété.
### Property 1 : Round-trip du hachage de mot de passe
*Pour tout* mot de passe non vide, le hachage produit une valeur différente du clair, et `PasswordHasher.VerifyHashedPassword(hash, motDePasse)` retourne `Success` ; de plus deux hachages du même mot de passe diffèrent (présence de sel).
**Validates: Requirements 1.5, 8.1, 8.3**
### Property 2 : Émission/validation JWT et préservation des claims
*Pour toute* valeur d'`ExpirationMinutes` positive et tout compte, le jeton émis est validable avec les paramètres de validation, préserve les claims `sub` et `email`, et son `exp` vaut `iat + ExpirationMinutes` (à la tolérance d'horloge près).
**Validates: Requirements 2.1, 2.4, 5.1, 5.4, 5.5**
### Property 3 : Un jeton JWT expiré est rejeté
*Pour tout* jeton dont la date d'expiration est antérieure à l'instant courant, la validation échoue (jeton considéré invalide).
**Validates: Requirements 4.3, 5.2**
### Property 4 : Une signature JWT invalide est rejetée
*Pour tout* jeton signé avec une clé différente de la clé de validation, ou dont la charge utile a été altérée, la validation échoue.
**Validates: Requirements 5.3, 4.2**
### Property 5 : L'inscription crée le compte lorsqu'aucun compte n'existe
*Pour toute* paire (Email syntaxiquement valide, Mot_De_Passe d'au moins 8 caractères), à partir d'une base sans compte, l'inscription aboutit et il existe ensuite exactement un compte portant cet Email.
**Validates: Requirements 1.1**
### Property 6 : L'inscription est rejetée lorsqu'un compte existe déjà
*Pour toute* seconde tentative d'inscription (avec n'importe quelle paire valide) alors qu'un compte existe déjà, la demande est rejetée avec un code 409 et le nombre de comptes reste égal à un.
**Validates: Requirements 1.2**
### Property 7 : Un mot de passe trop court est refusé à l'inscription
*Pour toute* chaîne de longueur strictement inférieure à 8, l'inscription est rejetée avec un code 400 et aucun compte n'est créé.
**Validates: Requirements 1.3**
### Property 8 : La connexion émet un jeton sur identifiants corrects et rejette les identifiants invalides
*Pour tout* compte enregistré, se connecter avec les identifiants exacts produit un jeton JWT validable ; et *pour tout* couple d'identifiants incorrects (Email inconnu ou Mot_De_Passe erroné), la connexion est rejetée avec un code 401 sans émettre de jeton.
**Validates: Requirements 2.1, 2.2**
### Property 9 : Non-divulgation lors de la demande de réinitialisation
*Pour tout* Email fourni à `forgot-password`, qu'il corresponde ou non au compte, la réponse HTTP (statut et corps) est identique ; aucun jeton n'est créé pour un Email inconnu.
**Validates: Requirements 6.6**
### Property 10 : Expiration du jeton de reset paramétrée
*Pour toute* valeur positive de `ResetTokenLifetimeMinutes`, tout jeton de reset généré possède une expiration égale à sa date de création plus cette durée (à la tolérance près) et un `UsedAt` nul.
**Validates: Requirements 6.1, 6.2, 6.3**
### Property 11 : Une réinitialisation valide remplace le mot de passe
*Pour tout* jeton de reset valide, non expiré et non utilisé, et tout nouveau Mot_De_Passe d'au moins 8 caractères, après réinitialisation la connexion réussit avec le nouveau mot de passe et échoue avec l'ancien.
**Validates: Requirements 7.1**
### Property 12 : Un jeton de reset est à usage unique
*Pour tout* jeton de reset utilisé une fois avec succès, toute réutilisation ultérieure du même jeton est rejetée avec un code 400 (jeton invalidé, `UsedAt` renseigné).
**Validates: Requirements 7.2, 7.4**
### Property 13 : Un jeton de reset expiré ou inconnu est rejeté
*Pour tout* jeton de reset dont l'expiration est dépassée, ou pour tout jeton inconnu de la base, la réinitialisation est rejetée avec un code 400 et le mot de passe du compte reste inchangé.
**Validates: Requirements 7.3, 7.4**
### Property 14 : Un nouveau mot de passe trop court est refusé à la réinitialisation
*Pour tout* jeton de reset valide et toute chaîne de longueur strictement inférieure à 8, la réinitialisation est rejetée avec un code 400 et le mot de passe du compte reste inchangé.
**Validates: Requirements 7.5**
## Error Handling
Les erreurs sont renvoyées au format `ProblemDetails` (déjà activé via `AddProblemDetails()` et `UseExceptionHandler()`). `AuthController` traduit les exceptions métier en codes HTTP :
| Situation | Code HTTP | Message | Exigence |
|-----------|-----------|---------|----------|
| Compte déjà existant à l'inscription | **409 Conflict** | « Un compte existe déjà. » | 1.2 |
| Mot de passe < 8 caractères (inscription/reset) | **400 Bad Request** | « Le mot de passe doit comporter au moins 8 caractères. » | 1.3, 7.5 |
| Email syntaxiquement invalide | **400 Bad Request** | « Adresse email invalide. » | 1.4 |
| Identifiants de connexion invalides | **401 Unauthorized** | « Identifiants invalides. » (message **générique**) | 2.2 |
| Route protégée sans/avec JWT invalide ou expiré | **401 Unauthorized** | (réponse standard `JwtBearer`) | 4.2, 4.3, 5.2, 5.3 |
| Jeton de reset expiré | **400 Bad Request** | « Le lien de réinitialisation a expiré. » | 7.3 |
| Jeton de reset inconnu ou déjà utilisé | **400 Bad Request** | « Le lien de réinitialisation est invalide. » | 7.2, 7.4 |
| Demande de réinitialisation (email connu ou non) | **200 OK** | Réponse **générique** de succès | 6.6 |
**Principe de non-divulgation** : `forgot-password` renvoie **toujours 200** avec le même corps, que l'Email corresponde ou non à un compte, afin de ne pas révéler l'existence du compte (Exigence 6.6). De même, le message de connexion invalide est volontairement générique et ne distingue pas « email inconnu » de « mot de passe incorrect » (Exigence 2.2).
## Frontend (Angular zoneless, signals)
### AuthService
`AuthService` (`providedIn: 'root'`, `core/services/auth.service.ts`) expose l'état d'authentification via **signals** (contrainte zoneless) :
```typescript
private readonly _token = signal<string | null>(this.lireTokenStocke());
readonly estAuthentifie = computed(() => this._token() !== null);
private readonly http = inject(HttpClient);
private readonly router = inject(Router);
private readonly baseUrl = `${environment.apiUrl}/auth`; // cohérent avec ProfilService
login(dto: LoginDto): Observable<LoginResult> { /* POST /auth/login -> stocke token */ }
register(dto: RegisterDto): Observable<void> { /* POST /auth/register */ }
forgotPassword(email: string): Observable<void> { /* POST /auth/forgot-password */ }
resetPassword(dto: ResetPasswordDto): Observable<void> { /* POST /auth/reset-password */ }
compteExiste(): Observable<{ existe: boolean }> { /* GET /auth/compte-existe */ }
logout(): void { this.supprimerToken(); this.router.navigate(['/login']); } // Exigence 3
getToken(): string | null { return this._token(); }
```
**Stockage du JWT — `localStorage` vs `sessionStorage`** : on retient **`localStorage`**. Justification : sur un poste personnel mono‑utilisateur, une session persistante de 12 h (fermeture/réouverture d'onglet sans reconnexion) correspond à l'usage attendu ; `sessionStorage` forcerait une reconnexion à chaque nouvel onglet. **Risque XSS** : tout stockage accessible en JavaScript (localStorage/sessionStorage) est exposé si une faille XSS est présente — un attaquant pourrait exfiltrer le jeton. Atténuations retenues : Angular échappe le HTML par défaut, expiration courte du JWT (12 h) limitant la fenêtre, et purge automatique sur 401. Une alternative plus robuste (cookie `HttpOnly` + `SameSite`) est notée mais non retenue ici car elle impose une gestion CSRF et complexifie le reverse‑proxy same‑origin ; elle reste une évolution possible.
### authInterceptor
`authInterceptor` (`HttpInterceptorFn`, `core/interceptors/auth.interceptor.ts`) :
- ajoute l'en‑tête `Authorization: Bearer <token>` aux requêtes vers `environment.apiUrl` lorsque `AuthService.getToken()` est non nul ;
- **n'ajoute pas** l'en‑tête aux endpoints publics d'auth ni aux appels externes (BAN) ;
- sur une réponse **401**, purge le token (`AuthService`) et redirige vers `/login` (Exigence 4.7).
Il s'enregistre dans `app.config.ts` **avant** le `timeoutInterceptor` existant :
```typescript
provideHttpClient(withInterceptors([authInterceptor, timeoutInterceptor]))
```
L'ordre place `authInterceptor` en premier pour injecter le Bearer avant le contrôle de timeout, tout en préservant la logique `ServeurIndisponibleError` existante.
### authGuard
`authGuard` (`CanActivateFn`, `core/guards/auth.guard.ts`) : si `AuthService.estAuthentifie()` est faux, retourne un `UrlTree` vers `/login` ; sinon `true` (Exigences 4.6, 9.3). Elle se compose avec la `profilConfigureGuard` existante : sur les routes de données, on applique `[authGuard, profilConfigureGuard]` (l'authentification est vérifiée d'abord, puis la configuration du profil).
### Écrans standalone
- `LoginComponent` (`/login`) — email + mot de passe ; sur succès, stocke le token puis navigue vers la cible protégée.
- `RegisterComponent` (`/register`) — **Etape_1_Onboarding** ; sur succès redirige vers `/login` (Exigences 1.6, 9.2).
- `ForgotPasswordComponent` (`/forgot-password`) — saisie de l'email, affiche un message générique quel que soit le résultat.
- `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` (Exigence 7.6).
Tous ces écrans exposent leur état async (chargement, erreur) via des **signals**, conformément au mode zoneless.
### Réorganisation de l'onboarding et routes
Le parcours devient : **inscription (Etape 1)** → **connexion** → **saisie profil + premier véhicule (Etape 2)**. L'`OnboardingComponent` existant reste l'écran de l'Etape 2 (comportement inchangé — Exigence 9.5), désormais protégé par `authGuard` puis piloté par la `profilConfigureGuard` existante.
Routes mises à jour (`app.routes.ts`) :
```typescript
export const routes: Routes = [
// Routes PUBLIQUES (aucune garde)
{ path: 'login', component: LoginComponent },
{ path: 'register', component: RegisterComponent },
{ path: 'forgot-password', component: ForgotPasswordComponent },
{ path: 'reset-password', component: ResetPasswordComponent },
// Routes PROTEGEES : authGuard d'abord, puis profilConfigureGuard
{ path: 'onboarding', component: OnboardingComponent, canActivate: [authGuard] }, // Etape 2 (Exigence 9.3, 9.4)
{ path: 'profil', component: ProfilComponent, canActivate: [authGuard] },
{ path: 'vehicules', component: VehiculesComponent, canActivate: [authGuard] },
{ path: 'consultation', component: ConsultationJourComponent, canActivate: [authGuard, profilConfigureGuard] },
{ path: 'carnet-adresses', component: CarnetAdressesComponent, canActivate: [authGuard, profilConfigureGuard] },
{ path: 'resume-mensuel', component: ResumeMensuelComponent, canActivate: [authGuard, profilConfigureGuard] },
{ path: 'resume-annuel', component: ResumeAnnuelComponent, canActivate: [authGuard, profilConfigureGuard] },
{ path: 'bareme', component: BaremeComponent, canActivate: [authGuard, profilConfigureGuard] },
{ path: 'montants-regles', component: MontantsReglesComponent, canActivate: [authGuard, profilConfigureGuard] },
{ path: '', redirectTo: 'consultation', pathMatch: 'full' },
{ path: '**', redirectTo: 'consultation' }
];
```
## Configuration et déploiement
### Variables d'environnement à ajouter
| Variable (env) | Section config | Défaut | Rôle |
|----------------|----------------|--------|------|
| `JWT__SecretKey` | `Jwt:SecretKey` | *(aucun)* | **Secret** de signature HMAC — jamais commité |
| `JWT__Issuer` | `Jwt:Issuer` | `SchotzIadKmTracker` | Émetteur du jeton |
| `JWT__Audience` | `Jwt:Audience` | `SchotzIadKmTracker` | Audience du jeton |
| `JWT__ExpirationMinutes` | `Jwt:ExpirationMinutes` | `720` | Durée de vie du JWT (12 h) — Exigence 5.5 |
| `Auth__ResetTokenLifetimeMinutes` | `Auth:ResetTokenLifetimeMinutes` | `60` | Durée de vie du jeton de reset — Exigence 6.3 |
| `Auth__FrontendBaseUrl` | `Auth:FrontendBaseUrl` | `http://localhost:8080` | Base des liens de reset |
| `Smtp__Host` | `Smtp:Host` | *(vide)* | Hôte SMTP ; **vide ⇒ repli journalisation** |
| `Smtp__Port` | `Smtp:Port` | `587` | Port SMTP |
| `Smtp__User` / `Smtp__Password` | `Smtp:User/Password` | *(vide)* | Identifiants SMTP (**secrets**) |
| `Smtp__From` | `Smtp:From` | *(vide)* | Adresse expéditrice |
`JWT__SecretKey`, `Smtp__Password` (et `Smtp__User`) sont des **secrets** : ils vont dans `.env` (déjà gitignoré) et jamais dans `.env.example` avec une valeur réelle. En dev local, `JWT__SecretKey` peut aussi passer par les **User Secrets** (`UserSecretsId` déjà présent).
### `.env.example` (ajouts documentés)
```dotenv
# --- Authentification ---
# Secret de signature JWT (OBLIGATOIRE en production). Générez une valeur aléatoire longue (>= 32 octets).
# Ne mettez JAMAIS le secret réel ici ; renseignez-le dans .env (non commité).
JWT_SECRET_KEY=change-me-generate-a-long-random-secret
JWT_EXPIRATION_MINUTES=720
AUTH_RESET_TOKEN_LIFETIME_MINUTES=60
AUTH_FRONTEND_BASE_URL=http://localhost:8080
# --- SMTP (facultatif) : si SMTP_HOST est vide, le lien de reset est journalisé côté serveur ---
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=
```
### `docker-compose.yml` (ajouts au service `backend`)
```yaml
environment:
- ASPNETCORE_URLS=http://+:8080
- ConnectionStrings__Default=Data Source=/data/trajets.db
- ORS__ApiKey=${ORS_API_KEY:-}
# --- Authentification ---
- JWT__SecretKey=${JWT_SECRET_KEY:?JWT_SECRET_KEY requis}
- JWT__ExpirationMinutes=${JWT_EXPIRATION_MINUTES:-720}
- Auth__ResetTokenLifetimeMinutes=${AUTH_RESET_TOKEN_LIFETIME_MINUTES:-60}
- Auth__FrontendBaseUrl=${AUTH_FRONTEND_BASE_URL:-http://localhost:8080}
- Smtp__Host=${SMTP_HOST:-}
- Smtp__Port=${SMTP_PORT:-587}
- Smtp__User=${SMTP_USER:-}
- Smtp__Password=${SMTP_PASSWORD:-}
- Smtp__From=${SMTP_FROM:-}
```
La syntaxe `:?` sur `JWT_SECRET_KEY` fait échouer le démarrage si le secret est absent, évitant un déploiement avec une clé vide.
## Diagrammes
### Séquence — Inscription puis connexion
```mermaid
sequenceDiagram
participant U as Utilisateur
participant FE as Frontend (Register/Login)
participant AC as AuthController
participant SA as Service_Auth
participant DB as AppDbContext
U->>FE: Saisit email + mot de passe (inscription)
FE->>AC: POST /api/auth/register
AC->>SA: RegisterAsync(dto)
alt Aucun compte n'existe
SA->>SA: PasswordHasher.HashPassword(mdp)
SA->>DB: Insert Compte (Email, MotDePasseHash)
SA-->>AC: OK
AC-->>FE: 200
FE->>U: Redirige vers /login (Exig. 1.6)
else Compte déjà existant
SA-->>AC: CompteDejaExistant
AC-->>FE: 409 (Exig. 1.2)
end
U->>FE: Saisit email + mot de passe (connexion)
FE->>AC: POST /api/auth/login
AC->>SA: LoginAsync(dto)
SA->>DB: Lecture Compte par Email
alt Identifiants valides
SA->>SA: PasswordHasher.VerifyHashedPassword
SA->>SA: JwtService.GenererJeton(compte)
SA-->>AC: token + expiration
AC-->>FE: 200 { token }
FE->>FE: localStorage.setItem(token) (Exig. 2.3)
else Invalides
SA-->>AC: IdentifiantsInvalides
AC-->>FE: 401 message générique (Exig. 2.2)
end
```
### Séquence — Réinitialisation avec repli journalisation
```mermaid
sequenceDiagram
participant U as Utilisateur
participant FE as Frontend
participant AC as AuthController
participant SA as Service_Auth
participant DB as AppDbContext
participant ES as IEmailService
U->>FE: Demande réinitialisation (email)
FE->>AC: POST /api/auth/forgot-password
AC->>SA: ForgotPasswordAsync(email)
alt Email correspond au compte
SA->>SA: Génère token (usage unique) + hash
SA->>DB: Insert ResetPasswordToken (Expiration +60min)
SA->>SA: Construit lien {FrontendBaseUrl}/reset-password?token=...
alt SMTP configuré
SA->>ES: SmtpEmailService.EnvoyerLienResetAsync (Exig. 6.4)
else SMTP absent
SA->>ES: LoggingEmailService -> ILogger journalise le lien (Exig. 6.5)
end
else Email inconnu
SA->>SA: Ne crée rien
end
SA-->>AC: Succès générique
AC-->>FE: 200 identique dans tous les cas (Exig. 6.6)
U->>FE: Ouvre le lien, saisit nouveau mot de passe
FE->>AC: POST /api/auth/reset-password { token, nouveauMdp }
AC->>SA: ResetPasswordAsync(dto)
SA->>DB: Recherche token (hash), non expiré, non utilisé
alt Valide
SA->>SA: Hash nouveau mdp -> maj Compte
SA->>DB: UsedAt = now (Exig. 7.2)
AC-->>FE: 200 -> redirige /login (Exig. 7.6)
else Expiré / invalide / mdp court
AC-->>FE: 400 (Exig. 7.3, 7.4, 7.5)
end
```
### Flux — Garde de route frontend
```mermaid
flowchart TD
A[Navigation vers une route protégée] --> B{authGuard : token présent ?}
B -- Non --> C[UrlTree /login]
B -- Oui --> D{Route de données ?}
D -- Non ex. /profil, /onboarding --> E[Accès autorisé]
D -- Oui --> F{profilConfigureGuard : profil configuré ?}
F -- Non --> G[UrlTree /onboarding]
F -- Oui --> H[Accès autorisé]
```
### Composants — Backend
```mermaid
graph LR
AC[AuthController] --> SA[Service_Auth]
SA --> PH[PasswordHasher&lt;Compte&gt;]
SA --> JS[JwtService]
SA --> ES[IEmailService]
SA --> DB[(AppDbContext)]
ES --> SM[SmtpEmailService MailKit]
ES --> LG[LoggingEmailService ILogger]
```
## Testing Strategy
Le projet de tests existant `SchotzIadKmTracker.Tests` utilise **xUnit**, **FluentAssertions**, **FsCheck.Xunit**, **Moq** et **`Microsoft.AspNetCore.Mvc.Testing`** (`WebApplicationFactory<Program>`). La stratégie réutilise ces outils, sans dépendance nouvelle.
**PBT applicable ici** : le hachage, l'émission/validation JWT et le cycle du jeton de reset sont des logiques à entrées/sorties claires, avec de nombreuses propriétés universelles (round-trip, expiration, usage unique). On applique donc une approche duale.
### Tests de propriétés (FsCheck, ≥ 100 itérations)
Chaque test référence sa propriété de conception via un commentaire au format :
`// Feature: authentification, Property {n}: {texte}`. Chaque propriété est couverte par **un seul** test de propriété.
| Propriété | Test |
|-----------|------|
| P1 Hachage | Pour tout mdp : `hash != mdp`, `Verify=Success`, deux hachages diffèrent |
| P2 Round-trip JWT | Pour tout `ExpirationMinutes` > 0 : validation OK, claims `sub`/`email` préservés, `exp = iat + minutes` |
| P3 JWT expiré | Pour toute durée négative : validation échoue |
| P4 Signature JWT | Pour toute clé alternative / token altéré : validation échoue |
| P5 Inscription crée | Base vide + paire valide ⇒ 1 compte avec l'email |
| P6 Doublon 409 | Compte existant + 2e inscription ⇒ 409, toujours 1 compte |
| P7 Mdp court 400 | Longueur 0..7 ⇒ 400, aucun compte |
| P8 Login | Bons identifiants ⇒ token validable ; mauvais ⇒ 401 sans token |
| P9 Non-divulgation | Email connu vs inconnus ⇒ réponse identique, aucun token pour inconnu |
| P10 Expiration reset | Pour toute durée > 0 : `Expiration = CreatedAt + durée`, `UsedAt` nul |
| P11 Reset applique mdp | Reset valide ⇒ login OK avec nouveau mdp, KO avec ancien |
| P12 Usage unique | 2e usage du même token ⇒ 400, `UsedAt` renseigné |
| P13 Reset expiré/inconnu | Token expiré ou inconnu ⇒ 400, mdp inchangé |
| P14 Reset mdp court | Token valide + mdp < 8 ⇒ 400, mdp inchangé |
Les tests logiques (P1–P4) portent sur `IJwtService` / `PasswordHasher` en isolation. Les tests de flux (P5–P14) s'exécutent sur `IAuthService` avec un `AppDbContext` SQLite **en mémoire** (`Data Source=:memory:`) pour rester rapides et déterministes ; `IEmailService` est **mocké** (Moq).
### Tests unitaires et d'exemples
- **Validation d'entrée** (FluentValidation) : emails invalides ⇒ 400 (Exigence 1.4, edge-cases : `"a@"`, `"abc"`, `"@x"`, chaîne vide) ; email valide accepté.
- **Exclusion du hash** : les corps de réponse d'auth ne contiennent aucun champ de hash (Exigence 8.2).
- **Sélection DI de `IEmailService`** : `Smtp__Host` vide ⇒ `LoggingEmailService` ; renseigné ⇒ `SmtpEmailService` (Exigences 6.4, 6.5).
- **Frontend** (tests de service/garde/composant) : `AuthService` stocke/supprime le token dans `localStorage` (2.3, 3.1), `authGuard` redirige un non‑authentifié vers `/login` (4.6, 9.3), `authInterceptor` purge le token et redirige sur 401 (4.7), `ResetPasswordComponent` lit le token depuis l'URL.
### Tests d'intégration (`WebApplicationFactory<Program>`, 1–3 exemples)
- **Protection `[Authorize]`** (Exigence 4.4/4.5) : sur un échantillon de routes de données (`/api/trajets`, `/api/profil`, `/api/vehicules`, `/api/bareme`, `/api/resumes`, exports PDF, recalcul), un appel **sans** jeton renvoie **401** ; avec un jeton valide, renvoie 2xx (Exigence 4.1). Les routes `/api/auth/*` et `/health` restent accessibles sans jeton.
- **Bout en bout** : register → login → appel protégé avec le token obtenu ; puis forgot → reset → login avec le nouveau mot de passe.
### Configuration / smoke
- Liaison des options `SmtpOptions`, `JwtOptions`, `AuthOptions` depuis la configuration (Exigence 6.7) — vérification de démarrage.
---
*Cette conception couvre l'ensemble des exigences 1 à 9 du document de requirements. Si des lacunes apparaissent (par exemple sur le comportement précis attendu d'un écran ou une variable de configuration additionnelle), il est proposé de revenir à la phase de clarification des exigences avant de passer aux tâches.*
@@ -0,0 +1,139 @@
# Requirements Document
## Introduction
Cette fonctionnalité ajoute une couche d'authentification à l'application mono-utilisateur SchotzIAD KM Tracker. Elle introduit un compte unique (adresse email + mot de passe haché), une session par jeton JWT, la protection de toutes les routes de données existantes, un mécanisme de réinitialisation de mot de passe par email (avec repli de journalisation en l'absence de configuration SMTP), et réorganise l'onboarding en deux étapes : d'abord l'inscription et la connexion, puis la saisie du profil et du premier véhicule (comportement actuel inchangé).
L'application étant mono-utilisateur, la création de compte n'est autorisée qu'une seule fois : lorsqu'aucun compte n'existe encore. Une fois le compte créé, seule la connexion reste possible.
## Glossary
- **Systeme** : L'application SchotzIAD KM Tracker dans son ensemble (frontend Angular + backend ASP.NET Core).
- **Service_Auth** : Le composant backend responsable de l'inscription, de la connexion, de la déconnexion et de la réinitialisation de mot de passe.
- **API** : L'ensemble des points d'accès HTTP exposés par le backend ASP.NET Core.
- **Compte** : L'unique enregistrement utilisateur contenant l'adresse email (identifiant) et le mot de passe haché.
- **Email** : L'adresse électronique servant d'identifiant de connexion au Compte.
- **Mot_De_Passe** : Le secret d'authentification saisi par l'utilisateur, d'une longueur minimale de 8 caractères.
- **Jeton_JWT** : Le jeton JSON Web Token émis par le Service_Auth à la connexion, transmis par le navigateur pour authentifier les appels aux routes protégées.
- **Jeton_Reset** : Le jeton à usage unique et à durée de vie limitée émis lors d'une demande de réinitialisation de mot de passe.
- **Route_Protegee** : Toute route ou point d'API accessible uniquement à un utilisateur authentifié (trajets, profil, véhicules, carnet d'adresses, barème, résumés, montants, exports PDF, recalcul de distances).
- **Route_Publique** : Toute route ou point d'API accessible sans authentification (inscription lorsqu'aucun Compte n'existe, connexion, demande de réinitialisation, réinitialisation via Jeton_Reset).
- **Onboarding** : Le parcours de première utilisation, désormais organisé en deux étapes.
- **Etape_1_Onboarding** : La première étape de l'Onboarding : inscription (Email + Mot_De_Passe) puis redirection vers la page de connexion.
- **Etape_2_Onboarding** : La seconde étape de l'Onboarding, accessible uniquement après connexion : saisie du profil (nom, prénom, matricule, adresse de domicile) et du premier véhicule.
- **Garde_Profil** : La garde de route existante `profilConfigureGuard` qui vérifie l'état de configuration du profil via `GET /api/profil/etat`.
- **Garde_Auth** : La garde de route frontend qui empêche l'accès aux pages protégées tant que l'utilisateur n'est pas authentifié.
- **Service_SMTP** : Le serveur d'envoi de courriels configuré par variables d'environnement (hôte, port, identifiants, expéditeur).
- **Duree_Validite_Jeton** : La durée de vie configurée du Jeton_JWT, valant 12 heures (720 minutes) par défaut et configurable par variable d'environnement (par exemple `Jwt:ExpirationMinutes` / `JWT__ExpirationMinutes`).
- **Duree_Validite_Reset** : La durée de vie configurée du Jeton_Reset (durée limitée après laquelle le jeton expire), valant 1 heure (60 minutes) par défaut et configurable par variable d'environnement (par exemple `Auth:ResetTokenLifetimeMinutes`).
## Requirements
### Requirement 1 : Inscription du compte unique
**User Story:** En tant qu'utilisateur de première utilisation, je veux créer un compte avec mon adresse email et un mot de passe, afin de sécuriser l'accès à mon suivi kilométrique.
#### Acceptance Criteria
1. WHEN une inscription est demandée ET qu'aucun Compte n'existe, THE Service_Auth SHALL créer le Compte avec l'Email fourni et le Mot_De_Passe fourni.
2. IF une inscription est demandée ALORS qu'un Compte existe déjà, THEN THE Service_Auth SHALL rejeter la demande avec un code d'erreur HTTP 409 et un message indiquant qu'un compte existe déjà.
3. IF le Mot_De_Passe fourni comporte moins de 8 caractères, THEN THE Service_Auth SHALL rejeter la demande avec un code d'erreur HTTP 400 et un message indiquant la longueur minimale requise.
4. IF l'Email fourni n'est pas une adresse électronique valide sur le plan syntaxique, THEN THE Service_Auth SHALL rejeter la demande avec un code d'erreur HTTP 400.
5. WHEN le Service_Auth crée le Compte, THE Service_Auth SHALL stocker le Mot_De_Passe sous forme hachée.
6. WHEN le Compte est créé avec succès, THE Systeme SHALL rediriger l'utilisateur vers la page de connexion.
### Requirement 2 : Connexion
**User Story:** En tant qu'utilisateur disposant d'un compte, je veux me connecter avec mon email et mon mot de passe, afin d'accéder à mes données.
#### Acceptance Criteria
1. WHEN une connexion est demandée avec un Email et un Mot_De_Passe correspondant au Compte, THE Service_Auth SHALL émettre un Jeton_JWT et le retourner à l'utilisateur.
2. IF une connexion est demandée avec un Email inconnu ou un Mot_De_Passe incorrect, THEN THE Service_Auth SHALL rejeter la demande avec un code d'erreur HTTP 401 et un message générique d'identifiants invalides.
3. WHEN un Jeton_JWT est émis, THE Systeme SHALL stocker le Jeton_JWT côté navigateur.
4. WHEN un Jeton_JWT est émis, THE Service_Auth SHALL associer au Jeton_JWT une date d'expiration.
### Requirement 3 : Déconnexion
**User Story:** En tant qu'utilisateur connecté, je veux me déconnecter, afin que ma session ne reste pas active sur l'appareil.
#### Acceptance Criteria
1. WHEN l'utilisateur demande la déconnexion, THE Systeme SHALL supprimer le Jeton_JWT stocké côté navigateur.
2. WHEN le Jeton_JWT a été supprimé, THE Systeme SHALL rediriger l'utilisateur vers la page de connexion.
### Requirement 4 : Protection des routes et de l'API
**User Story:** En tant que propriétaire des données, je veux que toutes mes données soient inaccessibles sans authentification, afin de préserver leur confidentialité.
#### Acceptance Criteria
1. WHEN un appel est adressé à une Route_Protegee avec un Jeton_JWT valide et non expiré, THE API SHALL traiter la requête normalement.
2. IF un appel est adressé à une Route_Protegee sans Jeton_JWT ou avec un Jeton_JWT invalide, THEN THE API SHALL rejeter la requête avec un code d'erreur HTTP 401.
3. IF un appel est adressé à une Route_Protegee avec un Jeton_JWT expiré, THEN THE API SHALL rejeter la requête avec un code d'erreur HTTP 401.
4. THE API SHALL classer comme Route_Protegee les points d'accès de trajets, profil, véhicules, carnet d'adresses, barème, résumés, montants réglés, exports PDF et recalcul de distances.
5. THE API SHALL classer comme Route_Publique les points d'accès d'inscription, de connexion, de demande de réinitialisation et de réinitialisation via Jeton_Reset.
6. WHEN un utilisateur non authentifié tente d'accéder à une page frontend protégée, THE Garde_Auth SHALL rediriger l'utilisateur vers la page de connexion.
7. WHEN l'API rejette un appel frontend avec un code HTTP 401, THE Systeme SHALL supprimer le Jeton_JWT stocké et rediriger l'utilisateur vers la page de connexion.
### Requirement 5 : Expiration et validité du jeton
**User Story:** En tant que propriétaire des données, je veux que ma session expire après une durée définie, afin de limiter la fenêtre d'accès en cas de compromission de l'appareil.
#### Acceptance Criteria
1. WHILE le Jeton_JWT n'a pas atteint sa date d'expiration, THE API SHALL considérer le Jeton_JWT comme valide.
2. WHEN le Jeton_JWT atteint sa date d'expiration, THE API SHALL considérer le Jeton_JWT comme invalide.
3. IF un Jeton_JWT présente une signature non valide, THEN THE API SHALL considérer le Jeton_JWT comme invalide.
4. WHEN un Jeton_JWT est émis, THE Service_Auth SHALL fixer sa date d'expiration à 12 heures après l'émission par défaut.
5. THE Systeme SHALL permettre de configurer la Duree_Validite_Jeton via une variable d'environnement (par exemple `JWT__ExpirationMinutes`), avec une valeur par défaut de 720 minutes.
### Requirement 6 : Demande de réinitialisation de mot de passe
**User Story:** En tant qu'utilisateur ayant oublié mon mot de passe, je veux demander un lien de réinitialisation par email, afin de retrouver l'accès à mon compte.
#### Acceptance Criteria
1. WHEN une demande de réinitialisation est reçue pour un Email correspondant au Compte, THE Service_Auth SHALL générer un Jeton_Reset à usage unique assorti d'une Duree_Validite_Reset.
2. WHEN un Jeton_Reset est généré, THE Service_Auth SHALL fixer l'expiration du Jeton_Reset à 1 heure après sa génération par défaut.
3. THE Systeme SHALL permettre de configurer la Duree_Validite_Reset via une variable d'environnement (par exemple `Auth__ResetTokenLifetimeMinutes`), avec une valeur par défaut de 60 minutes.
4. WHEN un Jeton_Reset est généré ET que le Service_SMTP est configuré, THE Service_Auth SHALL envoyer par email un lien de réinitialisation contenant le Jeton_Reset.
5. IF aucun Service_SMTP n'est configuré, THEN THE Service_Auth SHALL journaliser côté serveur le lien de réinitialisation contenant le Jeton_Reset.
6. WHEN une demande de réinitialisation est reçue pour un Email inconnu, THE Service_Auth SHALL retourner la même réponse de succès générique que pour un Email connu, sans indiquer si l'Email correspond à un Compte.
7. THE Service_SMTP SHALL être configuré par les variables d'environnement d'hôte, de port, d'identifiants et d'expéditeur.
### Requirement 7 : Réinitialisation du mot de passe via jeton
**User Story:** En tant qu'utilisateur ayant reçu un lien de réinitialisation, je veux définir un nouveau mot de passe via ce lien, afin de restaurer l'accès à mon compte.
#### Acceptance Criteria
1. WHEN une réinitialisation est soumise avec un Jeton_Reset valide, non expiré et non utilisé, ET un nouveau Mot_De_Passe d'au moins 8 caractères, THE Service_Auth SHALL remplacer le Mot_De_Passe du Compte par la version hachée du nouveau Mot_De_Passe.
2. WHEN un Jeton_Reset est utilisé avec succès, THE Service_Auth SHALL invalider le Jeton_Reset pour tout usage ultérieur.
3. IF une réinitialisation est soumise avec un Jeton_Reset expiré, THEN THE Service_Auth SHALL rejeter la demande avec un code d'erreur HTTP 400 et un message indiquant que le lien a expiré.
4. IF une réinitialisation est soumise avec un Jeton_Reset inconnu ou déjà utilisé, THEN THE Service_Auth SHALL rejeter la demande avec un code d'erreur HTTP 400 et un message indiquant que le lien est invalide.
5. IF une réinitialisation est soumise avec un nouveau Mot_De_Passe de moins de 8 caractères, THEN THE Service_Auth SHALL rejeter la demande avec un code d'erreur HTTP 400 et un message indiquant la longueur minimale requise.
6. WHEN la réinitialisation aboutit, THE Systeme SHALL rediriger l'utilisateur vers la page de connexion.
### Requirement 8 : Sécurité des mots de passe
**User Story:** En tant que propriétaire des données, je veux que mon mot de passe ne soit jamais stocké ou exposé en clair, afin de me protéger en cas de fuite de données.
#### Acceptance Criteria
1. THE Service_Auth SHALL stocker le Mot_De_Passe uniquement sous forme hachée avec un algorithme de hachage de mot de passe à sel.
2. THE Service_Auth SHALL exclure le Mot_De_Passe haché de toute réponse d'API.
3. WHEN le Service_Auth vérifie un Mot_De_Passe lors de la connexion, THE Service_Auth SHALL comparer la valeur saisie à la version hachée stockée sans reconstituer le Mot_De_Passe en clair.
### Requirement 9 : Onboarding en deux étapes
**User Story:** En tant qu'utilisateur de première utilisation, je veux créer mon compte puis me connecter avant de renseigner mon profil, afin que mes données de profil soient protégées dès le départ.
#### Acceptance Criteria
1. WHEN l'utilisateur démarre l'Onboarding ET qu'aucun Compte n'existe, THE Systeme SHALL présenter l'Etape_1_Onboarding d'inscription.
2. WHEN l'Etape_1_Onboarding d'inscription aboutit, THE Systeme SHALL rediriger l'utilisateur vers la page de connexion.
3. WHILE l'utilisateur n'est pas authentifié, THE Systeme SHALL empêcher l'accès à l'Etape_2_Onboarding.
4. WHEN l'utilisateur est authentifié ET que le profil n'est pas encore configuré, THE Garde_Profil SHALL autoriser l'accès à l'Etape_2_Onboarding de saisie du profil et du premier véhicule.
5. THE Etape_2_Onboarding SHALL conserver le comportement actuel de saisie du profil (nom, prénom, matricule, adresse de domicile) et du premier véhicule.
6. WHEN l'utilisateur authentifié a configuré son profil, THE Garde_Profil SHALL autoriser l'accès aux Routes_Protegees applicatives.
+250
View File
@@ -0,0 +1,250 @@
# 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<Compte>` et `DbSet<ResetPasswordToken>` 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<Compte>` en DI
- Enregistrer `IPasswordHasher<Compte>` -> `PasswordHasher<Compte>` 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<Program>` : 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"] }
]
}
```
@@ -0,0 +1,654 @@
{
"pbtResults": {},
"executionHistory": {
"1.1 Créer les entités `Compte` et `ResetPasswordToken`": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367186782
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367202818
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367320120
}
],
"1.2 Déclarer les DbSet et la configuration `OnModelCreating`": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367186971
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367333400
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367482585
}
],
"1.3 Générer la migration EF `AjoutAuthentification`": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367187066
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789368256169
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789368286823
}
],
"2.1 Créer les classes d'options et les lier à la configuration": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367187157
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367204303
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367321104
}
],
"3.1 Intégrer `PasswordHasher<Compte>` en DI": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367187246
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367496182
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367655104
}
],
"4.1 Implémenter `IJwtService` / `JwtService`": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367187341
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367677548
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367800474
}
],
"5.1 Implémenter `IEmailService` et ses deux implémentations": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367187432
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367334259
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367483557
}
],
"5.2 Câbler la sélection DI selon `Smtp__Host`": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367187526
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367817812
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367870446
}
],
"6.1 Créer les DTOs d'authentification et les validateurs FluentValidation": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367187612
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367678436
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367801818
}
],
"6.2 Implémenter `IAuthService` / `AuthService`": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367187704
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367871329
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789368076060
}
],
"7.1 Implémenter `AuthController` avec les 5 endpoints": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367187799
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789368077733
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789368159447
}
],
"8.1 Configurer l'authentification JWT et l'autorisation globale": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367187890
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789368160204
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789368254979
}
],
"9. Checkpoint backend": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367188045
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789369554684
}
],
"10.1 Créer les modèles TS et `AuthService` (signals + localStorage)": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367188156
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367205129
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367322447
}
],
"10.2 Implémenter `authInterceptor` et l'enregistrer avant `timeoutInterceptor`": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367188248
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367335669
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367484979
}
],
"10.3 Implémenter `authGuard`": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367188340
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367337185
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367486334
}
],
"11.1 Implémenter `LoginComponent` (`/login`)": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367188465
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367337987
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367487169
}
],
"11.2 Implémenter `RegisterComponent` (`/register`, Etape 1)": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367188550
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367498335
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367651865
}
],
"11.3 Implémenter `ForgotPasswordComponent` (`/forgot-password`)": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367188651
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367498477
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367652784
}
],
"11.4 Implémenter `ResetPasswordComponent` (`/reset-password`)": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367188757
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367499473
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367654113
}
],
"12.1 Mettre à jour `app.routes.ts`": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367188847
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367679881
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367802793
}
],
"13.1 Mettre à jour `.env.example` et `docker-compose.yml`": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367188965
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367680868
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367803900
}
],
"13.2 Vérifier les builds": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367189105
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789368287520
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789368322862
}
],
"14. Checkpoint final": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367189198
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789369555493
}
],
"2. Backend — Options de configuration": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367321242
}
],
"10. Frontend — Service, modèles, interceptor et garde": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367486494
}
],
"11. Frontend — Écrans standalone (signals)": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367654216
}
],
"3. Backend — Hachage des mots de passe": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367655230
}
],
"4. Backend — Service JWT": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367800614
}
],
"12. Frontend — Routes et onboarding en deux étapes": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367802947
}
],
"5. Backend — Service email": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789367870646
}
],
"6. Backend — DTOs, validateurs et service d'authentification": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789368076248
}
],
"7. Backend — Controller d'authentification": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789368159632
}
],
"8. Backend — Sécurité du pipeline": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789368255118
}
],
"1. Backend — Modèle de données et persistance": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789368286988
}
],
"13. Configuration et déploiement": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789368322956
}
],
"3.2 Écrire le test de propriété du hachage": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789368508497
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789369503520
}
],
"6.3 Écrire le test de propriété inscription crée le compte": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789368509798
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789369504375
}
],
"8.2 Écrire le test d'intégration de protection des routes": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789368511227
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789369505830
}
],
"10.4 Écrire les tests de service, interceptor et garde": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789368512087
},
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789369506703
}
],
"2.2 Écrire un test de liaison des options": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789369516660
}
],
"4.2 Écrire le test de propriété émission/validation et claims": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789369517664
}
],
"4.3 Écrire le test de propriété jeton expiré": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789369519001
}
],
"4.4 Écrire le test de propriété signature invalide": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789369519964
}
],
"5.3 Écrire le test de sélection DI de `IEmailService`": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789369521121
}
],
"6.4 Écrire le test de propriété inscription rejetée si compte existant": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789369525805
}
],
"6.5 Écrire le test de propriété mot de passe trop court à l'inscription": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789369527195
}
],
"6.6 Écrire le test de propriété connexion (émission/rejet)": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789369528183
}
],
"6.7 Écrire le test de propriété non-divulgation forgot-password": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789369529620
}
],
"6.8 Écrire le test de propriété expiration du jeton de reset": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789369530512
}
],
"6.9 Écrire le test de propriété réinitialisation valide remplace le mot de passe": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789369535219
}
],
"6.10 Écrire le test de propriété jeton de reset à usage unique": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789369536677
}
],
"6.11 Écrire le test de propriété jeton de reset expiré ou inconnu": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789369537543
}
],
"6.12 Écrire le test de propriété nouveau mot de passe trop court à la réinitialisation": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789369538945
}
],
"6.13 Écrire les tests d'exemples de validation et d'exclusion du hash": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789369540419
}
],
"8.3 Écrire le test d'intégration bout en bout": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789369541873
}
],
"11.5 Écrire le test de lecture du token par `ResetPasswordComponent`": [
{
"executionId": "dc2d2428-de30-4d42-8076-ba61cb79b626",
"chatSessionId": "sess_20c9bee5-b332-43f8-922c-20b9225bb8d8",
"timestamp": 1789369543071
}
]
}
}