Prepares the single-host layout for deployment. The customer's public website moves from wwwroot/ to wwwroot/web/, so a CMS deploy can no longer overwrite content it does not own: with the website in its own directory, the release directory can be swapped without touching it. Each front-end gets its own file provider, and both tolerate a missing directory at startup — a fresh deployment has no website until a separate workspace deploys one, and the CMS must still serve /admin and the API. When the website's index.html is absent, an embedded placeholder is served instead of a 404, which also doubles as proof the CMS itself is running. The placeholder is embedded in the assembly rather than shipped into wwwroot/web/, because that directory is owned and overwritten by the website workspace. Adds GET /health for uptime monitoring. It reports infrastructure liveness only and is deliberately NOT the same thing as /api/v1/Availability/status or /api/v1/System/capabilities: those are CMS domain state that also serve the master/slave protocol. A healthy instance can be switched off by design, and a switched-on instance can be unhealthy, so conflating them would alert on business state and stay silent on real outages. /health is on the availability gate's bypass list for the same reason. Fixes a real defect in the gate's admin bypass. It parsed the bearer token with ReadJwtToken, which reads claims without verifying the signature, so an unauthenticated caller could forge an unsigned token carrying an Owner role claim and bypass the gate that suspends a customer's site. Protected endpoints still rejected them, so nothing leaked — but the gate itself was bypassable. The token is now fully validated against the same parameters as the bearer scheme, resolved from one shared source so the two cannot drift apart. Host wiring for these changes lands with the data-durability commit, since both units touch the same lines of Program.cs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
5.8 KiB
Code Generation Summary — U1 Hosting & Serving
Date: 2026-07-27 Requirements: FR-07, FR-10, FR-24
Files Created
| Path | Purpose |
|---|---|
src/SlpModularCms.Core/Hosting/JwtTokenValidation.cs |
Single source of the JWT validation parameters |
src/SlpModularCms.Core/Hosting/Health/HealthReport.cs |
Liveness response model |
src/SlpModularCms.Core/Hosting/Health/HealthCheckExtensions.cs |
AddCmsHealthChecks() / MapCmsHealthChecks() |
src/SlpModularCms.Core/Hosting/Security/IAdminTokenValidator.cs |
Contract for the validated admin bypass |
src/SlpModularCms.Core/Hosting/Security/AdminTokenValidator.cs |
Implementation |
src/SlpModularCms.Api/Extensions/StaticContentExtensions.cs |
Two-mount composition and SPA fallbacks |
src/SlpModularCms.Api/Extensions/WebsitePlaceholder.html |
Embedded placeholder page |
src/SlpModularCms.Core.Tests/Hosting/AdminTokenValidatorTests.cs |
13 tests |
src/SlpModularCms.Core.Tests/Hosting/HealthReportTests.cs |
3 tests |
Files Modified
| Path | Change |
|---|---|
src/SlpModularCms.Core/Hosting/ServiceCollectionExtensions.cs |
Validation parameters built once via the factory, registered as a singleton and shared with the bearer scheme; IAdminTokenValidator registered |
src/SlpModularCms.Api/Program.cs |
AddCmsHealthChecks(), UseCmsStaticContent(), MapCmsHealthChecks(), MapCmsSpaFallbacks() |
src/SlpModularCms.Api/SlpModularCms.Api.csproj |
Placeholder embedded as a resource |
src/SlpModularCms.Api.Slave/Program.cs |
Health checks; no static mounts |
src/SlpModularCms.Modules.Availability/Middleware/AvailabilityMiddleware.cs |
/health bypass; IsAdminBypass delegates to the validator; unvalidated ReadJwtToken removed |
src/SlpModularCms.Modules.Availability.Tests/AvailabilityMiddlewareTests.cs |
Constructor change; new bypass and forged-token cases |
src/SlpModularCms.Modules.Availability.Tests/AvailabilityMiddlewareMasterGateTests.cs |
Constructor change; /health bypass case |
No duplicate or parallel files were created — every existing file was modified in place.
Implementation Decisions
The forged-token fix is proven against the real validator, not only a substitute
The middleware's own tests substitute IAdminTokenValidator, which is correct unit-testing practice — the middleware's job is to ask, not to validate. But a substitute keeps passing even if the middleware were later rewired back to unvalidated token parsing.
A nested WithRealValidator class therefore wires the middleware to the actual AdminTokenValidator and asserts both halves of the fix: a forged unsigned Owner token is rejected, and a genuine Owner token still bypasses. The second matters as much as the first — an administrator must always be able to reach a disabled instance to switch it back on.
AddCmsHealthChecks() deliberately takes no options
Adding a database probe therefore requires editing this method, which is visible in review, rather than flipping a setting. Liveness-only is enforced by the shape of the API instead of by discipline.
The placeholder is an embedded resource, and that was verified
wwwroot/web/ is owned and overwritten by a separate website workspace, so a placeholder file there would be deleted by the first real deployment or mistaken for part of the customer's site. Embedding keeps it outside that boundary.
Because a wrong resource name would fail silently — falling back to a minimal inline HTML string — the compiled assembly's manifest was inspected to confirm the name resolves: SlpModularCms.Api.Extensions.WebsitePlaceholder.html.
Static mounts are resolved at startup
RegisterMount only registers a mount when its directory exists, so a directory created after the process started is not served until the next restart. This is correct for the intended deployment model — the atomic release switch links wwwroot/web/ into place before the process starts — but it is behaviour worth knowing: dropping a website into a running instance requires a restart.
Deviation From the Plan
Step 11 (StaticContentTests) was not implemented as written. The plan placed it in SlpModularCms.Core.Tests, but StaticContentExtensions lives in the SlpModularCms.Api project, which Core.Tests does not reference and must not.
SlpModularCms.Api has no test project, by the same deliberate convention that gives SlpModularCms.Api.Slave none — the Clients solution folder holds deployables, not tested libraries. Creating one would have been a structural change outside this unit's scope.
What the step was meant to cover is mostly ASP.NET Core's own static-file behaviour rather than this project's logic. The genuinely project-specific behaviours — mount ordering, fallback precedence, the nonfile constraint, the placeholder path and the /admin redirect — require a composed host and are therefore carried to the phase-level Build and Test stage, where both hosts are started.
Carried to Build and Test:
/adminredirects to/admin/- A missing asset under either mount returns
404, never HTML - A client-side route under
/adminserves the adminindex.html - A client-side route at the root serves the website
index.html, or the placeholder when absent /healthanswers while the instance is availability-disabled
Verification
| Check | Result |
|---|---|
dotnet build SlpModularCms.sln -c Release |
✅ 0 errors |
SlpModularCms.Core.Tests |
✅ 83 passed (was 54) |
SlpModularCms.Modules.Availability.Tests |
✅ 82 passed (was 78) |
SlpModularCms.Modules.Identity.Tests |
✅ 37 passed (unchanged) |
SlpModularCms.Modules.Master.Tests |
✅ 51 passed (was 50) |
| Embedded resource name resolves | ✅ Verified against the compiled assembly manifest |
No failures occurred during generation; nothing needed fixing and retrying.