using MaBibli.Api.Data; using MaBibli.Api.Endpoints; using MaBibli.Api.Services.Catalogue; using MaBibli.Api.Services.Couvertures; using MaBibli.Api.Services.Identite; using MaBibli.Api.Services.Isbn; using MaBibli.Api.Services.Prets; using MaBibli.Api.Services.Revues; using MaBibli.Api.Services.Series; using MaBibli.Api.Services.Souhaits; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddOpenApi(); builder.Services.AddDbContext(options => options.UseSqlite(builder.Configuration.GetConnectionString("MaBibli"))); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Identité : lue dans les en-têtes injectés par SSOwat, avec repli sur un utilisateur simulé // configuré en développement (il n'y a pas de portail YunoHost en local). builder.Services.AddHttpContextAccessor(); builder.Services.Configure(builder.Configuration.GetSection(OptionsIdentite.Section)); builder.Services.AddScoped(); // Lookup ISBN : clients typés via IHttpClientFactory (pas de HttpClient instancié à la main). // Timeout court : une source lente ne doit pas bloquer la cascade, l'appelant bascule sur la suivante. var timeoutSources = TimeSpan.FromSeconds(10); // ⚠️ Le relais de couvertures a son PROPRE délai, et bien plus long. Ce n'est pas une largesse : // les deux appels n'ont pas le même témoin. Un lookup ISBN fait patienter quelqu'un devant un // formulaire — d'où les 10 s ci-dessus, et la bascule sur la source suivante. Le relais, lui, // remplit un cache en tâche de fond : personne n'attend derrière, et abandonner ne fait pas // gagner du temps, cela perd la couverture. // // Mesuré le 2026-08-21 : les couvertures OpenLibrary aboutissent sur `archive.org`, qui les // EXTRAIT D'UNE ARCHIVE ZIP à la volée et met 5 à 15 s. À 10 s, le relais répondait 404 une fois // sur trois — indistinguable, pour l'appelant, d'une URL inconnue. L'image n'était alors jamais // mise en cache, et le client rejouait CORS puis relais à CHAQUE affichage : exactement le // défaut que ce relais existe pour corriger. var timeoutRelaisCouvertures = TimeSpan.FromSeconds(30); builder.Services.AddHttpClient(http => { http.BaseAddress = new Uri(BnfClient.UrlBase); http.Timeout = timeoutSources; http.DefaultRequestHeaders.UserAgent.ParseAdd("MaBibli/0.1 (bibliotheque personnelle auto-hebergee)"); }); builder.Services.AddHttpClient(http => { http.BaseAddress = new Uri(OpenLibraryClient.UrlBase); http.Timeout = timeoutSources; http.DefaultRequestHeaders.UserAgent.ParseAdd("MaBibli/0.1 (bibliotheque personnelle auto-hebergee)"); }); builder.Services.AddScoped(); // Relais de couvertures : rend une image de MÊME ORIGINE, seule façon de la mettre en cache // hors-ligne quand son hébergeur n'envoie pas d'en-tête CORS (le formulaire livre accepte // n'importe quelle URL de couverture). // // ⚠️ Les deux réglages du handler ne sont PAS des détails de confort : // - ConnectCallback n'ouvre la socket que vers une adresse publiquement routable, ce qui // ferme le SSRF y compris par DNS rebinding (voir GardeAdresses) ; // - les redirections sont SUIVIES, mais bornées à 3. Elles avaient d'abord été coupées, à // tort : `covers.openlibrary.org` répond 302 (mesuré), et beaucoup de CDN font de même — // les couper refusait donc les couvertures les plus courantes. Les suivre reste sûr parce // que le garde agit à la CONNEXION : il s'applique à chaque saut, cible de redirection // comprise. C'est la position du garde, et non l'interdiction des redirections, qui ferme // le SSRF. builder.Services.AddHttpClient(http => { http.Timeout = timeoutRelaisCouvertures; http.DefaultRequestHeaders.UserAgent.ParseAdd("MaBibli/0.1 (bibliotheque personnelle auto-hebergee)"); }) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { AllowAutoRedirect = true, MaxAutomaticRedirections = 3, ConnectCallback = GardeAdresses.ConnecterAsync, }); var app = builder.Build(); // Une seule base SQLite, migrée au démarrage : pas d'étape manuelle à l'installation YunoHost. using (var portee = app.Services.CreateScope()) { var db = portee.ServiceProvider.GetRequiredService(); db.Database.Migrate(); // Les colonnes normalisées (sans accents) ne peuvent pas être calculées en SQL : la // migration ne les remplit qu'approximativement, ce passage les remet d'aplomb et réunit // les auteurs que la normalisation révèle identiques. Idempotent, donc sans coût ensuite. await new ServiceRenormalisation( db, portee.ServiceProvider.GetRequiredService>()) .ExecuterAsync(); } if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } // Le client Blazor WebAssembly est servi comme fichiers statiques par cette API : // un seul processus tourne sur le serveur. // // ⚠️ Les empreintes WASM sont désactivées (voir CLAUDE.md) : tous ces fichiers portent des noms // stables. Sans en-tête Cache-Control, un navigateur peut garder une version périmée en cache // heuristique (par exemple MaBibli.Client.styles.css) même si le service worker, lui, revalide // déjà à l'installation — mais seulement quand il parvient à s'activer. `no-cache` force une // revalidation conditionnelle (ETag/Last-Modified, déjà posés par défaut) à chaque requête : // pas de re-téléchargement si le fichier n'a pas changé, mais jamais de contenu périmé servi // sans repasser par le serveur. var optionsFichiersStatiques = new StaticFileOptions { OnPrepareResponse = ctx => ctx.Context.Response.Headers.CacheControl = "no-cache, must-revalidate", }; app.UseBlazorFrameworkFiles(); app.UseStaticFiles(optionsFichiersStatiques); app.MapIsbnEndpoints(); app.MapLivresEndpoints(); app.MapAuteursEndpoints(); app.MapPretsEndpoints(); app.MapSouhaitsEndpoints(); app.MapSeriesEndpoints(); app.MapRevuesEndpoints(); app.MapBibliographieEndpoints(); app.MapIdentiteEndpoints(); app.MapCouverturesEndpoints(); app.MapVersionEndpoints(); // ⚠️ Le fallback a son propre pipeline : il NE passe PAS par les StaticFileOptions posées // ci-dessus. Sans lui repasser les mêmes options, « / » — c'est-à-dire le start_url de la PWA, // la seule URL qu'ouvre l'application installée — repartait sans aucun Cache-Control, donc en // cache heuristique du navigateur. Mesuré : /index.html portait bien l'en-tête, « / » non. // C'est la coquille HTML qui décide de tout le reste : la servir périmée suffit à mélanger deux // versions sur l'appareil. app.MapFallbackToFile("index.html", optionsFichiersStatiques); app.Run();