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:
+149
@@ -0,0 +1,149 @@
|
||||
# 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)
|
||||
- [x] Create `src/SlpModularCms.Core/Hosting/JwtTokenValidation.cs` — a factory producing `TokenValidationParameters` from `JwtSettings`, with `ClockSkew.Zero`, matching the current inline configuration exactly
|
||||
- [x] Modify `src/SlpModularCms.Core/Hosting/ServiceCollectionExtensions.cs` so `AddJwtBearer` consumes the factory instead of building parameters inline
|
||||
- [x] 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)
|
||||
- [x] Create `src/SlpModularCms.Core/Hosting/Health/HealthCheckExtensions.cs` with `AddCmsHealthChecks()` and `MapCmsHealthChecks()`
|
||||
- [x] Create the `HealthReport` response model — `status`, `timestamp`, `version`, `modules`
|
||||
- [x] Compose the report from in-process state only: no database call, no dependency probe (BR-U1-15)
|
||||
- [x] Read module names from the existing `ModuleOrchestrator`
|
||||
- [x] Read the version from the assembly's informational version
|
||||
- [x] 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)
|
||||
- [x] Create `src/SlpModularCms.Core/Hosting/Security/IAdminTokenValidator.cs` and `AdminTokenValidator.cs`
|
||||
- [x] Validate the bearer token against the shared `TokenValidationParameters` from Step 1 — signature, issuer, audience and lifetime (BR-U1-11)
|
||||
- [x] Return true only when validation succeeds **and** the principal carries role `Owner` or `Administrator` (BR-U1-13)
|
||||
- [x] Return false — never throw — for absent, malformed, forged or expired tokens (BR-U1-12, BR-U1-14)
|
||||
- [x] Register in `ServiceCollectionExtensions.AddCoreInfrastructure`
|
||||
|
||||
### Step 4: Static content composition (Api host)
|
||||
- [x] Create `src/SlpModularCms.Api/Extensions/StaticContentExtensions.cs` with `UseCmsStaticContent()`
|
||||
- [x] Register the `/admin` mount **first**, then the root mount, each with its own `PhysicalFileProvider` (BR-U1-01)
|
||||
- [x] Enable default-file handling per mount; leave directory browsing disabled (BR-U1-07)
|
||||
- [x] Tolerate a missing physical directory at startup for both mounts (BR-U1-20, BR-U1-22)
|
||||
- [x] Log a warning naming the absolute expected path when a directory is absent (BR-U1-21)
|
||||
- [x] Redirect the exact path `/admin` to `/admin/`, leaving deeper paths untouched (BR-U1-03)
|
||||
|
||||
### Step 5: Placeholder page (Api host)
|
||||
- [x] Create `src/SlpModularCms.Api/Extensions/WebsitePlaceholder.html` — states no website is deployed, names the expected target path, links to `/admin`
|
||||
- [x] Contain no version, environment name, module list or configuration (domain-entities.md)
|
||||
- [x] Modify `src/SlpModularCms.Api/SlpModularCms.Api.csproj` to embed it as an `EmbeddedResource`
|
||||
- [x] 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
|
||||
- [x] Modify `src/SlpModularCms.Api/Program.cs`:
|
||||
- [x] Replace `UseDefaultFiles()` + `UseStaticFiles()` with `UseCmsStaticContent()`
|
||||
- [x] Register `AddCmsHealthChecks()` alongside the existing service registrations
|
||||
- [x] Map `MapCmsHealthChecks()` after `MapControllers()`
|
||||
- [x] Retarget both `MapFallbackToFile` registrations to the two mounts, preserving the `nonfile` constraint (BR-U1-04, BR-U1-05)
|
||||
|
||||
### Step 7: Slave host composition
|
||||
- [x] Modify `src/SlpModularCms.Api.Slave/Program.cs`:
|
||||
- [x] Register `AddCmsHealthChecks()` and map `MapCmsHealthChecks()`
|
||||
- [x] Add **no** static mounts — the Slave serves no static content (Q2 of Application Design = A)
|
||||
|
||||
### Step 8: Availability middleware (Modules.Availability)
|
||||
- [x] Modify `src/SlpModularCms.Modules.Availability/Middleware/AvailabilityMiddleware.cs`:
|
||||
- [x] Add `/health` to `_bypassPrefixes` (BR-U1-09), leaving the existing entries unchanged (BR-U1-10)
|
||||
- [x] Replace the `JwtSecurityTokenHandler.ReadJwtToken` call in `IsAdminBypass` with the injected `IAdminTokenValidator`
|
||||
- [x] Remove the now-unused `System.IdentityModel.Tokens.Jwt` usage
|
||||
|
||||
### Step 9: Core unit tests
|
||||
- [x] 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
|
||||
- [x] 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
|
||||
- [x] 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
|
||||
- [x] 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
|
||||
- [x] Behaviour requiring a composed pipeline recorded in the unit summary as carried to the phase-level Build and Test stage
|
||||
|
||||
### Step 12: Documentation
|
||||
- [x] 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)
|
||||
- [x] `dotnet build SlpModularCms.sln -c Release`
|
||||
- [x] `dotnet test` for `SlpModularCms.Core.Tests` and `SlpModularCms.Modules.Availability.Tests`
|
||||
- [x] Fix any failure directly and re-run until green
|
||||
- [x] 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
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
# Functional Design Questions — U1 Hosting & Serving
|
||||
|
||||
Vul je keuze in achter elke `[Answer]:`-tag. Kies de laatste optie (`Anders`) als niets past.
|
||||
|
||||
---
|
||||
|
||||
## Question 1 — Hoe implementeren we de FR-24-fix?
|
||||
|
||||
**Context**: dit is het conflict uit § 5.2 van het applicatieontwerp. `AvailabilityMiddleware` wordt geïnstalleerd door `orchestrator.UseModules(app)`, wat **vóór** `app.UseAuthentication()` staat. Op dat moment is `HttpContext.User` dus nog leeg — de middleware kan niet simpelweg de al geauthenticeerde gebruiker uitlezen.
|
||||
|
||||
Dat is precies waarom de huidige code `ReadJwtToken` gebruikt: die werkt zonder authenticatie, maar valideert de handtekening niet.
|
||||
|
||||
A) Valideer het token in de middleware zelf, met dezelfde `TokenValidationParameters` als het bearer-schema — die parameters worden dan uit één gedeelde bron gehaald in plaats van gekopieerd. Afgebakend: alleen deze middleware verandert
|
||||
B) Verplaats `app.UseAuthentication()` naar vóór `orchestrator.UseModules(app)`, zodat de middleware `HttpContext.User` kan gebruiken. Kleinere wijziging in regels code, maar verandert de pipeline voor élke module — ook toekomstige
|
||||
C) Laat de gate-bypass helemaal vervallen en gebruik in plaats daarvan een vaste bypass-prefix voor de admin-endpoints — geen tokenlogica meer in de middleware
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
|
||||
---
|
||||
|
||||
## Question 2 — Wat gebeurt er als `wwwroot/web/` niet bestaat?
|
||||
|
||||
**Context**: bij een verse deploy is er nog geen publieke website — die komt uit een andere workspace. De CMS moet dan gewoon starten en `/admin` en `/api/v1` blijven serveren. Maar wat krijgt een bezoeker op `/` te zien?
|
||||
|
||||
A) Een standaard 404 — er is niets, dus dat is het eerlijke antwoord
|
||||
B) Een ingebouwde placeholderpagina met de melding dat er nog geen website is geplaatst, plus een verwijzing naar `/admin` — handig bij een verse installatie, en meteen bewijs dat de CMS draait
|
||||
C) Een redirect naar `/admin` — de enige zinvolle bestemming zolang er geen website is
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:B
|
||||
|
||||
---
|
||||
|
||||
## Question 3 — Moet de applicatie hierover iets loggen bij het opstarten?
|
||||
|
||||
**Context**: een ontbrekende `wwwroot/web/` is bij een verse installatie normaal, maar op een draaiende productieomgeving zou het betekenen dat de website van de klant verdwenen is — precies het scenario dat we met de release-opzet proberen te voorkomen.
|
||||
|
||||
A) Waarschuwing bij opstarten als de map ontbreekt, met het verwachte pad erbij — zichtbaar in Sentry en de console, zonder het opstarten te blokkeren
|
||||
B) Alleen een informatieregel — het is een normale toestand bij een verse installatie
|
||||
C) Niets loggen — de 404 of placeholder zegt genoeg
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
|
||||
---
|
||||
|
||||
## Question 4 — Wat geeft `/health` terug?
|
||||
|
||||
**Context**: het framework-standaardantwoord is platte tekst `Healthy` met status 200, of `Unhealthy` met 503. UptimeRobot heeft aan de statuscode genoeg.
|
||||
|
||||
A) De standaard platte tekst — minimaal, snel, en geeft niets prijs over de applicatie
|
||||
B) Een klein JSON-object met status en tijdstip — iets makkelijker te lezen bij handmatig controleren
|
||||
C) JSON met status, tijdstip, versie en geladen modules — dan zie je meteen of alle modules geladen zijn na een deploy
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:C
|
||||
|
||||
---
|
||||
|
||||
## Question 5 — Moet `/admin` zonder slash doorverwijzen naar `/admin/`?
|
||||
|
||||
**Context**: de admin-SPA is gebouwd met `base: '/admin/'`. Als iemand `/admin` intypt zonder afsluitende slash, worden relatieve verwijzingen in de pagina één niveau te hoog opgelost, waardoor de SPA stuk kan gaan. Een redirect naar `/admin/` voorkomt dat.
|
||||
|
||||
A) Ja, redirect `/admin` naar `/admin/` — voorkomt een categorie fouten die lastig te herkennen is
|
||||
B) Nee, laat de SPA-fallback het afhandelen — minder magie in de pipeline
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
# Functional Design Plan — U1 Hosting & Serving
|
||||
|
||||
**Unit**: U1 Hosting & Serving
|
||||
**Round**: R1 (with U2 Data Durability)
|
||||
**Requirements**: FR-07, FR-10, FR-24
|
||||
**Components**: C-04, C-10, C-13, U1 portion of C-16
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Analyze unit context
|
||||
- [x] Read the U1 definition from `unit-of-work.md`
|
||||
- [x] Read the requirement assignment from `unit-of-work-story-map.md`
|
||||
- [x] Read the carried-in design items — § 5.2 pipeline ordering, missing-directory startup behaviour
|
||||
|
||||
## Step 2: Design static-file serving behaviour
|
||||
- [x] Define mount registration order and request-path resolution for the two mounts
|
||||
- [x] Define default-file handling per mount
|
||||
- [x] Define SPA fallback precedence between `/admin/{*path:nonfile}` and `{*path:nonfile}`
|
||||
- [x] Define behaviour when `wwwroot/web/` is absent at startup
|
||||
- [x] Define behaviour when `wwwroot/web/` exists but has no `index.html`
|
||||
- [x] Define trailing-slash handling for `/admin`
|
||||
- [x] Confirm directory browsing stays disabled
|
||||
|
||||
## Step 3: Design the health endpoint
|
||||
- [x] Define the response contract for healthy and unhealthy states
|
||||
- [x] Confirm no dependency probing is performed
|
||||
- [x] Define behaviour while the instance is availability-disabled
|
||||
- [x] Define what the endpoint must never expose
|
||||
|
||||
## Step 4: Design the availability-gate changes
|
||||
- [x] Define the `/health` bypass placement within the existing prefix list
|
||||
- [x] Resolve the FR-24 implementation approach — pipeline ordering versus in-middleware validation
|
||||
- [x] Define admin-bypass behaviour for valid, forged, expired and absent tokens
|
||||
- [x] Confirm the preserved behaviour: a valid Owner or Administrator token still bypasses the gate
|
||||
|
||||
## Step 5: Define business rules
|
||||
- [x] Enumerate path-resolution rules with precedence
|
||||
- [x] Enumerate health-reporting rules
|
||||
- [x] Enumerate admin-bypass rules
|
||||
- [x] Identify error and edge-case scenarios
|
||||
|
||||
## Step 6: Generate artifacts
|
||||
- [x] Generate `business-logic-model.md`
|
||||
- [x] Generate `business-rules.md`
|
||||
- [x] Generate `domain-entities.md`
|
||||
- [x] Validate all diagrams against the Mermaid standards
|
||||
- [x] Verify Security Baseline compliance for this unit's design
|
||||
Reference in New Issue
Block a user