# SlpModularCms Een modulaire monolith CMS gebouwd met .NET 10. ## Projectstructuur - `src/SlpModularCms.Api`: De host applicatie en API shell (lokale ontwikkeling / dev-host). - `src/SlpModularCms.Api.SlpSoftware`: Tweede Client-project, de daadwerkelijk gedeployde API voor `test.slpsoftware.nl`/`slpsoftware.nl` (CI/CD-cutover vanaf `SlpModularCms.Api`). Host dezelfde modules als `SlpModularCms.Api` via dezelfde gedeelde `CmsHost`-compositie (zie hieronder), plus de `Offerings`-module. Heeft een eigen, geïsoleerde lokale ontwikkeldatabase — zie `appsettings.Development.json` in dat project. - `src/SlpModularCms.Core`: Kern functionaliteiten, data modellen en interfaces. Bevat ook `Core/Hosting/CmsHost.cs` — de gedeelde service- en pipeline-compositie die elk Client-project (`Api`, `Api.SlpSoftware`) vanuit zijn eigen dunne `Program.cs` aanroept, zodat beide projecten niet uit elkaar kunnen groeien. - `src/SlpModularCms.Modules.*`: Onafhankelijke functionele modules. - `frontend/`: De CMS admin web-UI (Vite + React + TypeScript). Bewust buiten `src/` gehouden om de .NET solution schoon te houden. ## Development Setup ### Vereisten - .NET 10 SDK - Podman of Docker (voor MariaDB) ### 1. Database opstarten Start een MariaDB-container met de volgende opdracht: ```powershell podman run -e "MARIADB_ROOT_PASSWORD=" -p 3306:3306 --name mariadb -d docker.io/library/mariadb:latest ``` > **Migratie-notitie**: dit project draaide tot 2026-07-28 op Microsoft SQL Server. Overgestapt naar > MariaDB omdat de productie-Pi geen SQL Server kan draaien (geen ARM64-build bestaat) en al MariaDB > heeft geïnstalleerd. Zie `aidlc-docs/features/gitea-deployment-workflow/` voor de volledige > rationale. Bestaande lokale SQL Server-databases/migraties zijn niet compatibel — begin met een > verse MariaDB-database. ### 2. Configuratie De applicatie maakt gebruik van een drie-bestanden patroon voor configuratie: - `appsettings.json`: Productie baseline. - `appsettings.Development.json`: Ontwikkelinstellingen. - `appsettings.local.json`: Lokale overrides (niet in Git). Zorg dat er een `src/SlpModularCms.Api/appsettings.local.json` aanwezig is met de juiste connection string: ```json { "ConnectionStrings": { "DefaultConnection": "Server=127.0.0.1;Port=3306;Database=SlpModularCms;Uid=root;Pwd=" } } ``` ### 3. Applicatie starten Voer de applicatie uit vanaf de root: ```powershell dotnet run --project src/SlpModularCms.Api ``` De API is daarna bereikbaar op `https://localhost:7221` (of de geconfigureerde poort). De OpenAPI documentatie (Scalar) is beschikbaar op `/scalar`. `SlpModularCms.Api.SlpSoftware` start op dezelfde manier (`dotnet run --project src/SlpModularCms.Api.SlpSoftware`, bereikbaar op `https://localhost:7223` — `7222` is al in gebruik door `SlpModularCms.Api.Slave`), maar verwacht een **eigen**, aparte lokale database (zie dat project's eigen `appsettings.Development.json`) — niet dezelfde als `SlpModularCms.Api`. ## Initiële Setup (Bootstrapping) ... ## Authenticatie & Security (Unit 0) De API gebruikt een beveiligde flow voor authenticatie: - **Login**: `POST /api/v1/auth/login`. Retourneert een `accessToken` in de body en een `refreshToken` in een beveiligde `httpOnly` cookie. - **Refresh**: `POST /api/v1/auth/refresh`. Gebruikt de `refreshToken` cookie om een nieuwe `accessToken` en `refreshToken` (rotatie) te genereren. - **Revoke**: `POST /api/v1/auth/revoke`. Trekt het token in en wist de cookie. ### Belangrijke NFR Details: - **CORS**: Alleen toegestane origins uit `appsettings.json → Cors:AllowedOrigins` worden geaccepteerd. De frontend moet draaien op een van deze origins. - **Cookies**: De `refreshToken` cookie is `httpOnly`, `SameSite=Strict` en heeft het pad `/api/v1/auth`. - **Rate Limiting**: Login endpoints hebben rate limiting (Fixed window 5/min, Sliding window 20/min). - **Error Handling**: Foutmeldingen volgen de RFC 9457 `ProblemDetails` standaard. ## Frontend Development (CMS Admin UI) De frontend is een Vite + React 19 + TypeScript single-page application in de map `frontend/`. Hij gebruikt TanStack Router, Tailwind v4 (hoofdkleur `#ac0000`) met shadcn/ui-stijl componenten, react-i18next (NL/EN), en MSW voor mocking in tests. ### Vereisten - Node.js 20+ (getest met v24) - pnpm 9+ (getest met v11) ### Snel starten ```powershell cd frontend pnpm install pnpm dev ``` De dev-server draait op `http://localhost:5173`. ### Configuratie De frontend leest de API-basis-URL uit een environment-variabele (zie `frontend/.env.example`): ``` VITE_API_BASE_URL=http://localhost:5000 ``` Kopieer `.env.example` naar `.env.local` en pas de waarde aan indien nodig. Optioneel kan met `VITE_ENABLE_MSW=true` de MSW-mockbackend in de browser worden ingeschakeld voor frontend-ontwikkeling zonder draaiende API. ### Vereiste backend De app verwacht de .NET API (Unit 0) draaiend op de geconfigureerde origin met: - **CORS** die de frontend-origin (`http://localhost:5173`) toestaat met `credentials`. - De `httpOnly` `refreshToken` cookie op pad `/api/v1/auth` (silent refresh bij opstarten). - RFC 9457 `ProblemDetails` foutmeldingen. > Let op: de API-poort in `.env.example` (`5000`) moet overeenkomen met de werkelijke API-poort en de `Cors:AllowedOrigins` configuratie van de backend. ### Scripts ```powershell pnpm dev # ontwikkelserver (HMR) pnpm build # type-check (tsc) + productie-build pnpm preview # productie-build lokaal bekijken pnpm test # unit/integratietests (Vitest + Testing Library + MSW) pnpm test:coverage # tests met coverage-rapport pnpm lint # ESLint pnpm format # Prettier (4-space indent) ``` ## Database Migraties Alle database commando's moeten worden uitgevoerd vanaf de **root** van de projectmap. ### Nieuwe migratie toevoegen Wanneer je wijzigingen aanbrengt in de modellen (in `SlpModularCms.Core`): ```powershell dotnet ef migrations add --project src\SlpModularCms.Core --startup-project src\SlpModularCms.Api ``` ### Database bijwerken Om de migraties toe te passen op de database: ```powershell dotnet ef database update --project src\SlpModularCms.Core --startup-project src\SlpModularCms.Api ``` ### Per-module migraties Sommige modules (`SlpModularCms.Modules.Master`, `SlpModularCms.Modules.Availability`) hebben een **eigen** `DbContext` met eigen migraties, los van `SlpModularCms.Core`. Deze worden automatisch toegepast bij het opstarten van de applicatie (via `Database.Migrate()` in de module's `UseModule`-methode), maar een nieuwe migratie genereren doe je expliciet per project: ```powershell dotnet ef migrations add --project src\SlpModularCms.Modules.Master --startup-project src\SlpModularCms.Api dotnet ef migrations add --project src\SlpModularCms.Modules.Availability --startup-project src\SlpModularCms.Api --context AvailabilityDbContext ``` > Let op: voor `SlpModularCms.Modules.Availability` is `--context AvailabilityDbContext` verplicht, omdat de API-startup-project meerdere `DbContext`-typen samenvoegt en de EF CLI anders niet kan bepalen welke bedoeld wordt. ## Nieuwe Module Toevoegen Het systeem is ontworpen om eenvoudig uitgebreid te worden met nieuwe functionele modules. Volg deze stappen om een nieuwe module toe te voegen: ### 1. Project aanmaken Maak een nieuw .NET 10 Class Library project aan in de `src/` map. Gebruik de naamconventie `SlpModularCms.Modules.`. ```powershell dotnet new classlib -n SlpModularCms.Modules.MijnNieuweModule -o src\SlpModularCms.Modules.MijnNieuweModule -f net10.0 ``` ### 2. Referenties toevoegen Voeg de benodigde referentie naar `Core` toe en voeg het project toe aan de solution: ```powershell dotnet add src\SlpModularCms.Modules.MijnNieuweModule reference src\SlpModularCms.Core dotnet sln SlpModularCms.sln add src\SlpModularCms.Modules.MijnNieuweModule ``` ### 3. IModule implementeren Maak een class aan die de `IModule` interface implementeert: ```csharp using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using SlpModularCms.Core.Modules; namespace SlpModularCms.Modules.MijnNieuweModule; public class MijnNieuweModule : IModule { public string Name => "MijnNieuweModule"; public string Version => "1.0.0"; public void RegisterServices(IServiceCollection services) { // Registreer hier module-specifieke services } public void UseModule(IApplicationBuilder app) { // Configureer hier middleware of andere pipeline zaken } } ``` ### 4. Registreren in de API Om ervoor te zorgen dat de module tijdens ontwikkeling wordt meegenomen in de build-output van de API (zodat de `ModuleOrchestrator` de DLL kan vinden), voeg je een referentie toe aan het API project: ```powershell dotnet add src\SlpModularCms.Api reference src\SlpModularCms.Modules.MijnNieuweModule ``` De `ModuleOrchestrator` zal de module nu automatisch ontdekken en laden bij het opstarten. ## Master CMS Module De `SlpModularCms.Modules.Master` module laat een Owner op één "Master"-CMS de beschikbaarheid van andere ("slave") CMS-instanties centraal beheren. Elke slave die de `SlpModularCms.Modules.Availability`-module draait, respecteert een master-gecontroleerde aan/uit-status naast zijn eigen lokale beschikbaarheidsschakelaar. ### Architectuur - **Master** (`SlpModularCms.Modules.Master`): eigen `MasterDbContext` met de `CmsInstance`-entiteit (URL, versleutelde API key, status). Bevat `CmsInstanceController` (`/CmsInstances`, Owner-only), `SlaveApiClient` (uitgaande HTTP-calls naar slaves), `SlaveStatusController` (`GET /api/v1/SlaveStatus`, laat een slave zijn eigen status ophalen) en `IntegrityCheckBackgroundService` (periodieke reconciliatie + status-herpush, standaard elk uur). - **Slave-extensie** (`SlpModularCms.Modules.Availability`): eigen `MasterRegistration`-entiteit (incl. `LastPolledAt`), `MasterController` (interne endpoints onder `/api/v1/master/*`, buiten de beschikbaarheids-gate om), `MasterStatusPollingBackgroundService` (periodiek pullen van de eigen status bij de Master) en een uitgebreide `AvailabilityMiddleware` die zowel de lokale als de master-gate evalueert. ### Registratie- en statusflow 1. Owner voegt op de Master `/cms`-pagina een slave toe met diens URL. 2. De Master genereert een API key, versleutelt deze (Data Protection) en slaat hem op bij de `CmsInstance`. 3. De Master pusht de registratie naar de slave: `POST /api/v1/master/register` met header `X-Master-Api-Key`. 4. Zet de Owner de status van een slave om (Available / NotAvailable / Inactive), dan pusht de Master dit synchroon door naar de slave. Bij **Inactive** stuurt de Master expliciet `Available` (de gate wordt vrijgegeven — de Master beheert de slave niet meer). **Twee onafhankelijke synchronisatiepaden** (push én pull), zodat lokale manipulatie of een gemiste update op de slave zichzelf herstelt: - **Push** (Master → Slave, direct): elke statuswijziging via de UI, plus elke `IntegrityCheckBackgroundService`-cyclus (herpusht de laatst opgeslagen status naar elke actieve slave — vangt slaves op die net herstart zijn). - **Pull** (Slave → Master, periodiek): `MasterStatusPollingBackgroundService` op de slave haalt zelf zijn status op bij `GET /api/v1/SlaveStatus` (`MasterPolling:PollIntervalSeconds`, standaard 30s). Dit is de guard tegen lokale manipulatie van de slave-status en tegen gemiste pushes. - **Fail-open**: is de Master langer dan `MasterPolling:FailOpenAfterMinutes` (standaard 5 min) onbereikbaar via de pull, dan valt de slave automatisch terug naar `Available` — een dode of onbereikbare Master mag een slave nooit permanent blokkeren. ### Configuratie Nieuwe sectie `MasterModule` in `appsettings.json` (zie ook `appsettings.Development.json`): ```json "MasterModule": { "IntegrityCheckIntervalMinutes": 60, "HttpTimeoutSeconds": 10, "MasterUrl": "https://jouw-master-domein" } ``` - `MasterUrl` is de publieke URL van déze master-instantie, gebruikt door `IntegrityCheckBackgroundService` (buiten een HTTP-requestcontext heeft de background service geen `HttpContext` om dit uit af te leiden). Nieuwe sectie `MasterPolling` (op elke instantie die `Modules.Availability` laadt — dus ook de slave): ```json "MasterPolling": { "PollIntervalSeconds": 30, "FailOpenAfterMinutes": 5, "HttpTimeoutSeconds": 5 } ``` - Heeft geen effect zolang er geen `MasterRegistration` bestaat (bijv. op de Master zelf, of op een slave die nog niet gekoppeld is). ### Vergrendeld Instellingen-scherm op een master-gecontroleerde slave Zolang de master-gate een slave op `NotAvailable` heeft gezet (via push of pull), toont de slave's eigen `/settings`-pagina (`SettingsPage.tsx`) dit als een vergrendelde toestand in plaats van een normaal te wijzigen instelling: - Een banner legt uit dat de Master CMS deze status beheert. - De modusknoppen, het redenveld en de opslaanknop zijn disabled. - Een lokale poging om de status alsnog te wijzigen (bijv. via een directe API-call) wordt door de backend geweigerd met `409 Conflict` (`MasterControlledAvailabilityException` in `PersistentAvailabilityService.UpdateStatusAsync`) — de master-gate kan dus niet per ongeluk of expres lokaal worden omzeild. - `GET /api/v1/Availability/status` geeft dit door via het veld `isMasterControlled`. ## Lokaal Master + Slave Draaien (Dev) Om de master↔slave-connectie (zie "Master CMS Module" hierboven) lokaal te kunnen testen, kun je twee backend-instanties tegelijk draaien: een volledige "master" (met de `SlpModularCms.Modules.Master`-module) en een "slave"-instantie zonder die module. Dit is puur een lokale ontwikkel-/testopstelling — er is geen nieuwe functionaliteit aan de master/slave-protocol zelf toegevoegd. ### 1. Backends starten **Master** (bestaande `SlpModularCms.Api`, ongewijzigd): ```powershell dotnet run --project src/SlpModularCms.Api --launch-profile https ``` Bereikbaar op `https://localhost:7221` (Scalar op `/scalar`). **Slave** (nieuwe `SlpModularCms.Api.Slave`, zonder de Master-module): ```powershell dotnet run --project src/SlpModularCms.Api.Slave --launch-profile https ``` Bereikbaar op `https://localhost:7222` (Scalar op `/scalar`). Vereist een eigen `src/SlpModularCms.Api.Slave/appsettings.local.json` — kopieer `appsettings.local.json.example` naar `appsettings.local.json` en vul een **eigen** lokale database in (bijv. `Database=SlpModularCmsSlave`), zodat master- en slave-data gescheiden blijven. De `Modules.Availability`-migraties worden automatisch toegepast bij het opstarten (zie "Per-module migraties" hierboven). De `SlpModularCms.Core`-migraties (Identity) worden **nooit** automatisch toegepast — dit moet je, net als bij de master, één keer handmatig doen voor de nieuwe slave-database: ```powershell dotnet ef database update --project src\SlpModularCms.Core --startup-project src\SlpModularCms.Api.Slave --context ApplicationDbContext ``` ### 2. Frontend starten **Tegen de master** (standaard): ```powershell cd frontend pnpm dev ``` Draait op `http://localhost:5173`, gebruikt `.env.local` (`VITE_API_BASE_URL=https://localhost:7221`). **Tegen de slave** (optioneel, alleen nodig als je de slave ook via de admin-UI wilt bekijken): ```powershell cd frontend pnpm dev:slave ``` Draait op `http://localhost:5174`, gebruikt `.env.slave.local` (`VITE_API_BASE_URL=https://localhost:7222`) — kopieer eerst `.env.example` naar `.env.slave.local` met die waarde. **Beide tegelijk** (zoals een Compound-configuratie in Rider): ```powershell cd frontend pnpm dev:all ``` Start `pnpm dev` en `pnpm dev:slave` parallel in één terminal, met gekleurde `master`/`slave`-prefixes per regel zodat de output van elkaar te onderscheiden blijft. Stoppen met `Ctrl+C` sluit beide dev-servers af. ### 3. Slave koppelen aan de master Met beide backends (en de master-frontend) draaiend: 1. Log in op de master-frontend (`http://localhost:5173`) en ga naar de `/cms`-pagina. 2. Gebruik de bestaande **"Add CMS Instance"**-dialoog om de lokale slave toe te voegen met URL `https://localhost:7222`. 3. De master genereert en pusht een API key naar de slave (`POST /api/v1/master/register`); de instantie zou daarna als **verbonden/gezond** moeten worden getoond. Zie `aidlc-docs/features/local-dev-master-slave-setup/inception/requirements/requirements.md` voor de volledige requirements en rationale achter deze opstelling. ## Productie Setup ### 0. Routing op de webhost Op een shared-hosting omgeving (zoals mijnhostingpartner.nl) is er meestal maar ruimte voor 1 website/app-pool. Daarom serveert `SlpModularCms.Api` alles zelf, vanuit één proces: | Pad | Inhoud | |---|---| | `/` | De publieke website van de klant, uit `wwwroot/web/` — **geen onderdeel van deze repo**. Zie [`WEBSITE_WORKSPACE.md`](WEBSITE_WORKSPACE.md) voor het volledige contract (verplichte structuur, verboden/gereserveerde paden, SPA-fallback, same-origin API-calls, CSP, Umami) | | `/admin` | De CMS admin-UI (`frontend/`), gebouwd met `base: '/admin/'` en automatisch gekopieerd naar `wwwroot/admin/` bij `dotnet publish` | | `/api/v1/...` | Deze API | | `/health` | Infrastructuur-liveness — zie "Health-check endpoint" hieronder | Beide SPA's krijgen een fallback naar hun eigen `index.html` zodat client-side routes (bijv. `/admin/dashboard`) werken; ontbrekende bestanden (met een extensie, bijv. `/admin/assets/x.js`) blijven gewoon 404'en. Zie `Program.cs` (`MapFallbackToFile`) en `SlpModularCms.Api.csproj` (`BuildAndCopyAdminFrontend`-target). Ontbreekt `wwwroot/web/` nog (een verse deployment vóórdat een website-workspace er iets in heeft gezet), dan start de applicatie gewoon door en toont `/` een ingebouwde placeholder-pagina — `/admin` en `/api/v1` blijven onverminderd werken. Voor lokale ontwikkeling verandert er niets: `pnpm dev` blijft op `http://localhost:5173` draaien zonder `/admin`-prefix. ### 1. Build & Publish Publiceren gebeurt via de Gitea Actions-pipeline (`.gitea/workflows/continuous_integration.yaml` → `deploy-scp.yaml`), niet met een losse handmatige `dotnet publish`-stap: elke push naar `master` deployt automatisch naar test, productie alleen via een expliciete `workflow_dispatch` met de `deploy_production`-vlag aan. De pipeline publiceert framework-dependent voor `linux-arm64` en bouwt daarbij automatisch ook de admin-frontend mee naar `wwwroot/admin/`. Elke deploy plaatst de nieuwe release in een verse map en wisselt pas daarna atomisch over naar `current` — er is dus nooit een moment waarop de applicatie een halfklare release serveert, en `wwwroot/web/` (de klantsite) staat structureel buiten die verwisselde map, zodat een deploy hem nooit kan raken. Volledige host-setup, rollback-procedure en de exacte deploy-stappen staan in de Operations-documentatie van deze feature, niet hier — dit README beschrijft alleen wat er gebouwd is, niet hoe je het voor het eerst inricht. Wil je toch lokaal handmatig publiceren (bijv. om de output te inspecteren): ```powershell dotnet publish src/SlpModularCms.Api -c Release -r linux-arm64 --self-contained false -o ./publish ``` ### 2. Runtime Configuratie In productie moeten gevoelige instellingen worden doorgegeven via Environment Variables: - `ConnectionStrings__DefaultConnection` - `JwtSettings__Secret` - `JwtSettings__Issuer` - `JwtSettings__Audience` - `MasterModule__MasterUrl` — publieke URL van deze master-instantie (alleen relevant als de Master CMS Module actief is) - `Observability__SentryDsn` — leeg is een normale, ondersteunde staat: Sentry wordt dan overgeslagen en alleen console-logging blijft actief - `SecurityHeaders__AllowedScriptOrigins__0`, `SecurityHeaders__AllowedConnectOrigins__0`, ... — externe origins die de CSP van `/admin` en `/api/v1` mag toestaan (bijv. de Umami-scripthost en Sentry's ingest-endpoint). Zie `appsettings.json → SecurityHeaders` voor de volledige optiesvorm; een onbekende `PathPolicies`-policy-naam laat de applicatie bij **opstarten** falen, niet pas bij de eerste request ### 2a. Data Protection key ring (Master CMS Module) De API keys van geregistreerde slaves worden versleuteld opgeslagen met ASP.NET Core Data Protection. De key ring wordt **automatisch** persistent opgeslagen in de database (`PersistKeysToDbContext`, met een expliciete, stabiele applicatie-discriminator) — dit hoeft niet meer apart geconfigureerd te worden. Dit is bewust zo gebouwd omdat de atomische release-switch (zie hierboven) bij elke deploy een nieuwe content-root-map gebruikt: zonder een database-backed key ring zou dat elke keer de key ring weggooien en alle opgeslagen slave-API-keys onleesbaar maken. ### 3. Database Migraties op `ApplicationDbContext` (Identity/Core) worden bij het opstarten **automatisch** toegepast (`MigrateCoreDatabase()`), en falen hard (fail-fast) als dat niet lukt — de applicatie start dan bewust niet door. Voor productie-deploys wordt vóór elke deploy een databasebackup genomen; zie de Operations-documentatie van deze feature voor de exacte procedure. Per-module migraties (`Modules.Master`, `Modules.Availability`) worden zoals voorheen automatisch toegepast via `UseModule` (zie "Per-module migraties" hierboven) — daar verandert niets aan. ### 4. Health-check endpoint `GET /health` geeft alleen **infrastructuur-liveness** aan ("draait het proces en kan het een basale response geven") — géén databasecheck, om te voorkomen dat het endpoint rood kleurt om redenen die niets met "leeft het proces nog" te maken hebben. Dit is expliciet **iets anders** dan `GET /api/v1/Availability/status` (de CMS-eigen aan/uit-schakelaar) of `GET /api/v1/System/capabilities` — een instantie kan gezond zijn en toch bewust uitgeschakeld, en andersom. `/health` staat op de bypass-lijst van `AvailabilityMiddleware`, zodat een uitgeschakelde instantie dit endpoint altijd blijft beantwoorden.