- Retire app.UseHttpsRedirection() : le conteneur écoute en HTTP, la terminaison TLS est assurée par le reverse-proxy (Caddy) placé devant. Évite les redirections parasites derrière le proxy. - Ajoute Caddyfile.example (Caddy devant le compose existant : TLS auto + reverse_proxy vers le frontend nginx qui sert l'app et proxifie /api). - .env.example : précise AUTH_FRONTEND_BASE_URL = URL publique HTTPS en production (liens de réinitialisation de mot de passe).
229 lines
9.3 KiB
C#
229 lines
9.3 KiB
C#
using System.Text;
|
|
using FluentValidation;
|
|
using FluentValidation.AspNetCore;
|
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
using Microsoft.AspNetCore.Diagnostics;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.IdentityModel.Tokens;
|
|
using SchotzIadKmTracker.Api.Data;
|
|
using SchotzIadKmTracker.Api.Models.Entities;
|
|
using SchotzIadKmTracker.Api.Validators;
|
|
|
|
using SchotzIadKmTracker.Api.Configuration;
|
|
using SchotzIadKmTracker.Api.Repositories;
|
|
using SchotzIadKmTracker.Api.Services;
|
|
|
|
// Licence QuestPDF (usage communautaire, gratuit) — requise avant toute génération de PDF.
|
|
QuestPDF.Settings.License = QuestPDF.Infrastructure.LicenseType.Community;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
// --- Options de configuration liées depuis appsettings.json ---
|
|
builder.Services.Configure<BANOptions>(
|
|
builder.Configuration.GetSection(BANOptions.SectionName));
|
|
builder.Services.Configure<ORSOptions>(
|
|
builder.Configuration.GetSection(ORSOptions.SectionName));
|
|
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));
|
|
|
|
// --- Client typé BAN (Base Adresses Nationale) ---
|
|
// BaseAddress et Timeout proviennent de BANOptions (appsettings.json).
|
|
var banOptions = builder.Configuration
|
|
.GetSection(BANOptions.SectionName)
|
|
.Get<BANOptions>() ?? new BANOptions();
|
|
|
|
builder.Services.AddHttpClient<IBANClient, BANClient>(client =>
|
|
{
|
|
client.BaseAddress = new Uri(banOptions.BaseUrl);
|
|
client.Timeout = TimeSpan.FromSeconds(banOptions.TimeoutSeconds);
|
|
});
|
|
|
|
// --- Client typé ORS (OpenRouteService) ---
|
|
// BaseAddress, Timeout et en-tête de clé d'API proviennent de ORSOptions (appsettings.json).
|
|
var orsOptions = builder.Configuration
|
|
.GetSection(ORSOptions.SectionName)
|
|
.Get<ORSOptions>() ?? new ORSOptions();
|
|
|
|
builder.Services.AddHttpClient<IORSClient, ORSClient>(client =>
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(orsOptions.BaseUrl))
|
|
{
|
|
// Le slash final garantit la résolution correcte des chemins relatifs.
|
|
var baseUrl = orsOptions.BaseUrl.EndsWith('/')
|
|
? orsOptions.BaseUrl
|
|
: orsOptions.BaseUrl + "/";
|
|
client.BaseAddress = new Uri(baseUrl);
|
|
}
|
|
|
|
client.Timeout = TimeSpan.FromSeconds(orsOptions.TimeoutSeconds);
|
|
|
|
if (!string.IsNullOrWhiteSpace(orsOptions.ApiKey))
|
|
{
|
|
// ORS attend la clé brute dans l'en-tête Authorization (sans schéma « Bearer »).
|
|
// Les clés ORS (JWT) contiennent des caractères refusés par la validation stricte
|
|
// de DefaultRequestHeaders.Add ; on utilise TryAddWithoutValidation pour l'insérer
|
|
// telle quelle et éviter une FormatException à la création du client.
|
|
client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", orsOptions.ApiKey);
|
|
}
|
|
});
|
|
|
|
// --- Calculateur de distance (orchestration BAN + ORS + fallback Haversine) ---
|
|
builder.Services.AddScoped<IDistanceCalculator, DistanceCalculatorService>();
|
|
|
|
// --- Cache des adresses géocodées ---
|
|
builder.Services.AddScoped<IAdresseCacheRepository, AdresseCacheRepository>();
|
|
builder.Services.AddScoped<IAdresseCacheService, AdresseCacheService>();
|
|
|
|
// --- Services de base ---
|
|
// Autorisation GLOBALE par défaut : toutes les routes MVC exigent un utilisateur authentifié.
|
|
// AuthController porte [AllowAnonymous] ; /health (endpoint minimal) n'est pas couvert par les filtres MVC.
|
|
builder.Services.AddControllers(options =>
|
|
{
|
|
var policy = new Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder()
|
|
.RequireAuthenticatedUser()
|
|
.Build();
|
|
options.Filters.Add(new Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter(policy));
|
|
});
|
|
|
|
// --- Health checks (endpoint GET /health, utilisé par Docker Compose) ---
|
|
builder.Services.AddHealthChecks();
|
|
|
|
// --- Validation des requêtes (FluentValidation) ---
|
|
// Enregistre tous les validateurs de l'assembly et active la validation automatique côté MVC.
|
|
builder.Services.AddValidatorsFromAssemblyContaining<CreateTrajetDtoValidator>();
|
|
builder.Services.AddFluentValidationAutoValidation();
|
|
|
|
// --- Base de données (EF Core + SQLite) ---
|
|
// La chaîne de connexion provient de la configuration ("ConnectionStrings:Default").
|
|
var connectionString = builder.Configuration.GetConnectionString("Default")
|
|
?? "Data Source=trajets.db";
|
|
builder.Services.AddDbContext<AppDbContext>(options =>
|
|
options.UseSqlite(connectionString));
|
|
|
|
// --- Hachage des mots de passe (ASP.NET Core Identity PasswordHasher) ---
|
|
builder.Services.AddSingleton<IPasswordHasher<Compte>, PasswordHasher<Compte>>();
|
|
|
|
// --- Repositories & services métier ---
|
|
builder.Services.AddScoped<IJwtService, JwtService>();
|
|
|
|
// --- 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)
|
|
|
|
builder.Services.AddScoped<IAuthService, AuthService>();
|
|
|
|
// --- Authentification JWT Bearer ---
|
|
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
|
|
};
|
|
});
|
|
|
|
builder.Services.AddScoped<SchotzIadKmTracker.Api.Repositories.IVehiculeRepository,
|
|
SchotzIadKmTracker.Api.Repositories.VehiculeRepository>();
|
|
builder.Services.AddScoped<IVehiculeService, VehiculeService>();
|
|
builder.Services.AddScoped<IProfilService, ProfilService>();
|
|
builder.Services.AddScoped<IAdresseNommeeService, AdresseNommeeService>();
|
|
builder.Services.AddScoped<IBaremeService, BaremeService>();
|
|
builder.Services.AddScoped<IMontantRegleService, MontantRegleService>();
|
|
builder.Services.AddScoped<IRemboursementService, RemboursementService>();
|
|
builder.Services.AddScoped<ITotauxService, TotauxService>();
|
|
builder.Services.AddScoped<ITrajetRepository, TrajetRepository>();
|
|
builder.Services.AddScoped<ITrajetService, TrajetService>();
|
|
builder.Services.AddScoped<IResumeService, ResumeService>();
|
|
builder.Services.AddScoped<IPdfService, PdfService>();
|
|
|
|
// Exploration des endpoints + Swagger / OpenAPI
|
|
builder.Services.AddEndpointsApiExplorer();
|
|
builder.Services.AddSwaggerGen(options =>
|
|
{
|
|
options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
|
|
{
|
|
Title = "SchotzIAD KM Tracker API",
|
|
Version = "v1",
|
|
Description = "API de saisie, calcul et consultation des trajets kilométriques."
|
|
});
|
|
});
|
|
|
|
// ProblemDetails : réponses d'erreur normalisées (RFC 7807)
|
|
builder.Services.AddProblemDetails();
|
|
|
|
// CORS pour le frontend Angular.
|
|
// Les origines autorisées sont configurables via appsettings ("Cors:AllowedOrigins").
|
|
const string CorsPolicyName = "AngularFrontend";
|
|
var allowedOrigins = builder.Configuration
|
|
.GetSection("Cors:AllowedOrigins")
|
|
.Get<string[]>() ?? new[] { "http://localhost:4200" };
|
|
|
|
builder.Services.AddCors(options =>
|
|
{
|
|
options.AddPolicy(CorsPolicyName, policy =>
|
|
{
|
|
policy.WithOrigins(allowedOrigins)
|
|
.AllowAnyHeader()
|
|
.AllowAnyMethod();
|
|
});
|
|
});
|
|
|
|
var app = builder.Build();
|
|
|
|
// --- Pipeline HTTP ---
|
|
|
|
// Gestion centralisée des exceptions -> ProblemDetails
|
|
app.UseExceptionHandler();
|
|
app.UseStatusCodePages();
|
|
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
app.UseSwagger();
|
|
app.UseSwaggerUI();
|
|
}
|
|
|
|
// Pas de redirection HTTPS côté application : le conteneur écoute en HTTP et la
|
|
// terminaison TLS est assurée par le reverse-proxy (Caddy/nginx) placé devant.
|
|
// Activer UseHttpsRedirection ici provoquerait des redirections parasites derrière le proxy.
|
|
app.UseCors(CorsPolicyName);
|
|
|
|
app.UseAuthentication();
|
|
|
|
app.UseAuthorization();
|
|
|
|
app.MapControllers();
|
|
|
|
// Endpoint de santé — utilisé par le healthcheck Docker Compose (depends_on: service_healthy).
|
|
app.MapHealthChecks("/health");
|
|
|
|
// --- Application automatique des migrations EF Core au démarrage ---
|
|
// Au premier lancement, la base (ex. /data/trajets.db sur le volume monté) est créée ;
|
|
// aux lancements suivants, les migrations en attente sont appliquées sans intervention manuelle.
|
|
using (var scope = app.Services.CreateScope())
|
|
{
|
|
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
await dbContext.Database.MigrateAsync();
|
|
}
|
|
|
|
app.Run();
|
|
|
|
// Exposé pour les tests d'intégration (WebApplicationFactory<Program>).
|
|
public partial class Program { }
|