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
This commit is contained in:
+86
@@ -0,0 +1,86 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user