Files
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.9 KiB

Domain Entities — U1 Hosting & Serving

Note: U1 introduces no persisted entity. It adds no table, no migration and no database column. Its "domain" consists of in-memory configuration descriptors and a response model. They are documented here because they are the data structures the unit's logic operates on, and because reviewers should be able to confirm that nothing is being persisted.


Concept Relationships

graph TD
    host["Host application"]
    mountweb["StaticMount: website<br/>request path /"]
    mountadmin["StaticMount: admin SPA<br/>request path /admin"]
    provider["File provider<br/>per mount"]
    fallbackweb["SPA fallback: website"]
    fallbackadmin["SPA fallback: admin"]
    placeholder["Placeholder page<br/>embedded resource"]
    report["HealthReport<br/>response model"]
    orchestrator["ModuleOrchestrator<br/>existing"]
    bypass["Bypass prefix list<br/>existing, extended"]
    tokenparams["Token validation parameters<br/>existing, now shared"]

    host -->|"registers"| mountweb
    host -->|"registers"| mountadmin
    mountweb -->|"resolves files via"| provider
    mountadmin -->|"resolves files via"| provider
    mountweb -->|"falls back to"| fallbackweb
    mountadmin -->|"falls back to"| fallbackadmin
    fallbackweb -->|"substitutes when index absent"| placeholder
    host -->|"exposes"| report
    report -->|"reads module names from"| orchestrator
    host -->|"configures"| bypass
    host -->|"shares"| tokenparams
    bypass -->|"used by availability gate"| tokenparams

    classDef hostnode fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
    classDef mount fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
    classDef model fill:#fbd38d,stroke:#c05621,stroke-width:1px,color:#000;
    classDef existing fill:#e2e8f0,stroke:#4a5568,stroke-width:1px,color:#000;
    class host hostnode;
    class mountweb,mountadmin,provider,fallbackweb,fallbackadmin,placeholder mount;
    class report model;
    class orchestrator,bypass,tokenparams existing;

Text alternative: the host registers two independent static mounts each with its own file provider and SPA fallback, plus a health response model that reads module names from the existing orchestrator; the bypass list and token validation parameters are existing structures this unit extends and shares.


Static Mount Descriptor (configuration, in-memory)

Not a class to be persisted — this describes what each UseStaticFiles registration is configured with.

Field Website mount Admin mount
Physical root {contentRoot}/wwwroot/web {contentRoot}/wwwroot/admin
Request path "" (root) /admin
Default file index.html index.html
Directory browsing Disabled Disabled
Tolerates missing root Yes — logs a warning Yes — logs a warning
Registration order Second First

Registration order matters: the admin mount must be registered first, or /admin/... would be resolved against the website root.


HealthReport (response model, not persisted)

Field Type Purpose
status string "Healthy". A running process always reports healthy; absence of a response is the unhealthy signal
timestamp timestamp with offset When the report was produced, so a cached response is recognisable
version string The application's informational version, so a deploy can be confirmed without host access
modules string array Names of modules loaded by ModuleOrchestrator

Validation and constraints:

  • Every field is derived from in-process state. No field may require a database query, file read or network call (BR-U1-15).
  • No field may contain configuration values, paths, connection details or environment variable contents (BR-U1-18).
  • modules is read from the existing ModuleOrchestrator.ModuleNames, which is already exposed anonymously by /api/v1/System/capabilities — so this field introduces no new disclosure.

Placeholder Page (embedded static content)

Property Value
Storage Embedded resource in the assembly, not a file in wwwroot
Served when The website fallback is needed and wwwroot/web/index.html is absent
Status code 200
Content A statement that no website has been deployed yet, the expected target path, and a link to /admin

Why embedded rather than a file: a file in wwwroot/web/ would be inside the directory a website workspace owns and overwrites — it would be deleted by the first real website deployment, or worse, mistaken for part of the customer's site. Embedding keeps it outside that boundary entirely.

What it must not contain: no version, no environment name, no module list, no configuration. It is served anonymously to any visitor of the site root, which is a wider audience than /health.


Extended Existing Structures

Bypass prefix list (AvailabilityMiddleware)

Aspect Detail
Current contents /api/v1/Availability/status, /api/v1/Auth/, /api/v1/Setup/status, /api/v1/master/, /api/v1/SlaveStatus
Added by U1 /health
Matching Case-insensitive prefix match, unchanged

Shared token validation parameters

Aspect Detail
Currently Configured once inside AddJwtBearer in ServiceCollectionExtensions.AddCoreInfrastructure
Change Extracted so the same instance is used by both the bearer scheme and the availability gate
Constraint Exactly one definition. Two copies could drift, and a gate more permissive than the scheme would silently re-open the hole FR-24 closes

Persistence Summary

Question Answer
New tables? None
New migrations? None
New columns? None
New configuration sections? None — U1 adds no appsettings section
Anything written to disk at runtime? No

All persistence work in Round 1 belongs to U2, which adds the Data Protection keys table and the automatic Core migration.