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
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:
@@ -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).**
|
||||
Reference in New Issue
Block a user