Plans the Gitea deployment feature and refreshes the codebase analysis

Adds the AI-DLC inception record for deploying the CMS as a single .NET
application on hosting where no server configuration is possible.

The reverse-engineering artifacts were regenerated: the previous set
predated the Master module, the Slave host, the solution reorganisation
and single-host serving, all of which matter for deployment. Findings
were verified by running the build, both test suites and the linter
rather than inferred, which surfaced two facts the plan depends on:
the frontend lint gate currently fails (5 errors), and two transitive
packages carry high-severity advisories.

Records 24 functional requirements, 32 traced decisions and a
seven-unit decomposition whose ordering is load-bearing: durability
work must land before the first automated deploy, or the very first
deploy is the one that silently breaks master/slave trust.

Two conflicts found while designing and carried into the units:
- Both modules call AddDataProtection(), which runs after the host and
  would override a persistent key store while still passing any
  registration test.
- The availability gate runs before authentication, so its admin
  bypass cannot read HttpContext.User.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
This commit is contained in:
2026-07-27 23:59:30 +02:00
co-authored by Claude Opus 5
parent 38857038a0
commit 8568ca43c6
25 changed files with 4233 additions and 503 deletions
@@ -0,0 +1,159 @@
# Components
Component definitions and high-level responsibilities. Detailed business logic is designed per unit in Functional Design.
**Placement rule (Q1 = A)**: every new cross-cutting concern lives in `SlpModularCms.Core` and is exposed as an extension method. Both hosts therefore get identical behaviour with no duplicated implementation. **Q2 = A**: `SlpModularCms.Api.Slave` receives everything except the static-file mounts, because it serves as a reference for what a customer-facing API instance looks like — not merely as a local dev tool.
---
## New Components
### C-01 `SecurityHeadersMiddleware`
- **Project**: `SlpModularCms.Core` (`Hosting/Security/`)
- **Purpose**: Apply HTTP security headers that would normally come from nginx or IIS configuration, which NFR-01 forbids relying on.
- **Responsibilities**:
- Attach headers at response start, so the response content type is known before deciding what applies
- Apply per-header scoping (FU1 = A): `X-Content-Type-Options` and `Strict-Transport-Security` on **all** responses; `Content-Security-Policy`, `X-Frame-Options` and `Referrer-Policy` on **HTML** responses only
- Select the CSP policy for the request path
- Never overwrite a header another component has already set
- **Interfaces**: standard middleware — `InvokeAsync(HttpContext)`. Registered via `UseCmsSecurityHeaders()`.
- **Requirements**: FR-18, SECURITY-04
### C-02 `SecurityHeadersOptions`
- **Project**: `SlpModularCms.Core` (`Hosting/Security/`)
- **Purpose**: Bind the `SecurityHeaders` configuration section (Options pattern, consistent with `JwtSettings`, `MasterModule`, `MasterPolling`).
- **Responsibilities**: Carry the path-to-policy assignment, the default policy, and the environment-specific allowed origins.
- **Design rule (Q5 = B + Q6 = B, confirmed FU2 = A)**: **policy definitions live in code; path assignment and origins live in configuration.** Adding a path later needs no code change; inventing a new policy does.
- **Requirements**: FR-18
### C-03 `CspPolicyBuilder`
- **Project**: `SlpModularCms.Core` (`Hosting/Security/`)
- **Purpose**: Compose a Content-Security-Policy string from a named policy plus the configured origins.
- **Responsibilities**:
- Define the two named policies in code: `Strict` (for `/admin` and `/api/v1`) and `Relaxed` (for the public website)
- Inject the configured Umami script origin and Sentry ingest origin into the relevant directives
- Build each policy once at startup rather than per request
- **Rationale for existing separately from C-01**: keeps policy composition unit-testable without a request pipeline, and keeps the middleware free of string building.
- **Requirements**: FR-18, D-31
### C-04 Health-check registration
- **Project**: `SlpModularCms.Core` (`Hosting/Health/`)
- **Purpose**: Expose infrastructure liveness, strictly separate from CMS domain state.
- **Responsibilities**:
- Register the framework health-check services (no package required)
- Map `GET /health` returning `200`/`Healthy` or `503`/`Unhealthy`
- **Liveness only** — no database call, no dependency probing (D-21)
- **Explicit non-responsibility**: this component says nothing about availability or capabilities. Those are CMS domain functionality and are never to be used for monitoring.
- **Requirements**: FR-10
### C-05 `CmsDataProtection` registration
- **Project**: `SlpModularCms.Core` (`Hosting/`)
- **Purpose**: Persist the Data Protection key ring in the database so redeploys and atomic release switches cannot render stored slave API keys unreadable.
- **Responsibilities**: Configure `PersistKeysToDbContext<ApplicationDbContext>` and set a stable application discriminator so both hosts and all replicas derive the same keys.
- **Requirements**: FR-12, D-17
### C-06 `ApplicationDbContext` extension — `IDataProtectionKeyContext`
- **Project**: `SlpModularCms.Core` (`Data/`) — **modification of an existing component**
- **Purpose**: Host the Data Protection keys table (Q7 = A).
- **Responsibilities**: Add `DbSet<DataProtectionKey> DataProtectionKeys` and implement `IDataProtectionKeyContext`. Requires one new Core migration.
- **Why here rather than a fourth context**: keys are application-wide infrastructure, not module-owned, and `ApplicationDbContext` now migrates automatically (FR-11) so the table is created without manual steps.
- **Requirements**: FR-12
### C-07 Startup migration runner
- **Project**: `SlpModularCms.Core` (`Hosting/`)
- **Purpose**: Apply `ApplicationDbContext` migrations at startup, removing the need for CLI access on the host.
- **Responsibilities**:
- Run `Database.Migrate()` for `ApplicationDbContext` during startup, before the request pipeline accepts traffic
- **Fail fast (Q8 = A)**: on failure, let the exception propagate so the process does not start
- **Design interaction worth stating**: fail-fast is what makes the liveness-only health check meaningful. A process that cannot migrate never starts, `/health` stops answering, and UptimeRobot goes red. Had this logged-and-continued, the app would look healthy while being unusable.
- **Requirements**: FR-11, D-13
### C-08 `CmsLogging` registration
- **Project**: `SlpModularCms.Core` (`Hosting/Observability/`)
- **Purpose**: Configure structured logging, independent of whether Sentry is enabled (Q10 = B).
- **Responsibilities**: Console logging with structured output and a correlation/request identifier on every entry; exclude secrets and PII.
- **Open item**: OPEN-01 — the correlation-ID mechanism (ASP.NET Core `TraceIdentifier` versus W3C `traceparent`) is decided in NFR Design for Unit 4.
- **Requirements**: D-20, SECURITY-03
### C-09 `CmsSentry` registration
- **Project**: `SlpModularCms.Core` (`Hosting/Observability/`)
- **Purpose**: Report errors and structured logs to Sentry, tagged by environment.
- **Responsibilities**:
- Initialise `Sentry.AspNetCore` when a DSN is configured, and **skip silently when it is not**, leaving console logging active
- Tag events with environment and release
- Emit security-relevant events for alerting (FR-19)
- **Separate from C-08 (Q10 = B)**: structured logging must work without Sentry.
- **Requirements**: FR-14, FR-19, D-19
### C-10 Static-file mount composition
- **Project**: `SlpModularCms.Api` **only** — host-specific, not in `Core` (Q2 = A: the Slave has no static content)
- **Purpose**: Serve two independent front-ends from one process.
- **Responsibilities** (Q3 = A):
- Mount `wwwroot/web/` at `/` with its own `PhysicalFileProvider`
- Mount `wwwroot/admin/` at `/admin` with its own `PhysicalFileProvider`
- Provide default-file handling per mount
- Map two SPA fallbacks, preserving the `nonfile` route constraint so missing assets still return `404`
- **Design consequence**: two explicit registrations rather than one, so each mount can later carry its own headers or caching without disturbing the other.
- **Requirements**: FR-07, FR-08, D-06
### C-11 Deploy transport workflows
- **Project**: `.gitea/workflows/` — not C#
- **Purpose**: Transfer a published release to a target host.
- **Responsibilities** (Q11 = B): one reusable workflow per transport, sharing an identical input interface. `deploy-scp.yaml` is implemented now; `deploy-ftps.yaml` can be added later without changing callers.
- **Requirements**: FR-02, D-02, NFR-09
### C-12 CI workflow
- **Project**: `.gitea/workflows/continuous_integration.yaml`
- **Purpose**: Validate every change and drive deployment.
- **Responsibilities**: Six blocking gates, two environment-specific builds, artifact publication, and the calls into C-11 for test and production.
- **Requirements**: FR-01, FR-03, FR-04, FR-05
---
## Modified Existing Components
### C-13 `AvailabilityMiddleware`
- **Project**: `SlpModularCms.Modules.Availability` (`Middleware/`)
- **Changes**:
1. Add `/health` to `_bypassPrefixes` so the availability gate cannot mask infrastructure liveness (FR-10, D-22)
2. **Fix `IsAdminBypass` to stop trusting an unvalidated token** (FR-24, Q12 = A) — currently `ReadJwtToken` parses without signature verification, so an unauthenticated caller can forge an `Owner` claim and bypass the gate
- **Behaviour that must be preserved**: an Owner or Administrator with a *valid* token still passes, so administrators can always reach a disabled instance to switch it back on.
- **Requirements**: FR-10, FR-24
### C-14 Frontend application configuration (`frontend/src/lib/config.ts`)
- **Changes**: treat an absent or empty `VITE_API_BASE_URL` as same-origin while still accepting an explicit absolute URL for local development against `https://localhost:7221` / `:7222`. Zod validation relaxed accordingly, without silently accepting malformed values.
- **Requirements**: FR-13, D-14
### C-15 Frontend observability
- **Project**: `frontend/src/`
- **Changes**: initialise `@sentry/react` with environment and release tags, skipping gracefully without a DSN; add the Umami tracking script with a per-environment website ID, absent during local development.
- **Requirements**: FR-15, FR-16
### C-16 Host composition (`Program.cs`, both hosts)
- **Changes**: call the new `Core` extension methods in the correct order. **Q9 = A**: the two files stay separate — with the implementation in `Core`, what remains duplicated is an explicit list of calls, which is intentional readability rather than accidental duplication.
- **Requirements**: FR-07, FR-10, FR-11, FR-12, FR-14, FR-18
---
## Component Summary
| ID | Component | Project | Type | Slave gets it? |
|---|---|---|---|---|
| C-01 | `SecurityHeadersMiddleware` | Core | New | Yes |
| C-02 | `SecurityHeadersOptions` | Core | New | Yes |
| C-03 | `CspPolicyBuilder` | Core | New | Yes |
| C-04 | Health-check registration | Core | New | Yes |
| C-05 | `CmsDataProtection` registration | Core | New | Yes |
| C-06 | `ApplicationDbContext` keys table | Core | Modified | Yes |
| C-07 | Startup migration runner | Core | New | Yes |
| C-08 | `CmsLogging` registration | Core | New | Yes |
| C-09 | `CmsSentry` registration | Core | New | Yes |
| C-10 | Static-file mount composition | Api | New | **No** |
| C-11 | Deploy transport workflows | `.gitea/` | New | n/a |
| C-12 | CI workflow | `.gitea/` | New | n/a |
| C-13 | `AvailabilityMiddleware` | Modules.Availability | Modified | Yes |
| C-14 | Frontend config | frontend | Modified | n/a |
| C-15 | Frontend observability | frontend | New | n/a |
| C-16 | Host composition | Both hosts | Modified | Yes |
**14 code components** (9 new, 5 modified) plus **2 workflow components**.