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
17 KiB
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 buitensrc/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:
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:
{
"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:
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 eenaccessTokenin de body en eenrefreshTokenin een beveiligdehttpOnlycookie. - Refresh:
POST /api/v1/auth/refresh. Gebruikt derefreshTokencookie om een nieuweaccessTokenenrefreshToken(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:AllowedOriginsworden geaccepteerd. De frontend moet draaien op een van deze origins. - Cookies: De
refreshTokencookie ishttpOnly,SameSite=Stricten 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
ProblemDetailsstandaard.
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
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 metcredentials. - De
httpOnlyrefreshTokencookie op pad/api/v1/auth(silent refresh bij opstarten). - RFC 9457
ProblemDetailsfoutmeldingen.
Let op: de API-poort in
.env.example(5000) moet overeenkomen met de werkelijke API-poort en deCors:AllowedOriginsconfiguratie van de backend.
Scripts
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):
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:
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:
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.Availabilityis--context AvailabilityDbContextverplicht, omdat de API-startup-project meerdereDbContext-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>.
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:
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:
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:
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): eigenMasterDbContextmet deCmsInstance-entiteit (URL, versleutelde API key, status). BevatCmsInstanceController(/CmsInstances, Owner-only),SlaveApiClient(uitgaande HTTP-calls naar slaves),SlaveStatusController(GET /api/v1/SlaveStatus, laat een slave zijn eigen status ophalen) enIntegrityCheckBackgroundService(periodieke reconciliatie + status-herpush, standaard elk uur). - Slave-extensie (
SlpModularCms.Modules.Availability): eigenMasterRegistration-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 uitgebreideAvailabilityMiddlewaredie zowel de lokale als de master-gate evalueert.
Registratie- en statusflow
- Owner voegt op de Master
/cms-pagina een slave toe met diens URL. - De Master genereert een API key, versleutelt deze (Data Protection) en slaat hem op bij de
CmsInstance. - De Master pusht de registratie naar de slave:
POST /api/v1/master/registermet headerX-Master-Api-Key. - 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):
MasterStatusPollingBackgroundServiceop de slave haalt zelf zijn status op bijGET /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 naarAvailable— een dode of onbereikbare Master mag een slave nooit permanent blokkeren.
Configuratie
Nieuwe sectie MasterModule in appsettings.json (zie ook appsettings.Development.json):
"MasterModule": {
"IntegrityCheckIntervalMinutes": 60,
"HttpTimeoutSeconds": 10,
"MasterUrl": "https://jouw-master-domein"
}
MasterUrlis de publieke URL van déze master-instantie, gebruikt doorIntegrityCheckBackgroundService(buiten een HTTP-requestcontext heeft de background service geenHttpContextom dit uit af te leiden).
Nieuwe sectie MasterPolling (op elke instantie die Modules.Availability laadt — dus ook de slave):
"MasterPolling": {
"PollIntervalSeconds": 30,
"FailOpenAfterMinutes": 5,
"HttpTimeoutSeconds": 5
}
- Heeft geen effect zolang er geen
MasterRegistrationbestaat (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(MasterControlledAvailabilityExceptioninPersistentAvailabilityService.UpdateStatusAsync) — de master-gate kan dus niet per ongeluk of expres lokaal worden omzeild. GET /api/v1/Availability/statusgeeft dit door via het veldisMasterControlled.
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):
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):
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:
dotnet ef database update --project src\SlpModularCms.Core --startup-project src\SlpModularCms.Api.Slave --context ApplicationDbContext
2. Frontend starten
Tegen de master (standaard):
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):
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):
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:
- Log in op de master-frontend (
http://localhost:5173) en ga naar de/cms-pagina. - Gebruik de bestaande "Add CMS Instance"-dialoog om de lokale slave toe te voegen met URL
https://localhost:7222. - 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/:
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__DefaultConnectionJwtSettings__SecretJwtSettings__IssuerJwtSettings__AudienceMasterModule__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).