Plans the Gitea deployment feature and refreshes the codebase analysis
Adds the AI-DLC inception record for deploying the CMS as a single .NET application on hosting where no server configuration is possible. The reverse-engineering artifacts were regenerated: the previous set predated the Master module, the Slave host, the solution reorganisation and single-host serving, all of which matter for deployment. Findings were verified by running the build, both test suites and the linter rather than inferred, which surfaced two facts the plan depends on: the frontend lint gate currently fails (5 errors), and two transitive packages carry high-severity advisories. Records 24 functional requirements, 32 traced decisions and a seven-unit decomposition whose ordering is load-bearing: durability work must land before the first automated deploy, or the very first deploy is the one that silently breaks master/slave trust. Two conflicts found while designing and carried into the units: - Both modules call AddDataProtection(), which runs after the host and would override a persistent key store while still passing any registration test. - The availability gate runs before authentication, so its admin bypass cannot read HttpContext.User. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
This commit is contained in:
+103
@@ -0,0 +1,103 @@
|
||||
# Requirements Clarification Questions — Ronde 2
|
||||
|
||||
Je antwoorden waren grotendeels eenduidig. Vier punten hebben opheldering nodig: één directe vraag van jou, twee combinaties van antwoorden die technisch niet samen kunnen, en één gevolg van je keuze voor blokkerende beveiligingsregels.
|
||||
|
||||
---
|
||||
|
||||
## 1. SFTP is niet FTPS — jouw vraag beantwoord
|
||||
|
||||
Je vroeg bij Question 1: *"Op de pi kan ik met FileZilla ook verbinden met SFTP. Is dat voldoende om FTP als uitgangspunt te nemen?"*
|
||||
|
||||
**Nee, en gelukkig maakt dat je keuze eenvoudiger.** Ondanks de vergelijkbare namen zijn het losstaande protocollen:
|
||||
|
||||
- **SFTP** = SSH File Transfer Protocol. Draait volledig *binnen* een SSH-verbinding op poort 22. Dat FileZilla via SFTP verbindt, betekent dus dat **SSH werkt op de Pi** — en `scp` gebruikt exact dezelfde SSH-verbinding. De referentie-aanpak werkt daar dus zonder aanpassing.
|
||||
- **FTPS** = FTP met TLS-versleuteling. Een compleet ander protocol op andere poorten, met een eigen server (vsftpd, IIS FTP). Dat SFTP werkt zegt hier **niets** over.
|
||||
|
||||
Kortom: voor de Pi is SSH/SCP het juiste antwoord, niet FTPS. FTPS is alleen relevant als productie op shared hosting komt, want daar is SSH doorgaans afwezig.
|
||||
|
||||
Daarmee blijft de echte vraag open: **jouw antwoord op Question 1 ging over de Pi, maar de vraag ging over productie.** Question 2 zegt dat de *test*-omgeving op de Pi draait. De commit-message van `3885703` noemt shared hosting (mijnhostingpartner.nl) als reden voor de single-host-opzet.
|
||||
|
||||
### Clarification Question 1
|
||||
Waar draait de **productie**-omgeving van deze CMS?
|
||||
|
||||
A) Ook op je eigen Pi-infrastructuur — dan is SSH/SCP goed voor beide omgevingen en is FTPS voorlopig nergens nodig
|
||||
B) Op shared hosting (mijnhostingpartner.nl) — dan bouwen we test via SSH/SCP naar de Pi én productie via FTPS, twee verschillende transportmechanismen in dezelfde workflow
|
||||
C) Voorlopig op de Pi, maar shared hosting is het einddoel — bouw nu SSH/SCP en zet het transport zo op dat FTPS er later naast kan zonder de workflow te herstructureren
|
||||
D) Nog onbekend — bouw alleen de test-deploy (SSH/SCP) en documenteer productie als open punt
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:C
|
||||
|
||||
---
|
||||
|
||||
## 2. Één bundel versus omgevingstags — die twee kunnen niet samen
|
||||
|
||||
Bij Question 11 gaf je aan dat same-origin mag, dat lokaal een expliciete URL moet blijven werken, en dat de URL-variabele geen enkele bundel in de weg hoeft te staan. Dat kan ik prima bouwen (same-origin standaard + optionele override).
|
||||
|
||||
**Maar je andere antwoorden maken één bundel alsnog onmogelijk:**
|
||||
|
||||
- Question 14/15 = Sentry in de admin-SPA, met een `environment`-tag om test en productie te scheiden
|
||||
- Question 20 = Umami ook op de admin-SPA, en Umami gebruikt per omgeving een **eigen website-ID**
|
||||
|
||||
Vite-variabelen worden op **build-time** in de bundel gebakken. Eén `dist/` kan dus niet tegelijk `environment: test` en `environment: production` zijn, en niet twee verschillende Umami-website-ID's bevatten. Dit is exact de reden dat je referentie-workflow een aparte `build-production`-job heeft.
|
||||
|
||||
### Clarification Question 2
|
||||
Hoe lossen we dit op?
|
||||
|
||||
A) Accepteer twee builds, precies zoals de referentie — een test-build en een productie-build met eigen Vite-variabelen. Same-origin voor de API-URL blijft alsnog nuttig (minder configuratie, geen fout mogelijk), maar levert geen enkele bundel op
|
||||
B) Maak de omgevingsconfiguratie runtime in plaats van build-time — de API levert Sentry-DSN, environment en Umami-website-ID uit zijn eigen configuratie, en de SPA haalt dat bij het opstarten op. Eén bundel geldig voor alle omgevingen, maar wel nieuw werk (endpoint + laadmoment in de SPA)
|
||||
C) Twee builds nu (optie A), met runtime-configuratie (optie B) als later te overwegen verbetering
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:C
|
||||
|
||||
---
|
||||
|
||||
## 3. `app_offline.htm` werkt niet op de Pi
|
||||
|
||||
Bij Question 22 koos je `app_offline.htm` vóór de upload plaatsen en erna weghalen. Dat is een prima techniek — maar het is een functie van de **ASP.NET Core Module in IIS**: IIS ziet dat bestand, stopt de app en serveert het als reactie op elk verzoek.
|
||||
|
||||
Op de Pi (Question 2 = B) draait je app als Kestrel-proces, waarschijnlijk achter nginx en beheerd door systemd. Daar heeft `app_offline.htm` **geen enkel effect** — het bestand wordt gewoon genegeerd, en de upload overschrijft DLL's van een draaiend proces, wat op Linux tot halve of vastgelopen requests leidt.
|
||||
|
||||
Er is bovendien een tweede reden waarom dit aandacht nodig heeft: de publieke website in `wwwroot/web/` wordt statisch geserveerd. Bij een nette stop is die dus óók onbereikbaar tijdens de deploy, terwijl die website niets met de CMS-deploy te maken heeft.
|
||||
|
||||
### Clarification Question 3
|
||||
Hoe regelen we downtime-beheersing per omgeving?
|
||||
|
||||
A) Per omgeving de passende techniek: op de Pi het systemd-proces stoppen vóór de upload en erna starten; op IIS-hosting `app_offline.htm`. De workflow kiest op basis van de omgeving
|
||||
B) Alleen de Pi-aanpak nu (proces stoppen/starten), en `app_offline.htm` toevoegen zodra er daadwerkelijk IIS-hosting is
|
||||
C) Deploy naar een tijdelijke map en wissel dan van map (atomaire switch) — kortste downtime, en de publieke website blijft continu bereikbaar
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:C
|
||||
|
||||
---
|
||||
|
||||
## 4. Blokkerende beveiligingsregels raken meer dan de workflow
|
||||
|
||||
Je koos voor het afdwingen van alle beveiligingsregels als **blokkerende** vereisten. Voor de pipeline zelf sluit dat mooi aan op je andere keuzes (vulnerability-gate blokkerend, lockfile aanwezig, geen hardcoded secrets, rate limiting bestaat al).
|
||||
|
||||
Twee regels vragen echter werk in de applicatie dat nu volledig ontbreekt, en ze zijn blokkerend — dus ik moet weten of je ze binnen deze feature wilt of expliciet wilt uitstellen:
|
||||
|
||||
- **SECURITY-04 — HTTP-securityheaders.** De app zet er nu geen enkele: geen `Content-Security-Policy`, `Strict-Transport-Security`, `X-Content-Type-Options`, `X-Frame-Options` of `Referrer-Policy`. Normaal regel je die in nginx of IIS, maar jouw uitgangspunt is juist dat serverconfiguratie niet mogelijk is — dus horen ze in middleware in de app thuis. Let op: een CSP raakt ook de publieke website die uit een andere workspace komt, want die wordt door hetzelfde proces geserveerd. Umami en Sentry hebben daarnaast expliciete CSP-uitzonderingen nodig.
|
||||
- **SECURITY-14 — alerting en logretentie.** Vereist alerting op authenticatiefouten en autorisatieschendingen, plus minimaal 90 dagen logretentie. Question 16 koos structured logging naar Sentry; Sentry's gratis plan bewaart events standaard **30 dagen**, dus 90 dagen is daarmee niet haalbaar zonder een betaald plan of een tweede bestemming.
|
||||
|
||||
### Clarification Question 4
|
||||
Wat doen we met deze twee regels?
|
||||
|
||||
A) Beide binnen deze feature: securityheaders-middleware bouwen (inclusief CSP-uitzonderingen voor Umami/Sentry), en alerting via Sentry-alertregels — met de logretentie gedocumenteerd als bewuste afwijking op wat Sentry's plan biedt
|
||||
B) Alleen SECURITY-04 (securityheaders) binnen deze feature; SECURITY-14 vastleggen als gedocumenteerde afwijking, omdat volledige alerting en 90-daagse retentie een aparte beslissing over kosten is
|
||||
C) Beide vastleggen als gedocumenteerde afwijking en later oppakken — deze feature blijft strikt de deployment-workflow plus health check
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
|
||||
### Clarification Question 5
|
||||
Als er securityheaders komen (Question 4 = A of B): hoe strikt mag de `Content-Security-Policy` zijn, gegeven dat hetzelfde proces een publieke website serveert die jij niet in deze repo beheert?
|
||||
|
||||
A) Strikt (`default-src 'self'` plus expliciete uitzonderingen voor Umami en Sentry) en de website-instructies leggen vast waar een website-workspace zich aan moet houden
|
||||
B) Strikt voor `/admin` en `/api`, ruimer voor de publieke website — een website-bouwer wordt dan niet beperkt door een CSP die hij niet kent
|
||||
C) Alleen rapporterend beginnen (`Content-Security-Policy-Report-Only`) zodat niets stilletjes breekt, en later afdwingen
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: B
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
# Requirements Clarification Questions — Gitea Deployment Workflow
|
||||
|
||||
Vul je keuze in achter elke `[Answer]:`-tag. Kies de laatste optie (`Anders`) als niets past en beschrijf dan je voorkeur.
|
||||
|
||||
Waar ik iets al uit de code of uit je SlpSoftware-workflow kon opmaken, staat dat als context boven de vraag — dan hoef je alleen te bevestigen of te corrigeren.
|
||||
|
||||
---
|
||||
|
||||
## A. Hosting en transport
|
||||
|
||||
### Question 1
|
||||
**Context**: de referentie-workflow uploadt via `scp` met `sshpass` naar een Raspberry Pi. Shared hosting zoals mijnhostingpartner.nl biedt doorgaans geen SSH, maar wel FTP/FTPS en soms Web Deploy (msdeploy). Dit bepaalt de hele deploy-stap.
|
||||
|
||||
Hoe komt de gepubliceerde .NET-applicatie op de **productie**-host terecht?
|
||||
|
||||
A) FTPS — FTP over TLS, meestal standaard beschikbaar op shared hosting
|
||||
B) Web Deploy / msdeploy — de MSBuild-native manier voor IIS-hosting, ondersteunt `app_offline` en incrementele sync
|
||||
C) SSH/SCP — zoals de referentie, alleen als de host SSH aanbiedt
|
||||
D) Ik weet nog niet wat de host ondersteunt — neem FTPS als uitgangspunt en documenteer hoe je overstapt
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: X, Op de pi kan ik met FileZilla ook verbinden met SFTP. Is dat voldoende om FTP als uitgangspunt te nemen? Anders beginnen met opzetten zoals de referentie met SSH/SCP en later aanpassen naar FTPS als dat nodig is.
|
||||
|
||||
### Question 2
|
||||
**Context**: je hebt lokaal, test en productie. De referentie draait test én productie op dezelfde Raspberry Pi's.
|
||||
|
||||
Waar draait de **testomgeving** van de CMS?
|
||||
|
||||
A) Op dezelfde shared host als productie, als een tweede site/subdomein (bijv. `test.<domein>`)
|
||||
B) Op je eigen Raspberry Pi-infrastructuur (zoals de referentie), met een .NET runtime erop
|
||||
C) Op een andere shared-hostingaccount of -pakket
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: B
|
||||
|
||||
### Question 3
|
||||
**Context**: de referentie gebruikt een zelf-gehoste Gitea Actions runner op Podman, waarbij container-based actions (`appleboy/scp-action`) faalden met een 409-fout — vandaar shell-stappen. De publish van deze CMS heeft bovendien **zowel de .NET 10 SDK als Node + pnpm** nodig.
|
||||
|
||||
Op welke runner draait deze workflow?
|
||||
|
||||
A) Dezelfde zelf-gehoste Podman-runner als de SlpSoftware-workflow — ik zorg dat .NET 10 SDK en Node/pnpm beschikbaar zijn (of laat de workflow ze installeren)
|
||||
B) Dezelfde runner, maar installeer de toolchain expliciet in de workflow met `actions/setup-dotnet` en `pnpm/action-setup`
|
||||
C) Een nieuwe/aparte runner speciaal voor .NET-builds
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: B
|
||||
|
||||
---
|
||||
|
||||
## B. De publieke website in `wwwroot`
|
||||
|
||||
### Question 4
|
||||
**Belangrijk risico dat ik in de code vond**: de publieke website van de klant staat in `wwwroot/`, en de admin-SPA in `wwwroot/admin/`. Een deploy die de hele `wwwroot` overschrijft of spiegelt, **wist daarmee de website van de klant** (die immers uit een andere workspace komt en niet in deze repo zit).
|
||||
|
||||
Hoe moet de deploy hiermee omgaan?
|
||||
|
||||
A) De CMS-deploy raakt `wwwroot/` nooit behalve `wwwroot/admin/` — de rest blijft staan, en de website-workspace deployt onafhankelijk zijn eigen bestanden in `wwwroot/`
|
||||
B) De CMS-deploy overschrijft alles behalve een expliciete uitsluitingslijst, die ik in de workflow configureer
|
||||
C) De publieke website komt in een aparte submap (bijv. `wwwroot/site/`) zodat de scheiding fysiek duidelijker is — vereist een kleine wijziging in `Program.cs`
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: C, noem de map "web" in plaats van "site", Dus dan krijgen we uiteindelijk 3 mappen in wwwroot: admin, web en eventueel een map voor de API (indien nodig).
|
||||
|
||||
### Question 5
|
||||
Wat moeten de instructies voor een website-workspace precies vastleggen? (Het bouwen/deployen van de website zelf blijft buiten scope — dit gaat om het contract waaraan zo'n workspace zich moet houden.)
|
||||
|
||||
A) Alleen het doelpad en de mapstructuur — waar de build-output heen moet en welke paden verboden zijn (`admin/`)
|
||||
B) Doelpad plus technische randvoorwaarden — routing/SPA-fallback-gedrag, verboden bestandsnamen, hoe je `/api/v1` vanaf de website aanroept
|
||||
C) Doelpad, randvoorwaarden én een voorbeeld-deploystap (een YAML-snippet die een website-workspace kan overnemen in zijn eigen workflow)
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: B
|
||||
|
||||
---
|
||||
|
||||
## C. Workflow-opzet en gates
|
||||
|
||||
### Question 6
|
||||
**Context**: in de referentie draait de testdeploy automatisch bij een push/merge naar `master`, en productie alleen bij een handmatige `workflow_dispatch` met een `deploy_production`-vinkje — bewust, zodat niemand per ongeluk productie deployt.
|
||||
|
||||
Wil je diezelfde triggerstrategie hier?
|
||||
|
||||
A) Ja, identiek aan de referentie — PR's valideren, `master` deployt naar test, productie alleen expliciet via `workflow_dispatch`
|
||||
B) Ja, maar productie moet ook een handmatige goedkeuringsstap of aparte bevestiging hebben bovenop het vinkje
|
||||
C) Productie mag automatisch bij een tag/release (bijv. `v1.2.3`) in plaats van via een vinkje
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: A
|
||||
|
||||
### Question 7
|
||||
**Gemeten feit**: `pnpm run lint` faalt nu met 5 errors en 1 warning (in `AddCmsInstanceDialog.tsx`, `InviteUserDialog.tsx` 2×, `SettingsPage.tsx`, `SetStatusDialog.tsx`). Alle 219 backend- en 213 frontend-tests slagen wél. Een lint-gate wordt dus meteen rood.
|
||||
|
||||
Hoe gaan we hiermee om?
|
||||
|
||||
A) Los de 5 lint-errors op als onderdeel van deze feature, en zet de lint-gate daarna blokkerend
|
||||
B) Zet de lint-gate blokkerend en laat de fixes over aan `tech-debt-backlog` — de workflow is dan pas groen ná die feature
|
||||
C) Neem lint op als niet-blokkerende stap (rapporteert, faalt de build niet) en maak hem later blokkerend
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
|
||||
### Question 8
|
||||
**Gemeten feit**: `Microsoft.OpenApi` 2.0.0 en `System.Security.Cryptography.Xml` 10.0.9 hebben high-severity advisories; beide komen transitief binnen.
|
||||
|
||||
Wil je een vulnerability-gate in de workflow?
|
||||
|
||||
A) Ja, blokkerend — en we pinnen de twee packages naar gepatchte versies als onderdeel van deze feature
|
||||
B) Ja, maar niet-blokkerend (rapporteert alleen), zodat de deploy niet vastloopt op transitieve advisories
|
||||
C) Nee, geen vulnerability-scan in deze workflow
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
|
||||
### Question 9
|
||||
Welke gates moeten er in de pipeline zitten vóór een deploy? (Meerdere letters mogen, bijv. `A, B, C`.)
|
||||
|
||||
A) Backend build (`dotnet build -c Release`)
|
||||
B) Backend tests (`dotnet test`)
|
||||
C) Frontend type-check + build (`tsc -b && vite build`)
|
||||
D) Frontend tests (`vitest run`)
|
||||
E) Frontend lint / format-check
|
||||
F) Code coverage-drempel (nu is er geen enkele drempel afgedwongen)
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: A, B, C, D, E
|
||||
|
||||
---
|
||||
|
||||
## D. Database en configuratie
|
||||
|
||||
### Question 10
|
||||
**Gemeten feit**: `AvailabilityDbContext` en `MasterDbContext` migreren zichzelf bij het opstarten, maar `ApplicationDbContext` (Identity) **nooit**. Een verse deploy start dus zonder Identity-tabellen tot iemand handmatig `dotnet ef database update` draait. Op shared hosting kun je vaak geen CLI-commando's op de server uitvoeren.
|
||||
|
||||
Hoe worden migraties in test en productie toegepast?
|
||||
|
||||
A) De pipeline genereert een idempotent SQL-script (`dotnet ef migrations script --idempotent`) als build-artifact, dat ik zelf uitvoer op de database — expliciet en controleerbaar
|
||||
B) De pipeline past migraties direct toe op de database vanuit de runner (vereist dat de runner de database kan bereiken)
|
||||
C) Laat `ApplicationDbContext` bij het opstarten automatisch migreren, net als de twee module-contexts — dan is deploy self-contained
|
||||
D) Combinatie: automatisch bij opstarten voor test, idempotent script voor productie
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: C
|
||||
|
||||
### Question 11
|
||||
**Gemeten feit**: de admin-SPA eist `VITE_API_BASE_URL` als **absolute** URL (Zod-gevalideerd in `config.ts`). In het single-host model staat de API echter op dezelfde origin als `/admin`, dus dit is technisch onnodig — maar zolang het zo is, moet je per omgeving een aparte bundel bouwen.
|
||||
|
||||
Wat doen we hiermee?
|
||||
|
||||
A) Pas `config.ts` aan zodat de API-basis-URL leeg/relatief mag zijn en standaard same-origin is — dan is één bundel geldig voor test én productie
|
||||
B) Laat het zoals het is en bouw twee keer (aparte test- en productie-build), zoals de referentie doet voor `VITE_APP_ENV`
|
||||
C) Pas het aan naar same-origin, maar houd een optionele override-variabele voor uitzonderingsgevallen
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: X, Het mag naar same-origin, maar de frontend kan ook een aparte test-build hebben net als de referentie website. Ook is het zo dat nu lokaal wel een url moet worden opgegeven. Als dat verder ook blijft werken hoeft de url variabele niet blokkerend te zijn voor een enkele bundel
|
||||
|
||||
### Question 12
|
||||
**Gemeten feit**: er is geen `appsettings.Test.json` en geen `ASPNETCORE_ENVIRONMENT`-waarde voor test. Productiegeheimen worden verwacht als environment-variabelen (`ConnectionStrings__DefaultConnection`, `JwtSettings__Secret`, etc.).
|
||||
|
||||
Hoe wordt de test/productie-configuratie geleverd?
|
||||
|
||||
A) Environment-variabelen op de host, per omgeving handmatig ingesteld — de workflow raakt ze niet
|
||||
B) De workflow schrijft ze bij deploy in een bestand (bijv. `appsettings.Production.json`) op basis van Gitea Secrets
|
||||
C) Environment-variabelen op de host voor geheimen, plus een nieuw `appsettings.Test.json` in de repo voor niet-geheime test-instellingen
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
|
||||
### Question 13
|
||||
**Gemeten feit**: Data Protection gebruikt de standaard bestandssysteem-key-ring zonder persistente store. Een redeploy die die map wist, maakt opgeslagen slave API keys **onleesbaar** — master↔slave-communicatie stopt dan tot instanties opnieuw worden toegevoegd. Dit staat al als waarschuwing in de README.
|
||||
|
||||
Nemen we dit mee in deze feature?
|
||||
|
||||
A) Ja — configureer een persistente key ring (`PersistKeysToDbContext`, gebruikt de bestaande database) zodat een redeploy veilig is
|
||||
B) Ja, maar alleen documenteren welke map bij een deploy niet overschreven mag worden — geen codewijziging
|
||||
C) Nee, buiten scope — apart oppakken
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
|
||||
---
|
||||
|
||||
## E. Monitoring en observability
|
||||
|
||||
### Question 14
|
||||
**Context**: je gebruikt Sentry plus console-logging. De referentie heeft alleen een frontend (`@sentry/react`); hier is er ook een .NET-backend.
|
||||
|
||||
Waar komt Sentry?
|
||||
|
||||
A) Beide — `Sentry.AspNetCore` in de API én `@sentry/react` in de admin-SPA
|
||||
B) Alleen de backend — de admin-SPA is intern gebruik, daar volstaat console-logging
|
||||
C) Alleen de frontend, net als de referentie
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
|
||||
### Question 15
|
||||
**Context**: de referentie gebruikt één Sentry-project voor beide omgevingen en onderscheidt ze met een `environment`-tag.
|
||||
|
||||
Hoe richten we Sentry-projecten in voor de CMS?
|
||||
|
||||
A) Eén Sentry-project voor de CMS, met `environment`-tags voor test en productie (zoals de referentie)
|
||||
B) Aparte Sentry-projecten per omgeving
|
||||
C) Aparte Sentry-projecten voor backend en frontend, elk met environment-tags
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
|
||||
### Question 16
|
||||
**Gemeten feit**: het productie-loglevel staat op `Warning` en er is alleen de console-logger. Op shared hosting is een console vaak niet zichtbaar — logs zijn dan effectief nergens.
|
||||
|
||||
Wat is de logging-opzet in productie?
|
||||
|
||||
A) Console-logging blijft (voor lokaal/test) en Sentry vangt fouten op — dat is voldoende
|
||||
B) Console plus een logbestand op de host, met dagelijkse rotatie
|
||||
C) Console plus structured logging naar Sentry inclusief informatievere niveaus dan alleen exceptions
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:C
|
||||
|
||||
### Question 17
|
||||
**Context**: `AddHealthChecks()` + `MapHealthChecks("/health")` kost geen package; `.AddDbContextCheck<ApplicationDbContext>()` kost er één. `/health` moet op de bypass-lijst van `AvailabilityMiddleware`, anders geeft de gate een 503 als een instantie is uitgezet.
|
||||
|
||||
Wat moet de health check controleren?
|
||||
|
||||
A) Alleen liveness — draait het proces? Geen database-aanroep, snelste en meest stabiele signaal voor UptimeRobot
|
||||
B) Liveness plus database-connectiviteit (`AddDbContextCheck`)
|
||||
C) Liveness, database én openstaande migraties — dan zie je ook een half-gedeployde staat
|
||||
D) Twee endpoints: `/health` voor liveness (voor UptimeRobot) en `/health/ready` met database en migraties (voor jezelf)
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
|
||||
### Question 18
|
||||
Wat moet UptimeRobot monitoren?
|
||||
|
||||
A) Alleen `/health` per omgeving
|
||||
B) `/health` plus de publieke website (`/`) — die wordt namelijk buiten de availability-gate om geserveerd
|
||||
C) `/health`, de publieke website en `/admin`
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:C
|
||||
|
||||
### Question 19
|
||||
**Context**: de referentie heeft een zelf-gehoste Umami op `analytics.slpsoftware.nl` (Podman op de Pi), met per omgeving een eigen website-ID.
|
||||
|
||||
Hoe gebruiken we Umami hier?
|
||||
|
||||
A) Hergebruik de bestaande zelf-gehoste Umami — voeg alleen nieuwe website-entries toe voor de CMS-omgevingen
|
||||
B) Nieuwe, aparte Umami-instantie voor de CMS
|
||||
C) Hergebruik de bestaande instantie, maar dit is per-klant: elke CMS-installatie krijgt zijn eigen website-ID
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: A
|
||||
|
||||
### Question 20
|
||||
**Context**: de admin-SPA is een intern beheerscherm; de publieke website komt uit een andere workspace.
|
||||
|
||||
Wat wordt er met Umami gemeten?
|
||||
|
||||
A) Alleen de publieke website — en de instructies leggen vast hoe een website-workspace het script meekrijgt
|
||||
B) Publieke website plus de admin-SPA (om te zien hoe beheerders het CMS gebruiken)
|
||||
C) Alleen de admin-SPA — de publieke website regelt zijn eigen analytics volledig zelf
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:B
|
||||
|
||||
---
|
||||
|
||||
## F. Rollback en betrouwbaarheid
|
||||
|
||||
### Question 21
|
||||
**Context**: de referentie rolt terug door een eerdere commit opnieuw te bouwen en te uploaden. Bij een .NET-app met database-migraties is dat riskanter, omdat een migratie niet zomaar terugdraait.
|
||||
|
||||
Wat is de rollback-strategie?
|
||||
|
||||
A) Opnieuw bouwen en deployen van een eerdere commit, en migraties bewust voorwaarts-compatibel houden (nooit destructief)
|
||||
B) Zoals A, plus de vorige publish-output bewaren op de host zodat je snel kunt terugzetten zonder te bouwen
|
||||
C) Zoals B, plus een database-backup vóór elke productie-deploy
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:C
|
||||
|
||||
### Question 22
|
||||
**Context**: op IIS-hosting kun je met een `app_offline.htm`-bestand de app netjes stilleggen tijdens een deploy — zonder serverconfiguratie. Zonder dat kunnen bezoekers halve bestanden of vergrendelde DLL's tegenkomen.
|
||||
|
||||
Wil je downtime-beheersing tijdens de deploy?
|
||||
|
||||
A) Ja — plaats `app_offline.htm` vóór de upload en verwijder het erna
|
||||
B) Nee, korte downtime tijdens de upload is acceptabel
|
||||
C) Ja, en gebruik daarnaast de bestaande availability-functionaliteit om de instantie in onderhoudsmodus te zetten
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
|
||||
---
|
||||
|
||||
## G. Workflow-extensies en fasering
|
||||
|
||||
### Vraag: Beveiligingsextensies
|
||||
Moeten de beveiligingsregels als harde vereisten worden afgedwongen voor dit project?
|
||||
|
||||
A) Ja — dwing alle BEVEILIGINGSREGELS af als blokkerende vereisten (aanbevolen voor productietoepassingen)
|
||||
B) Nee — sla alle BEVEILIGINGSREGELS over (geschikt voor PoC's, prototypes en experimentele projecten)
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
|
||||
### Vraag: Property-Based Testing Extensie
|
||||
Moeten de property-based testing (PBT) regels worden afgedwongen voor dit project?
|
||||
|
||||
A) Ja — dwing alle PBT-regels af als blokkerende vereisten (aanbevolen voor projecten met bedrijfslogica, datatransformaties, serialisatie of stateful componenten)
|
||||
B) Gedeeltelijk — dwing PBT-regels alleen af voor pure functies en serialisatie round-trips (geschikt voor projecten met beperkte algoritmische complexiteit)
|
||||
C) Nee — sla alle PBT-regels over (geschikt voor eenvoudige CRUD-applicaties, UI-only projecten of dunne integratielagen zonder significante bedrijfslogica)
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:C
|
||||
|
||||
### Vraag: Operations-fase
|
||||
Moet deze feature na Construction door de Operations-fase (deployment- en monitoring-opzet)?
|
||||
|
||||
**Toelichting**: voor deze feature is dat vermoedelijk het zwaartepunt — de deployment-instructies, monitoring-opzet en productie-readiness-checklist horen daar thuis.
|
||||
|
||||
A) Ja — draai de Operations-fase na Construction (deployment + monitoring setup)
|
||||
B) Nee — stop na Build and Test (deployment/monitoring vallen buiten scope voor deze feature)
|
||||
C) Nog niet zeker — vraag het me opnieuw na de Construction-fase
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
@@ -0,0 +1,366 @@
|
||||
# Requirements — Gitea Deployment Workflow
|
||||
|
||||
**Feature**: `gitea-deployment-workflow`
|
||||
**Branch**: `feature/gitea-deployment-workflow`
|
||||
**Date**: 2026-07-27
|
||||
**Requirements depth**: Comprehensive
|
||||
|
||||
---
|
||||
|
||||
## 1. Intent Analysis
|
||||
|
||||
### User Request (verbatim, as logged in `audit.md`)
|
||||
> "Ik wil een Gitea workflow gaan opzetten om de CMS te kunnen deployen. In de instructies moet ook behandeld worden waar de frontend van de website moet gaan komen, maar de workflow voor de website zelf zal hierin niet worden gebouwd, dat wordt per website in hun eigen workspace gedaan. Gebruik de aidlc workflow om alles op te zetten. Voor Uptime gebruik ik UptimeRobot, voor analytics gebruik ik Umami en voor logging console logs samen met Sentry. Ik heb in de workspace `K:\Development\SlpSoftware\Projects\SlpSoftware` al een werkende workflow. Dit is voor een react vite front-end, maar is een goed startpunt voor deze workspace. Op dit moment zijn er alleen een lokale omgeving, test en productie. Het doel van deze workflow en eventuele deployment-opzet is dat we boel moeten kunnen uploaden als een .NET-applicatie en dat we geen server-configuratie hoeven te doen omdat dat voor andere websites waarschijnlijk niet kan. Vandaar dat ik een opzet wilde met de api en de 2 frontends als 1 website."
|
||||
|
||||
### Analysis
|
||||
|
||||
| Dimension | Assessment |
|
||||
|---|---|
|
||||
| **Request Clarity** | Clear on goals and tooling; initially incomplete on hosting/transport, migration delivery and `wwwroot` ownership. All resolved across two clarification rounds. |
|
||||
| **Request Type** | New Feature (CI/CD + operational tooling), with supporting Enhancement work inside the application. |
|
||||
| **Scope Estimate** | Multiple Components — new `.gitea/workflows/`, changes to `SlpModularCms.Api` (`Program.cs`, `.csproj`), `SlpModularCms.Core` (security headers, Data Protection, health checks), `SlpModularCms.Modules.Availability` (bypass list), `frontend/` (config, Sentry, Umami, lint fixes), plus documentation and operations artifacts. |
|
||||
| **Complexity Estimate** | Moderate-to-Complex. Not algorithmically hard; the difficulty is the constraint set — one process serving three surfaces, a customer-owned `wwwroot` that must survive deploys, no server configuration permitted, two different hosting targets, asymmetric migration behaviour, and build-time environment coupling in the SPA. |
|
||||
| **Primary Business Driver** | Make deployment of the CMS repeatable and safe on hosting where nothing can be configured server-side, so the product can be shipped per customer without bespoke server work. |
|
||||
|
||||
### Core Constraint (drives most design decisions)
|
||||
**No server configuration may be required.** This is why the API serves the public website, the admin SPA and the API from one process, and it is why anything normally solved in nginx or IIS configuration — security headers, routing, SPA fallbacks — must live inside the application instead.
|
||||
|
||||
---
|
||||
|
||||
## 2. Scope
|
||||
|
||||
### In Scope
|
||||
- Gitea Actions CI workflow with build, test and lint gates for both backend and frontend.
|
||||
- Gitea Actions deploy workflow: automated to test, explicit opt-in to production.
|
||||
- `wwwroot` restructuring so the customer's public website can never be destroyed by a CMS deploy.
|
||||
- A documented contract for what a per-website workspace must deliver.
|
||||
- A dedicated health-check endpoint.
|
||||
- Sentry (backend + frontend), Umami analytics, UptimeRobot monitoring.
|
||||
- HTTP security-headers middleware.
|
||||
- Persistent Data Protection key ring.
|
||||
- Automatic `ApplicationDbContext` migration at startup.
|
||||
- Same-origin default for the admin SPA's API base URL.
|
||||
- Fixing the 5 existing frontend lint errors and pinning 2 vulnerable transitive packages.
|
||||
- Rollback strategy including pre-deploy database backup.
|
||||
|
||||
### Explicitly Out of Scope
|
||||
- **The public website's own build and deploy workflow.** Handled per website in its own workspace (user's explicit instruction). This feature delivers only the contract and the instructions.
|
||||
- **Any fourth environment.** Only local, test and production exist.
|
||||
- **Backend integration/contract/e2e tests.** Their absence is recorded in `code-quality-assessment.md` as existing technical debt; adding them is not part of this feature.
|
||||
- **Code coverage thresholds** (Q9: F not selected).
|
||||
- **Property-based testing** (extension opted out).
|
||||
- **90-day log retention** — accepted as a documented deviation (see § 6, DEV-01).
|
||||
|
||||
---
|
||||
|
||||
## 3. Decisions Register
|
||||
|
||||
Every decision below traces to a question answer. `Q*` = first round, `CQ*` = clarification round.
|
||||
|
||||
### Hosting and transport
|
||||
|
||||
| ID | Decision | Source |
|
||||
|---|---|---|
|
||||
| D-01 | **Test** runs on the user's own Raspberry Pi infrastructure with a .NET runtime. | Q2 = B |
|
||||
| D-02 | **Production** runs on the Pi for now, with shared hosting as the eventual target. The transport layer must be structured so FTPS can be added alongside SSH/SCP later **without restructuring the workflow**. | CQ1 = C |
|
||||
| D-03 | Transport is **SSH/SCP** (or SFTP — same SSH transport). FTPS is not needed yet. Confirmed viable because FileZilla connects to the Pi over SFTP, which proves SSH is available. | Q1 = X + CQ1 = C |
|
||||
| D-04 | Workflow runs on the **existing self-hosted Podman runner**, with the toolchain installed explicitly in the workflow via `actions/setup-dotnet` and `pnpm/action-setup` rather than assumed present. | Q3 = B |
|
||||
| D-05 | Container-based actions must be avoided in favour of plain shell steps, because they fail on this runner with a 409 attach error. | Reference project's documented experience |
|
||||
|
||||
### `wwwroot` layout and the public website
|
||||
|
||||
| ID | Decision | Source |
|
||||
|---|---|---|
|
||||
| D-06 | The public website moves into **`wwwroot/web/`** (user's chosen name, not `site`). Final layout: `wwwroot/admin/` (CMS admin SPA, shipped by this repo) and `wwwroot/web/` (customer website, deployed separately). Requires a change to `Program.cs`. | Q4 = C |
|
||||
| D-07 | An additional `wwwroot` folder for the API is kept as an option but is **not required** — see ASM-02. | Q4 = C ("eventueel … indien nodig") |
|
||||
| D-08 | The website-workspace contract documents the target path **plus technical constraints**: routing/SPA-fallback behaviour, forbidden paths and filenames, and how to call `/api/v1` from the website. A ready-made YAML deploy snippet is not required. | Q5 = B |
|
||||
|
||||
### Workflow shape and gates
|
||||
|
||||
| ID | Decision | Source |
|
||||
|---|---|---|
|
||||
| D-09 | Trigger strategy is **identical to the reference**: pull requests validate; push/merge to `master` deploys to test; production only via manual `workflow_dispatch` with an explicit `deploy_production` checkbox. | Q6 = A |
|
||||
| D-10 | Pipeline gates: backend build, backend tests, frontend type-check + build, frontend tests, frontend lint/format-check. **No coverage threshold.** | Q9 = A,B,C,D,E |
|
||||
| D-11 | The 5 existing frontend lint errors are **fixed within this feature**, after which the lint gate is blocking. | Q7 = A |
|
||||
| D-12 | A **blocking** vulnerability gate is added, and `Microsoft.OpenApi` and `System.Security.Cryptography.Xml` are pinned to patched versions within this feature. | Q8 = A |
|
||||
|
||||
### Database and configuration
|
||||
|
||||
| ID | Decision | Source |
|
||||
|---|---|---|
|
||||
| D-13 | `ApplicationDbContext` migrates **automatically at startup**, matching the two module contexts. Deployment becomes self-contained; no CLI access to the host is needed. | Q10 = C |
|
||||
| D-14 | The admin SPA defaults to **same-origin** for the API base URL, with an optional explicit override that must keep working for local development. | Q11 = X |
|
||||
| D-15 | **Two builds** are produced (test and production) with their own Vite variables, exactly like the reference — because Sentry's `environment` tag and Umami's per-environment website ID are build-time values. Runtime-delivered configuration is recorded as a possible later improvement, not built now. | CQ2 = C |
|
||||
| D-16 | Test and production configuration comes from **environment variables on the host**, set manually per environment. The workflow does not write or manage them. | Q12 = A |
|
||||
| D-17 | Data Protection uses a **persistent key ring via `PersistKeysToDbContext`**, reusing the existing database, so a redeploy can never make stored slave API keys unreadable. | Q13 = A |
|
||||
|
||||
### Monitoring and observability
|
||||
|
||||
| ID | Decision | Source |
|
||||
|---|---|---|
|
||||
| D-18 | Sentry on **both sides**: `Sentry.AspNetCore` in the API and `@sentry/react` in the admin SPA. | Q14 = A |
|
||||
| D-19 | **One** Sentry project for the CMS, with `environment` tags distinguishing test from production. | Q15 = A |
|
||||
| D-20 | Logging is console **plus structured logging to Sentry**, at levels more informative than exceptions alone. | Q16 = C |
|
||||
| D-21 | Health check is **liveness only** — no database call. Fastest and most stable signal, and it must not go red for reasons unrelated to the process being alive. | Q17 = A |
|
||||
| D-22 | `/health` must be added to `AvailabilityMiddleware._bypassPrefixes`, or the availability gate returns 503 for it on a disabled instance. | Established during Reverse Engineering |
|
||||
| D-23 | UptimeRobot monitors **`/health`, the public website (`/`) and `/admin`** per environment. | Q18 = C |
|
||||
| D-24 | **Reuse the existing self-hosted Umami** (`analytics.slpsoftware.nl`); add new website entries for the CMS environments. | Q19 = A |
|
||||
| D-25 | Umami measures **both the public website and the admin SPA**. | Q20 = B |
|
||||
|
||||
### Reliability
|
||||
|
||||
| ID | Decision | Source |
|
||||
|---|---|---|
|
||||
| D-26 | Rollback = rebuild and redeploy an earlier commit, **plus** retaining the previous publish output on the host for a fast restore without building, **plus** a database backup before every production deploy. Migrations must be kept forward-compatible and never destructive. | Q21 = C |
|
||||
| D-27 | Downtime control uses an **atomic release-directory switch**: deploy into a new directory, then switch. Shortest downtime, and the public website stays continuously reachable. `app_offline.htm` is not used, since it is an IIS-only mechanism that has no effect on the Pi. | Q22 = A → corrected to CQ3 = C |
|
||||
|
||||
### Security
|
||||
|
||||
| ID | Decision | Source |
|
||||
|---|---|---|
|
||||
| D-28 | The Security Baseline extension is **enabled and blocking**. | Extension opt-in = A |
|
||||
| D-29 | Property-Based Testing is **disabled**. | Extension opt-in = C |
|
||||
| D-30 | **SECURITY-04 and SECURITY-14 are both addressed within this feature**: security-headers middleware (with CSP exceptions for Umami and Sentry) and alerting through Sentry alert rules, with log retention documented as a deliberate deviation. | CQ4 = A |
|
||||
| D-31 | CSP is **strict for `/admin` and `/api/v1`, and more permissive for the public website**, so a website builder is not constrained by a policy they never see. | CQ5 = B |
|
||||
| D-32 | The Operations phase **runs** after Construction. | Operations opt-in = A |
|
||||
|
||||
---
|
||||
|
||||
## 4. Functional Requirements
|
||||
|
||||
### FR-01 — Continuous integration workflow
|
||||
A `.gitea/workflows/continuous_integration.yaml` MUST exist that triggers on:
|
||||
- `pull_request` (opened, synchronize, reopened) — validation only, any branch
|
||||
- `push` to `master` — validation plus test deploy
|
||||
- `workflow_dispatch` — with a `deploy_production` boolean input, default `false`
|
||||
|
||||
It MUST run these gates, all blocking (D-10, D-11, D-12):
|
||||
1. `dotnet build SlpModularCms.sln -c Release`
|
||||
2. `dotnet test SlpModularCms.sln -c Release`
|
||||
3. `dotnet list package --vulnerable --include-transitive` — fails the build on any advisory
|
||||
4. `pnpm run build` in `frontend/` (includes `tsc -b`)
|
||||
5. `pnpm test` in `frontend/`
|
||||
6. `pnpm run lint` and `pnpm run format:check` in `frontend/`
|
||||
|
||||
Tool versions MUST be pinned (`actions/setup-dotnet`, `pnpm/action-setup`) and no action may rely on a `latest` tag (D-04, SECURITY-10).
|
||||
|
||||
### FR-02 — Reusable deploy workflow
|
||||
A `.gitea/workflows/deploy.yaml` MUST exist as a `workflow_call` workflow accepting at minimum `artifact_name`, `environment` and `deploy_path`. It MUST use plain shell steps rather than container actions (D-05).
|
||||
|
||||
Its transport step MUST be structured so a second transport (FTPS) can be added later as an alternative without restructuring the workflow or its interface (D-02) — for example by taking the transport as an input with SSH/SCP as the only implemented value for now.
|
||||
|
||||
Environment-specific paths MUST come from Gitea Actions **variables** and credentials from **secrets**, never hardcoded, following the reference project's split.
|
||||
|
||||
### FR-03 — Test deployment
|
||||
On push/merge to `master`, or on any `workflow_dispatch` run, the pipeline MUST deploy to the test environment on the Pi automatically (D-01, D-09).
|
||||
|
||||
### FR-04 — Production deployment
|
||||
Production MUST deploy **only** on a `workflow_dispatch` run with `deploy_production = true`. Pushing to `master` MUST NOT be able to deploy production under any circumstance (D-09).
|
||||
|
||||
### FR-05 — Separate test and production builds
|
||||
The pipeline MUST produce two distinct build artifacts, each built with its own environment-specific Vite variables (`VITE_APP_ENV`, `VITE_SENTRY_DSN`, `VITE_UMAMI_SCRIPT_URL`, `VITE_UMAMI_WEBSITE_ID_<ENV>`) so Sentry events and analytics are attributed to the correct environment (D-15).
|
||||
|
||||
### FR-06 — Atomic release switch
|
||||
Deployment MUST place the new publish output in a fresh directory and then switch to it atomically, rather than overwriting a live directory (D-27). The application process MUST be restarted as part of the switch, since a running .NET process holds its assemblies.
|
||||
|
||||
The previous release directory MUST be retained on the host to allow a fast restore without rebuilding (D-26).
|
||||
|
||||
### FR-07 — `wwwroot` restructuring
|
||||
`Program.cs` MUST serve the public website from `wwwroot/web/` at `/` instead of directly from `wwwroot/` (D-06). The existing behaviour MUST be preserved:
|
||||
- `/` and non-file paths fall back to the public website's `index.html`
|
||||
- `/admin` and `/admin/**` non-file paths fall back to `wwwroot/admin/index.html`
|
||||
- Missing paths that look like files (with an extension) still return `404`
|
||||
|
||||
`frontend/vite.config.ts` keeps `base: '/admin/'` for builds; no change is needed there.
|
||||
|
||||
### FR-08 — The public website must survive every CMS deploy
|
||||
No deployment step may delete, overwrite or mirror-sync the contents of `wwwroot/web/` (D-06). Because releases are switched atomically (FR-06), `wwwroot/web/` MUST live outside the swapped release directory and be linked or mounted into it — otherwise an atomic switch would silently discard the customer's website along with the old release. See ASM-01.
|
||||
|
||||
### FR-09 — Website workspace contract
|
||||
Documentation MUST specify, for a per-website workspace (D-08):
|
||||
- The exact target path (`wwwroot/web/`) and required structure, including `index.html`
|
||||
- That `wwwroot/admin/` and the application root are forbidden targets
|
||||
- How SPA-fallback routing behaves for the website, and which paths are reserved (`/admin`, `/api/v1`, `/health`)
|
||||
- How to call `/api/v1` from the website — same-origin, so relative URLs work and no CORS configuration is needed
|
||||
- Which CSP applies to the public website and what that permits (D-31)
|
||||
- How to include the Umami tracking script (D-25)
|
||||
|
||||
### FR-10 — Health-check endpoint
|
||||
The application MUST expose `GET /health` returning `200`/`Healthy` when the process is alive and `503`/`Unhealthy` otherwise, using `AddHealthChecks()` and `MapHealthChecks("/health")` (D-21). It MUST NOT perform a database call.
|
||||
|
||||
`/health` MUST be added to `AvailabilityMiddleware._bypassPrefixes` so the availability gate cannot mask it (D-22).
|
||||
|
||||
`/health` MUST NOT be presented as, or conflated with, `/api/v1/Availability/status` or `/api/v1/System/capabilities`, which are CMS domain functionality.
|
||||
|
||||
### FR-11 — Automatic Core migrations
|
||||
`ApplicationDbContext` MUST be migrated automatically at application startup, consistent with `AvailabilityDbContext` and `MasterDbContext` (D-13), so a fresh deployment needs no CLI access to the host.
|
||||
|
||||
Migrations MUST be forward-compatible and non-destructive, so redeploying an earlier commit remains a valid rollback (D-26).
|
||||
|
||||
### FR-12 — Persistent Data Protection key ring
|
||||
Data Protection MUST persist keys via `PersistKeysToDbContext` against the existing database (D-17), so redeploys and the atomic release switch cannot render stored slave API keys unreadable.
|
||||
|
||||
### FR-13 — Same-origin API base URL for the admin SPA
|
||||
`frontend/src/lib/config.ts` MUST treat an absent or empty `VITE_API_BASE_URL` as same-origin, while continuing to accept an explicit absolute URL for local development against `https://localhost:7221` or `:7222` (D-14). Its Zod validation MUST be relaxed accordingly, without silently accepting malformed values.
|
||||
|
||||
### FR-14 — Sentry on the backend
|
||||
`Sentry.AspNetCore` MUST be integrated into the API, reporting unhandled exceptions and structured logs at levels beyond exceptions alone (D-18, D-20), tagged with the environment (D-19). Sentry MUST be optional: an absent DSN disables it and leaves console logging active, mirroring the reference project's behaviour.
|
||||
|
||||
### FR-15 — Sentry in the admin SPA
|
||||
`@sentry/react` MUST be integrated into the admin SPA with environment and release tags, and MUST be skipped gracefully when no DSN is configured (D-18, D-19).
|
||||
|
||||
### FR-16 — Umami analytics
|
||||
The Umami tracking script MUST be included in the admin SPA, using per-environment website IDs from build-time variables, and MUST be absent during local development (D-24, D-25). Website entries are added to the existing self-hosted Umami instance. Inclusion for the public website is covered by FR-09.
|
||||
|
||||
### FR-17 — UptimeRobot monitors
|
||||
Documentation MUST specify UptimeRobot monitors for `/health`, `/` and `/admin`, per environment (D-23), including what each one does and does not prove.
|
||||
|
||||
### FR-18 — HTTP security headers
|
||||
Middleware MUST set, on all HTML-serving responses (D-30, SECURITY-04):
|
||||
|
||||
| Header | Value |
|
||||
|---|---|
|
||||
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` |
|
||||
| `X-Content-Type-Options` | `nosniff` |
|
||||
| `X-Frame-Options` | `DENY` |
|
||||
| `Referrer-Policy` | `strict-origin-when-cross-origin` |
|
||||
| `Content-Security-Policy` | Path-scoped per D-31 |
|
||||
|
||||
CSP MUST be strict for `/admin` and `/api/v1` (`default-src 'self'` plus explicit allowances for the Umami script origin and Sentry's ingest endpoint) and more permissive for the public website (D-31). Any use of `unsafe-inline` or `unsafe-eval` MUST be documented with justification.
|
||||
|
||||
### FR-19 — Security alerting
|
||||
Sentry alert rules MUST be configured for repeated authentication failures and authorization violations (D-30, SECURITY-14). The corresponding events MUST be emitted by the application with enough context to alert on, and MUST NOT contain passwords, tokens or PII.
|
||||
|
||||
### FR-20 — Database backup before production deploy
|
||||
A database backup MUST be taken before every production deployment (D-26). If it cannot be automated from the runner, the deployment instructions MUST make it an explicit, verifiable manual step in the production procedure.
|
||||
|
||||
### FR-21 — Fix existing lint errors
|
||||
The 5 errors and 1 warning currently reported by `pnpm run lint` MUST be fixed (D-11): `AddCmsInstanceDialog.tsx:55`, `InviteUserDialog.tsx:50` and `:54`, `SettingsPage.tsx:40`, `SetStatusDialog.tsx:32` and `:72`.
|
||||
|
||||
### FR-22 — Pin vulnerable packages
|
||||
`Microsoft.OpenApi` (currently 2.0.0) and `System.Security.Cryptography.Xml` (currently 10.0.9) MUST be pinned to patched versions so the blocking vulnerability gate passes (D-12).
|
||||
|
||||
### FR-24 — Validate the token in the availability gate's admin bypass
|
||||
*(Added 2026-07-27 at Application Design, Q12 = A — resolves OPEN-02.)*
|
||||
|
||||
`AvailabilityMiddleware.IsAdminBypass` currently calls `JwtSecurityTokenHandler.ReadJwtToken`, which parses a token **without validating its signature**. An unauthenticated caller can therefore forge an unsigned token carrying an `Owner` role claim and bypass the availability gate.
|
||||
|
||||
The bypass MUST instead rely on a properly authenticated principal — either by validating the token with the same parameters used by the JWT bearer scheme, or by evaluating `HttpContext.User` after authentication has run. Protected endpoints already authenticate correctly, so this is not a privilege escalation; the defect is that the gate itself is bypassable. *(SECURITY-08.)*
|
||||
|
||||
Existing behaviour that MUST be preserved: an Owner or Administrator with a **valid** token still passes the gate, so administrators can always reach the system to switch it back on.
|
||||
|
||||
### FR-23 — Deployment and rollback documentation
|
||||
Operations artifacts MUST document: one-time host setup, required Gitea variables and secrets, the deployment procedure per environment, the rollback procedure, and how to switch production to FTPS when it moves to shared hosting (D-02).
|
||||
|
||||
---
|
||||
|
||||
## 5. Non-Functional Requirements
|
||||
|
||||
### NFR-01 — No server configuration required
|
||||
Nothing in the deployment may depend on configuring nginx, IIS, or any server-side software beyond placing files and running the application. Anything normally handled by server configuration — security headers, routing, SPA fallbacks — must be handled inside the application. *(Core constraint.)*
|
||||
|
||||
### NFR-02 — Deployment safety
|
||||
No deployment step may destroy data it does not own. Specifically: the customer's public website (FR-08), the database (FR-11, FR-20), and Data Protection keys (FR-12). This is the single highest-priority non-functional property of this feature.
|
||||
|
||||
### NFR-03 — Production cannot be deployed accidentally
|
||||
Production deployment requires a deliberate, explicit action. No push, merge or ordinary workflow run may reach production (FR-04).
|
||||
|
||||
### NFR-04 — Minimal and predictable downtime
|
||||
Deployment downtime is limited to the process restart in the atomic switch (FR-06). The public website remains reachable throughout the CMS deploy.
|
||||
|
||||
### NFR-05 — Reproducible builds
|
||||
Tool versions and actions are pinned; `pnpm install --frozen-lockfile` is used. *(SECURITY-10; note the absence of `packages.lock.json` — see DEV-02.)*
|
||||
|
||||
### NFR-06 — Observability sufficient to trust a deployment
|
||||
After a deploy it must be possible to determine, without host access, whether the application is alive (FR-10), whether it is throwing errors (FR-14, FR-15), and whether it is being used (FR-16). This matters more than usual here because `ModuleOrchestrator` logs rather than throws when a module fails to load, so a deployment can silently start with reduced capability.
|
||||
|
||||
### NFR-07 — Secrets never in source or logs
|
||||
No credential, connection string or DSN in the repository or in workflow output. Credentials live in Gitea Actions secrets; runtime secrets live in host environment variables (D-16). *(SECURITY-12.)*
|
||||
|
||||
### NFR-08 — Separation of health from product state
|
||||
Infrastructure health (`/health`) and CMS domain state (availability, capabilities) remain strictly separate concerns, in both implementation and documentation (FR-10).
|
||||
|
||||
### NFR-09 — Extensibility to a second hosting target
|
||||
Adding FTPS for shared hosting must not require restructuring the workflow (FR-02, D-02).
|
||||
|
||||
### NFR-10 — Maintainability of the workflow
|
||||
Environment-specific values are declared once and referenced, not duplicated across jobs — following the reference project's `env:` block plus `config` job pattern, which exists because the `env` context is unavailable in a reusable workflow's `with:` block.
|
||||
|
||||
---
|
||||
|
||||
## 6. Security Compliance (Security Baseline extension — enabled, blocking)
|
||||
|
||||
Assessed at Requirements Analysis. Rules are evaluated against what this feature's requirements commit to.
|
||||
|
||||
| Rule | Status | Notes |
|
||||
|---|---|---|
|
||||
| SECURITY-01 Encryption at rest/transit | **Addressed** | Connection strings must enforce TLS (`Encrypt=True`); HSTS via FR-18. Database-level encryption at rest depends on the host and is a documented host-setup step in FR-23. |
|
||||
| SECURITY-02 Access logging on intermediaries | **N/A** | No load balancer, API gateway or CDN is part of this architecture — the application is the only network-facing component, covered by SECURITY-03. |
|
||||
| SECURITY-03 Application-level logging | **Addressed** | FR-14, FR-20 (D-20): structured logging to Sentry with environment tagging, no secrets or PII. Correlation/request ID must be included — see OPEN-01. |
|
||||
| SECURITY-04 HTTP security headers | **Addressed** | FR-18, with the path-scoped CSP of D-31. |
|
||||
| SECURITY-05 Input validation | **Pre-existing, unchanged** | The API validates via model binding and typed requests, uses EF Core parameterised queries, and returns RFC 9457. This feature adds no new input surface except `/health`, which takes no input. |
|
||||
| SECURITY-06 Least-privilege access policies | **Addressed** | Deploy credentials must be scoped to the deployment target only, and Gitea secrets scoped to this repository (FR-02, FR-23). |
|
||||
| SECURITY-07 Restrictive network configuration | **Partially N/A** | No cloud networking to configure. What applies — not exposing the database publicly, and restricting SSH access — is a documented host-setup requirement in FR-23. |
|
||||
| SECURITY-08 Application-level access control | **Improved** | Hierarchical policies, JWT validation, per-origin CORS are pre-existing. This feature adds `/health` as a deliberately anonymous endpoint exposing no data beyond liveness, and **fixes** the pre-existing unvalidated-JWT gap in `AvailabilityMiddleware.IsAdminBypass` (`code-quality-assessment.md` item 16) via FR-24. |
|
||||
| SECURITY-09 Hardening and misconfiguration | **Addressed** | Scalar/OpenAPI already Development-only; production errors already go through `GlobalExceptionHandler` as `ProblemDetails`; no default credentials (D-16); static-file serving must not enable directory browsing. |
|
||||
| SECURITY-10 Supply chain | **Addressed** | Blocking vulnerability gate (FR-01, FR-22), pinned tool versions and actions (FR-01), `--frozen-lockfile`. SBOM generation and the missing `packages.lock.json` — see DEV-02. |
|
||||
| SECURITY-11 Secure design | **Addressed** | Rate limiting already exists on login/refresh; security-critical logic is already isolated in `Core/Identity`; the misuse case explicitly considered by this feature is a deployment destroying the customer's website (NFR-02) and a redeploy silently breaking master↔slave trust (FR-12). |
|
||||
| SECURITY-12 Authentication and credentials | **Pre-existing, unchanged, plus FR-12** | Identity password policy meets the 8-character minimum with complexity; httpOnly/Secure/SameSite refresh cookie; rate-limited login. Breached-password-list checking and MFA are **not** implemented — see DEV-03. |
|
||||
| SECURITY-13 Software and data integrity | **Addressed** | External scripts (Umami) must be loaded with SRI where the provider supports it, and are constrained by CSP (FR-18). Pipeline definitions are version-controlled and reviewable. Data-modification auditing beyond what exists — see DEV-04. |
|
||||
| SECURITY-14 Alerting and monitoring | **Addressed with documented deviation** | Alerting via FR-19; monitoring via FR-17. Retention deviates — see DEV-01. |
|
||||
| SECURITY-15 Exception handling and fail-safe defaults | **Pre-existing, unchanged** | `GlobalExceptionHandler` is registered globally; `UseExceptionHandler` is first in the pipeline; the availability gate fails closed while the master gate fails open **by deliberate design** (an unreachable Master must never permanently disable a customer site) — documented as an intentional, business-driven exception to "fail closed". |
|
||||
|
||||
### Documented Deviations
|
||||
|
||||
| ID | Deviation | Rationale | Decided |
|
||||
|---|---|---|---|
|
||||
| DEV-01 | **Log retention below the 90-day SECURITY-14 minimum.** Sentry's free plan retains events for ~30 days. | Accepted knowingly; extending retention is a cost decision, not a technical one. Revisit if a compliance requirement appears. | CQ4 = A |
|
||||
| DEV-02 | **No `packages.lock.json` for .NET projects, and no SBOM generation.** | The frontend is locked via `pnpm-lock.yaml`; the .NET side relies on the blocking vulnerability gate instead. Recorded as follow-up work rather than silently ignored. | Derived from SECURITY-10 assessment |
|
||||
| DEV-03 | **No breached-password-list checking and no MFA.** | Pre-existing product scope, unrelated to deployment. Belongs to an Identity feature, not this one. | Derived from SECURITY-12 assessment |
|
||||
| DEV-04 | **No before/after audit trail on critical data changes.** | Pre-existing; `CmsInstance` does track `LastStatusPushedAt`/`LastContactedAt`, but there is no general audit log. Out of scope for a deployment feature. | Derived from SECURITY-13 assessment |
|
||||
| DEV-05 | **Data Protection keys are stored unencrypted at rest** in the database, whereas SECURITY-01 requires encryption at rest for persisted data. *(Added 2026-07-27 at U2 Functional Design.)* | DPAPI is unavailable on Linux, and X.509 certificate encryption relocates the loss problem to the certificate — reintroducing the very failure mode FR-12 exists to eliminate. Compensating controls per BR-U2-08: TLS enforced on the database connection, and the database not publicly reachable. Certificate-based encryption is recorded as a separate follow-up. | U2 FD Q2 = C |
|
||||
|
||||
DEV-01…04 are **pre-existing or cost-driven** and none is introduced by this feature. **DEV-05 is the one deviation this feature does introduce** — it is a consequence of moving the key ring into the database, which on balance removes a far larger risk (silent, permanent loss of Master↔slave trust on every redeploy) than it adds.
|
||||
|
||||
---
|
||||
|
||||
## 7. Assumptions
|
||||
|
||||
| ID | Assumption | Why it matters | If wrong |
|
||||
|---|---|---|---|
|
||||
| ASM-01 | With the atomic release switch (FR-06), `wwwroot/web/` must live **outside** the swapped release directory and be linked into it (e.g. a symlink to a persistent path on the host). | Otherwise switching releases silently discards the customer's website — precisely the failure mode D-06 was chosen to prevent. This follows necessarily from combining Q4 = C with CQ3 = C, so it is stated rather than asked. | Raise it and the deploy design changes materially; flag before Construction if this is not acceptable. |
|
||||
| ASM-02 | **No `wwwroot` folder is needed for the API.** The API is not static content — its assemblies live in the application root and it serves `/api/v1` through routing. The option is kept open but nothing is built for it. | Avoids building an unused folder. | If something static under an API path is intended, say so and FR-07 gains a third mount. |
|
||||
| ASM-03 | The Pi already runs, or can run, a .NET 10 runtime, and the app is managed by a process manager (systemd) that the deploy can restart over SSH. | The atomic switch requires restarting the process. | Restart mechanism changes; deploy step is rewritten. |
|
||||
| ASM-04 | The Pi's SQL Server database is reachable from the application, and a backup can be taken before a production deploy. | FR-20 depends on it. | FR-20 becomes a documented manual precondition only. |
|
||||
| ASM-05 | The existing Umami instance at `analytics.slpsoftware.nl` remains available and its script origin can be added to the CSP. | FR-16, FR-18. | Umami setup gains host work, as in the reference project. |
|
||||
| ASM-06 | One Sentry project with environment tags is acceptable for both backend and frontend events of this CMS. | D-19. | Split into more projects; only configuration changes. |
|
||||
| ASM-07 | Existing Gitea secrets for the Pi (`PI_MAIN_*` in the reference project) can be reused or replicated for this repository. | FR-02. | New secrets are created; documented in FR-23. |
|
||||
|
||||
---
|
||||
|
||||
## 8. Open Items
|
||||
|
||||
| ID | Item | To be resolved |
|
||||
|---|---|---|
|
||||
| OPEN-01 | **Correlation/request ID in logs** is required by SECURITY-03 but does not exist today. Needs a decision on mechanism (ASP.NET Core `TraceIdentifier` versus `W3C traceparent`). | NFR Design / Construction |
|
||||
| ~~OPEN-02~~ | ~~`AvailabilityMiddleware.IsAdminBypass` reads the JWT without validating its signature.~~ **RESOLVED 2026-07-27** at Application Design (Q12 = A): folded into this feature as **FR-24**, landing in the same unit as the `/health` bypass since both touch the same middleware. | Closed |
|
||||
| OPEN-03 | **Exact patched versions** for `Microsoft.OpenApi` and `System.Security.Cryptography.Xml` (FR-22) must be resolved and verified against the advisories. | Construction |
|
||||
| OPEN-04 | **Whether production stays on the Pi long enough** that FTPS is never built. D-02 requires only that the design allows it; the trigger for actually building it is a business decision. | Deferred by design |
|
||||
|
||||
---
|
||||
|
||||
## 9. Summary
|
||||
|
||||
This feature turns a manually deployed modular-monolith CMS into one with an automated, auditable pipeline, on hosting where nothing can be configured server-side.
|
||||
|
||||
**24 functional requirements, 10 non-functional requirements, 32 traced decisions, 7 assumptions, 3 remaining open items, 4 documented security deviations.**
|
||||
|
||||
*(FR-24 added and OPEN-02 closed at Application Design on 2026-07-27.)*
|
||||
|
||||
The three requirements that carry the most risk if implemented carelessly:
|
||||
|
||||
1. **FR-08 with ASM-01** — the customer's public website must survive every deploy. Combining the `wwwroot/web/` split with an atomic release switch protects it, but only if `web/` lives outside the swapped directory.
|
||||
2. **FR-12** — a persistent Data Protection key ring, without which a redeploy silently breaks master↔slave trust in a way that looks like a network problem.
|
||||
3. **FR-04** — production must be unreachable by accident.
|
||||
|
||||
Three findings from Reverse Engineering are resolved as a by-product rather than left as debt: the migration asymmetry (FR-11), the environment-coupled SPA bundle (FR-13), and the ephemeral key ring (FR-12).
|
||||
Reference in New Issue
Block a user