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
9.5 KiB
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 producingTokenValidationParametersfromJwtSettings, withClockSkew.Zero, matching the current inline configuration exactly - Modify
src/SlpModularCms.Core/Hosting/ServiceCollectionExtensions.cssoAddJwtBearerconsumes the factory instead of building parameters inline - Register the produced
TokenValidationParametersas 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.cswithAddCmsHealthChecks()andMapCmsHealthChecks() - Create the
HealthReportresponse 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.csandAdminTokenValidator.cs - Validate the bearer token against the shared
TokenValidationParametersfrom Step 1 — signature, issuer, audience and lifetime (BR-U1-11) - Return true only when validation succeeds and the principal carries role
OwnerorAdministrator(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.cswithUseCmsStaticContent() - Register the
/adminmount first, then the root mount, each with its ownPhysicalFileProvider(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
/adminto/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.csprojto embed it as anEmbeddedResource - Serve it with status
200when the website fallback is needed andwwwroot/web/index.htmlis 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()withUseCmsStaticContent() - Register
AddCmsHealthChecks()alongside the existing service registrations - Map
MapCmsHealthChecks()afterMapControllers() - Retarget both
MapFallbackToFileregistrations to the two mounts, preserving thenonfileconstraint (BR-U1-04, BR-U1-05)
- Replace
Step 7: Slave host composition
- Modify
src/SlpModularCms.Api.Slave/Program.cs:- Register
AddCmsHealthChecks()and mapMapCmsHealthChecks() - Add no static mounts — the Slave serves no static content (Q2 of Application Design = A)
- Register
Step 8: Availability middleware (Modules.Availability)
- Modify
src/SlpModularCms.Modules.Availability/Middleware/AvailabilityMiddleware.cs:- Add
/healthto_bypassPrefixes(BR-U1-09), leaving the existing entries unchanged (BR-U1-10) - Replace the
JwtSecurityTokenHandler.ReadJwtTokencall inIsAdminBypasswith the injectedIAdminTokenValidator - Remove the now-unused
System.IdentityModel.Tokens.Jwtusage
- Add
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.csfor the constructor change, and add:/healthbypasses 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.csfor the constructor change
Step 11: Static content unit tests — DEVIATED, see generation-summary.md
- [~]
StaticContentTests.csnot created: the plan placed it inSlpModularCms.Core.Tests, butStaticContentExtensionslives inSlpModularCms.Api, whichCore.Testsdoes not reference.SlpModularCms.Apihas no test project by the same convention that givesSlpModularCms.Api.Slavenone - 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 Releasedotnet testforSlpModularCms.Core.TestsandSlpModularCms.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