using System.Text.Json.Serialization; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using SlpModularCms.Core.Hosting.Health; using SlpModularCms.Core.Hosting.Observability; using SlpModularCms.Core.Hosting.Security; using Scalar.AspNetCore; namespace SlpModularCms.Core.Hosting; /// /// Shared hosting composition for every Client project (Api, Api.SlpSoftware, ...). /// /// /// Extracted from what was originally SlpModularCms.Api/Program.cs in full, so that adding a /// second (and any future) Client project does not mean duplicating this composition — a change /// made here applies to every Client project automatically. The two methods mirror the two halves /// of a minimal ASP.NET Core Program.cs: service registration (before builder.Build()) /// and pipeline configuration (after it). Each caller's own Program.cs keeps only the two /// lines that must stay per-project: constructing the itself and /// loading that project's own appsettings.local.json. /// public static class CmsHost { /// /// Registers every service a Client project needs: logging, Sentry, module discovery and /// registration, core infrastructure, and MVC controllers. /// /// /// The used during registration, so the caller can pass the /// same instance into after builder.Build(). /// public static ModuleOrchestrator ConfigureServices(WebApplicationBuilder builder, CmsHostOptions options) { // Logging FIRST, so a problem initialising Sentry below is itself logged. Puts the W3C trace id // into the scope of every entry from every category — the correlation id that also travels to // the slave via traceparent and appears as `traceId` in ProblemDetails responses. builder.Logging.AddCmsLogging(builder.Environment); // Then Sentry. Does nothing at all when no DSN is configured, which is a normal, fully // supported state rather than an error. builder.WebHost.UseCmsSentry(builder.Configuration); // 1. Initialize Module Orchestrator var loggerFactory = LoggerFactory.Create(lb => lb.AddConsole()); var orchestrator = new ModuleOrchestrator(loggerFactory.CreateLogger()); orchestrator.DiscoverModules(); // 2. Add Core Infrastructure builder.Services.AddCoreInfrastructure(builder.Configuration); builder.Services.AddCmsCors(builder.Configuration); builder.Services.AddCmsRateLimiting(builder.Configuration); builder.Services.AddCmsHealthChecks(); builder.Services.AddCmsSecurityHeaders(builder.Configuration); builder.Services.AddCmsObservability(builder.Configuration); // Registered BEFORE module services: modules must not configure Data Protection themselves, // because a later registration would override this persistent key store (see // DataProtectionExtensions). builder.Services.AddCmsDataProtection(); // 3. Add Module Services orchestrator.RegisterModuleServices(builder.Services); builder.Services.AddSingleton(orchestrator); // 4. Global Controller Configuration with Conventions builder.Services.AddControllers(controllerOptions => { controllerOptions.Conventions.Add(new ApiPrefixConvention("api/v1")); }) .AddJsonOptions(jsonOptions => { jsonOptions.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); }); return orchestrator; } /// /// Configures the HTTP pipeline every Client project needs, in the exact order the original /// Api/Program.cs used — that order encodes real constraints, documented inline below. /// public static void ConfigurePipeline(WebApplication app, ModuleOrchestrator orchestrator, CmsHostOptions options) { // Bring the Core schema up to date before serving any traffic. Runs before the module // middleware below, because the Data Protection keys table lives in this context and the // modules resolve an IDataProtector as soon as they start. Fails fast: a host that cannot // migrate does not start, so /health goes silent and monitoring goes red — which is exactly // what makes a liveness-only health check trustworthy. app.MigrateCoreDatabase(); // 5. Global Exception Handling app.UseExceptionHandler(); // First thing INSIDE the exception handler, and before the static-file middleware below. // Both directions matter: the exception handler re-executes the pipeline from within itself, // so anything registered outside it never sees the ProblemDetails response; and static files // short-circuit the pipeline, so anything after them is invisible to the public website — // which is almost all of the HTML this host serves. app.UseCmsSecurityHeaders(); app.UseRateLimiter(); // 6. Configure Pipeline if (app.Environment.IsDevelopment()) { app.MapOpenApi(); app.MapScalarApiReference(); } app.UseHttpsRedirection(); // Serve the public website ('/') and the CMS admin SPA ('/admin') from wwwroot. // wwwroot/web/index.html + assets -> public website (built and deployed separately, not part of this repo) // wwwroot/admin/index.html + assets -> CMS admin build (see frontend/, copied in on publish) // Registered before the module middleware below: static files short-circuit the pipeline, so // anything that must observe them has to come first. app.UseCmsStaticContent(); app.UseCors(); // 7. Use Module Middleware orchestrator.UseModules(app); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); // Infrastructure liveness. Deliberately outside /api/v1 and on the availability gate's bypass // list: this reports whether the process is alive, which is a different question from whether // the CMS is switched on (/api/v1/Availability/status) or which modules it carries // (/api/v1/System/capabilities). Those are CMS domain state and must not be used for monitoring. app.MapCmsHealthChecks(); // Forwards browser Sentry envelopes through this origin, because ad blockers block requests to // Sentry domains outright. Mapped before the SPA catch-all below, and deliberately NOT on the // availability gate's bypass list: if the instance is switched off, losing admin-SPA error // reports is acceptable, and that is one fewer anonymous outbound-capable endpoint reachable on // a disabled instance. app.MapSentryTunnel(); // SPA fallbacks so client-side routes (e.g. /admin/dashboard) resolve to the right index.html // instead of 404ing. The "nonfile" constraint keeps genuinely missing assets (e.g. /admin/assets/x.js) as 404s. app.MapCmsSpaFallbacks(); } }