using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using SIFinance.Api; using SIFinance.Modules.Achats.Endpoints; using SIFinance.Modules.Finance.Endpoints; using SIFinance.Modules.SousTraitants.Endpoints; using SIFinance.Shared; using SIFinance.Shared.Entities; var builder = WebApplication.CreateBuilder(args); builder.Services.AddOpenApi(); builder.Services.AddDbContext(options => options.UseSqlServer(builder.Configuration.GetConnectionString("Default"))); builder.Services.AddHostedService(); builder.Services.AddHttpClient(); builder.Services.Configure(builder.Configuration.GetSection(AzureAdOptions.SectionName)); builder.Services.Configure(builder.Configuration.GetSection(JwtOptions.SectionName)); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); // Valide le jeton "maison" émis par SIFinance.Api après l'échange OAuth2 avec Entra ID // (voir EntraAuthEndpoints) — pas de validation directe de jeton Entra ID : le backend // pilote lui-même le flux de connexion, comme l'appli NDF existante (JWT maison + // synchro périodique du groupe Entra ID via EntraSyncService), plutôt que MSAL.js + // Microsoft.Identity.Web côté frontend. var jwtSecret = builder.Configuration["Jwt:Secret"] ?? throw new InvalidOperationException("Configuration Jwt:Secret manquante — voir appsettings.Development.json."); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Convert.FromBase64String(jwtSecret)), }; }); builder.Services.AddAuthorization(); builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); builder.Services.Configure(builder.Configuration.GetSection(SharePointOptions.SectionName)); // Pas de client Graph enregistré directement en DI : tant que TenantId/ClientId/ // ClientSecret ne sont pas renseignés (ou invalides), ClientSecretCredential lève une // ArgumentException dès sa construction — avant même le premier appel Graph. Si le // client était un singleton construit par le conteneur DI, cette exception surviendrait // pendant la résolution de la requête, hors de portée d'un try/catch dans l'endpoint, // et remonterait comme une erreur 500 brute. SharePointGraphClientFactory construit le // client à la demande, à l'intérieur du bloc try/catch de l'endpoint. builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); const string FrontendCorsPolicy = "Frontend"; builder.Services.AddCors(options => { options.AddPolicy(FrontendCorsPolicy, policy => policy.WithOrigins( "http://localhost:5173", "http://localhost:5174", "http://127.0.0.1:5173", "http://127.0.0.1:5174") .AllowAnyHeader() .AllowAnyMethod() // Le navigateur ne mémorise le résultat d'un preflight OPTIONS que sur la durée // indiquée ici (par défaut ~5s) : on l'étend pour éviter un aller-retour OPTIONS // avant chaque POST/PUT/PATCH répété (création/édition) pendant la session. .SetPreflightMaxAge(TimeSpan.FromMinutes(30))); }); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } if (!app.Environment.IsDevelopment()) { app.UseHttpsRedirection(); } app.UseCors(FrontendCorsPolicy); app.UseAuthentication(); app.UseAuthorization(); app.MapAchatsEndpoints(); app.MapContratsEndpoints(); app.MapBudgetsEndpoints(); app.MapFacturesEndpoints(); app.MapAvoirsEndpoints(); app.MapPaiementsEndpoints(); app.MapDashboardEndpoints(); app.MapSharePointEndpoints(); app.MapEntraAuthEndpoints(); app.MapOrganismesEndpoints(); app.MapContratsPrestatairesEndpoints(); app.Run();