Adds SlpModularCms.Api.SlpSoftware and extracts shared CmsHost composition
Continuous Integration / config (pull_request) Successful in 11s
Continuous Integration / changes (pull_request) Successful in 21s
Continuous Integration / backend-build (pull_request) Successful in 6m10s
Continuous Integration / vulnerability-scan (pull_request) Successful in 4m59s
Continuous Integration / frontend-prepare (pull_request) Successful in 1m27s
Continuous Integration / backend-test (pull_request) Failing after 7m48s
Continuous Integration / frontend-build (pull_request) Successful in 2m5s
Continuous Integration / frontend-test (pull_request) Successful in 4m24s
Continuous Integration / frontend-lint (pull_request) Successful in 2m0s
Continuous Integration / publish-test (pull_request) Skipped
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-test (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped

Unit 1 of the slpsoftware-api feature (FR-1/FR-2/FR-3): a new Client project
in the Clients solution folder, intended to eventually become the deployed
API for test.slpsoftware.nl/slpsoftware.nl, hosting the same four modules as
SlpModularCms.Api plus a future Offerings module.

- Extracts SlpModularCms.Api/Program.cs's hosting-pipeline composition into
  SlpModularCms.Core.Hosting.CmsHost (ConfigureServices/ConfigurePipeline),
  shared by both Client projects so they cannot drift apart
- Moves StaticContentExtensions.cs + WebsitePlaceholder.html from Api into
  Core, since CmsHost cannot live in Api but Core cannot depend on Api
- Adds SlpModularCms.Api.SlpSoftware with its own isolated local dev database
  and dev ports (5286/7223, distinct from Api's and Api.Slave's)
- Adds SlpModularCms.Api.Tests with WebApplicationFactory-based pipeline
  regression tests (security headers, health check, SPA fallback, rate
  limiting), scoped to Api per NFR Design
- Adds a frontend dev:slpsoftware pnpm script mirroring dev:slave
- Fixes GlobalExceptionHandler logging routine 401s (e.g. an expired/missing
  refresh token) as unhandled errors -- pre-existing, unrelated to this
  feature's own scope, found while testing the new instance

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWyStNL2ZsjrS7FLd7xvvN
This commit is contained in:
2026-08-02 01:28:39 +02:00
co-authored by Claude Sonnet 5
parent dcc82cdf62
commit fa389e42ee
51 changed files with 3119 additions and 127 deletions
@@ -0,0 +1,167 @@
# Requirements — SlpSoftware Production API
## Intent Analysis
- **User Request**: Add a new deployable API in the `Clients` solution folder, `SlpModularCms.Api.SlpSoftware`, that will eventually replace `SlpModularCms.Api` as the API deployed to `test.slpsoftware.nl` / `slpsoftware.nl`. It hosts the existing Master module plus a new module covering what the SlpSoftware website needs, per the external hand-off doc `packages-api-handoff.md`.
- **Request Type**: New Feature (new Client project + new module), with a CI/CD retarget as a downstream consequence.
- **Scope Estimate**: Multiple Components — new Client project, shared hosting-pipeline extraction in `SlpModularCms.Core`, a new module + its test project, and (in the Operations phase) an extension of the existing deployment pipeline.
- **Complexity Estimate**: Complex — touches solution structure, an in-flight Operations-phase feature's deploy pipeline (`gitea-deployment-workflow`), and a cross-workspace content contract owned by another repo's frontend.
---
## System Context
- The `Clients` solution folder exists but is currently empty; `SlpModularCms.Api` and `SlpModularCms.Api.Slave` sit under `Application` today (per `CLAUDE.md` / `AGENTS.md`). `SlpModularCms.Api.SlpSoftware` will be the first project ever placed in `Clients`.
- `SlpModularCms.Api` hosts four modules today: Core, Identity, Availability, Master (see `Program.cs` / module orchestrator), plus a composed hosting pipeline built during `gitea-deployment-workflow`: static content + SPA fallback for `/admin` and `wwwroot/web/`, health checks, security headers/CSP, rate limiting, Sentry, Data Protection, and startup migrations.
- The external hand-off doc (`K:\Development\SlpSoftware\Projects\SlpSoftware\...\packages-api-handoff.md`, read-only reference) asks for a single public, unauthenticated `GET /api/v1/packages` backing the "Drie manieren om te starten" section of the website. The frontend already calls `fetch('/api/v1/packages')` with a relative, same-origin path.
- That same reference workspace's `WEBSITE_WORKSPACE.md`, together with the (merged) `gitea-deployment-workflow` feature, establishes that in **this** repo the public site (`wwwroot/web/`) and the API are already served same-origin by one Client API process — so the hand-off doc's "add an nginx proxy_pass" open item does not apply here once `Api.SlpSoftware` is the deployed host (Q6 = A).
- Roles available today (`HierarchicalRoleHandler`, `ServiceCollectionExtensions`): `Owner` (100) > `Administrator` (50) > `User` (10), with existing `OwnerOnly` / `AdminOnly` / `UserOnly` authorization policies.
---
## Decisions (traced to clarification questions)
| # | Decision | Source |
|---|---|---|
| D-1 | `SlpModularCms.Api.SlpSoftware` is a new project in `Clients`; `SlpModularCms.Api` is unchanged and stays in `Application` as the dev host. | Q1 = A |
| D-2 | `Api.SlpSoftware` hosts the same four modules as `Api` (Core, Identity, Availability, Master) plus the new module. | Q2 = A |
| D-3 | The shared hosting/pipeline composition in `Program.cs` is extracted into a reusable method in `SlpModularCms.Core` (e.g. `CmsHost.Configure(...)`) so both Client `Program.cs` files stay thin and cannot drift apart. | Q3 = A |
| D-4 | This feature includes admin CRUD (create/edit/delete/reorder) for the new content, not just the public read endpoint from the hand-off doc — that's the reason this becomes a CMS-managed module rather than staying hardcoded. | Q4 = B |
| D-5 | No automatic seed migration for the three current packages. The user will enter content manually, but the **current live values must be preserved as documented reference data** so they aren't lost. Retrieved directly from the reference workspace's `content.ts` — see [Reference Content](#reference-content-not-auto-seeded) below. | Q5 = X (custom) |
| D-6 | No nginx changes are needed for `/api/v1/` once `Api.SlpSoftware` is the deployed, same-origin host. | Q6 = A |
| D-7 | Retargeting the CI/CD pipeline (`deploy-scp.yaml`, `continuous_integration.yaml`, `deployment-instructions.md` — currently owned by `gitea-deployment-workflow`) to `Api.SlpSoftware` is **in scope for this feature's Operations phase**, extending the existing pipeline rather than duplicating it. | Q7 = A |
| D-8 | `SlpModularCms.Api` remains permanently as the local dev host, same role as `Api.Slave` today — no removal planned. | Q8 = A |
| D-9 | The new module is named `SlpModularCms.Modules.Offerings` — deliberately domain-generic (not `Packages`, which reads as a software/library package, and not `SlpSoftware`-specific), since it must be reusable later for unrelated tenants/domains (the user's stated example: a photography business selling photoshoot packages). | Q9 = A (custom, follow-up round) |
| D-10 | The module gets its own `DbContext` (e.g. `OfferingsDbContext`), consistent with the existing Availability/Master module-isolation pattern. | Q10 = A |
| D-11 | The Security Baseline extension is **enabled and blocking** for this feature. | Q11 = A |
| D-12 | The Property-Based Testing extension is **not enforced** for this feature. | Q12 = C |
| D-13 | This feature runs the Operations phase after Construction. | Q13 = A |
| D-14 | The public route is `/api/v1/offerings`, matching the module name, not `/api/v1/packages`. The corresponding frontend fetch-path change (`usePackagesQuery.ts`) in the external reference workspace is **out of scope for this feature** — the user will update it separately, on their own. | User feedback, post-Requirements-approval; confirmed via chat clarification (frontend update: "dat regel ik zelf") |
| D-15 | The CI/CD pipeline retarget (FR-9) is a **cutover**: `SlpModularCms.Api` is replaced by `SlpModularCms.Api.SlpSoftware` as the pipeline's build/deploy target, not run side by side. | User feedback, post-Requirements-approval |
---
## Functional Requirements
### FR-1 — New Client Project: `SlpModularCms.Api.SlpSoftware`
A new project `SlpModularCms.Api.SlpSoftware` is created under the `Clients` solution folder (first project ever placed there), per D-1. `SlpModularCms.Api` is not moved, renamed, or otherwise modified in role.
### FR-2 — Module Composition
`SlpModularCms.Api.SlpSoftware` references and hosts: `SlpModularCms.Core`, `SlpModularCms.Modules.Identity`, `SlpModularCms.Modules.Availability`, `SlpModularCms.Modules.Master`, and the new `SlpModularCms.Modules.Offerings` (D-2, D-9).
### FR-3 — Shared Hosting Pipeline Extraction
The composed hosting pipeline currently duplicated in `SlpModularCms.Api/Program.cs` (static content + SPA fallback for `/admin` and `wwwroot/web/`, health checks, security headers/CSP, rate limiting, Sentry, Data Protection, startup migrations, module orchestrator) is extracted into a reusable method in `SlpModularCms.Core` (e.g. `CmsHost.Configure(...)`). Both `SlpModularCms.Api/Program.cs` and `SlpModularCms.Api.SlpSoftware/Program.cs` call this shared method; project-specific differences (e.g. which modules are registered) remain explicit at each call site (D-3).
### FR-4 — New Module: `SlpModularCms.Modules.Offerings`
A new module `SlpModularCms.Modules.Offerings` is created following the existing `IModule` pattern, with its test project `SlpModularCms.Modules.Offerings.Tests` under `Tests/Modules` (per `CLAUDE.md` structure rules). The module owns an `Offering` entity and its own `OfferingsDbContext` + migrations, isolated from other modules' schemas (D-9, D-10).
### FR-5 — `Offering` Entity
The `Offering` entity carries the fields required by the public API contract (see FR-6) and by admin management (FR-7): a stable slug-like `Id`, `Title`, `Description`, `Price` (pre-formatted display string, not a number — e.g. `"€ 300"` or `"Op maat"`), `PriceNote`, an ordered list of `Features` (plain text), `CtaLabel`, a `Featured` flag, and an explicit `DisplayOrder` (or equivalent ordering field) so admin reordering (FR-7) has something durable to persist, independent of the public array's rendered order.
### FR-6 — Public Endpoint: `GET /api/v1/offerings`
A public, unauthenticated `GET /api/v1/offerings` endpoint returns a JSON array of offerings in display order, matching the field contract from the hand-off doc:
```json
[
{
"id": "pakket_01",
"title": "Landingspagina",
"description": "Eén overtuigende pagina die je product of dienst helder neerzet.",
"price": "€ 300",
"priceNote": "eenmalig, excl. btw",
"features": ["Eén pagina in HTML & CSS", "..."],
"ctaLabel": "Kies landingspagina",
"featured": false
}
]
```
**Note on the frontend dependency**: the frontend in the external, read-only reference workspace currently fetches the old path (`fetch('/api/v1/packages')`). Changing the route to `/api/v1/offerings` (D-14) means that fetch call needs to change too — the user has confirmed this is **out of scope for this feature**; they will update it separately in that workspace. Until that frontend change happens, the website's package section will show its error state against a deployed `Api.SlpSoftware`, same as today against no backend at all.
### FR-7 — Admin CRUD for Offerings
The admin SPA (served under `/admin` by the same Client API, per the existing hosting pipeline) gets management screens to create, edit, delete, and reorder offerings, backed by authenticated API endpoints (`POST` / `PUT` / `DELETE` / a reorder operation on `/api/admin/offerings` or equivalent). These endpoints require the existing `AdminOnly` authorization policy (`Administrator` role or higher), consistent with other content-management operations in this system (D-4).
### FR-8 — Reference Content (Not Auto-Seeded)
<a id="reference-content-not-auto-seeded"></a>
No database seed migration is created (D-5). Instead, the three packages currently live on the website are captured here as the reference values the user will enter manually through the new admin CRUD (FR-7), retrieved from the external reference workspace's `content.ts`:
| `id` | `title` | `description` | `price` | `priceNote` | `features` | `ctaLabel` | `featured` |
|---|---|---|---|---|---|---|---|
| `pakket_01` | Landingspagina | Eén overtuigende pagina die je product of dienst helder neerzet. | € 300 | eenmalig, excl. btw | Eén pagina in HTML & CSS; Ontwerp op maat, geen template; Responsive op elk scherm; Snelle laadtijd & SEO-basis | Kies landingspagina | false |
| `pakket_02` | Website | Een complete website met meerdere pagina's, klaar om te groeien. | € 750 | eenmalig, excl. btw | Drie pagina's in HTML & CSS; Extra pagina's als optie bij te bestellen; Consistente huisstijl over alle pagina's; Responsive, snel & SEO-basis | Kies website | true |
| `pakket_03` | Maatwerk | Grotere websites, een eigen back-end of andere .NET-projecten. | Op maat | offerte na intake | Grotere websites & webapplicaties; Losse back-end in .NET / C#; Koppelingen & API's; Advies over de beste aanpak | Vraag offerte aan | false |
Per the hand-off doc's business rule BR-4 (content fidelity), these values must be reproduced exactly, not paraphrased, if/when entered.
### FR-9 — CI/CD Retarget (Operations Phase)
The existing Gitea Actions pipeline (`continuous_integration.yaml`, `deploy-scp.yaml`) and `deployment-instructions.md` — currently targeting `SlpModularCms.Api`, owned by the `gitea-deployment-workflow` feature — is extended (not duplicated) to build, test, and deploy `SlpModularCms.Api.SlpSoftware` as the artifact shipped to `test.slpsoftware.nl` / `slpsoftware.nl`. This is executed in this feature's Operations phase, coordinated with `gitea-deployment-workflow` rather than forking its pipeline (D-7). Per D-15, this is a **cutover**: the pipeline's build/deploy target switches from `SlpModularCms.Api` to `SlpModularCms.Api.SlpSoftware` — it does not build and deploy both APIs side by side.
---
## Non-Functional Requirements
### NFR-1 — No Nginx Changes Required
No nginx `location /api/v1/` proxy is added. `Api.SlpSoftware` serves the site, `/admin`, and `/api/v1` same-origin from one process, matching the existing `Api` hosting model (D-6).
### NFR-2 — Module Test Coverage
`SlpModularCms.Modules.Offerings` follows this project's existing test coverage standard for new modules, with unit tests for the `Offering` entity, `OfferingsDbContext`, the public read endpoint, and the admin CRUD endpoints (including authorization checks).
### NFR-3 — Long-Term Reusability of the Offerings Module
The module's naming, entity design, and any tenant/site-scoping must not bake in SlpSoftware-specific assumptions (e.g. hardcoded copy, hardcoded routes beyond the one fixed contract in FR-6), since the user's stated intent is to reuse this module for unrelated future sites/domains (D-9).
### NFR-4 — Property-Based Testing Not Required
No PBT tooling or rules are introduced for this feature's tests (D-12).
---
## Security Compliance (Security Baseline extension — enabled, blocking)
| Rule | Status | Notes |
|---|---|---|
| SECURITY-01 Encryption at rest/transit | **Pre-existing, unchanged** | `OfferingsDbContext` uses the same MariaDB connection (TLS-enforced) as Availability/Master; no new data store technology introduced. |
| SECURITY-02 Access logging on intermediaries | **N/A** | No load balancer, API gateway, or CDN in this architecture — the application itself is the only network-facing component. |
| SECURITY-03 Application-level logging | **Pre-existing, unchanged** | Reuses the shared logging/Sentry setup extracted in FR-3. |
| SECURITY-04 HTTP security headers | **Pre-existing, unchanged** | Reuses the shared security-headers middleware extracted in FR-3; applies identically on `Api.SlpSoftware`. |
| SECURITY-05 Input validation | **Addressed** | Admin CRUD endpoints (FR-7) validate all `Offering` fields (type, length bounds on `Title`/`Description`/`Price`/`PriceNote`/`CtaLabel`, array bounds on `Features`) and use EF Core parameterized queries. |
| SECURITY-06 Least-privilege access policies | **Addressed** | Admin endpoints use the existing `AdminOnly` policy, not `OwnerOnly` or an ad-hoc broader policy (FR-7). |
| SECURITY-07 Restrictive network configuration | **N/A** | No cloud networking/firewall resources introduced by this feature. |
| SECURITY-08 Application-level access control | **Addressed** | The public `GET /api/v1/offerings` (FR-6) is deliberately and explicitly anonymous, matching the hand-off doc's requirement; all mutation endpoints (FR-7) require authentication and the `AdminOnly` policy, following deny-by-default. |
| SECURITY-09 Hardening and misconfiguration | **Pre-existing, unchanged** | Reuses the shared pipeline's existing error handling (`GlobalExceptionHandler`) and Development-only OpenAPI/Scalar exposure. |
| SECURITY-10 Supply chain | **Pre-existing, unchanged** | New module adds no new third-party dependencies beyond what the solution already uses (EF Core, ASP.NET Core); covered by the existing blocking vulnerability gate from `gitea-deployment-workflow`. |
| SECURITY-11 Secure design | **Addressed** | Admin (security-critical) operations are isolated behind `AdminOnly`, distinct from the public read path; rate limiting is inherited from the shared pipeline (FR-3) and therefore also applies to the new public endpoint. |
| SECURITY-12 Authentication and credentials | **Pre-existing, unchanged** | Reuses the existing Identity module; no new credential handling introduced. |
| SECURITY-13 Software and data integrity | **Addressed** | Admin mutations to `Offering` records should be auditable at the same level as other content mutations in this system (who/when) — tracked as an open item (see below) if no existing audit mechanism covers module-level content changes. |
| SECURITY-14 Alerting and monitoring | **Pre-existing, unchanged** | Reuses the shared Sentry-based alerting from `gitea-deployment-workflow`; no new alert categories introduced by this feature. |
| SECURITY-15 Exception handling and fail-safe defaults | **Pre-existing, unchanged** | Reuses the shared `GlobalExceptionHandler`; new endpoints follow the same fail-closed pattern (auth failure → 401/403, not silent fallback). |
**Open item**: SECURITY-13 audit-trail coverage for `Offering` create/edit/delete/reorder is not yet confirmed against an existing system-wide audit mechanism (or its absence) — to be resolved at Functional Design / NFR Design for the Offerings unit, not blocking Requirements sign-off.
---
## Scope Boundaries
| In Scope | Out of Scope |
|---|---|
| New `SlpModularCms.Api.SlpSoftware` project in `Clients` | Moving/renaming `SlpModularCms.Api` |
| Shared `CmsHost.Configure(...)`-style extraction in `SlpModularCms.Core` | Removing or deprecating `SlpModularCms.Api` |
| New `SlpModularCms.Modules.Offerings` module + `SlpModularCms.Modules.Offerings.Tests` | Multi-tenant/site-scoping infrastructure for reuse by a future, unrelated site (only the *naming/design* must not preclude it — D-9, NFR-3) |
| `Offering` entity + `OfferingsDbContext` + migrations | Automatic data seeding of the three current packages (D-5) |
| Public `GET /api/v1/offerings` (unauthenticated) | nginx configuration changes (D-6) |
| Admin CRUD (create/edit/delete/reorder) for offerings, `AdminOnly`-protected | Frontend changes in the external `SlpSoftware` reference workspace, **including** updating `usePackagesQuery.ts`'s fetch path to `/api/v1/offerings` (D-14 — user's own follow-up) |
| CI/CD retarget of the existing pipeline to `Api.SlpSoftware` as a **cutover** (Operations phase, D-15) | Running `Api` and `Api.SlpSoftware` pipelines side by side |
| Documenting the current live package copy as reference content (FR-8) | Entering that content into the running system (the user will do this manually) |
---
## Assumptions
| # | Assumption | Risk if wrong |
|---|---|---|
| ~~A-1~~ | ~~The public route stays `/api/v1/packages` even though the module is `Offerings`.~~ **Resolved 2026-08-01**: route is `/api/v1/offerings` (D-14); frontend update is the user's own, separate follow-up. | Closed |
| A-2 | Admin CRUD for offerings requires `AdminOnly` (Administrator role), not `OwnerOnly`. | Low-medium — if the user wants offering management restricted to Owner only, this is a one-line policy change, best confirmed at Application Design. |
---
**9 functional requirements, 4 non-functional requirements, 15 traced decisions, 1 open assumption (1 resolved), 1 open item (security audit-trail coverage).**