SluijsensandClaude Sonnet 5 d51285d9dd
Continuous Integration / config (pull_request) Successful in 10s
Continuous Integration / backend-build (pull_request) Successful in 4m34s
Continuous Integration / vulnerability-scan (pull_request) Successful in 4m39s
Continuous Integration / frontend-prepare (pull_request) Successful in 1m45s
Continuous Integration / backend-test (pull_request) Successful in 5m15s
Continuous Integration / frontend-build (pull_request) Successful in 2m14s
Continuous Integration / frontend-test (pull_request) Successful in 4m27s
Continuous Integration / frontend-lint (pull_request) Successful in 1m57s
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Continuous Integration / publish-test (pull_request) Successful in 5m51s
Deploy (SCP) / deploy (pull_request) Failing after 11m46s
Continuous Integration / deploy-test (pull_request) Failing after 11m47s
Removes the secret-diagnosis debug step, fixes wwwroot symlink parent dir
The PI_MAIN_USERNAME/PASSWORD debug step served its purpose (found the
shell-metacharacter password bug fixed in 51488d6) and is removed now
that deploy-test gets past authentication.

"Link persistent website content" then failed with `ln: failed to
create symbolic link '.../wwwroot/web': No such file or directory` -
dotnet publish only emits a wwwroot/ folder when the source project's
own wwwroot has content, so a release built before any frontend/website
content exists can land with no wwwroot/ directory at all, not merely
an empty wwwroot/web/. `ln -s` can't create the missing parent
directory itself. Added `mkdir -p $RELEASE_DIR/wwwroot` before the
existing rm/ln pair to guarantee the parent exists either way.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015FffvxxJp5wG34Ru48GBig
2026-07-30 00:28:29 +02:00

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 MariaDB)

1. Database opstarten

Start een MariaDB-container met de volgende opdracht:

podman run -e "MARIADB_ROOT_PASSWORD=<your_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:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=127.0.0.1;Port=3306;Database=SlpModularCms;Uid=root;Pwd=<your_password>"
  }
}

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 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

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

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.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>.

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): 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):

"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):

"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):

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:

  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 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.yamldeploy-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):

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.

S
Description
No description provided
Readme
3.3 MiB
Languages
C# 57.4%
TypeScript 40.9%
JavaScript 1.1%
CSS 0.3%
HTML 0.3%