Files
slp-modular-cms/aidlc-docs/features/gitea-deployment-workflow/construction/plans/u1-hosting-serving-code-generation-plan.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

9.5 KiB

Code Generation Plan — U1 Hosting & Serving

This plan is the single source of truth for Code Generation of U1. Generation executes exactly these steps in order; no step is added or skipped during execution.


Unit Context

Aspect Detail
Unit U1 Hosting & Serving
Round R1 (with U2 Data Durability)
Workspace root K:\Development\Projects\SlpModularCms
Project type Brownfield — existing structure retained, files modified in place
Requirements FR-07, FR-10, FR-24
Components C-04 health checks, C-10 static mounts, C-13 availability middleware, U1 portion of C-16
Business rules BR-U1-01 … BR-U1-22
Depends on Nothing. U1 and U2 are mutually independent
Depended on by U3 (path layout for CSP scoping), U6 (deploys into this layout)
New database entities None — U1 adds no table, migration or configuration section

Requirement traceability

Requirement Implemented by steps
FR-07 — serve / from wwwroot/web/, /admin from wwwroot/admin/ 4, 5, 6
FR-10 — /health liveness endpoint on the availability bypass list 2, 6, 7
FR-24 — validate the token in the availability gate's admin bypass 3, 7, 8

Generation Steps

Step 1: Shared JWT validation parameters (Core)

  • Create src/SlpModularCms.Core/Hosting/JwtTokenValidation.cs — a factory producing TokenValidationParameters from JwtSettings, with ClockSkew.Zero, matching the current inline configuration exactly
  • Modify src/SlpModularCms.Core/Hosting/ServiceCollectionExtensions.cs so AddJwtBearer consumes the factory instead of building parameters inline
  • Register the produced TokenValidationParameters as a singleton so the availability gate resolves the same instance

Implements the BR-U1-11 single-source constraint: two copies could drift, and a gate more permissive than the bearer scheme would silently re-open the hole FR-24 closes.

Step 2: Health check registration (Core)

  • Create src/SlpModularCms.Core/Hosting/Health/HealthCheckExtensions.cs with AddCmsHealthChecks() and MapCmsHealthChecks()
  • Create the HealthReport response model — status, timestamp, version, modules
  • Compose the report from in-process state only: no database call, no dependency probe (BR-U1-15)
  • Read module names from the existing ModuleOrchestrator
  • Read the version from the assembly's informational version
  • Deliberately expose no options parameter, so adding a database check later is a visible code change rather than configuration drift

Step 3: Admin token validator (Core)

  • Create src/SlpModularCms.Core/Hosting/Security/IAdminTokenValidator.cs and AdminTokenValidator.cs
  • Validate the bearer token against the shared TokenValidationParameters from Step 1 — signature, issuer, audience and lifetime (BR-U1-11)
  • Return true only when validation succeeds and the principal carries role Owner or Administrator (BR-U1-13)
  • Return false — never throw — for absent, malformed, forged or expired tokens (BR-U1-12, BR-U1-14)
  • Register in ServiceCollectionExtensions.AddCoreInfrastructure

Step 4: Static content composition (Api host)

  • Create src/SlpModularCms.Api/Extensions/StaticContentExtensions.cs with UseCmsStaticContent()
  • Register the /admin mount first, then the root mount, each with its own PhysicalFileProvider (BR-U1-01)
  • Enable default-file handling per mount; leave directory browsing disabled (BR-U1-07)
  • Tolerate a missing physical directory at startup for both mounts (BR-U1-20, BR-U1-22)
  • Log a warning naming the absolute expected path when a directory is absent (BR-U1-21)
  • Redirect the exact path /admin to /admin/, leaving deeper paths untouched (BR-U1-03)

Step 5: Placeholder page (Api host)

  • Create src/SlpModularCms.Api/Extensions/WebsitePlaceholder.html — states no website is deployed, names the expected target path, links to /admin
  • Contain no version, environment name, module list or configuration (domain-entities.md)
  • Modify src/SlpModularCms.Api/SlpModularCms.Api.csproj to embed it as an EmbeddedResource
  • Serve it with status 200 when the website fallback is needed and wwwroot/web/index.html is absent (BR-U1-06)

Embedded rather than placed in wwwroot/web/, because that directory is owned and overwritten by a website workspace — a file there would be deleted by the first real deployment or mistaken for part of the customer's site.

Step 6: Api host composition

  • Modify src/SlpModularCms.Api/Program.cs:
    • Replace UseDefaultFiles() + UseStaticFiles() with UseCmsStaticContent()
    • Register AddCmsHealthChecks() alongside the existing service registrations
    • Map MapCmsHealthChecks() after MapControllers()
    • Retarget both MapFallbackToFile registrations to the two mounts, preserving the nonfile constraint (BR-U1-04, BR-U1-05)

