Files
mabibli/MaBibli.Api/Program.cs
T
mathieuandClaude Opus 5 498579f27b Dis à l'application quelle version elle est, et sous quelle licence
Page « À propos » (/a-propos) : manuel annoncé à venir, contact, site de
l'auteur, version publiée avec sa date de build, et licence AGPL v3 avec le
lien vers le dépôt — l'AGPL attend que les utilisateurs d'un service en réseau
puissent en obtenir la source, ce lien n'est donc pas un ornement.

L'application ne connaissait pas sa version : elle vivait dans le manifeste du
paquet et dans les tags git, jamais dans le binaire. `build/publier-release.sh`
pose désormais `-p:Version` et `-p:MaBibliDateBuild` ; l'API rend les deux par
`GET /api/version`.

⚠️ L'horodatage est le TÉMOIN de l'injection, et rien ne le calcule côté
MSBuild. Sans lui, la version lue serait le « 1.0.0 » que le SDK pose par
défaut : il se lirait comme une vraie version alors qu'il ne désigne rien, et
c'est exactement la valeur qu'on ira chercher pour diagnostiquer un appareil au
cache dépareillé. Mieux vaut ne rien annoncer — un binaire compilé à la main se
déclare « version de développement », ce qui est vrai.

Autres décisions :

- l'entrée du menu est DÉTACHÉE des six destinations, par un filet au-dessus
  sur téléphone et à gauche en rangée sur PC : « À propos » est une annexe, pas
  une septième destination. Ses règles vivent en feuille GLOBALE, comme tout le
  menu — une règle scopée n'atteint pas ce que rend un NavLink ;
- le « mailto: » reste un lien même hors-ligne : il ne charge aucune page et
  passe la main au client de messagerie, qui sait mettre un message en attente.
  Le site et le dépôt, eux, basculent en boutons désactivés portant leur motif,
  comme les liens d'export des envies ;
- la version est un sixième instantané hors-ligne : on la lit justement quand
  quelque chose ne va pas, et un appareil qu'on soupçonne est souvent celui qui
  n'a plus de réseau. L'écran dit alors que c'est la dernière version vue du
  serveur.

Le point de rupture de 40 rem reste identique dans les deux feuilles, et
/a-propos est inscrite dans la table de remontée des routes.

Vérifié en exécution, l'API lancée : sans injection
`{"numero":null,"publiee":false}`, avec `-p:Version=0.4.1
-p:MaBibliDateBuild=…` `{"numero":"0.4.1","publiee":true}`. Le rendu des écrans
n'a PAS été vérifié en navigateur. 618 tests au vert (605 avant).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 14:50:34 +02:00

140 lines
6.3 KiB
C#

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<MaBibliDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("MaBibli")));
builder.Services.AddScoped<IServiceAuteurs, ServiceAuteurs>();
builder.Services.AddScoped<IServiceCatalogue, ServiceCatalogue>();
builder.Services.AddScoped<IServicePrets, ServicePrets>();
builder.Services.AddScoped<IServiceSouhaits, ServiceSouhaits>();
builder.Services.AddScoped<IServiceRevuesSouhaitees, ServiceRevuesSouhaitees>();
builder.Services.AddScoped<IServiceBibliographie, ServiceBibliographie>();
builder.Services.AddScoped<IServiceSeries, ServiceSeries>();
builder.Services.AddScoped<IServiceRevues, ServiceRevues>();
// 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<OptionsIdentite>(builder.Configuration.GetSection(OptionsIdentite.Section));
builder.Services.AddScoped<IFournisseurUtilisateur, FournisseurUtilisateurSsowat>();
// 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);
builder.Services.AddHttpClient<IBnfClient, BnfClient>(http =>
{
http.BaseAddress = new Uri(BnfClient.UrlBase);
http.Timeout = timeoutSources;
http.DefaultRequestHeaders.UserAgent.ParseAdd("MaBibli/0.1 (bibliotheque personnelle auto-hebergee)");
});
builder.Services.AddHttpClient<IOpenLibraryClient, OpenLibraryClient>(http =>
{
http.BaseAddress = new Uri(OpenLibraryClient.UrlBase);
http.Timeout = timeoutSources;
http.DefaultRequestHeaders.UserAgent.ParseAdd("MaBibli/0.1 (bibliotheque personnelle auto-hebergee)");
});
builder.Services.AddScoped<IIsbnLookupService, IsbnLookupService>();
// 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<IServiceCouvertures, ServiceCouvertures>(http =>
{
http.Timeout = timeoutSources;
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<MaBibliDbContext>();
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<ILogger<ServiceRenormalisation>>())
.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();