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).
134 lines
5.4 KiB
C#
134 lines
5.4 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using SchotzIadKmTracker.Api.Models.Entities;
|
|
|
|
namespace SchotzIadKmTracker.Api.Data;
|
|
|
|
/// <summary>
|
|
/// Contexte EF Core de l'application. Déclare les <see cref="DbSet{TEntity}"/>, les contraintes CHECK,
|
|
/// les index et les index uniques conformément à la section « Schéma SQLite » de la conception.
|
|
/// </summary>
|
|
public class AppDbContext : DbContext
|
|
{
|
|
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
|
|
{
|
|
}
|
|
|
|
public DbSet<Trajet> Trajets => Set<Trajet>();
|
|
public DbSet<Vehicule> Vehicules => Set<Vehicule>();
|
|
public DbSet<Profil> Profil => Set<Profil>();
|
|
public DbSet<AdresseCache> AdresseCache => Set<AdresseCache>();
|
|
public DbSet<AdresseNommee> AdressesNommees => Set<AdresseNommee>();
|
|
public DbSet<TrancheBareme> TranchesBareme => Set<TrancheBareme>();
|
|
public DbSet<MontantRegle> MontantsRegles => Set<MontantRegle>();
|
|
public DbSet<Compte> Comptes => Set<Compte>();
|
|
public DbSet<ResetPasswordToken> ResetPasswordTokens => Set<ResetPasswordToken>();
|
|
|
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
{
|
|
base.OnModelCreating(modelBuilder);
|
|
|
|
// --- Trajets ---
|
|
modelBuilder.Entity<Trajet>(entity =>
|
|
{
|
|
entity.ToTable("Trajets", t =>
|
|
{
|
|
// Distance nulle autorisée (Exigence 2.4 : distance non calculable) ; sinon strictement positive.
|
|
t.HasCheckConstraint("CK_Trajets_Distance", "\"Distance\" IS NULL OR \"Distance\" > 0");
|
|
t.HasCheckConstraint("CK_Trajets_PuissanceFiscale", "\"PuissanceFiscale\" > 0");
|
|
});
|
|
|
|
entity.HasIndex(t => t.Date, "IX_Trajets_Date");
|
|
entity.HasIndex(t => new { t.Date, t.Position }, "IX_Trajets_Date_Position");
|
|
entity.HasIndex(t => t.VehiculeId, "IX_Trajets_Vehicule");
|
|
|
|
entity.HasOne(t => t.Vehicule)
|
|
.WithMany(v => v.Trajets)
|
|
.HasForeignKey(t => t.VehiculeId)
|
|
.OnDelete(DeleteBehavior.Restrict);
|
|
});
|
|
|
|
// --- Vehicules ---
|
|
modelBuilder.Entity<Vehicule>(entity =>
|
|
{
|
|
entity.ToTable("Vehicules", t =>
|
|
t.HasCheckConstraint("CK_Vehicules_PuissanceFiscale", "\"PuissanceFiscale\" > 0"));
|
|
|
|
entity.HasIndex(v => v.DateEffet, "IX_Vehicules_DateEffet");
|
|
});
|
|
|
|
// --- Profil (singleton Id = 1) ---
|
|
modelBuilder.Entity<Profil>(entity =>
|
|
{
|
|
entity.ToTable("Profil");
|
|
entity.HasKey(p => p.Id);
|
|
entity.Property(p => p.Id).ValueGeneratedNever();
|
|
entity.HasIndex(p => p.Id, "UX_Profil_Singleton").IsUnique();
|
|
});
|
|
|
|
// --- AdresseCache (Adresse unique) ---
|
|
modelBuilder.Entity<AdresseCache>(entity =>
|
|
{
|
|
entity.ToTable("AdresseCache");
|
|
entity.HasIndex(a => a.Adresse).IsUnique();
|
|
});
|
|
|
|
// --- AdressesNommees (unicité sur DenominationNormalisee) ---
|
|
modelBuilder.Entity<AdresseNommee>(entity =>
|
|
{
|
|
entity.ToTable("AdressesNommees");
|
|
entity.HasIndex(a => a.DenominationNormalisee, "UX_AdresseNommee_Denomination").IsUnique();
|
|
});
|
|
|
|
// --- TranchesBareme ---
|
|
modelBuilder.Entity<TrancheBareme>(entity =>
|
|
{
|
|
// Convention [KmMin, KmMax[ : borne max EXCLUSIVE, ou NULL = illimité.
|
|
// La contrainte tolère KmMax NULL et impose sinon KmMin < KmMax.
|
|
// Convention [KmMin, KmMax[ : borne max EXCLUSIVE, ou NULL = illimité.
|
|
// Les décimaux sont stockés en TEXT par SQLite : on CAST en REAL pour comparer
|
|
// numériquement (sinon "5000" > "20000" en comparaison lexicographique de chaînes).
|
|
entity.ToTable("TranchesBareme", t =>
|
|
t.HasCheckConstraint(
|
|
"CK_TranchesBareme_Km",
|
|
"\"KmMax\" IS NULL OR CAST(\"KmMin\" AS REAL) < CAST(\"KmMax\" AS REAL)"));
|
|
|
|
entity.HasIndex(t => new { t.Annee, t.PuissanceFiscale }, "IX_TranchesBareme_Annee_PF");
|
|
});
|
|
|
|
// --- MontantsRegles ---
|
|
modelBuilder.Entity<MontantRegle>(entity =>
|
|
{
|
|
entity.ToTable("MontantsRegles", t =>
|
|
{
|
|
t.HasCheckConstraint("CK_MontantsRegles_Montant", "\"Montant\" >= 0");
|
|
t.HasCheckConstraint("CK_MontantsRegles_Mois", "\"Mois\" BETWEEN 1 AND 12");
|
|
});
|
|
|
|
entity.HasIndex(m => new { m.Annee, m.Mois, m.VehiculeId }, "UX_MontantsRegles_Periode_Vehicule")
|
|
.IsUnique();
|
|
|
|
entity.HasOne(m => m.Vehicule)
|
|
.WithMany()
|
|
.HasForeignKey(m => m.VehiculeId)
|
|
.OnDelete(DeleteBehavior.Restrict);
|
|
});
|
|
|
|
// --- Comptes (singleton Id = 1) ---
|
|
modelBuilder.Entity<Compte>(entity =>
|
|
{
|
|
entity.ToTable("Comptes");
|
|
entity.HasKey(c => c.Id);
|
|
entity.Property(c => c.Id).ValueGeneratedNever();
|
|
entity.HasIndex(c => c.Email, "UX_Comptes_Email").IsUnique();
|
|
});
|
|
|
|
// --- ResetPasswordTokens (jetons de réinitialisation à usage unique) ---
|
|
modelBuilder.Entity<ResetPasswordToken>(entity =>
|
|
{
|
|
entity.ToTable("ResetPasswordTokens");
|
|
entity.HasIndex(t => t.TokenHash, "UX_ResetTokens_TokenHash").IsUnique();
|
|
entity.HasIndex(t => t.Expiration, "IX_ResetTokens_Expiration");
|
|
});
|
|
}
|
|
}
|