Files
slp-modular-cms/aidlc-docs/features/gitea-deployment-workflow/construction/u1-hosting-serving/code/generation-summary.md
T
SluijsensandClaude Opus 5 29a93ef873 Separates website and admin roots, adds /health, hardens the availability gate
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
2026-07-28 00:00:13 +02:00

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:

  • /admin redirects to /admin/
  • A missing asset under either mount returns 404, never HTML
  • A client-side route under /admin serves the admin index.html
  • A client-side route at the root serves the website index.html, or the placeholder when absent
  • /health answers 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.