using FluentValidation; using FluentValidation.AspNetCore; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using SchotzIadKmTracker.Api.Data; 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( builder.Configuration.GetSection(BANOptions.SectionName)); builder.Services.Configure( builder.Configuration.GetSection(ORSOptions.SectionName)); // --- Client typé BAN (Base Adresses Nationale) --- // BaseAddress et Timeout proviennent de BANOptions (appsettings.json). var banOptions = builder.Configuration .GetSection(BANOptions.SectionName) .Get() ?? new BANOptions(); builder.Services.AddHttpClient(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() ?? new ORSOptions(); builder.Services.AddHttpClient(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(); // --- Cache des adresses géocodées --- builder.Services.AddScoped(); builder.Services.AddScoped(); // --- Services de base --- builder.Services.AddControllers(); // --- 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(); 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(options => options.UseSqlite(connectionString)); // --- Repositories & services métier --- builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // 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() ?? 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(); } app.UseHttpsRedirection(); app.UseCors(CorsPolicyName); 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(); await dbContext.Database.MigrateAsync(); } app.Run(); // Exposé pour les tests d'intégration (WebApplicationFactory). public partial class Program { }