Shared hosting (e.g. mijnhostingpartner.nl) typically allows only one site/app-pool, so SlpModularCms.Api now serves everything itself: '/' for the customer's public website (deployed separately, not part of this repo), '/admin' for the CMS admin SPA, and '/api/v1' for the API as before. - Program.cs: static files from wwwroot + SPA fallbacks per path so client-side routing works for both frontends. - frontend/: builds with base '/admin/' in production (dev unchanged), router basepath follows suit. - SlpModularCms.Api.csproj: publish now builds the admin frontend and copies its output into wwwroot/admin automatically. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wi5qHAuq8UbzN4NLUFeKkJ
317 lines
17 KiB
Markdown
317 lines
17 KiB
Markdown
# SlpModularCms
|
|
|
|
Een modulaire monolith CMS gebouwd met .NET 10.
|
|
|
|
## Projectstructuur
|
|
|
|
- `src/SlpModularCms.Api`: De host applicatie en API shell.
|
|
- `src/SlpModularCms.Core`: Kern functionaliteiten, data modellen en interfaces.
|
|
- `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 SQL Server)
|
|
|
|
### 1. Database opstarten
|
|
Start een SQL Server container met de volgende opdracht:
|
|
```powershell
|
|
podman run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=MSSQL_s3cr3t_pw!" -p 1433:1433 --name sql-server -d mcr.microsoft.com/mssql/server:2022-latest
|
|
```
|
|
|
|
### 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,1433;User ID=sa;Password=MSSQL_s3cr3t_pw!;Database=SlpModularCms;TrustServerCertificate=True;MultipleActiveResultSets=true"
|
|
}
|
|
}
|
|
```
|
|
|
|
### 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`.
|
|
|
|
## 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 <NaamVanDeMigratie> --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 <NaamVanDeMigratie> --project src\SlpModularCms.Modules.Master --startup-project src\SlpModularCms.Api
|
|
dotnet ef migrations add <NaamVanDeMigratie> --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.<Naam>`.
|
|
```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 — **geen onderdeel van deze repo**, wordt los aangeleverd/gedeployed in `wwwroot/` |
|
|
| `/admin` | De CMS admin-UI (`frontend/`), gebouwd met `base: '/admin/'` en automatisch gekopieerd naar `wwwroot/admin/` bij `dotnet publish` |
|
|
| `/api/v1/...` | Deze API |
|
|
|
|
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).
|
|
|
|
Voor lokale ontwikkeling verandert er niets: `pnpm dev` blijft op `http://localhost:5173` draaien zonder `/admin`-prefix.
|
|
|
|
### 1. Build & Publish
|
|
Compileer de applicatie voor productie — dit bouwt en kopieert automatisch ook de admin-frontend naar `wwwroot/admin/`:
|
|
```powershell
|
|
dotnet publish src/SlpModularCms.Api -c Release -o ./publish
|
|
```
|
|
Kopieer daarna de publieke website van de klant naar `./publish/wwwroot/` (alles behalve de `admin/`-submap, die blijft ongemoeid).
|
|
|
|
### 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)
|
|
|
|
### 2a. Data Protection key ring (Master CMS Module)
|
|
De API keys van geregistreerde slaves worden versleuteld opgeslagen met ASP.NET Core Data Protection, standaard met een bestandssysteem-key-store. Voor gecontaineriseerde of multi-instance deployments **moet** een persistente key ring geconfigureerd worden (bijv. `PersistKeysToDbContext` of `PersistKeysToAzureBlobStorage`). Zonder dit worden alle opgeslagen API keys onleesbaar zodra de container herstart, waardoor master↔slave-communicatie stopt totdat instanties opnieuw worden toegevoegd.
|
|
|
|
### 3. Database
|
|
Zorg dat de doeltabel bestaat en de migraties zijn uitgevoerd. In productie kan dit via een CI/CD pipeline worden afgehandeld met `dotnet ef migrations script` of door de applicatie bij startup migraties te laten draaien (indien geconfigureerd).
|