Step 7: Slave host composition

  • Modify src/SlpModularCms.Api.Slave/Program.cs:
    • Register AddCmsHealthChecks() and map MapCmsHealthChecks()
    • Add no static mounts — the Slave serves no static content (Q2 of Application Design = A)

Step 8: Availability middleware (Modules.Availability)

  • Modify src/SlpModularCms.Modules.Availability/Middleware/AvailabilityMiddleware.cs:
    • Add /health to _bypassPrefixes (BR-U1-09), leaving the existing entries unchanged (BR-U1-10)
    • Replace the JwtSecurityTokenHandler.ReadJwtToken call in IsAdminBypass with the injected IAdminTokenValidator
    • Remove the now-unused System.IdentityModel.Tokens.Jwt usage

Step 9: Core unit tests

  • Create src/SlpModularCms.Core.Tests/Hosting/AdminTokenValidatorTests.cs — valid Owner token accepted; valid Administrator accepted; valid User rejected; forged unsigned token rejected; expired token rejected; malformed header rejected; absent header rejected
  • Create src/SlpModularCms.Core.Tests/Hosting/HealthReportTests.cs — report composition; module names sourced from the orchestrator; no configuration or path values present

Step 10: Availability module unit tests

  • Modify src/SlpModularCms.Modules.Availability.Tests/AvailabilityMiddlewareTests.cs for the constructor change, and add: /health bypasses while the instance is disabled; a forged Owner token grants no bypass; a valid Owner token still bypasses
  • Modify src/SlpModularCms.Modules.Availability.Tests/AvailabilityMiddlewareMasterGateTests.cs for the constructor change

Step 11: Static content unit tests — DEVIATED, see generation-summary.md

  • [~] StaticContentTests.cs not created: the plan placed it in SlpModularCms.Core.Tests, but StaticContentExtensions lives in SlpModularCms.Api, which Core.Tests does not reference. SlpModularCms.Api has no test project by the same convention that gives SlpModularCms.Api.Slave none
  • Behaviour requiring a composed pipeline recorded in the unit summary as carried to the phase-level Build and Test stage

Step 12: Documentation

  • Create aidlc-docs/features/gitea-deployment-workflow/construction/u1-hosting-serving/code/generation-summary.md — files created and modified, decisions taken, and any deviation from this plan

Step 13: Build and test verification (automatic)

  • dotnet build SlpModularCms.sln -c Release
  • dotnet test for SlpModularCms.Core.Tests and SlpModularCms.Modules.Availability.Tests
  • Fix any failure directly and re-run until green
  • Record the outcome for the completion message

Files Touched

Created

Path Purpose
src/SlpModularCms.Core/Hosting/JwtTokenValidation.cs Shared validation parameters
src/SlpModularCms.Core/Hosting/Health/HealthCheckExtensions.cs Health registration and endpoint
src/SlpModularCms.Core/Hosting/Security/IAdminTokenValidator.cs Contract
src/SlpModularCms.Core/Hosting/Security/AdminTokenValidator.cs Implementation
src/SlpModularCms.Api/Extensions/StaticContentExtensions.cs Two-mount composition
src/SlpModularCms.Api/Extensions/WebsitePlaceholder.html Embedded placeholder
src/SlpModularCms.Core.Tests/Hosting/AdminTokenValidatorTests.cs Tests
src/SlpModularCms.Core.Tests/Hosting/HealthReportTests.cs Tests
src/SlpModularCms.Core.Tests/Hosting/StaticContentTests.cs Tests

Modified

Path Change
src/SlpModularCms.Core/Hosting/ServiceCollectionExtensions.cs Use the shared factory; register the validator
src/SlpModularCms.Api/Program.cs Static content, health checks, retargeted fallbacks
src/SlpModularCms.Api/SlpModularCms.Api.csproj Embed the placeholder
src/SlpModularCms.Api.Slave/Program.cs Health checks only
src/SlpModularCms.Modules.Availability/Middleware/AvailabilityMiddleware.cs /health bypass; validated admin bypass
src/SlpModularCms.Modules.Availability.Tests/AvailabilityMiddlewareTests.cs Constructor change plus new cases
src/SlpModularCms.Modules.Availability.Tests/AvailabilityMiddlewareMasterGateTests.cs Constructor change

Brownfield rule: every file above that exists is modified in place. No *_new, *_modified or parallel copies.


Out of Scope for U1

  • Security headers — U3
  • Sentry, Umami, same-origin frontend config — U4
  • Data Protection and automatic migrations — U2
  • Anything under .gitea/ — U5 and U6
  • README and website contract — U7