Compare commits
85 Commits
feat/5-acl
...
feat/67-se
| Author | SHA1 | Date | |
|---|---|---|---|
| 074101e836 | |||
| 5089c2aea6 | |||
| 29f3dcc6cf | |||
| 2c196245c2 | |||
| 72c2bdfae7 | |||
| 311aab0aba | |||
| fcdb117768 | |||
| c3f0710a18 | |||
| 7c363099ff | |||
| a069ab07a2 | |||
| 34969659f7 | |||
| fd90c4abe2 | |||
| 3824f85af6 | |||
| ef877ebc80 | |||
| 9c961f9a13 | |||
| 0b82841b14 | |||
| 5a4331a416 | |||
| 96d447832f | |||
| a07d8277d6 | |||
| 69d6e80378 | |||
| 5d32d4f15e | |||
| d767430ad7 | |||
| 751ca006a7 | |||
| fea806848b | |||
| 2f5d656b54 | |||
| 72efab3ae0 | |||
| 1edd34e2db | |||
| f885e0a3be | |||
| ac874bf746 | |||
| 67f0ffb88d | |||
| 5a3f28ac6d | |||
| e9a873c152 | |||
| 79dcd8f14b | |||
| 22ab38f328 | |||
| 0d34d60797 | |||
| 6d4adaf957 | |||
| 39b2388a9d | |||
| 8d176c2603 | |||
| 53751fd1bc | |||
| cc9e7852e1 | |||
| c9edf27a48 | |||
| c3ccffe417 | |||
| 0d0778036e | |||
| fa8382fc02 | |||
| 06d8d13e19 | |||
| a111e5cc20 | |||
| 7ef63c7ae9 | |||
| 017cd5e66b | |||
| c70840e5b7 | |||
| 32c98f00db | |||
| d49443353e | |||
| a256db1a23 | |||
| 4d07285dcd | |||
| f3e9db7147 | |||
| 86cc65f4d9 | |||
| 4474585606 | |||
| 3829cb0b68 | |||
| 855a5565fe | |||
| 09de500fb8 | |||
| 4322c607cb | |||
| d0582cef65 | |||
| f2e575b427 | |||
| fd5fa5ac3c | |||
| 5f3dd31925 | |||
| 347713766e | |||
| 7ecc184111 | |||
| e8510bf9c3 | |||
| 6ac2fca384 | |||
| 10816f5303 | |||
| 89b097d015 | |||
| 5a83216395 | |||
| f9e123dfcb | |||
| e87113da24 | |||
| dda4c58e1c | |||
| b349dff496 | |||
| 6d8e1d0830 | |||
| a0aa22c80b | |||
| 12049a0f35 | |||
| 9ff7937055 | |||
| 88de47d1bb | |||
| 8528664660 | |||
| f32fc4e8c0 | |||
| eaca611842 | |||
| 28041228bd | |||
| 4b2af5c635 |
20
.config/dotnet-tools.json
Normal file
20
.config/dotnet-tools.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"dotnet-stryker": {
|
||||||
|
"version": "4.15.0",
|
||||||
|
"commands": [
|
||||||
|
"dotnet-stryker"
|
||||||
|
],
|
||||||
|
"rollForward": false
|
||||||
|
},
|
||||||
|
"dotnet-ef": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"commands": [
|
||||||
|
"dotnet-ef"
|
||||||
|
],
|
||||||
|
"rollForward": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
.editorconfig
Normal file
17
.editorconfig
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Editor configuration, see http://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
# .NET sources use 4-space indent (dotnet format enforces this). The 2-space default
|
||||||
|
# above is for the frontend (TS/HTML/CSS/JSON); C# keeps the .NET convention.
|
||||||
|
[*.cs]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
||||||
@@ -17,7 +17,7 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: respellion-linux
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: https://github.com/actions/checkout@v4
|
- uses: https://github.com/actions/checkout@v4
|
||||||
- uses: https://github.com/actions/setup-dotnet@v4
|
- uses: https://github.com/actions/setup-dotnet@v4
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- run: make lint
|
- run: make lint
|
||||||
|
|
||||||
build:
|
build:
|
||||||
runs-on: respellion-linux
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: https://github.com/actions/checkout@v4
|
- uses: https://github.com/actions/checkout@v4
|
||||||
- uses: https://github.com/actions/setup-dotnet@v4
|
- uses: https://github.com/actions/setup-dotnet@v4
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
- run: make build
|
- run: make build
|
||||||
|
|
||||||
unit:
|
unit:
|
||||||
runs-on: respellion-linux
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: https://github.com/actions/checkout@v4
|
- uses: https://github.com/actions/checkout@v4
|
||||||
- uses: https://github.com/actions/setup-dotnet@v4
|
- uses: https://github.com/actions/setup-dotnet@v4
|
||||||
@@ -43,8 +43,91 @@ jobs:
|
|||||||
dotnet-version: '10.0.x'
|
dotnet-version: '10.0.x'
|
||||||
- run: make unit
|
- run: make unit
|
||||||
|
|
||||||
compose-smoke:
|
# Frontend (Nx/Angular) lane: install with pnpm, then Nx lint + test + build.
|
||||||
runs-on: respellion-linux
|
frontend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: https://github.com/actions/checkout@v4
|
- uses: https://github.com/actions/checkout@v4
|
||||||
- run: make smoke
|
- uses: https://github.com/pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 11
|
||||||
|
- uses: https://github.com/actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '24'
|
||||||
|
cache: 'pnpm'
|
||||||
|
- run: make frontend
|
||||||
|
|
||||||
|
mutation:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: https://github.com/actions/checkout@v4
|
||||||
|
- uses: https://github.com/actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: '10.0.x'
|
||||||
|
- run: make mutation
|
||||||
|
# Publish the Stryker HTML reports. `if: always()` uploads them even when the
|
||||||
|
# ratchet fails — that is exactly when you want to inspect the survivors.
|
||||||
|
# `continue-on-error` keeps the upload best-effort: the mutation *gate* is the
|
||||||
|
# ratchet (make mutation's exit code), not the report, so a Gitea artifact-backend
|
||||||
|
# 500 must not fail the job (gitea-actions-gotchas.md §4). Glob handles Stryker's
|
||||||
|
# non-deterministic StrykerOutput/<timestamp>/ dir. Pinned @v3: @v4's bundled
|
||||||
|
# @actions/artifact hard-aborts on non-github.com (GHES guard) — see the runbook.
|
||||||
|
- uses: https://github.com/actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
name: acl-mutation-report
|
||||||
|
path: services/acl/StrykerOutput/**/reports/mutation-report.html
|
||||||
|
if-no-files-found: warn
|
||||||
|
- uses: https://github.com/actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
name: event-subscriber-mutation-report
|
||||||
|
path: services/event-subscriber/StrykerOutput/**/reports/mutation-report.html
|
||||||
|
if-no-files-found: warn
|
||||||
|
- uses: https://github.com/actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
name: domain-mutation-report
|
||||||
|
path: services/domain/StrykerOutput/**/reports/mutation-report.html
|
||||||
|
if-no-files-found: warn
|
||||||
|
- uses: https://github.com/actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
name: bff-mutation-report
|
||||||
|
path: services/bff/StrykerOutput/**/reports/mutation-report.html
|
||||||
|
if-no-files-found: warn
|
||||||
|
|
||||||
|
# One stage for every check that needs the live stack. On the single self-hosted
|
||||||
|
# runner jobs run sequentially, so booting OpenZaak once (instead of once per job)
|
||||||
|
# is the cheapest layout (issue #58). No setup-dotnet: the ACL test runs in a built
|
||||||
|
# image and everything reaches services by container IP. Needs Docker + egress
|
||||||
|
# (base images, nuget, selectielijst.openzaak.nl).
|
||||||
|
verify-stack:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: https://github.com/actions/checkout@v4
|
||||||
|
# Bring the full stack up + wait for health — this also is the DoD "compose up
|
||||||
|
# reaches green health" smoke (it replaces the old compose-smoke job).
|
||||||
|
- name: Bring up the full stack & wait for health
|
||||||
|
run: make verify-up
|
||||||
|
- name: ACL ↔ OpenZaak integration tests
|
||||||
|
run: make verify-acl
|
||||||
|
- name: OpenZaak → NRC notification delivery
|
||||||
|
run: make verify-nrc
|
||||||
|
- name: OpenZaak → NRC → Event Subscriber → projection-api
|
||||||
|
run: make verify-projection
|
||||||
|
- name: Domain → Flowable → ACL → OpenZaak
|
||||||
|
run: make verify-domain
|
||||||
|
- name: BFF → Keycloak + domain + projection
|
||||||
|
run: make verify-bff
|
||||||
|
# Log dump must precede teardown (which removes the containers).
|
||||||
|
- name: Dump container logs on failure
|
||||||
|
if: failure()
|
||||||
|
run: docker compose -f infra/docker-compose.yml logs --no-color --tail=100 oz-init openzaak nrc-init nrc-web nrc-celery nrc-beat flowable-db flowable-rest flowable-init keycloak acl bff domain projection-db event-subscriber projection-api 2>&1 || true
|
||||||
|
- name: Tear down
|
||||||
|
if: always()
|
||||||
|
run: make down
|
||||||
|
|||||||
23
.gitignore
vendored
23
.gitignore
vendored
@@ -5,6 +5,9 @@ obj/
|
|||||||
[Rr]elease/
|
[Rr]elease/
|
||||||
*.user
|
*.user
|
||||||
|
|
||||||
|
# Reqnroll-generated test code (regenerated from *.feature on build)
|
||||||
|
*.feature.cs
|
||||||
|
|
||||||
# Test results / coverage
|
# Test results / coverage
|
||||||
[Tt]est[Rr]esults/
|
[Tt]est[Rr]esults/
|
||||||
*.trx
|
*.trx
|
||||||
@@ -12,6 +15,9 @@ coverage*.json
|
|||||||
coverage*.xml
|
coverage*.xml
|
||||||
*.coverage
|
*.coverage
|
||||||
|
|
||||||
|
# Stryker.NET mutation-testing reports (regenerated by `make mutation`)
|
||||||
|
StrykerOutput/
|
||||||
|
|
||||||
# Rider / VS / VS Code
|
# Rider / VS / VS Code
|
||||||
.idea/
|
.idea/
|
||||||
.vs/
|
.vs/
|
||||||
@@ -29,3 +35,20 @@ site/
|
|||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# ── Frontend (Nx / Angular / pnpm) ──
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
tmp/
|
||||||
|
out-tsc/
|
||||||
|
/coverage
|
||||||
|
.angular/
|
||||||
|
.nx/cache
|
||||||
|
.nx/workspace-data
|
||||||
|
.nx/self-healing
|
||||||
|
.nx/migrate-runs
|
||||||
|
.nx/polygraph
|
||||||
|
vite.config.*.timestamp*
|
||||||
|
vitest.config.*.timestamp*
|
||||||
|
|
||||||
|
.angular
|
||||||
|
|||||||
8
.prettierignore
Normal file
8
.prettierignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Add files here to ignore them from prettier formatting
|
||||||
|
/dist
|
||||||
|
/coverage
|
||||||
|
/.nx/cache
|
||||||
|
/.nx/workspace-data
|
||||||
|
.angular
|
||||||
|
|
||||||
|
.nx/self-healing
|
||||||
3
.prettierrc
Normal file
3
.prettierrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
17
BACKLOG.md
17
BACKLOG.md
@@ -151,17 +151,16 @@ The skeleton proves the spine end-to-end: a registration, a workflow, a zaak in
|
|||||||
|
|
||||||
### S-08 · Self-Service portal (Angular, NL DS) — submit a registration
|
### S-08 · Self-Service portal (Angular, NL DS) — submit a registration
|
||||||
|
|
||||||
**Outcome:** The self-service Angular app, in the Nx monorepo, lets a zorgprofessional log in via mock DigiD and submit a registration. NL Design System styling. Generated API client.
|
> **S-08 was split** (CLAUDE.md §13; issue #9 closed) into the sub-slices below — it bundled the
|
||||||
|
> Nx bootstrap, the generated client, the NL DS + DigiD form, and a full-stack Playwright e2e, well
|
||||||
|
> past 1–2 days. Each sub-slice is independently demoable and CI-green.
|
||||||
|
|
||||||
**Acceptance:**
|
- **S-08a (#65)** · Nx monorepo + Angular tooling + CI Node lane. Placeholder `self-service` app; `nx lint/test/build` green in a new CI Node lane.
|
||||||
|
- **S-08b (#66)** · Generated api-client lib from `services/bff/openapi.json` (never hand-written, §10) + a mocked-BFF unit test.
|
||||||
|
- **S-08c (#67)** · Self-service submit form — NL Design System `libs/ui`, DigiD OIDC `libs/auth`, component tests (Angular Testing Library), axe WCAG 2.1 AA on the submit page.
|
||||||
|
- **S-08d (#68)** · Playwright happy-path e2e (login → submit → success) against the full stack + compose serving + CI e2e lane.
|
||||||
|
|
||||||
- E2E test (Playwright): full happy path, login → submit → success page.
|
**Out of scope (whole of S-08):** document upload, status tracking page.
|
||||||
- Component tests (Testing Library) for the form.
|
|
||||||
- Accessibility audit (axe-core) passes WCAG 2.1 AA on the submit page.
|
|
||||||
|
|
||||||
**Touches:** `apps/self-service/`, `libs/ui/`, `libs/auth/`, `libs/api-client/`, tests.
|
|
||||||
|
|
||||||
**Out of scope:** document upload, status tracking page.
|
|
||||||
|
|
||||||
### S-09 · Openbaar Register portal — public lookup
|
### S-09 · Openbaar Register portal — public lookup
|
||||||
|
|
||||||
|
|||||||
160
Makefile
160
Makefile
@@ -5,9 +5,24 @@
|
|||||||
# `make ci` locally runs exactly what the pipeline runs — no drift. Until a
|
# `make ci` locally runs exactly what the pipeline runs — no drift. Until a
|
||||||
# self-hosted runner is registered, `make ci` is the gate (see docs/runbooks/ci.md).
|
# self-hosted runner is registered, `make ci` is the gate (see docs/runbooks/ci.md).
|
||||||
|
|
||||||
SLN := services/bff/Bff.slnx
|
SLN := register-referentie.slnx
|
||||||
COMPOSE := infra/docker-compose.yml
|
COMPOSE := infra/docker-compose.yml
|
||||||
HEALTH_URL := http://localhost:8080/health
|
# Long-running services with a healthcheck — the smoke polls these for readiness
|
||||||
|
# (infra/wait-healthy.sh). One-shot init jobs (oz-init, nrc-init, flowable-init)
|
||||||
|
# are not polled; they only need to have run. See docs/runbooks/gitea-actions-gotchas.md.
|
||||||
|
WAIT_SVCS := openzaak nrc-web acl bff domain event-subscriber projection-api
|
||||||
|
# Config files (OpenZaak data.yaml, Keycloak realms, Flowable BPMN) are streamed
|
||||||
|
# into external named volumes via `docker cp` (infra/seed-config.sh) instead of
|
||||||
|
# bind-mounted, because bind mounts don't reach sibling containers on the
|
||||||
|
# containerized CI runner. SEED populates them; run it before every `up`. The
|
||||||
|
# volumes are `external`, so compose won't remove them — CFG_VOLS lists them for
|
||||||
|
# explicit teardown. See docs/runbooks/gitea-actions-gotchas.md.
|
||||||
|
SEED := bash infra/seed-config.sh
|
||||||
|
CFG_VOLS := rr-oz-config rr-nrc-config rr-kc-realms rr-fl-bpmn
|
||||||
|
# Local-only stack: same services but config is bind-mounted (no seed step), so a
|
||||||
|
# plain `docker compose -f infra/docker-compose.local.yml up` works on any local
|
||||||
|
# engine. This is the no-make / Windows-friendly path. See that file's header.
|
||||||
|
LOCAL_COMPOSE := infra/docker-compose.local.yml
|
||||||
OZ_COMPOSE := infra/openzaak/docker-compose.yml
|
OZ_COMPOSE := infra/openzaak/docker-compose.yml
|
||||||
OZ_BASE := http://localhost:8000
|
OZ_BASE := http://localhost:8000
|
||||||
NRC_COMPOSE := infra/opennotificaties/docker-compose.yml
|
NRC_COMPOSE := infra/opennotificaties/docker-compose.yml
|
||||||
@@ -28,10 +43,16 @@ export DOCKER_HOST := unix://$(PODMAN_SOCK)
|
|||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: ci lint build unit smoke down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down flowable-up flowable-smoke flowable-down help
|
.PHONY: ci lint build unit mutation frontend integration verify verify-up verify-acl verify-nrc verify-projection verify-bff verify-domain verify-notifications smoke up down local local-down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down flowable-up flowable-smoke flowable-down help
|
||||||
|
|
||||||
## ci: run the full pipeline — lint, build, unit, smoke (mirrors Gitea Actions)
|
## ci: run the full pipeline — lint, build, unit, mutation, frontend, verify (mirrors Gitea Actions)
|
||||||
ci: lint build unit smoke
|
## `verify` is the live-stack stage (full stack up once → ACL + notification checks).
|
||||||
|
ci: lint build unit mutation frontend verify
|
||||||
|
|
||||||
|
## frontend: install deps and run the Nx lint/test/build for the portals (pnpm + Node required)
|
||||||
|
frontend:
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
pnpm nx run-many -t lint test build
|
||||||
|
|
||||||
## lint: verify formatting (no changes)
|
## lint: verify formatting (no changes)
|
||||||
lint:
|
lint:
|
||||||
@@ -41,30 +62,126 @@ lint:
|
|||||||
build:
|
build:
|
||||||
dotnet build $(SLN) -c Release
|
dotnet build $(SLN) -c Release
|
||||||
|
|
||||||
## unit: run unit tests
|
## unit: run unit tests (excludes the container-backed Integration lane)
|
||||||
unit:
|
unit:
|
||||||
dotnet test $(SLN) -c Release
|
dotnet test $(SLN) -c Release --filter "Category!=Integration"
|
||||||
|
|
||||||
## smoke: compose up (wait for healthy), curl /health, then tear down
|
## mutation: run the Stryker.NET ratchet on each service with branching logic (fails below baseline)
|
||||||
|
# Stryker is pinned as a local dotnet tool (.config/dotnet-tools.json); `tool restore`
|
||||||
|
# makes `make mutation` work from a fresh clone. Each service owns its config + break
|
||||||
|
# threshold (the ratchet, CLAUDE.md §5): each services/<svc>/stryker-config.json.
|
||||||
|
# Scores never regress below baseline.
|
||||||
|
mutation:
|
||||||
|
dotnet tool restore
|
||||||
|
cd services/acl && dotnet stryker
|
||||||
|
cd services/event-subscriber && dotnet stryker
|
||||||
|
cd services/domain && dotnet stryker
|
||||||
|
cd services/bff && dotnet stryker
|
||||||
|
|
||||||
|
## smoke: seed config, bring the whole stack up, wait for health-checked services, tear down
|
||||||
|
# SEED populates the external config volumes first (upstream images used verbatim;
|
||||||
|
# only our acl/bff are built). `up -d --build` starts EVERYTHING. Readiness is
|
||||||
|
# checked by infra/wait-healthy.sh polling the durable, health-checked services
|
||||||
|
# ($(WAIT_SVCS)) via `docker inspect` — portable across docker compose and
|
||||||
|
# podman-compose, and needing no `--wait` flag or host port access. The one-shots
|
||||||
|
# (oz-init, flowable-init) aren't polled; they just need to have run.
|
||||||
smoke:
|
smoke:
|
||||||
docker compose -f $(COMPOSE) up -d --build --wait
|
$(SEED) oz nrc kc fl
|
||||||
bash -c 'curl -fsS $(HEALTH_URL); rc=$$?; docker compose -f $(COMPOSE) down --volumes; exit $$rc'
|
docker compose -f $(COMPOSE) up -d --build
|
||||||
|
bash -c 'WAIT_TIMEOUT=420 bash infra/wait-healthy.sh $(WAIT_SVCS); rc=$$?; docker compose -f $(COMPOSE) down --volumes; docker volume rm -f $(CFG_VOLS) >/dev/null 2>&1; exit $$rc'
|
||||||
|
|
||||||
## down: stop and remove the local stack
|
## up: seed config volumes and start the full stack (use instead of bare
|
||||||
|
## `docker compose up`, which can't self-seed the external config volumes)
|
||||||
|
up:
|
||||||
|
$(SEED) oz nrc kc fl
|
||||||
|
docker compose -f $(COMPOSE) up -d --build
|
||||||
|
|
||||||
|
## down: stop and remove the local stack (incl. the external config volumes)
|
||||||
down:
|
down:
|
||||||
docker compose -f $(COMPOSE) down --volumes
|
docker compose -f $(COMPOSE) down --volumes
|
||||||
|
-docker volume rm -f $(CFG_VOLS)
|
||||||
|
|
||||||
|
## local: bring up the bind-mount stack (no seed step) and wait for health
|
||||||
|
## (Windows / no-make users: run `docker compose -f infra/docker-compose.local.yml up -d --build` directly)
|
||||||
|
local:
|
||||||
|
docker compose -f $(LOCAL_COMPOSE) up -d --build
|
||||||
|
WAIT_TIMEOUT=420 bash infra/wait-healthy.sh $(WAIT_SVCS)
|
||||||
|
|
||||||
|
## local-down: stop and remove the bind-mount stack
|
||||||
|
local-down:
|
||||||
|
docker compose -f $(LOCAL_COMPOSE) down --volumes
|
||||||
|
|
||||||
## changelog: regenerate CHANGELOG.md from Conventional Commits (git-cliff)
|
## changelog: regenerate CHANGELOG.md from Conventional Commits (git-cliff)
|
||||||
changelog:
|
changelog:
|
||||||
git-cliff --output CHANGELOG.md
|
git-cliff --output CHANGELOG.md
|
||||||
|
|
||||||
|
# ── ZGW verification ───────────────────────────────────────────────────────
|
||||||
|
# On the single runner CI jobs run sequentially, so the OpenZaak-dependent checks
|
||||||
|
# share ONE full-stack bring-up: the `verify-stack` CI job runs `verify-up` then
|
||||||
|
# `verify-acl` + `verify-nrc` as steps against the same stack (issue #58). The
|
||||||
|
# check logic lives in stack-agnostic runners that reach services by container IP
|
||||||
|
# (gitea-actions-gotchas.md §5/§6); `integration` / `verify-notifications` are local
|
||||||
|
# convenience wrappers that bring up a lighter stack and call the same runners.
|
||||||
|
|
||||||
|
## verify-up: bring the FULL stack up and wait for health (CI verify-stack step 1;
|
||||||
|
## subsumes the old compose-smoke health gate — the DoD "up reaches green" check).
|
||||||
|
verify-up:
|
||||||
|
$(SEED) oz nrc kc fl
|
||||||
|
docker compose -f $(COMPOSE) up -d --build
|
||||||
|
WAIT_TIMEOUT=420 bash infra/wait-healthy.sh $(WAIT_SVCS)
|
||||||
|
|
||||||
|
## verify-acl: ACL ↔ OpenZaak integration tests against the already-running stack.
|
||||||
|
verify-acl:
|
||||||
|
bash infra/run-acl-integration.sh
|
||||||
|
|
||||||
|
## verify-nrc: OpenZaak → NRC notification delivery against the already-running stack.
|
||||||
|
verify-nrc:
|
||||||
|
bash infra/run-notification-check.sh
|
||||||
|
|
||||||
|
## verify-projection: OpenZaak → NRC → Event Subscriber → projection-api end-to-end (S-06),
|
||||||
|
## against the already-running stack.
|
||||||
|
verify-projection:
|
||||||
|
bash infra/run-projection-check.sh
|
||||||
|
|
||||||
|
## verify-domain: domain → Flowable → ACL → OpenZaak end-to-end (S-05), against the
|
||||||
|
## already-running stack. Recreates the acl service to inject the seeded zaaktype URL.
|
||||||
|
verify-domain:
|
||||||
|
bash infra/run-domain-check.sh
|
||||||
|
|
||||||
|
## verify-bff: BFF end-to-end (S-07) against the up stack — token validation on self-service
|
||||||
|
## + anonymous public-safe openbaar register (ADR-0010).
|
||||||
|
verify-bff:
|
||||||
|
bash infra/run-bff-check.sh
|
||||||
|
|
||||||
|
## verify: local mirror of the CI verify-stack job — full stack up once, all checks,
|
||||||
|
## tear down (always). For fast single-concern local iteration use `integration`
|
||||||
|
## (oz-only) or `verify-notifications` (oz+nrc) instead.
|
||||||
|
verify:
|
||||||
|
$(SEED) oz nrc kc fl
|
||||||
|
docker compose -f $(COMPOSE) up -d --build
|
||||||
|
@bash -c 'set -e; rc=0; \
|
||||||
|
WAIT_TIMEOUT=420 bash infra/wait-healthy.sh $(WAIT_SVCS) \
|
||||||
|
&& bash infra/run-acl-integration.sh \
|
||||||
|
&& bash infra/run-notification-check.sh \
|
||||||
|
&& bash infra/run-projection-check.sh \
|
||||||
|
&& bash infra/run-domain-check.sh \
|
||||||
|
&& bash infra/run-bff-check.sh || rc=$$?; \
|
||||||
|
docker compose -f $(COMPOSE) down --volumes >/dev/null 2>&1; \
|
||||||
|
docker volume rm -f $(CFG_VOLS) >/dev/null 2>&1; \
|
||||||
|
exit $$rc'
|
||||||
|
|
||||||
|
## integration: local convenience — ACL integration test against a throwaway
|
||||||
|
## OpenZaak-only stack (fast iteration). CI uses verify-acl on the shared stack.
|
||||||
|
integration:
|
||||||
|
bash infra/run-integration.sh
|
||||||
|
|
||||||
## openzaak-up: start the OpenZaak stack (migrations run on first start)
|
## openzaak-up: start the OpenZaak stack (migrations run on first start)
|
||||||
openzaak-up:
|
openzaak-up:
|
||||||
|
$(SEED) oz
|
||||||
docker compose -f $(OZ_COMPOSE) up -d
|
docker compose -f $(OZ_COMPOSE) up -d
|
||||||
|
|
||||||
## openzaak-smoke: start OpenZaak, then assert it is up with auth enforced
|
## openzaak-smoke: start OpenZaak, then assert it is up with auth enforced
|
||||||
openzaak-smoke:
|
openzaak-smoke: openzaak-up
|
||||||
docker compose -f $(OZ_COMPOSE) up -d
|
|
||||||
@bash -c 'set -e; \
|
@bash -c 'set -e; \
|
||||||
echo "waiting for OpenZaak to respond..."; \
|
echo "waiting for OpenZaak to respond..."; \
|
||||||
for i in $$(seq 1 60); do \
|
for i in $$(seq 1 60); do \
|
||||||
@@ -88,10 +205,18 @@ openzaak-seed: openzaak-up
|
|||||||
## openzaak-down: stop and remove the OpenZaak stack (wipes data)
|
## openzaak-down: stop and remove the OpenZaak stack (wipes data)
|
||||||
openzaak-down:
|
openzaak-down:
|
||||||
docker compose -f $(OZ_COMPOSE) down --volumes
|
docker compose -f $(OZ_COMPOSE) down --volumes
|
||||||
|
-docker volume rm -f rr-oz-config
|
||||||
|
|
||||||
## stack-up: start OpenZaak + Open Notificaties together (shared network)
|
## verify-notifications: local convenience — OpenZaak → NRC notification delivery
|
||||||
|
## against a throwaway oz+nrc stack (S-01-c). CI uses verify-nrc on the shared stack.
|
||||||
|
verify-notifications:
|
||||||
|
bash infra/verify-notifications.sh
|
||||||
|
|
||||||
|
## stack-up: start OpenZaak + Open Notificaties together (shared network), with
|
||||||
|
## OpenZaak publishing notifications to NRC (S-01-c).
|
||||||
stack-up:
|
stack-up:
|
||||||
docker compose $(STACK_FILES) up -d
|
$(SEED) oz nrc
|
||||||
|
OZ_NOTIFICATIONS_DISABLED=false docker compose $(STACK_FILES) up -d
|
||||||
|
|
||||||
## stack-smoke: start both, assert OpenZaak (403/302/200) and NRC (302) are reachable
|
## stack-smoke: start both, assert OpenZaak (403/302/200) and NRC (302) are reachable
|
||||||
stack-smoke: stack-up
|
stack-smoke: stack-up
|
||||||
@@ -110,9 +235,11 @@ stack-smoke: stack-up
|
|||||||
## stack-down: stop and remove both stacks (wipes data)
|
## stack-down: stop and remove both stacks (wipes data)
|
||||||
stack-down:
|
stack-down:
|
||||||
docker compose $(STACK_FILES) down --volumes
|
docker compose $(STACK_FILES) down --volumes
|
||||||
|
-docker volume rm -f rr-oz-config rr-nrc-config
|
||||||
|
|
||||||
## keycloak-up: start Keycloak with the four imported realms
|
## keycloak-up: start Keycloak with the four imported realms
|
||||||
keycloak-up:
|
keycloak-up:
|
||||||
|
$(SEED) kc
|
||||||
docker compose -f $(KC_COMPOSE) up -d
|
docker compose -f $(KC_COMPOSE) up -d
|
||||||
|
|
||||||
## keycloak-smoke: start Keycloak, then verify each realm logs in + returns its claim
|
## keycloak-smoke: start Keycloak, then verify each realm logs in + returns its claim
|
||||||
@@ -125,9 +252,11 @@ keycloak-smoke: keycloak-up
|
|||||||
## keycloak-down: stop and remove Keycloak
|
## keycloak-down: stop and remove Keycloak
|
||||||
keycloak-down:
|
keycloak-down:
|
||||||
docker compose -f $(KC_COMPOSE) down --volumes
|
docker compose -f $(KC_COMPOSE) down --volumes
|
||||||
|
-docker volume rm -f rr-kc-realms
|
||||||
|
|
||||||
## flowable-up: start Flowable (deploys registratie.bpmn on boot)
|
## flowable-up: start Flowable (deploys registratie.bpmn on boot)
|
||||||
flowable-up:
|
flowable-up:
|
||||||
|
$(SEED) fl
|
||||||
docker compose -f $(FL_COMPOSE) up -d
|
docker compose -f $(FL_COMPOSE) up -d
|
||||||
|
|
||||||
## flowable-smoke: start Flowable, then verify a started instance waits on the external task
|
## flowable-smoke: start Flowable, then verify a started instance waits on the external task
|
||||||
@@ -140,6 +269,7 @@ flowable-smoke: flowable-up
|
|||||||
## flowable-down: stop and remove Flowable
|
## flowable-down: stop and remove Flowable
|
||||||
flowable-down:
|
flowable-down:
|
||||||
docker compose -f $(FL_COMPOSE) down --volumes
|
docker compose -f $(FL_COMPOSE) down --volumes
|
||||||
|
-docker volume rm -f rr-fl-bpmn
|
||||||
|
|
||||||
## help: list available targets
|
## help: list available targets
|
||||||
help:
|
help:
|
||||||
|
|||||||
34
apps/self-service/eslint.config.mjs
Normal file
34
apps/self-service/eslint.config.mjs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import nx from '@nx/eslint-plugin';
|
||||||
|
import baseConfig from '../../eslint.config.mjs';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...nx.configs['flat/angular'],
|
||||||
|
...nx.configs['flat/angular-template'],
|
||||||
|
...baseConfig,
|
||||||
|
{
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
rules: {
|
||||||
|
'@angular-eslint/directive-selector': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
type: 'attribute',
|
||||||
|
prefix: 'app',
|
||||||
|
style: 'camelCase',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@angular-eslint/component-selector': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
type: 'element',
|
||||||
|
prefix: 'app',
|
||||||
|
style: 'kebab-case',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.html'],
|
||||||
|
// Override or add rules here
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
80
apps/self-service/project.json
Normal file
80
apps/self-service/project.json
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"name": "self-service",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"projectType": "application",
|
||||||
|
"prefix": "app",
|
||||||
|
"sourceRoot": "apps/self-service/src",
|
||||||
|
"tags": [],
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"executor": "@angular/build:application",
|
||||||
|
"outputs": ["{options.outputPath}"],
|
||||||
|
"defaultConfiguration": "production",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/apps/self-service",
|
||||||
|
"browser": "apps/self-service/src/main.ts",
|
||||||
|
"tsConfig": "apps/self-service/tsconfig.app.json",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "apps/self-service/public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": ["apps/self-service/src/styles.css"]
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "1mb",
|
||||||
|
"maximumError": "2mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "4kb",
|
||||||
|
"maximumError": "8kb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"optimization": false,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"continuous": true,
|
||||||
|
"executor": "@angular/build:dev-server",
|
||||||
|
"defaultConfiguration": "development",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "self-service:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "self-service:build:development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nx/eslint:lint"
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"executor": "@angular/build:unit-test",
|
||||||
|
"options": {
|
||||||
|
"watch": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serve-static": {
|
||||||
|
"continuous": true,
|
||||||
|
"executor": "@nx/web:file-server",
|
||||||
|
"options": {
|
||||||
|
"buildTarget": "self-service:build",
|
||||||
|
"staticFilePath": "dist/apps/self-service/browser",
|
||||||
|
"spa": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
apps/self-service/public/favicon.ico
Normal file
BIN
apps/self-service/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
22
apps/self-service/src/app/app.config.ts
Normal file
22
apps/self-service/src/app/app.config.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||||
|
import {
|
||||||
|
ApplicationConfig,
|
||||||
|
provideBrowserGlobalErrorListeners,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { authInterceptor, provideDigiadAuth } from 'auth';
|
||||||
|
import { appRoutes } from './app.routes';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [
|
||||||
|
provideBrowserGlobalErrorListeners(),
|
||||||
|
provideRouter(appRoutes),
|
||||||
|
provideHttpClient(withInterceptors([authInterceptor()])),
|
||||||
|
// Dev defaults (host ports). The compose-served app overrides these for the stack (S-08d).
|
||||||
|
provideDigiadAuth({
|
||||||
|
authority: 'http://localhost:8180/realms/digid',
|
||||||
|
redirectUrl: typeof window !== 'undefined' ? window.location.origin : '/',
|
||||||
|
secureApiOrigin: 'http://localhost:8080',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
0
apps/self-service/src/app/app.css
Normal file
0
apps/self-service/src/app/app.css
Normal file
1
apps/self-service/src/app/app.html
Normal file
1
apps/self-service/src/app/app.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<router-outlet></router-outlet>
|
||||||
7
apps/self-service/src/app/app.routes.ts
Normal file
7
apps/self-service/src/app/app.routes.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Route } from '@angular/router';
|
||||||
|
import { authenticatedGuard } from 'auth';
|
||||||
|
import { RegistrationPage } from './registration/registration-page';
|
||||||
|
|
||||||
|
export const appRoutes: Route[] = [
|
||||||
|
{ path: '', component: RegistrationPage, canActivate: [authenticatedGuard] },
|
||||||
|
];
|
||||||
15
apps/self-service/src/app/app.spec.ts
Normal file
15
apps/self-service/src/app/app.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { render, screen } from '@testing-library/angular';
|
||||||
|
import { App } from './app';
|
||||||
|
|
||||||
|
describe('App', () => {
|
||||||
|
it('renders the router outlet shell', async () => {
|
||||||
|
const { container } = await render(App, {
|
||||||
|
providers: [provideRouter([])],
|
||||||
|
});
|
||||||
|
|
||||||
|
// The shell is a thin host for routed pages (the RegistrationPage owns the heading).
|
||||||
|
expect(container.querySelector('router-outlet')).toBeTruthy();
|
||||||
|
expect(screen).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
12
apps/self-service/src/app/app.ts
Normal file
12
apps/self-service/src/app/app.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
imports: [RouterModule],
|
||||||
|
selector: 'app-root',
|
||||||
|
templateUrl: './app.html',
|
||||||
|
styleUrl: './app.css',
|
||||||
|
})
|
||||||
|
export class App {
|
||||||
|
protected title = 'self-service';
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<main utrecht-document class="utrecht-theme">
|
||||||
|
<utrecht-article>
|
||||||
|
<utrecht-heading-1>Zelfservice — BIG-registratie</utrecht-heading-1>
|
||||||
|
|
||||||
|
@if (submitted()) {
|
||||||
|
<p utrecht-paragraph role="status">
|
||||||
|
Uw registratie is ontvangen. Referentie: {{ reference() }}.
|
||||||
|
</p>
|
||||||
|
} @else {
|
||||||
|
<p utrecht-paragraph>U bent ingelogd met BSN {{ bsn() }}.</p>
|
||||||
|
<button
|
||||||
|
utrecht-button
|
||||||
|
appearance="primary-action-button"
|
||||||
|
type="button"
|
||||||
|
[disabled]="submitting()"
|
||||||
|
(click)="submit()"
|
||||||
|
>
|
||||||
|
Registratie indienen
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</utrecht-article>
|
||||||
|
</main>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { signal } from '@angular/core';
|
||||||
|
import { fireEvent, render, screen } from '@testing-library/angular';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { AuthService } from 'auth';
|
||||||
|
import { BffApiV1Service } from 'api-client';
|
||||||
|
import { axe } from 'vitest-axe';
|
||||||
|
import { RegistrationPage } from './registration-page';
|
||||||
|
|
||||||
|
class FakeAuth extends AuthService {
|
||||||
|
readonly isAuthenticated = signal(true);
|
||||||
|
readonly bsn = signal<string | undefined>('123456782');
|
||||||
|
login(): void {
|
||||||
|
/* noop */
|
||||||
|
}
|
||||||
|
logout(): void {
|
||||||
|
/* noop */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function providers(post = vi.fn().mockReturnValue(of({ registrationId: 'reg-9', status: 'Ingediend' }))) {
|
||||||
|
return {
|
||||||
|
post,
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthService, useClass: FakeAuth },
|
||||||
|
{ provide: BffApiV1Service, useValue: { postSelfServiceRegistrations: post } },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('RegistrationPage', () => {
|
||||||
|
it('shows the signed-in BSN', async () => {
|
||||||
|
await render(RegistrationPage, { providers: providers().providers });
|
||||||
|
expect(screen.getByText(/123456782/)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('submits the registration and confirms', async () => {
|
||||||
|
const { post, providers: p } = providers();
|
||||||
|
await render(RegistrationPage, { providers: p });
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /indienen/i }));
|
||||||
|
|
||||||
|
expect(post).toHaveBeenCalledTimes(1);
|
||||||
|
expect(await screen.findByText(/ontvangen/i)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has no WCAG 2.1 AA violations on the submit page', async () => {
|
||||||
|
// The portal is Dutch; the real index.html sets lang. Set it here so the document-level
|
||||||
|
// html-has-lang rule reflects the app, not the bare jsdom document.
|
||||||
|
document.documentElement.lang = 'nl';
|
||||||
|
const { container } = await render(RegistrationPage, { providers: providers().providers });
|
||||||
|
|
||||||
|
const results = await axe(container, {
|
||||||
|
runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'] },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(results.violations).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
33
apps/self-service/src/app/registration/registration-page.ts
Normal file
33
apps/self-service/src/app/registration/registration-page.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Component, inject, signal } from '@angular/core';
|
||||||
|
import { BffApiV1Service, type SubmitAccepted } from 'api-client';
|
||||||
|
import { AuthService } from 'auth';
|
||||||
|
import { UtrechtComponentsModule } from 'ui';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The self-service submit page: a signed-in zorgprofessional confirms and submits their BIG
|
||||||
|
* registration. The bsn comes from the DigiD token (not a form field), so this is a confirm-and-
|
||||||
|
* submit flow that posts to the BFF and shows the returned reference (ADR-0010; S-08c).
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'app-registration-page',
|
||||||
|
imports: [UtrechtComponentsModule],
|
||||||
|
templateUrl: './registration-page.html',
|
||||||
|
})
|
||||||
|
export class RegistrationPage {
|
||||||
|
private readonly auth = inject(AuthService);
|
||||||
|
private readonly bff = inject(BffApiV1Service);
|
||||||
|
|
||||||
|
protected readonly bsn = this.auth.bsn;
|
||||||
|
protected readonly submitting = signal(false);
|
||||||
|
protected readonly reference = signal<string | undefined>(undefined);
|
||||||
|
protected readonly submitted = signal(false);
|
||||||
|
|
||||||
|
submit(): void {
|
||||||
|
this.submitting.set(true);
|
||||||
|
this.bff.postSelfServiceRegistrations().subscribe((accepted: SubmitAccepted) => {
|
||||||
|
this.reference.set(accepted.registrationId);
|
||||||
|
this.submitted.set(true);
|
||||||
|
this.submitting.set(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
13
apps/self-service/src/index.html
Normal file
13
apps/self-service/src/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="nl">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>self-service</title>
|
||||||
|
<base href="/" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5
apps/self-service/src/main.ts
Normal file
5
apps/self-service/src/main.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
|
import { appConfig } from './app/app.config';
|
||||||
|
import { App } from './app/app';
|
||||||
|
|
||||||
|
bootstrapApplication(App, appConfig).catch((err) => console.error(err));
|
||||||
2
apps/self-service/src/styles.css
Normal file
2
apps/self-service/src/styles.css
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/* NL Design System theme — Utrecht design tokens (docs/frontend-decisions.md). */
|
||||||
|
@import '@utrecht/design-tokens/dist/index.css';
|
||||||
9
apps/self-service/tsconfig.app.json
Normal file
9
apps/self-service/tsconfig.app.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"]
|
||||||
|
}
|
||||||
31
apps/self-service/tsconfig.json
Normal file
31
apps/self-service/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"target": "es2022",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"emitDecoratorMetadata": false,
|
||||||
|
"module": "preserve"
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"strictTemplates": true
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
8
apps/self-service/tsconfig.spec.json
Normal file
8
apps/self-service/tsconfig.spec.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": ["vitest/globals"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.d.ts"]
|
||||||
|
}
|
||||||
44
docs/architecture/adr-0003-default-fill.md
Normal file
44
docs/architecture/adr-0003-default-fill.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# ADR-0003: ACL default-fill strategy
|
||||||
|
|
||||||
|
- **Status:** Accepted
|
||||||
|
- **Date:** 2026-06-04
|
||||||
|
- **Deciders:** Respellion engineering
|
||||||
|
- **Relates to:** S-04 (#5); builds on ADR-0001 (loose coupling)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The ACL is the only code that talks to ZGW APIs (ADR-0001 / CLAUDE.md §8.1). When the
|
||||||
|
domain asks it to "open a zaak", the domain payload is intentionally free of ZGW
|
||||||
|
specifics — it carries domain facts (e.g. the registrant's BSN), not OpenZaak fields. But
|
||||||
|
OpenZaak's `POST /zaken` requires ZGW-mandatory fields: `bronorganisatie`,
|
||||||
|
`verantwoordelijkeOrganisatie`, `startdatum`, `vertrouwelijkheidaanduiding`, and a
|
||||||
|
`zaaktype` URL. Something has to supply those, and it must not leak into the domain.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**The ACL default-fills the ZGW-mandatory zaak fields; the domain never sees them.**
|
||||||
|
|
||||||
|
- `bronorganisatie`, `verantwoordelijkeOrganisatie`, `vertrouwelijkheidaanduiding`, and the
|
||||||
|
`zaaktype` URL come from **ACL configuration** (`AclDefaults` options) — not hardcoded,
|
||||||
|
not from the domain. This keeps them operationally manageable (the beheer portal will
|
||||||
|
edit them in S-15) and environment-specific (the seeded BIG zaaktype URL differs per env).
|
||||||
|
- `startdatum` is derived from an injected **clock** (today's date), so it is
|
||||||
|
deterministic in tests.
|
||||||
|
- The mapping from domain payload → ZGW `ZaakRequest` lives entirely inside the ACL
|
||||||
|
(`Application` builds the request from payload + defaults; `Infrastructure` serialises and
|
||||||
|
POSTs it). No other service constructs ZGW payloads or URLs.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Positive:** the domain stays ZGW-agnostic; ZGW knowledge is in one named place; defaults
|
||||||
|
are config (testable, env-specific, later editable via the beheer portal).
|
||||||
|
- **Cost:** the ACL must be configured per environment (the seeded zaaktype URL, the
|
||||||
|
organisation RSINs). Missing/invalid config fails fast at the ACL boundary.
|
||||||
|
- **Follow-ups:** mapping the BSN onto the zaak (eigenschap/rol), status transitions, and
|
||||||
|
documents are explicitly out of scope for S-04 and get their own slices.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
- **Defaults in the domain payload** — rejected: leaks ZGW concerns into the domain,
|
||||||
|
violating ADR-0001.
|
||||||
|
- **Hardcoded defaults in code** — rejected: not env-specific, not operationally editable.
|
||||||
52
docs/architecture/adr-0004-bdd-framework.md
Normal file
52
docs/architecture/adr-0004-bdd-framework.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# ADR-0004: Reqnroll as the BDD acceptance framework
|
||||||
|
|
||||||
|
- **Status:** Accepted
|
||||||
|
- **Date:** 2026-06-04
|
||||||
|
- **Deciders:** Respellion engineering
|
||||||
|
- **Relates to:** S-04 (#5); supports CLAUDE.md §3 (BDD at the use-case level) and §11 (tests pyramid)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
CLAUDE.md §11 mandates that each user-visible flow is driven by a Gherkin acceptance
|
||||||
|
scenario living in `tests/acceptance/`, and §3 names "BDD at the use-case level" as a core
|
||||||
|
engineering principle. The foundational slices (S-00…S-03) added no acceptance layer; S-04
|
||||||
|
is the first slice with real domain behaviour to drive, so it is where the BDD framework is
|
||||||
|
introduced. We need a .NET tool that:
|
||||||
|
|
||||||
|
- parses Gherkin `.feature` files and binds steps to C#,
|
||||||
|
- integrates with the existing xUnit test runner (the repo standardises on xUnit), so
|
||||||
|
acceptance tests run under the same `dotnet test` / `make ci` gate as everything else,
|
||||||
|
- is actively maintained on modern .NET (we target net10.0).
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Use [Reqnroll](https://reqnroll.net/) (`Reqnroll.xUnit`) for acceptance tests.**
|
||||||
|
|
||||||
|
- Reqnroll is the actively-maintained, open-source successor to SpecFlow (which is no longer
|
||||||
|
maintained). It keeps the same Gherkin + `[Binding]` model, so the knowledge transfers.
|
||||||
|
- `Reqnroll.xUnit` generates one xUnit test per scenario, so acceptance tests are discovered
|
||||||
|
and run by the same runner as the unit tests — no second test framework, no extra CI step.
|
||||||
|
- Acceptance projects live under `tests/acceptance/` per the PRD §9 layout. Generated
|
||||||
|
`*.feature.cs` files are build artefacts and are git-ignored.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Positive:** one assertion/runner stack (xUnit) across unit and acceptance tests; scenarios
|
||||||
|
are written in business language (Dutch domain terms inline) and reviewed as the slice's
|
||||||
|
contract; maintained tooling on net10.0.
|
||||||
|
- **Cost:** a new dependency (`Reqnroll.xUnit`) and its xUnit v2 transitive graph. Reqnroll
|
||||||
|
pulls `xunit.core` but not the assertion library, so the `xunit` metapackage is referenced
|
||||||
|
explicitly to get `Assert`.
|
||||||
|
- **Replaceable by:** hand-written xUnit "scenario" tests with a Given/When/Then helper, at
|
||||||
|
the cost of losing Gherkin as the shared, readable contract — which is the whole point of §3.
|
||||||
|
- **Follow-ups:** the real-OpenZaak integration test (Testcontainers) and the Stryker mutation
|
||||||
|
baseline for S-04 are tracked as their own issues split off #5.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
- **SpecFlow** — rejected: unmaintained and without an official net10.0 story; Reqnroll is its
|
||||||
|
drop-in successor.
|
||||||
|
- **Plain xUnit Given/When/Then helpers** — rejected for user-visible flows: loses the
|
||||||
|
business-readable Gherkin contract that §3/§11 require. Still fine for unit-level tests.
|
||||||
|
- **Xunit.Gherkin.Quick** — rejected: lighter but less featureful (no hooks/scoped contexts,
|
||||||
|
smaller community) than Reqnroll.
|
||||||
68
docs/architecture/adr-0005-mutation-testing.md
Normal file
68
docs/architecture/adr-0005-mutation-testing.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# ADR-0005: Stryker.NET for mutation testing, baseline on the ACL
|
||||||
|
|
||||||
|
- **Status:** Accepted
|
||||||
|
- **Date:** 2026-06-25
|
||||||
|
- **Deciders:** Respellion engineering
|
||||||
|
- **Relates to:** S-04b (#47); proposed in #51; supports CLAUDE.md §5 (mutation ratchet) and §3 (Definition of Done)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
CLAUDE.md §5 mandates Stryker on every PR with a **ratchet**: CI fails on a regression
|
||||||
|
below the established baseline, and the baseline only ever moves up. §3 lists "mutation
|
||||||
|
(ratchet)" as a Definition-of-Done gate for **every** slice. Yet no baseline existed — so,
|
||||||
|
strictly, no slice could satisfy that gate. S-04b establishes it.
|
||||||
|
|
||||||
|
The ACL is the natural place to set the first baseline: it is the first service with real
|
||||||
|
branching logic — `OpenZaakGateway` (HTTP contract, geo CRS headers, error handling),
|
||||||
|
`ZgwToken` (HS256 JWT minting), and the `AclService` default-fill mapping. We need a tool
|
||||||
|
that:
|
||||||
|
|
||||||
|
- mutates C# and runs the existing xUnit suite per mutant,
|
||||||
|
- is reproducible (same version locally and in CI, no global install),
|
||||||
|
- understands this repo's `.slnx` solution format (used repo-wide),
|
||||||
|
- emits a break threshold CI can gate on.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Use [Stryker.NET](https://stryker-mutator.io/docs/stryker-net/) (`dotnet-stryker`),
|
||||||
|
pinned as a local dotnet tool**, configured in solution mode against `Acl.slnx`.
|
||||||
|
|
||||||
|
- Pinned in `.config/dotnet-tools.json` (v4.15.0); `dotnet tool restore` makes
|
||||||
|
`make mutation` reproducible from a fresh clone, locally and in CI — no global install.
|
||||||
|
- **Solution mode** (`stryker-config.json` → `solution: Acl.slnx`) mutates the two projects
|
||||||
|
under test (`Acl.Application`, `Acl.Infrastructure`); `Acl.Api` is untested and skipped.
|
||||||
|
Stryker 4.15 reads `.slnx` directly, so no throwaway `.sln` shim is needed.
|
||||||
|
- A `mutation` make target runs it; it is wired into `make ci` and a parallel Gitea Actions
|
||||||
|
`mutation` job, keeping `make ci` an exact mirror of the pipeline.
|
||||||
|
|
||||||
|
**Baseline:** writing S-04b's tests surfaced that the ACL suite was thin — the initial
|
||||||
|
score was **35%** (survivors: unasserted CRS headers, null guards, error paths, and JWT
|
||||||
|
claims). Those tests were strengthened (killing the mutants honestly rather than lowering
|
||||||
|
the bar), raising the score to **95%**. The enforced `break` threshold is set to **90%** —
|
||||||
|
one-mutant headroom over the ~20-mutant surface, since a single mutant is ≈5%.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Positive:** test *strength* is gated, not just coverage; the ratchet protects the ACL's
|
||||||
|
ZGW contract logic; the baseline is repo-wide and ratchets upward per §5.
|
||||||
|
- **Cost:** a new dependency (`dotnet-stryker`) and a slower CI job than unit tests (~25 s on
|
||||||
|
the small ACL). Pinned + tool-restored, so reproducible.
|
||||||
|
- **One accepted survivor:** a mutation of the empty-response *exception message string*.
|
||||||
|
Asserting exception message text is brittle and the behaviour (type + control flow) is
|
||||||
|
unchanged — treated as an equivalent mutant, not a test gap.
|
||||||
|
- **Commitment:** later slices ratchet the threshold up deliberately, never down (§5). New
|
||||||
|
services add their own mutation run as they gain branching logic (BFF, Domain, …).
|
||||||
|
- **Replaceable by:** no realistic .NET alternative — Stryker.NET is the tool §5 already
|
||||||
|
names; the fallback is no mutation testing, which §5 forbids.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
- **Global `dotnet tool install -g`** — rejected: not reproducible/pinned per clone; the
|
||||||
|
local manifest gives every checkout and the CI runner the same version.
|
||||||
|
- **Mutate the whole `register-referentie.slnx`** — rejected for this slice: scopes the
|
||||||
|
baseline to services with no logic yet (BFF skeleton), diluting the signal. Each service
|
||||||
|
opts in as it gains logic.
|
||||||
|
- **Application-only scope** — rejected: would leave `Acl.Infrastructure`'s HTTP/JWT logic —
|
||||||
|
the riskiest code — unguarded by the ratchet.
|
||||||
|
- **Coverage gate instead of mutation** — rejected: line coverage does not measure whether
|
||||||
|
tests would *catch* a regression; that is the whole point of §5.
|
||||||
92
docs/architecture/adr-0006-integration-test-provisioning.md
Normal file
92
docs/architecture/adr-0006-integration-test-provisioning.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# ADR-0006: Provision the ACL integration test against the compose stack
|
||||||
|
|
||||||
|
- **Status:** Accepted
|
||||||
|
- **Date:** 2026-06-29
|
||||||
|
- **Deciders:** Respellion engineering
|
||||||
|
- **Relates to:** S-04a (#46); proposed in #53; builds on ADR-0001 (loose coupling), ADR-0002 (catalogus design), ADR-0003 (default-fill); supports CLAUDE.md §11 (integration tests via real containers)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
S-04 delivered the ACL's one operation — `OpenZaakGateway.OpenZaakAsync` — with unit
|
||||||
|
tests against a stubbed `HttpMessageHandler` and a Reqnroll scenario over an in-memory
|
||||||
|
stand-in. The deferred S-04 acceptance criterion (S-04a) is the one a stub cannot meet:
|
||||||
|
|
||||||
|
> Integration test using Testcontainers against real OpenZaak passes.
|
||||||
|
|
||||||
|
The test must drive the gateway against a **real** OpenZaak — real ZGW JWT auth, the real
|
||||||
|
`POST /zaken/api/v1/zaken` contract, real CRS handling — and assert a zaak comes back.
|
||||||
|
|
||||||
|
Two ways to stand OpenZaak up were considered (the issue's open question): (a) a full
|
||||||
|
**Testcontainers** graph started by the test, or (b) target the **running compose stack**
|
||||||
|
the repo already defines (`infra/openzaak/docker-compose.yml`, `make openzaak-up`).
|
||||||
|
|
||||||
|
Investigation reversed the initially-favoured Testcontainers option:
|
||||||
|
|
||||||
|
1. **Testcontainers .NET has no docker-compose support.** OpenZaak needs PostGIS + Redis +
|
||||||
|
a `setup_configuration` one-shot (the JWT client) + the API. Honouring "full graph" would
|
||||||
|
mean re-implementing that five-service stack — init ordering, the config volume, health
|
||||||
|
gating — by hand in C#, duplicating the maintained compose file and rotting with it. That
|
||||||
|
rubs against CLAUDE.md §13 ("if a test is hard to write, the design is wrong").
|
||||||
|
2. **The test cannot be hermetic anyway.** OpenZaak's Zaken API rejects a zaak against a
|
||||||
|
*concept* zaaktype (`not-published`), and a *published* zaaktype requires ≥1 resultaattype,
|
||||||
|
which OpenZaak validates by fetching the external **Selectielijst** reference API
|
||||||
|
(`selectielijst.openzaak.nl`). So a real zaak POST already depends on outbound internet
|
||||||
|
from the OpenZaak container — the self-containment that motivated Testcontainers is lost
|
||||||
|
regardless of how the containers are started.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**The ACL integration test targets the running compose stack; it does not start containers
|
||||||
|
itself. No new test dependency is added.**
|
||||||
|
|
||||||
|
- A gated test project `Acl.IntegrationTests` (`[Trait("Category","Integration")]`) talks to
|
||||||
|
OpenZaak with a plain `HttpClient`, reusing the same endpoint + JWT-client config the seed
|
||||||
|
uses (`OZ_BASE` / `OZ_CLIENT_ID` / `OZ_SECRET`, defaulting to the local stack). It locates
|
||||||
|
the published `BIG-REGISTRATIE` zaaktype via the Catalogi API and exercises the real
|
||||||
|
`OpenZaakGateway` against it.
|
||||||
|
- **The lane is kept out of the fast checks.** `make unit` runs with
|
||||||
|
`--filter "Category!=Integration"`; Stryker is pinned to `Acl.Tests` (`test-projects`), so
|
||||||
|
neither the unit nor the mutation lane needs a live stack. A `make integration` target
|
||||||
|
(`infra/run-integration.sh`) brings up a throwaway OpenZaak and runs the lane locally.
|
||||||
|
In CI the check runs as the `verify-acl` step of the consolidated `verify-stack` job
|
||||||
|
(issue #58) — one shared full-stack bring-up. This matches `make` being the single
|
||||||
|
source of truth (ADR-0005).
|
||||||
|
- **Publishing is opt-in in the seed.** `infra/openzaak/seed_catalogus.py` gains an
|
||||||
|
`OZ_PUBLISH=1` path that adds the relations OpenZaak's publish requires — two statustypen
|
||||||
|
(begin/eind), a roltype, and a resultaattype whose Selectielijst procestype is matched onto
|
||||||
|
the zaaktype — then publishes. The default seed (S-01 / ADR-0002) still leaves the zaaktype
|
||||||
|
a concept; only `make integration` flips the switch.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Positive:** a small, honest test over the real ZGW contract with no bespoke orchestration
|
||||||
|
to maintain; the compose stack is exercised exactly as operators run it; no new dependency.
|
||||||
|
- **It caught a real bug.** The gateway sent the zaak body via `JsonContent` without a
|
||||||
|
`Content-Length`, so .NET framed it as `Transfer-Encoding: chunked`, which OpenZaak's uwsgi
|
||||||
|
rejects with 400. A stubbed handler accepts either framing, so only a real OpenZaak surfaced
|
||||||
|
it. Fixed by buffering the body (`LoadIntoBufferAsync`); guarded in the fast lane by a unit
|
||||||
|
test asserting a `Content-Length` is set. This is the concrete justification for §11's
|
||||||
|
integration tier.
|
||||||
|
- **External dependency:** the integration job needs the OpenZaak container to reach
|
||||||
|
`selectielijst.openzaak.nl`. It is a stable public reference API (the same one OpenZaak uses
|
||||||
|
in production) but it is a network touchpoint, and a CI environment without egress would need
|
||||||
|
a local Selectielijst service or a recorded fixture. `OZ_SELECTIELIJST` overrides the base URL.
|
||||||
|
- **Cost:** the lane needs the stack up first, so it is separate from the fast lanes.
|
||||||
|
- **Runs on the hosted runner.** A process *on* the runner can't reach the stack's published
|
||||||
|
ports (Compose starts sibling containers via the host daemon — gitea-actions-gotchas.md §5,
|
||||||
|
same split as §1), so `infra/run-integration.sh` runs both the seed and the test as containers
|
||||||
|
*joined to the OpenZaak network*, reaching it by **container IP** (a single-label host like
|
||||||
|
`openzaak` isn't URL-valid for OpenZaak's own `URLValidator`; an IPv4 literal is). Code is
|
||||||
|
delivered by image build / `docker cp`, never bind mounts. The CI job therefore needs only
|
||||||
|
Docker — no `setup-dotnet`. (This closed the follow-up that was originally split out as #55.)
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
- **Full Testcontainers graph** — rejected: re-implements the compose stack in C# (brittle,
|
||||||
|
duplicative) for no hermeticity gain, since the Selectielijst dependency remains.
|
||||||
|
- **Single OpenZaak container (sqlite/locmem)** — rejected: diverges from the real
|
||||||
|
PostGIS-backed, Redis-cached deployment; the Zaken API is a geo API and the divergence would
|
||||||
|
undermine the contract the test exists to verify.
|
||||||
|
- **Mock OpenZaak / record-replay** — rejected: that is what the existing stubbed-handler unit
|
||||||
|
tests already do; it cannot exercise the real contract, and would not have caught the chunked
|
||||||
|
body bug.
|
||||||
77
docs/architecture/adr-0007-notification-wiring.md
Normal file
77
docs/architecture/adr-0007-notification-wiring.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# ADR-0007: Wiring OpenZaak → Open Notificaties (NRC) for notifications
|
||||||
|
|
||||||
|
- **Status:** Accepted
|
||||||
|
- **Date:** 2026-06-29
|
||||||
|
- **Deciders:** Respellion engineering
|
||||||
|
- **Relates to:** S-01-c (#56); completes S-01 (#2); unblocks the Event Subscriber (#7); builds on ADR-0002 (catalogus/seed) and ADR-0006 (runner-safe container harnesses)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
S-01 brought OpenZaak + Open Notificaties (NRC) up in compose but **deferred the
|
||||||
|
notification wiring**: OpenZaak ran with `NOTIFICATIONS_DISABLED=true` and NRC's
|
||||||
|
`setup_configuration` was empty. The walking skeleton (PRD §12) needs the upstream
|
||||||
|
event path — a zaak created in OpenZaak must publish a notification NRC fans out to
|
||||||
|
subscribers — before the Event Subscriber (#7) can consume it.
|
||||||
|
|
||||||
|
The OpenZaak↔NRC handshake is intricate and several details are non-obvious; they
|
||||||
|
were nailed down by iterating `setup_configuration` against the running stack.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Provision both sides declaratively via `setup_configuration`, authenticate with the
|
||||||
|
existing `big-reference-seed` client, and run NRC's celery-beat so deliveries happen.**
|
||||||
|
|
||||||
|
- **OpenZaak** (`infra/openzaak/setup_configuration/data.yaml`): a `zgw_consumers`
|
||||||
|
service `nrc` (api_type `nrc`, the NRC API root) plus `notifications_config` naming
|
||||||
|
it. `NOTIFICATIONS_DISABLED` is flipped to `false` **only when NRC is present** —
|
||||||
|
the full stack and the local twin set it; OpenZaak-only bring-ups (`openzaak-up`,
|
||||||
|
the ACL integration test) default it back to `true` via `OZ_NOTIFICATIONS_DISABLED`
|
||||||
|
so they don't 500 publishing to an absent NRC.
|
||||||
|
- **NRC** (`infra/opennotificaties/setup_configuration/data.yaml`): the
|
||||||
|
`big-reference-seed` JWT credential (to verify OpenZaak's token), a `zgw_consumers`
|
||||||
|
`ac` service pointing at **OpenZaak's Autorisaties API**, the `autorisaties_api`
|
||||||
|
step delegating authorization to that AC, and the `zaken` kanaal. NRC's init
|
||||||
|
container switches from `migrate` to `/setup_configuration.sh`; its data.yaml is
|
||||||
|
delivered through the `rr-nrc-config` external volume by `infra/seed-config.sh`
|
||||||
|
(the same `docker cp` pattern as OpenZaak — bind mounts don't reach the CI runner's
|
||||||
|
daemon).
|
||||||
|
- **celery-beat is required.** NRC accepts a notification and writes a
|
||||||
|
`ScheduledNotification`; a periodic `execute_notifications` task (celery-beat,
|
||||||
|
every `NOTIFICATION_SEC_INTERVAL`s) drains it to the worker for delivery. The lean
|
||||||
|
S-01 stack dropped beat — so notifications were accepted but never delivered. An
|
||||||
|
`nrc-beat` service is added to every compose; the interval is lowered to 5s.
|
||||||
|
|
||||||
|
Verification is a runner-safe smoke (`infra/run-notification-check.sh`): it seeds a
|
||||||
|
published BIG zaaktype, registers an abonnement to a webhook sink, creates a zaak, and
|
||||||
|
asserts the sink receives the `zaken`/`create` notification — all from containers
|
||||||
|
**inside** the compose network (ADR-0006). Locally it runs via `make verify-notifications`
|
||||||
|
(a throwaway oz+nrc stack); in CI it runs as the `verify-nrc` step of the consolidated
|
||||||
|
`verify-stack` job (one shared full-stack bring-up — issue #58).
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Positive:** the walking-skeleton event path works end to end; #7 can consume real
|
||||||
|
notifications; the wiring is declarative and reproducible from a fresh `make`.
|
||||||
|
- **Gotchas captured (see gitea-actions-gotchas.md):**
|
||||||
|
- **Single-label hosts aren't URL-valid.** OpenZaak/NRC reject `http://openzaak…`
|
||||||
|
/`http://nrc-web…` in URLs they validate (Django `URLValidator`); the verify
|
||||||
|
harness reaches services and registers the sink callback **by container IP**.
|
||||||
|
- **Abonnement callbacks must enforce auth.** NRC probes the callback during
|
||||||
|
registration and refuses it (`no-auth-on-callback-url`) unless it returns 401
|
||||||
|
without the configured `Authorization`; the sink enforces a bearer token.
|
||||||
|
- **Cost:** an extra long-running service (`nrc-beat`) per stack, and the verify job
|
||||||
|
needs egress (base images + `selectielijst.openzaak.nl`, since the published
|
||||||
|
zaaktype the check creates a zaak against depends on it — ADR-0006).
|
||||||
|
- **Dev-only credentials** reused (`big-reference-seed` / its secret) across publish,
|
||||||
|
AC lookup, and seeding — acceptable for the reference app, not production.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
- **NRC with its own (non-AC) authorization** — rejected: delegating to OpenZaak's
|
||||||
|
Autorisaties API is the upstream-intended model and reuses the applicatie that
|
||||||
|
already grants `heeft_alle_autorisaties`.
|
||||||
|
- **Keep beat out, deliver synchronously** — not an option: Open Notificaties 1.16
|
||||||
|
delivers via scheduled notifications drained by beat; there is no sync path.
|
||||||
|
- **A persistent abonnement in `setup_configuration`** instead of registering one in
|
||||||
|
the verify harness — deferred: the real subscriber is #7; the harness's sink
|
||||||
|
abonnement is throwaway and IP-specific.
|
||||||
89
docs/architecture/adr-0008-read-projection-store.md
Normal file
89
docs/architecture/adr-0008-read-projection-store.md
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# ADR-0008: The read projection — a shared, rebuildable store with a writer and a reader
|
||||||
|
|
||||||
|
- **Status:** Accepted
|
||||||
|
- **Date:** 2026-06-30
|
||||||
|
- **Deciders:** Respellion engineering
|
||||||
|
- **Relates to:** S-06 (#7); builds on ADR-0001 (loose coupling), ADR-0007 (#56, OZ→NRC wiring); first EF Core usage in the repo
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
S-06 (#7) adds the upstream event path's destination: an **Event Subscriber** that consumes
|
||||||
|
NRC notifications and a **read projection** the openbaar register reads. The walking-skeleton
|
||||||
|
projection (PRD §8.4) holds one row per zaak — `id`, `bsn`, `naam_placeholder`, `status` —
|
||||||
|
and must be **idempotent** (NRC redelivers and reorders, CLAUDE.md §8.6) and **rebuildable**
|
||||||
|
(a derived artefact, never a write-only source of truth).
|
||||||
|
|
||||||
|
Two design questions had no obvious answer:
|
||||||
|
|
||||||
|
1. **Where does `bsn` come from?** The NRC `zaken`/`zaak`/`create` notification carries only the
|
||||||
|
zaak URL plus the fixed `kenmerken` (`bronorganisatie`, `zaaktype`, `vertrouwelijkheidaanduiding`).
|
||||||
|
It does **not** carry the bsn. Reading it means calling a ZGW API — which **only the ACL** may
|
||||||
|
do (CLAUDE.md §8.1). The issue's "Touches" lists only `event-subscriber` + `projection-api`,
|
||||||
|
not the ACL.
|
||||||
|
2. **Who owns the projection schema?** The subscriber writes the projection; the projection-api
|
||||||
|
reads it. CLAUDE.md §8.5 says "no direct DB access across services; each service owns its
|
||||||
|
schema." Two deployables on one table looks like a violation.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**One Postgres database is the read projection. The Event Subscriber writes it (projector) and
|
||||||
|
the projection-api reads it (query); both are processes of the single "Read Projection" bounded
|
||||||
|
context and share one schema, defined in a shared `Projection.ReadModel` library. `bsn` is
|
||||||
|
deferred.**
|
||||||
|
|
||||||
|
- **Schema ownership.** The read model — `register_projection` plus the subscriber's
|
||||||
|
`processed_notifications` log — lives in `services/projection-api/Projection.ReadModel`
|
||||||
|
(EF Core + Npgsql). Both services reference it. This is the textbook CQRS read-model split
|
||||||
|
(one writer, one reader over one derived store), **not** the cross-*domain* DB reach §8.5
|
||||||
|
forbids: no domain owns write-state here; the projection is rebuildable (§8.4). §8.5 still
|
||||||
|
holds for every domain database.
|
||||||
|
- **Idempotency** is the primary key on `processed_notifications.key` (a deterministic key
|
||||||
|
derived from the immutable notification content). A duplicate insert raises a unique violation,
|
||||||
|
caught and reported as "already recorded", so the duplicate never reaches the projection. The
|
||||||
|
projection upsert is itself idempotent on the zaak id, a second line of defence.
|
||||||
|
- **Rebuild replays the log, not OpenZaak.** `POST /admin/rebuild` clears `register_projection`
|
||||||
|
and reprojects every row in `processed_notifications`. So "rebuildable" needs **no** ZGW access
|
||||||
|
(§8.1) and no ACL dependency — keeping S-06 within its stated scope.
|
||||||
|
- **`bsn` and `naam_placeholder` are deferred.** They are columns (nullable) but the minimal slice
|
||||||
|
populates only `id` + `status` (`INGEDIEND`) from the notification. Populating personal data
|
||||||
|
requires reading the zaak **through the ACL** (§8.1) and is its own follow-up; the column shape
|
||||||
|
is in place so that change is additive.
|
||||||
|
- **New dependency: EF Core 10 + `Npgsql.EntityFrameworkCore.PostgreSQL`.** What it gives us: a
|
||||||
|
migrated relational schema, LINQ queries, and a clean port implementation. What we'd write
|
||||||
|
instead: hand-rolled SQL + a migration runner. Risk: ORM complexity and an extra dependency
|
||||||
|
graph — bounded here to a tiny two-table read model. `dotnet-ef` is pinned as a local tool for
|
||||||
|
migrations; `NuGetAuditMode=direct` keeps EF's design-time-only tooling transitive out of the
|
||||||
|
audited, shipped graph.
|
||||||
|
|
||||||
|
The end-to-end path is verified by a runner-safe live-stack smoke (`infra/run-projection-check.sh`,
|
||||||
|
the `verify-projection` step of the `verify-stack` job, #58): register an abonnement at the real
|
||||||
|
Event Subscriber's callback, create a zaak, assert projection-api serves an `INGEDIEND` row — all
|
||||||
|
in-network, reaching services by container IP (ADR-0006/0007).
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Positive:** the upstream event path reaches a queryable projection; idempotent and rebuildable
|
||||||
|
without OpenZaak; S-06 stays inside its stated touch-set (no ACL change); the projection-api is
|
||||||
|
ready for S-09 to tighten public-safe field filtering.
|
||||||
|
- **Negative / deferred:**
|
||||||
|
- `bsn`/`naam_placeholder` stay empty until a follow-up wires zaak reads via the ACL.
|
||||||
|
- The abonnement is registered by the verify harness (by container IP), not provisioned
|
||||||
|
persistently — ADR-0007 already deferred a persistent abonnement, and a single-label service
|
||||||
|
host is not URL-valid for NRC, so persistent registration needs a dotted network alias. Tracked
|
||||||
|
as a follow-up; a plain `make up` therefore needs the abonnement registered before the event
|
||||||
|
path flows.
|
||||||
|
- Two services share one database. Acceptable for a derived read model; revisit if the read and
|
||||||
|
write sides ever need independent scaling or storage.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
- **Subscriber reads OpenZaak directly to fill `bsn`** — rejected: breaks §8.1 (only the ACL talks
|
||||||
|
to ZGW) and would need its own ADR to bend the rule.
|
||||||
|
- **Extend the ACL with a zaak-read operation, consumed as a library** — viable and §8.1-clean, but
|
||||||
|
it grows S-06 beyond its stated scope (touches the ACL) and pulls personal-data handling forward;
|
||||||
|
deferred to a follow-up.
|
||||||
|
- **projection-api owns the DB and exposes an internal write endpoint the subscriber calls** —
|
||||||
|
rejected for the walking skeleton: adds an HTTP hop and a write surface on a read service for no
|
||||||
|
current benefit over a shared, rebuildable read model.
|
||||||
|
- **Separate databases for the log and the projection** — rejected as premature: both are the read
|
||||||
|
side's private, rebuildable state; one DB is simpler and still honours §8.5's intent.
|
||||||
88
docs/architecture/adr-0009-external-task-job-worker.md
Normal file
88
docs/architecture/adr-0009-external-task-job-worker.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# ADR-0009: The Domain Service drives Flowable as an external-task job worker
|
||||||
|
|
||||||
|
- **Status:** Accepted
|
||||||
|
- **Date:** 2026-06-30
|
||||||
|
- **Deciders:** Respellion engineering
|
||||||
|
- **Relates to:** S-05 (#6); proposal #60; builds on ADR-0001 (loose coupling, §8.1/§8.2), S-03 (#4, the `registratie` BPMN), S-04 (#5, the ACL `OpenZaak` operation)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
S-05 (#6) adds the **BIG Domain Service**. Submitting a registration must: create a
|
||||||
|
`Registration` aggregate, **start the Flowable `registratie` process** (S-03), have the
|
||||||
|
`OpenZaakAanmaken` task **open a zaak via the ACL** (S-04), and store the resulting zaak URL
|
||||||
|
back on the aggregate.
|
||||||
|
|
||||||
|
`OpenZaakAanmaken` is a Flowable **external-worker** service task (`flowable:type="external-worker"`,
|
||||||
|
topic `OpenZaakAanmaken`). Flowable does not push it anywhere — it parks the job and waits for a
|
||||||
|
worker to **acquire and lock** it, do the work, and **complete** it. Two coupling rules constrain
|
||||||
|
who may do what:
|
||||||
|
|
||||||
|
- **§8.2 — the Workflow Client is the only code that talks to Flowable.** BPMN models never embed
|
||||||
|
OpenZaak knowledge; they ask the Workflow Client to execute external tasks.
|
||||||
|
- **§8.1 — the ACL is the only code that talks to ZGW.** The worker opens the zaak *through the ACL*,
|
||||||
|
never by constructing ZGW URLs itself.
|
||||||
|
|
||||||
|
This is an ADR-worthy moment (§14): a service boundary is defined and both coupling rules are
|
||||||
|
exercised. The open question is *how* the external task is driven.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**The Domain Service drives the `OpenZaakAanmaken` task as a hosted external-task job worker
|
||||||
|
(PRD §36). Orchestration is eventually consistent, not request-synchronous.**
|
||||||
|
|
||||||
|
- **`POST /registrations` is fast and side-effecting only on the domain side.** It creates the
|
||||||
|
`Registration` aggregate in state `INGEDIEND`, persists it, and asks the Workflow Client to start
|
||||||
|
one `registratie` process instance, recording the process-instance id on the aggregate. It returns
|
||||||
|
immediately; it does **not** wait for the zaak to be opened.
|
||||||
|
- **A hosted worker polls Flowable for `OpenZaakAanmaken` jobs.** It acquires and locks a job, calls
|
||||||
|
the ACL `OpenZaak` operation (§8.1), attaches the returned zaak URL to the matching aggregate
|
||||||
|
(`Registration.AttachZaak`), and completes the job in Flowable. The process then runs to its end
|
||||||
|
event.
|
||||||
|
- **The Workflow Client is the only Flowable client (§8.2).** It lives in the Domain Service's
|
||||||
|
`Infrastructure` layer and speaks Flowable's REST API (start process-instance; acquire/lock/complete
|
||||||
|
external-worker jobs). No other code — not the Application layer, not the BPMN — knows Flowable
|
||||||
|
exists.
|
||||||
|
- **The worker *logic* is an Application service over ports**, not Flowable-aware code. `OpenZaakWorker`
|
||||||
|
takes an acquired job (topic + the registration id it carries), calls `IAclClient` and
|
||||||
|
`IRegistrationStore`, and returns the zaak URL to complete with. The **polling loop** is a thin
|
||||||
|
`BackgroundService` in `Infrastructure` that fetches jobs via the Workflow Client and feeds them to
|
||||||
|
the worker. So the orchestration is covered by fast unit tests against fakes; only the REST framing
|
||||||
|
needs a container integration test.
|
||||||
|
|
||||||
|
## Scope decisions for the minimal slice
|
||||||
|
|
||||||
|
- **Registration persistence is in-memory.** The walking skeleton's *read* path is fed by
|
||||||
|
NRC → Event Subscriber → projection (S-06, #7), not by the domain database. An EF-backed domain
|
||||||
|
store buys nothing the demo needs yet, so it is a documented follow-up; the `IRegistrationStore`
|
||||||
|
port keeps that change additive. (PRD §88 envisions EF Core for the domain DB eventually.)
|
||||||
|
- **The aggregate's state machine is minimal:** `INGEDIEND` on submission. Later flows (withdrawal,
|
||||||
|
beoordeling, herregistratie) add states in their own slices — they are out of scope here.
|
||||||
|
- **No bsn flows to ZGW yet.** The ACL `OpenZaak` operation already default-fills the ZGW-mandatory
|
||||||
|
fields (ADR-0003) and takes the bsn as its domain payload; the domain hands it through unchanged.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Positive:** the submit request is decoupled from ACL/OpenZaak latency; the documented Common
|
||||||
|
Ground pattern (external-task worker) is realised; both coupling rules (§8.1, §8.2) hold with the
|
||||||
|
Flowable knowledge isolated to one Infrastructure class; the orchestration is unit-testable.
|
||||||
|
- **Negative / deferred:**
|
||||||
|
- Eventual consistency: immediately after `POST /registrations` the aggregate has no zaak URL yet.
|
||||||
|
Acceptable — the read side is the projection, not the domain store.
|
||||||
|
- In-memory registration state is lost on restart; fine for the skeleton, replaced by an EF store
|
||||||
|
in a follow-up.
|
||||||
|
- The worker polls (no push); poll interval is a tuning knob, not a correctness concern, since
|
||||||
|
Flowable holds the job until completed.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
- **Synchronous acquire+complete inside the `POST /registrations` request** — rejected: simpler and
|
||||||
|
deterministic, but couples the submit request to ACL/OpenZaak latency and failure, and is not the
|
||||||
|
external-task worker pattern PRD §36 mandates. It would also make the request fail if OpenZaak is
|
||||||
|
briefly down, instead of the job simply staying parked for the worker to retry.
|
||||||
|
- **A standalone Workflow Client service, separate from the Domain Service** — rejected for this
|
||||||
|
slice: the worker needs the domain's aggregate store and the ACL client anyway, and PRD §9 places
|
||||||
|
the Workflow Client inside the Domain Service deployment. A separate process adds a hop and a
|
||||||
|
shared store for no current benefit.
|
||||||
|
- **Flowable pushes to a webhook instead of being polled** — rejected: Flowable's external-worker
|
||||||
|
model is pull-based (acquire/lock/complete); a push shim would re-implement it with weaker
|
||||||
|
delivery guarantees.
|
||||||
74
docs/architecture/adr-0010-bff-oidc.md
Normal file
74
docs/architecture/adr-0010-bff-oidc.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# ADR-0010: The BFF validates Keycloak tokens and is the portals' only backend
|
||||||
|
|
||||||
|
- **Status:** Accepted
|
||||||
|
- **Date:** 2026-07-01
|
||||||
|
- **Deciders:** Respellion engineering
|
||||||
|
- **Relates to:** S-07 (#8); proposal #63; builds on ADR-0001 (loose coupling, §8.3), S-02 (#3, Keycloak realms), S-05 (#6, Domain Service), S-06 (#7, read projection)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
S-07 (#8) adds the **BFF (Backend-for-Frontend)** — the single backend the Angular portals talk
|
||||||
|
to (CLAUDE.md §8.3). For the walking skeleton it exposes two endpoints and fans out to services
|
||||||
|
already built:
|
||||||
|
|
||||||
|
- `POST /self-service/registrations` → Domain Service `POST /registrations` (S-05).
|
||||||
|
- `GET /openbaar/register?q=…` → projection-api `GET /register` (S-06).
|
||||||
|
|
||||||
|
It must validate tokens issued by Keycloak (S-02). This is an ADR-worthy moment (§14): a new
|
||||||
|
dependency (JWT bearer authentication) and two new service boundaries (BFF→domain, BFF→projection).
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**The BFF is the portals' only backend; it validates Keycloak `digid`-realm JWTs on the
|
||||||
|
self-service endpoint, leaves the openbaar lookup anonymous, and fans out to the domain and
|
||||||
|
projection over typed HTTP clients.**
|
||||||
|
|
||||||
|
- **Auth model.** `POST /self-service/registrations` requires a valid `digid`-realm bearer token;
|
||||||
|
the BFF reads the `bsn` claim and forwards it to the domain. Missing / invalid / expired token →
|
||||||
|
**401**. `GET /openbaar/register` is **anonymous** — the openbaar register is a public lookup
|
||||||
|
(S-09), so no token is required.
|
||||||
|
- **Portals talk only to the BFF (§8.3).** They never call the Domain Service, ACL, projection, or
|
||||||
|
OpenZaak directly. The BFF orchestrates via typed `HttpClient`s whose base URLs come from config.
|
||||||
|
Downstream calls are unauthenticated on the internal network for the walking skeleton; a
|
||||||
|
service-to-service auth story (e.g. client-credentials) is a later slice, not this one.
|
||||||
|
- **Validation is `Microsoft.AspNetCore.Authentication.JwtBearer`** pointed at the Keycloak `digid`
|
||||||
|
realm authority. **New dependency justification:** it gives us standards-based OIDC/JWT validation
|
||||||
|
(signature, issuer, expiry, audience) maintained by the framework; rolling our own JWT validation
|
||||||
|
would be error-prone security code; the risk is a first-party ASP.NET Core package — minimal.
|
||||||
|
- **Tests mint their own tokens.** `WebApplicationFactory` tests override the bearer options with a
|
||||||
|
**test signing key**, so valid / invalid / expired tokens are minted in-process without a live
|
||||||
|
Keycloak. Real Keycloak validation is exercised by a live-stack `verify-bff` check.
|
||||||
|
- **OpenAPI is generated and committed** (`services/bff/openapi.json`) from .NET's built-in OpenAPI,
|
||||||
|
so S-08's Angular client is generated from the spec, never hand-written (§10).
|
||||||
|
|
||||||
|
## Known wrinkle — container OIDC issuer mismatch
|
||||||
|
|
||||||
|
Keycloak stamps tokens with an `iss` equal to its **browser-facing** URL (what the portal used to
|
||||||
|
log in), which differs from the BFF's **in-container** authority (`http://keycloak:8080/realms/digid`).
|
||||||
|
Strict issuer validation then rejects otherwise-valid tokens. Unit tests avoid this (test key).
|
||||||
|
`verify-bff` handles it by aligning the configured authority/issuer with the token's `iss` (and, if
|
||||||
|
needed, disabling metadata address rewriting). Recorded so it is not rediscovered each time.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Positive:** the walking skeleton gains its front door; §8.3 holds with all portal traffic going
|
||||||
|
through one backend; token validation is standard and testable without infra; the committed
|
||||||
|
OpenAPI unblocks S-08.
|
||||||
|
- **Negative / deferred:**
|
||||||
|
- Downstream service-to-service auth is deferred (internal-network trust for now).
|
||||||
|
- The openbaar endpoint is anonymous; when public-safe field filtering tightens (S-09) it stays
|
||||||
|
anonymous but the projection query narrows.
|
||||||
|
- The issuer-mismatch handling is dev-oriented; a production reverse-proxy setup would align the
|
||||||
|
browser and internal issuer URLs instead.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
- **Token-gate the openbaar endpoint too** — rejected: the openbaar register is public by design
|
||||||
|
(S-09); requiring a login would contradict the slice's intent.
|
||||||
|
- **Validate tokens by calling Keycloak's introspection endpoint per request** — rejected: adds a
|
||||||
|
network hop per call and a Keycloak dependency on the hot path; local JWT signature validation via
|
||||||
|
the realm's JWKS is the standard, faster choice.
|
||||||
|
- **Hand-written JWT parsing** — rejected: security-sensitive code we shouldn't own when a
|
||||||
|
first-party validator exists.
|
||||||
|
- **Generate the OpenAPI client by hand / keep the spec uncommitted** — rejected: §10 requires a
|
||||||
|
generated client from a committed spec.
|
||||||
148
docs/demo-script.md
Normal file
148
docs/demo-script.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# Demo script
|
||||||
|
|
||||||
|
A running log of demoable outcomes, one section per slice. Each entry is a short,
|
||||||
|
copy-pasteable walkthrough against a local `make up` stack.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## S-08c — Self-service submit form (NL Design System + DigiD)
|
||||||
|
|
||||||
|
**Outcome:** a zorgprofessional logs in via mock DigiD and submits a BIG registration through the
|
||||||
|
self-service portal (NL Design System styling); the page confirms with the reference returned by the
|
||||||
|
BFF. The bsn comes from the DigiD token, so it's a confirm-and-submit flow (no bsn field).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Bring the backend + Keycloak up (BFF on :8080, Keycloak on :8180).
|
||||||
|
make up
|
||||||
|
|
||||||
|
# 2. Serve the portal (dev server); it redirects to Keycloak for DigiD login.
|
||||||
|
pnpm nx serve self-service # → http://localhost:4200
|
||||||
|
|
||||||
|
# 3. In the browser: log in as the mock DigiD user jan-burger / test123, then submit.
|
||||||
|
# The page shows the returned registration reference.
|
||||||
|
```
|
||||||
|
|
||||||
|
> First real UI. The full **login → submit → success** happy path is automated in **S-08d**
|
||||||
|
> (Playwright, against the compose-served app). Component tests + an axe WCAG 2.1 AA check on the
|
||||||
|
> submit page run headless in the `frontend` CI lane. See `docs/frontend-decisions.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## S-08a — Nx workspace + self-service portal skeleton
|
||||||
|
|
||||||
|
**Outcome:** the frontend foundation — an Nx (pnpm) monorepo with the `self-service` Angular app
|
||||||
|
(standalone + signals), lint/test/build green in a CI Node lane. The login + submit form follow in
|
||||||
|
S-08c.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From a fresh clone (Node 24 + pnpm 11):
|
||||||
|
pnpm install # native builds are pre-approved in pnpm-workspace.yaml
|
||||||
|
pnpm nx test self-service # Vitest component test
|
||||||
|
pnpm nx build self-service # production build
|
||||||
|
pnpm nx serve self-service # → http://localhost:4200 (placeholder page)
|
||||||
|
# Or the CI-equivalent one-shot:
|
||||||
|
make frontend # install + nx lint/test/build
|
||||||
|
```
|
||||||
|
|
||||||
|
> Nx manages only `apps/`+`libs/`; the .NET services stay on `dotnet`/the Makefile. NL Design System
|
||||||
|
> and the real form arrive in S-08c (#67); see `docs/frontend-decisions.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## S-07 — BFF: the portals' single backend
|
||||||
|
|
||||||
|
**Outcome:** the BFF validates Keycloak `digid` tokens on the self-service submit (forwarding the
|
||||||
|
bsn to the domain) and serves the openbaar register anonymously with only public-safe fields — the
|
||||||
|
front door the portals (S-08/S-09) will talk to.
|
||||||
|
|
||||||
|
**The path:** portal → BFF `POST /self-service/registrations` (token-gated) → domain; and
|
||||||
|
BFF `GET /openbaar/register` (anonymous) → projection-api. See ADR-0010.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Bring the full stack up.
|
||||||
|
make up
|
||||||
|
|
||||||
|
# 2. Drive the BFF end-to-end (401 without a token, 202 with a real digid token, anonymous openbaar).
|
||||||
|
make verify-bff # → "OK — BFF: 401 without token, 202 with a digid token, anonymous ..."
|
||||||
|
|
||||||
|
# 3. Try it by hand (BFF on host port 8080).
|
||||||
|
# a) A digid access token for the mock user jan-burger (bsn 123456782):
|
||||||
|
tok=$(curl -s -X POST http://localhost:8180/realms/digid/protocol/openid-connect/token \
|
||||||
|
-d grant_type=password -d client_id=big-portal -d username=jan-burger -d password=test123 \
|
||||||
|
| python3 -c "import sys,json;print(json.load(sys.stdin)['access_token'])")
|
||||||
|
|
||||||
|
# b) Submit — without the token it is 401; with it, 202:
|
||||||
|
curl -s -o /dev/null -w "no token -> %{http_code}\n" -X POST http://localhost:8080/self-service/registrations
|
||||||
|
curl -s -o /dev/null -w "with token-> %{http_code}\n" -X POST http://localhost:8080/self-service/registrations \
|
||||||
|
-H "Authorization: Bearer $tok"
|
||||||
|
|
||||||
|
# c) The openbaar register is anonymous and exposes only id + status (never the bsn):
|
||||||
|
curl -fsS http://localhost:8080/openbaar/register | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
> The self-service token is validated against Keycloak's `digid` realm; the openbaar lookup needs no
|
||||||
|
> token (S-09). The generated contract lives at `services/bff/openapi.json` — S-08's client is built
|
||||||
|
> from it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## S-05 — BIG Domain Service: submit a registration
|
||||||
|
|
||||||
|
**Outcome:** submitting a registration starts a Flowable process; the external-task worker
|
||||||
|
opens a zaak via the ACL and records it on the aggregate — the upstream half of the skeleton
|
||||||
|
that produces the zaak S-06 then projects.
|
||||||
|
|
||||||
|
**The path:** domain `POST /registrations` → Flowable `registratie` process → `OpenZaakAanmaken`
|
||||||
|
worker → ACL → OpenZaak; `GET /registrations/{id}` shows the opened zaak (ADR-0009).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Bring the full stack up (seeds config, builds our services, waits for health).
|
||||||
|
make up
|
||||||
|
|
||||||
|
# 2. Drive the full path end-to-end. This also seeds a published BIG zaaktype and points the
|
||||||
|
# ACL at it (the zaak's zaaktype URL is server-assigned, so it isn't known at bring-up).
|
||||||
|
make verify-domain # → "OK — the domain opened a zaak and recorded it on the registration"
|
||||||
|
|
||||||
|
# 3. Submit one yourself (domain on host port 8130). Returns 202 + a Location to read back.
|
||||||
|
loc=$(curl -fsS -D - -o /dev/null -X POST http://localhost:8130/registrations \
|
||||||
|
-H 'Content-Type: application/json' -d '{"bsn":"123456782"}' | sed -n 's/\r$//; s/^[Ll]ocation: //p')
|
||||||
|
|
||||||
|
# 4. The worker opens the zaak off the request path (eventual consistency, ADR-0009); poll
|
||||||
|
# until zaakUrl is filled. (Step 2 must have run first, so the ACL knows the zaaktype.)
|
||||||
|
curl -fsS "http://localhost:8130$loc" | jq
|
||||||
|
# → { "registrationId": "...", "status": "Ingediend", "zaakUrl": "http://.../zaken/api/v1/zaken/<uuid>" }
|
||||||
|
```
|
||||||
|
|
||||||
|
> Registration state is in-memory for this slice (ADR-0009); the rebuildable read model is the
|
||||||
|
> projection (S-06), fed by the very zaak this flow opens.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## S-06 — Event Subscriber + read projection
|
||||||
|
|
||||||
|
**Outcome:** a zaak created in OpenZaak flows through NRC to the Event Subscriber, which
|
||||||
|
projects it into a rebuildable read projection the projection-api serves.
|
||||||
|
|
||||||
|
**The path:** OpenZaak → (notification) NRC → (abonnement callback) Event Subscriber →
|
||||||
|
`register_projection` → projection-api `GET /register`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Bring the full stack up (seeds config, builds our services, waits for health).
|
||||||
|
make up
|
||||||
|
|
||||||
|
# 2. Register the Event Subscriber's abonnement and create a zaak, then read it back.
|
||||||
|
# (The verify-projection check does exactly this end-to-end and asserts the result.)
|
||||||
|
make verify-projection # → "OK — projection-api serves zaak <uuid> with status INGEDIEND"
|
||||||
|
|
||||||
|
# 3. Observe the projection directly via the read API (host port 8120).
|
||||||
|
curl -fsS http://localhost:8120/register | jq
|
||||||
|
# → [ { "id": "<zaak-uuid>", "status": "INGEDIEND", "bsn": null, "naamPlaceholder": null } ]
|
||||||
|
|
||||||
|
# 4. Idempotency + rebuild: replays don't duplicate; a rebuild repopulates from the
|
||||||
|
# notification log (no OpenZaak access needed — ADR-0008).
|
||||||
|
curl -fsS -X POST http://localhost:8110/admin/rebuild # Event Subscriber, host port 8110
|
||||||
|
curl -fsS http://localhost:8120/register | jq 'length' # → unchanged
|
||||||
|
```
|
||||||
|
|
||||||
|
> `bsn` / `naam_placeholder` are deferred (ADR-0008) — the notification doesn't carry them and
|
||||||
|
> the subscriber may not read OpenZaak directly (§8.1). They surface in a later slice.
|
||||||
74
docs/frontend-decisions.md
Normal file
74
docs/frontend-decisions.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# Frontend decisions
|
||||||
|
|
||||||
|
A running log of frontend tooling and component decisions (CLAUDE.md §10). One entry per
|
||||||
|
decision; record *why*, and note any deviation from NL Design System.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workspace & tooling (S-08a, #65)
|
||||||
|
|
||||||
|
The portals live in an **Nx monorepo at the repository root**, alongside the .NET `services/`.
|
||||||
|
|
||||||
|
- **Package manager: pnpm.** Native build scripts are approved explicitly in `pnpm-workspace.yaml`
|
||||||
|
under `allowBuilds` (pnpm 11 fails the install otherwise). Node 24, pnpm 11.
|
||||||
|
- **Angular, standalone components + signals, no NgModules** (§10). Apps are generated with
|
||||||
|
`@nx/angular:application`.
|
||||||
|
- **Unit tests: Vitest** via Angular's built-in `@angular/build:unit-test` (the `vitest-angular`
|
||||||
|
runner). **Angular Testing Library** is added for component tests when the first real components
|
||||||
|
land (S-08c); the S-08a placeholder uses a plain `TestBed` render assertion.
|
||||||
|
- **Lint: ESLint** (flat config, `@nx/eslint`).
|
||||||
|
- **Nx is scoped to `apps/` + `libs/` only.** The `@nx/docker` and `@nx/dotnet` plugins are **not**
|
||||||
|
installed — the .NET services are built by `dotnet`/the Makefile, and `@nx/docker` would otherwise
|
||||||
|
infer every `services/*/Dockerfile` as an unnamed Nx project and break the project graph.
|
||||||
|
- **No Nx Cloud.** `nxCloudId` is stripped from `nx.json`; remote caching would depend on an
|
||||||
|
external service, and the repo is Gitea-only (§8.7). Nx's "configure-ai-agents" additions
|
||||||
|
(`.claude/settings.json`, a CLAUDE.md section referencing a GitHub marketplace) are **not**
|
||||||
|
committed for the same reason.
|
||||||
|
- **CI:** a `frontend` job (`make frontend` → `pnpm install --frozen-lockfile` + `nx run-many -t
|
||||||
|
lint test build`) runs on pnpm + Node, with pinned action URLs (§15).
|
||||||
|
|
||||||
|
**NL Design System:** not yet introduced — the S-08a app is a placeholder. NL DS components arrive
|
||||||
|
with the submit form (S-08c, #67); any deviation from NL DS will be recorded here.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API client generator (S-08b, #66)
|
||||||
|
|
||||||
|
`libs/api-client` is **generated from `services/bff/openapi.json`** — never hand-written (§10).
|
||||||
|
|
||||||
|
- **Generator: orval** (`client: 'angular'`), a **node-based** generator (no Java, unlike
|
||||||
|
`openapi-generator`), so it runs in the pnpm/Node CI lane. It emits an injectable
|
||||||
|
`BffApiV1Service` using Angular's `HttpClient` — which means the DigiD bearer token can be attached
|
||||||
|
by an **`HttpInterceptor`** (S-08c), the idiomatic Angular approach; a fetch-based SDK would bypass
|
||||||
|
the interceptor pipeline.
|
||||||
|
- **Config:** `libs/api-client/orval.config.ts` (single-file output into `src/lib/generated/`,
|
||||||
|
`clean: true`, prettier). **Regenerate with `nx run api-client:generate`** after the BFF spec
|
||||||
|
changes; the output is deterministic (idempotent), and `src/lib/generated/` is never hand-edited.
|
||||||
|
- **Tested** against a mocked BFF via `HttpClientTesting` (`libs/api-client/src/lib/bff-api.spec.ts`).
|
||||||
|
- The BFF endpoints carry no `operationId`, so orval synthesises method names
|
||||||
|
(`postSelfServiceRegistrations`, `getOpenbaarRegister`); adding explicit operation ids to the BFF
|
||||||
|
is a possible later polish.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-service form: NL DS, DigiD auth, testing (S-08c, #67)
|
||||||
|
|
||||||
|
- **NL Design System via `@utrecht/component-library-angular`** (`libs/ui`) + `@utrecht/design-tokens`
|
||||||
|
(imported once in `apps/self-service/src/styles.css`). Utrecht is NL DS's reference Angular
|
||||||
|
implementation. Its v3 components are **NgModule-based, not standalone**, so `libs/ui` re-exports
|
||||||
|
`UtrechtComponentsModule` (and the component classes, so the AOT compiler resolves the template
|
||||||
|
directives through the barrel); standalone components consume it via `imports: [UtrechtComponentsModule]`.
|
||||||
|
§10's "no NgModules in new code" governs *our* code — consuming a third-party module is fine.
|
||||||
|
- **DigiD login via `angular-auth-oidc-client`** (`libs/auth`): auth-code + PKCE against the Keycloak
|
||||||
|
`digid` realm (public client `big-portal`). A small **`AuthService` abstraction** (bsn /
|
||||||
|
isAuthenticated / login) wraps the library so components and the `authenticatedGuard` depend on a
|
||||||
|
mockable surface; a **token `HttpInterceptor`** attaches the bearer to BFF calls (secure route).
|
||||||
|
The OIDC `authority`/`secureApiOrigin` are dev defaults in `app.config.ts`; the compose-served app
|
||||||
|
overrides them (S-08d), and the browser-vs-container issuer alignment is handled there (ADR-0010).
|
||||||
|
- **Testing:** component tests use **`@testing-library/angular`** (§10) with `AuthService` and the
|
||||||
|
api-client mocked; the **axe** (`vitest-axe`) check runs scoped to WCAG 2.1 AA tags
|
||||||
|
(`wcag2a/2aa/21a/21aa`) with the document `lang` set, asserting zero violations on the submit page.
|
||||||
|
The real DigiD browser round-trip is exercised in S-08d (Playwright).
|
||||||
|
- **Module boundaries:** replaced the demo eslint `depConstraints` (`scope:shop`/`scope:shared`, left
|
||||||
|
over from the Nx angular template) with a permissive `*` default; scope/type tags can be
|
||||||
|
introduced when the portal set grows.
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
# CI runbook — Gitea Actions
|
# CI runbook — Gitea Actions
|
||||||
|
|
||||||
> **Status: no runner yet → run CI locally with `make ci`.** The workflow
|
> **Status: active.** The workflow `.gitea/workflows/ci.yaml` runs on Gitea's
|
||||||
> `.gitea/workflows/ci.yaml` is in place, but the pipeline cannot go green until a
|
> hosted `ubuntu-latest` runner — no self-hosted runner required.
|
||||||
> self-hosted `respellion-linux` runner is registered against the Gitea instance.
|
> **`make ci` is still the local gate** — it runs the exact same checks
|
||||||
> Until then, **`make ci` is the gate** — it runs the exact same checks locally
|
> (the workflow calls the same `make` targets).
|
||||||
> (the workflow calls the same `make` targets). Issue **#30 (S-00-c)** stays open
|
|
||||||
> until CI is verified green on a runner.
|
|
||||||
|
|
||||||
## The pipeline
|
## The pipeline
|
||||||
|
|
||||||
@@ -17,23 +15,102 @@ and CI cannot drift:
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `lint` | `make lint` → `dotnet format … --verify-no-changes` | .NET 10 SDK |
|
| `lint` | `make lint` → `dotnet format … --verify-no-changes` | .NET 10 SDK |
|
||||||
| `build` | `make build` → `dotnet build … -c Release` | .NET 10 SDK |
|
| `build` | `make build` → `dotnet build … -c Release` | .NET 10 SDK |
|
||||||
| `unit` | `make unit` → `dotnet test … -c Release` | .NET 10 SDK |
|
| `unit` | `make unit` → `dotnet test … -c Release --filter "Category!=Integration"` | .NET 10 SDK |
|
||||||
| `compose-smoke` | `make smoke` → compose up `--wait` → `curl /health` → `down` | container engine + compose v2 |
|
| `mutation` | `make mutation` → `dotnet tool restore` → `dotnet stryker` (ACL); uploads the HTML report as an artifact | .NET 10 SDK |
|
||||||
|
| `verify-stack` | the single live-stack stage — steps: `make verify-up` (full stack up + health, the DoD smoke) → `make verify-acl` (ACL ↔ OpenZaak) → `make verify-nrc` (OpenZaak → NRC delivery) → `make down` | container engine + egress (base images, nuget, `selectielijst.openzaak.nl`) |
|
||||||
|
|
||||||
|
> **Why one `verify-stack` job, not three.** The single self-hosted runner runs jobs
|
||||||
|
> **sequentially**, so booting OpenZaak once (instead of once per check) is the
|
||||||
|
> cheapest layout (issue #58). It subsumes the old `integration`, `notifications`, and
|
||||||
|
> `compose-smoke` jobs — the bring-up step *is* the "compose up reaches green health"
|
||||||
|
> gate. No `setup-dotnet`: the ACL test runs in a built image and every check reaches
|
||||||
|
> services by **container IP** (the runner can't reach published ports — see
|
||||||
|
> [gitea-actions-gotchas.md §5/§6](gitea-actions-gotchas.md)).
|
||||||
|
|
||||||
All `uses:` references are absolute, tag-pinned URLs (`https://github.com/actions/checkout@v4`,
|
All `uses:` references are absolute, tag-pinned URLs (`https://github.com/actions/checkout@v4`,
|
||||||
`https://github.com/actions/setup-dotnet@v4`) per CLAUDE.md §8.7 and §15 — Gitea
|
`https://github.com/actions/setup-dotnet@v4`) per CLAUDE.md §8.7 and §15 — Gitea
|
||||||
Actions resolves them from GitHub.
|
Actions resolves them from GitHub.
|
||||||
|
|
||||||
## Running CI locally (`make ci`)
|
> **`verify-stack` runs on a containerized runner.** Workspace bind mounts do
|
||||||
|
> **not** reach the sibling containers Compose starts, so config/assets are
|
||||||
|
> streamed into external named volumes via `docker cp` (`infra/seed-config.sh`),
|
||||||
|
> and the upstream images are used verbatim (no build). If you add a service that
|
||||||
|
> needs a repo file at runtime, seed it the same way — don't bind-mount it. Note:
|
||||||
|
> bare `docker compose up` no longer self-seeds; use `make up`. See
|
||||||
|
> [gitea-actions-gotchas.md](gitea-actions-gotchas.md).
|
||||||
|
|
||||||
Until the runner exists, run the full pipeline yourself before pushing:
|
## Mutation testing (the ratchet)
|
||||||
|
|
||||||
|
The `mutation` job enforces test *strength*, not just coverage (CLAUDE.md §5).
|
||||||
|
[Stryker.NET](https://stryker-mutator.io/docs/stryker-net/) is pinned as a local
|
||||||
|
dotnet tool (`.config/dotnet-tools.json`), so it runs identically locally and in CI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make ci # lint + build + unit + smoke — what the pipeline runs
|
make mutation # dotnet tool restore + dotnet stryker on the ACL
|
||||||
make lint # or a single stage
|
|
||||||
make smoke # compose up --wait, curl /health, tear down
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Config lives in [`services/acl/stryker-config.json`](../../services/acl/stryker-config.json).
|
||||||
|
It runs in **solution mode** against `Acl.slnx`, mutating the two projects under test
|
||||||
|
(`Acl.Application`, `Acl.Infrastructure`); `Acl.Api` has no tests and is skipped.
|
||||||
|
|
||||||
|
**Baseline (the ratchet):** the ACL is the first service with branching logic, so it
|
||||||
|
sets the repo-wide baseline. Observed score **95%**; enforced `break` threshold **90%**
|
||||||
|
(one-mutant headroom over the ~20-mutant surface). Stryker exits non-zero — failing the
|
||||||
|
job — when the score drops below `break`. Per §5 the baseline only moves **up**, and only
|
||||||
|
as a slice's stated outcome; never lower it. New services add their own mutation run as
|
||||||
|
they gain logic.
|
||||||
|
|
||||||
|
The HTML report is written to `services/acl/StrykerOutput/<timestamp>/reports/` (git-ignored);
|
||||||
|
open it to see survived vs. killed mutants.
|
||||||
|
|
||||||
|
In CI the `mutation` job publishes that report as the **`acl-mutation-report`** artifact
|
||||||
|
(download it from the run's summary page). The upload step uses `if: always()`, so the
|
||||||
|
report is available even when the ratchet *fails* — which is exactly when you want to inspect
|
||||||
|
the survivors. It is the repo's first use of `actions/upload-artifact`, pinned to **`@v3`**:
|
||||||
|
`@v4` refuses to run on Gitea (its `@actions/artifact` v2 library blocks any non-github.com
|
||||||
|
server as "GHES"), while `@v3` speaks the artifact protocol Gitea implements. See
|
||||||
|
[gitea-actions-gotchas.md §4](gitea-actions-gotchas.md) (§15).
|
||||||
|
|
||||||
|
## Running the stack locally without `make` (Windows / Docker Desktop)
|
||||||
|
|
||||||
|
`make` and the bash helpers assume a Unix shell. To bring the whole stack up on a
|
||||||
|
machine without them (e.g. Windows + Docker Desktop), use the **local compose
|
||||||
|
file**, which bind-mounts the config instead of seeding volumes — so it needs no
|
||||||
|
`make`, no seed step, and no bash:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f infra/docker-compose.local.yml up -d --build # any engine
|
||||||
|
docker compose -f infra/docker-compose.local.yml up -d --build --wait # Docker Desktop (Compose v2)
|
||||||
|
docker compose -f infra/docker-compose.local.yml down --volumes
|
||||||
|
```
|
||||||
|
|
||||||
|
On Linux/macOS the same thing is wrapped as `make local` / `make local-down`.
|
||||||
|
|
||||||
|
`infra/docker-compose.local.yml` mirrors the canonical `infra/docker-compose.yml`
|
||||||
|
but swaps the external config volumes for bind mounts — valid locally because a
|
||||||
|
local daemon can see the working directory (the seed/volume dance only exists for
|
||||||
|
the containerized CI runner). Keep the two files in sync.
|
||||||
|
|
||||||
|
## Running CI locally (`make ci`)
|
||||||
|
|
||||||
|
`make ci` runs the exact same checks as the pipeline — handy to run before pushing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make ci # lint + build + unit + mutation + verify — mirrors the pipeline
|
||||||
|
make lint # or a single stage
|
||||||
|
make mutation # Stryker.NET ratchet on the ACL
|
||||||
|
make verify # the live-stack stage: full stack up once → ACL + NRC checks → down
|
||||||
|
```
|
||||||
|
|
||||||
|
> **`make verify`** mirrors the CI `verify-stack` job: it boots the full stack once and
|
||||||
|
> runs both the ACL ↔ OpenZaak and OpenZaak → NRC checks against it. For fast,
|
||||||
|
> single-concern local iteration use a lighter throwaway stack instead:
|
||||||
|
>
|
||||||
|
> ```bash
|
||||||
|
> make integration # ACL ↔ OpenZaak only (no NRC)
|
||||||
|
> make verify-notifications # OpenZaak → NRC delivery only
|
||||||
|
> ```
|
||||||
|
|
||||||
**Prerequisites:** .NET 10 SDK, a container engine with Compose v2, and `curl`.
|
**Prerequisites:** .NET 10 SDK, a container engine with Compose v2, and `curl`.
|
||||||
|
|
||||||
On a **rootless Podman** box (the default dev setup here), the `smoke` target needs
|
On a **rootless Podman** box (the default dev setup here), the `smoke` target needs
|
||||||
@@ -49,56 +126,26 @@ The Makefile auto-points `DOCKER_HOST` at `/run/user/$(id -u)/podman/podman.sock
|
|||||||
when that socket exists and `DOCKER_HOST` is unset, so `make smoke` "just works"
|
when that socket exists and `DOCKER_HOST` is unset, so `make smoke` "just works"
|
||||||
locally while leaving real Docker hosts / CI runners untouched.
|
locally while leaving real Docker hosts / CI runners untouched.
|
||||||
|
|
||||||
## Runner: `respellion-linux`
|
## Runner: `ubuntu-latest`
|
||||||
|
|
||||||
The single self-hosted runner label this repo targets is **`respellion-linux`**
|
All jobs run on Gitea's hosted **`ubuntu-latest`** runner — no self-hosted runner
|
||||||
(declared here per §15). It is intended to run **co-located on the Gitea server**
|
setup is required. The hosted runner ships with Docker and Docker Compose v2, so
|
||||||
(`git.labs.respellion.tech` / `46.224.220.37`) so CI is durable and independent of
|
`make smoke` (`docker compose … up --wait`) works without extra configuration.
|
||||||
any developer machine.
|
|
||||||
|
|
||||||
### Host prerequisites
|
If Gitea's hosted runners are unavailable and a self-hosted fallback is needed,
|
||||||
|
register an `act_runner` with the `ubuntu-latest` label:
|
||||||
The runner executes jobs in **host mode** (see registration below), so the host
|
|
||||||
must have, on `PATH`:
|
|
||||||
|
|
||||||
- .NET 10 SDK (or let `setup-dotnet` install it into the runner tool cache)
|
|
||||||
- A container engine with Compose v2 — Docker, or Podman with the Docker-compatible
|
|
||||||
socket and the `docker-compose` provider (as configured on the dev box)
|
|
||||||
- `curl`
|
|
||||||
|
|
||||||
### Install & register `act_runner` (on the Gitea server)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Install the binary (pick the version matching the Gitea release line)
|
|
||||||
VER=0.2.11
|
VER=0.2.11
|
||||||
curl -fsSL -o /usr/local/bin/act_runner \
|
curl -fsSL -o /usr/local/bin/act_runner \
|
||||||
"https://dl.gitea.com/act_runner/${VER}/act_runner-${VER}-linux-amd64"
|
"https://dl.gitea.com/act_runner/${VER}/act_runner-${VER}-linux-amd64"
|
||||||
chmod +x /usr/local/bin/act_runner
|
chmod +x /usr/local/bin/act_runner
|
||||||
|
|
||||||
# 2. Obtain a registration token from the Gitea UI:
|
|
||||||
# Site Administration → Actions → Runners → "Create new Runner" (instance-level)
|
|
||||||
# (or Repo → Settings → Actions → Runners for a repo-scoped runner)
|
|
||||||
|
|
||||||
# 3. Register with the respellion-linux label in HOST execution mode.
|
|
||||||
# The ":host" suffix means jobs run directly on the host shell, so
|
|
||||||
# `docker compose` in compose-smoke uses the host engine (no docker-in-docker).
|
|
||||||
act_runner register --no-interactive \
|
act_runner register --no-interactive \
|
||||||
--instance https://git.labs.respellion.tech \
|
--instance https://git.labs.respellion.tech \
|
||||||
--token <REGISTRATION_TOKEN> \
|
--token <REGISTRATION_TOKEN> \
|
||||||
--name respellion-ci-1 \
|
--name respellion-ci-1 \
|
||||||
--labels "respellion-linux:host"
|
--labels "ubuntu-latest:docker://node:20-bookworm"
|
||||||
|
|
||||||
# 4. Run it (foreground to verify, then install as a systemd service)
|
|
||||||
act_runner daemon
|
act_runner daemon
|
||||||
```
|
```
|
||||||
|
|
||||||
Verify in the Gitea UI (Actions → Runners) that `respellion-ci-1` shows **Idle**,
|
|
||||||
then re-run the `CI` workflow; all four jobs should pass.
|
|
||||||
|
|
||||||
## Security note
|
|
||||||
|
|
||||||
A self-hosted runner in **host mode** executes workflow code directly on the Gitea
|
|
||||||
server host. Anyone who can push a workflow can run code there. This is acceptable
|
|
||||||
for a **private lab** instance with trusted contributors. For anything
|
|
||||||
internet-facing, switch to container/VM isolation (`--labels "respellion-linux:docker://..."`)
|
|
||||||
or a dedicated runner host, and gate workflow runs on approval for outside PRs.
|
|
||||||
|
|||||||
198
docs/runbooks/gitea-actions-gotchas.md
Normal file
198
docs/runbooks/gitea-actions-gotchas.md
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
# Gitea Actions gotchas
|
||||||
|
|
||||||
|
How our CI (Gitea Actions on the hosted **`ubuntu-latest`** runner) differs from a
|
||||||
|
local run, and the workarounds in this repo. Referenced by `CLAUDE.md` §8.7/§15.
|
||||||
|
|
||||||
|
**One root cause sits under most of this:** the runner executes the job **inside a
|
||||||
|
container**, so when a step runs `docker compose up`, Compose starts the stack as
|
||||||
|
**sibling containers** on the host's daemon. Anything that assumes the job and
|
||||||
|
those containers share a filesystem — or a `localhost` — breaks.
|
||||||
|
|
||||||
|
| Gotcha | Fix | Lives in |
|
||||||
|
|---|---|---|
|
||||||
|
| Bind-mounted config arrives empty | `docker cp` config into external volumes | `infra/seed-config.sh` |
|
||||||
|
| `docker compose up --wait` is unsupported / flaky | poll health with `docker inspect` | `infra/wait-healthy.sh` |
|
||||||
|
| `pg_isready` passes before PostGIS is ready | add a `PostGIS_Version()` probe | the db healthchecks |
|
||||||
|
| `upload-artifact@v4` fails ("not supported on GHES") | pin `@v3` | `.gitea/workflows/ci.yaml` (`mutation` job) |
|
||||||
|
| `upload-artifact@v3` fails with "Artifact service responded with 500" | mark the upload `continue-on-error: true` (server-side; issue #62) | `.gitea/workflows/ci.yaml` (`mutation` job) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Bind mounts don't reach the containers
|
||||||
|
|
||||||
|
**Symptom** — green locally, but `compose-smoke` fails with:
|
||||||
|
|
||||||
|
```
|
||||||
|
oz-init-1 | CommandError: Yaml file `/app/setup_configuration/data.yaml` does not exist.
|
||||||
|
```
|
||||||
|
|
||||||
|
Migrations run fine; only the step that reads a *mounted* file fails. The same
|
||||||
|
trap hits `nrc-init`, `flowable-init`, and `keycloak`.
|
||||||
|
|
||||||
|
**Why** — a relative bind mount like `./openzaak/setup_configuration:/app/...` is
|
||||||
|
resolved by Compose to a path *inside the job container*
|
||||||
|
(`/workspace/.../setup_configuration`). The daemon then looks for that path on
|
||||||
|
*its own host*, doesn't find it, and mounts an **empty directory**. (It works on a
|
||||||
|
runner that executes jobs on the host — which is why moving to `ubuntu-latest`
|
||||||
|
exposed it.)
|
||||||
|
|
||||||
|
**Fix** — use the upstream images verbatim (no build) and stream config into
|
||||||
|
**external named volumes** with `docker cp`, which copies over the Docker API and
|
||||||
|
so works wherever the daemon runs. `infra/seed-config.sh` creates each volume,
|
||||||
|
mounts it in a throwaway helper, and copies the files in:
|
||||||
|
|
||||||
|
| Asset | Volume | Mounted at |
|
||||||
|
|---|---|---|
|
||||||
|
| OpenZaak `data.yaml` | `rr-oz-config` | `oz-init:/app/setup_configuration` |
|
||||||
|
| Keycloak realms | `rr-kc-realms` | `keycloak:/opt/keycloak/data/import` |
|
||||||
|
| `registratie.bpmn` | `rr-fl-bpmn` | `flowable-init:/work` |
|
||||||
|
|
||||||
|
The volumes are `external: true` with fixed names, so they resolve identically
|
||||||
|
under docker compose and podman-compose. `make` seeds before every `up`; `make
|
||||||
|
down` removes them. (Open Notificaties needs nothing — `nrc-init` migrates only.)
|
||||||
|
|
||||||
|
**Consequence — bare `docker compose up` can't self-seed external volumes:**
|
||||||
|
|
||||||
|
- **CI / Linux / macOS:** `make up` or `make smoke` (seed, then start).
|
||||||
|
- **No-make / Windows:** `infra/docker-compose.local.yml` — a twin stack that
|
||||||
|
**bind-mounts** the config instead. Bind mounts are fine *locally* because a
|
||||||
|
local daemon can see your working directory, so
|
||||||
|
`docker compose -f infra/docker-compose.local.yml up -d` just works.
|
||||||
|
|
||||||
|
**Why not the obvious alternatives**
|
||||||
|
|
||||||
|
- *Bake config into an image* (incl. an inline Dockerfile) — `docker compose up`
|
||||||
|
would then work unaided, but it's a build; we wanted the upstream images as-is.
|
||||||
|
- *Compose `configs:` with inline `content`* — Compose writes a client-side temp
|
||||||
|
file and bind-mounts it, hitting the exact same problem.
|
||||||
|
- *A host-executing runner* — bind mounts would work with zero seeding, but it
|
||||||
|
reintroduces a self-hosted runner and undoes the move to `ubuntu-latest`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Readiness: poll health, don't use `--wait`
|
||||||
|
|
||||||
|
`docker compose up --wait` looks ideal but fails us three ways:
|
||||||
|
|
||||||
|
- **podman-compose doesn't implement it** (`unrecognized arguments: --wait`) — so
|
||||||
|
it would break local dev.
|
||||||
|
- A project-wide `--wait` **treats a one-shot exiting `0` as a failure** unless
|
||||||
|
something `depends_on` it with `service_completed_successfully`. `flowable-init`
|
||||||
|
deploys the BPMN and exits with no dependant, so `--wait` fails the moment it
|
||||||
|
does — last line `container infra-flowable-init-1 exited (0)`.
|
||||||
|
- The containerized runner **can't reach published host ports**, so an external
|
||||||
|
`curl localhost:8080/health` can't work either.
|
||||||
|
|
||||||
|
**Fix** — `infra/wait-healthy.sh` polls each durable service (`openzaak nrc-web
|
||||||
|
acl bff`, listed as `WAIT_SVCS` in the `Makefile`) with `docker ps` + `docker
|
||||||
|
inspect '{{.State.Health.Status}}'` until it reports `healthy`. It uses only
|
||||||
|
primitives both runtimes support, reads the **in-container** healthcheck (no host
|
||||||
|
port needed), and ignores the one-shots (they only need to have run).
|
||||||
|
`WAIT_TIMEOUT` defaults to 420 s — enough for the cold OpenZaak migrate (~90 s)
|
||||||
|
plus app start.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. `pg_isready` passes before PostGIS is ready
|
||||||
|
|
||||||
|
`pg_isready` succeeds as soon as the TCP port is open — *before* the
|
||||||
|
`postgis/postgis` image has finished running `CREATE EXTENSION postgis`. An init
|
||||||
|
container that starts migrating in that window can fail on a missing PostGIS. So
|
||||||
|
the db healthchecks add a `SELECT PostGIS_Version()` probe, making dependents wait
|
||||||
|
for the extension, not just the port.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. `actions/upload-artifact@v4` refuses to run on Gitea
|
||||||
|
|
||||||
|
**Symptom** — the `mutation` job's `make mutation` step passes (95% score), but the
|
||||||
|
upload step right after it fails the job:
|
||||||
|
|
||||||
|
```
|
||||||
|
::error::@actions/artifact v2.0.0+, upload-artifact@v4+ and download-artifact@v4+
|
||||||
|
are not currently supported on GHES.
|
||||||
|
❌ Failure - Main https://github.com/actions/upload-artifact@v4
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why** — `upload-artifact@v4` bundles `@actions/artifact` v2, which inspects the
|
||||||
|
server URL and **hard-aborts on anything that isn't `github.com`**, treating Gitea
|
||||||
|
as an unsupported GitHub Enterprise Server. The check fires regardless of whether
|
||||||
|
the Gitea server can actually store artifacts (1.24+ can). It is the *action*, not
|
||||||
|
the server, that refuses.
|
||||||
|
|
||||||
|
**Fix** — pin **`actions/upload-artifact@v3`** (and `download-artifact@v3` if ever
|
||||||
|
needed). v3 uses the older artifact protocol that Gitea implements, and has no GHES
|
||||||
|
guard. Inputs are the same (`name`, `path`, `if-no-files-found`), so it is a drop-in
|
||||||
|
swap. Do **not** bump to `@v4` until act_runner advertises github.com-compatible
|
||||||
|
artifact support.
|
||||||
|
|
||||||
|
**Second failure mode — the server's artifact backend returns 500.** Even on the
|
||||||
|
correctly-pinned `@v3`, uploads can fail with:
|
||||||
|
|
||||||
|
```
|
||||||
|
Create Artifact Container - Attempt 5 of 5 failed with error: Artifact service responded with 500
|
||||||
|
::error::Create Artifact Container failed: Artifact service responded with 500
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the **Gitea server's** artifact storage failing (not the action's GHES guard),
|
||||||
|
so it is outside the repo's control. Because the `mutation` job's upload steps run with
|
||||||
|
`if: always()`, that 500 would fail the job even though the ratchet passed. **Fix:** mark
|
||||||
|
the uploads `continue-on-error: true` (issue #62). The mutation *gate* is the Stryker
|
||||||
|
ratchet — `make mutation`'s exit code fails the job on a real regression — so the report
|
||||||
|
upload is best-effort: when the server's artifact storage is restored, reports publish
|
||||||
|
again with no workflow change.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. A runner process can't reach a service container's published port
|
||||||
|
|
||||||
|
**Symptom** — green locally, but a CI step that runs *on the runner* and talks to a
|
||||||
|
compose service over `localhost` fails. The ACL integration test's seed died with:
|
||||||
|
|
||||||
|
```
|
||||||
|
OpenZaak ready (000)
|
||||||
|
urllib.error.URLError: <urlopen error [Errno 111] Connection refused>
|
||||||
|
make: *** [Makefile:114: integration] Error 1
|
||||||
|
```
|
||||||
|
|
||||||
|
OpenZaak was demonstrably up — uwsgi had been serving for ~2 minutes — yet
|
||||||
|
`curl`/`urllib` to `localhost:8000` from the runner were refused the whole time.
|
||||||
|
|
||||||
|
**Why** — the same sibling-container split as §1. Compose starts the stack via the
|
||||||
|
host daemon, so `ports: ["8000:8000"]` publishes to the *daemon host*, not to the job
|
||||||
|
container. From the runner, `localhost:8000` has nothing listening. (`make smoke`
|
||||||
|
sidesteps this by polling readiness via `docker inspect` (§2), never a service port.)
|
||||||
|
|
||||||
|
**Fix** — don't talk to service ports from the runner. Either check state via `docker
|
||||||
|
inspect` (health), or run the client **inside the compose network** so it reaches the
|
||||||
|
service by name (`http://openzaak:8000`). For a test/seed that needs the repo's own
|
||||||
|
code, deliver it via a **built image** (not a bind mount — §1), then
|
||||||
|
`docker run --network <stack>_cg …`.
|
||||||
|
|
||||||
|
**Applied** — `make integration` (ADR-0006) and `make verify-notifications` (ADR-0007)
|
||||||
|
do exactly this: they run the seed/test/driver as containers on the stack network and
|
||||||
|
reach services by **container IP** (see §6).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. OpenZaak / NRC reject single-label hosts in URLs
|
||||||
|
|
||||||
|
**Symptom** — talking to OpenZaak or NRC by compose **service name** fails where a URL
|
||||||
|
is validated: catalogus/zaaktype filters, the zaak `zaaktype` URL, and abonnement
|
||||||
|
`callbackUrl` come back `400 "Voer een geldige URL in."` — even though the host
|
||||||
|
resolves and is reachable.
|
||||||
|
|
||||||
|
**Why** — these apps validate URLs with Django's `URLValidator`, which rejects a
|
||||||
|
**single-label** host like `openzaak` or `nrc-web` (no dot, and not `localhost`).
|
||||||
|
`localhost` passes (so it's invisible in host-port-based local runs); in-network the
|
||||||
|
reality is a service name or an IPv4 literal — and only the IP passes.
|
||||||
|
|
||||||
|
**Fix** — in-network tooling reaches OpenZaak/NRC by **container IP**
|
||||||
|
(`docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'`), not
|
||||||
|
service name; the notif verify harness also registers the sink callback by IP.
|
||||||
|
(`infra/run-acl-integration.sh`, `infra/run-notification-check.sh`.)
|
||||||
|
|
||||||
|
**Related — abonnement callbacks must enforce auth.** NRC probes a callback when an
|
||||||
|
abonnement is registered and refuses it (`no-auth-on-callback-url`) unless it returns
|
||||||
|
**401** without the configured `Authorization`. The verify sink
|
||||||
|
(`infra/notification-sink.py`) enforces a bearer token for exactly this reason.
|
||||||
@@ -71,5 +71,12 @@ The Makefile auto-points `DOCKER_HOST` at the Podman socket when it exists, so t
|
|||||||
but OZ→NRC delivery wiring + re-enabling lands with **S-06**.
|
but OZ→NRC delivery wiring + re-enabling lands with **S-06**.
|
||||||
- **Zaaktype is a concept**, not published (publishing needs roltypen/statustypen/
|
- **Zaaktype is a concept**, not published (publishing needs roltypen/statustypen/
|
||||||
resultaattypen — beyond the lean seed). List with `?status=alles`.
|
resultaattypen — beyond the lean seed). List with `?status=alles`.
|
||||||
- **Image tag.** Currently `openzaak/open-zaak:latest` via `${OPENZAAK_TAG}`; pin to
|
- **Image tag.** Pinned to `openzaak/open-zaak:1.28.2` via `${OPENZAAK_TAG}` (bump
|
||||||
a known-good tag (ADR-0002 follow-up).
|
deliberately, not via `:latest`).
|
||||||
|
- **Config arrives via a volume, not a bind mount.** `setup_configuration/data.yaml`
|
||||||
|
is streamed into the external `rr-oz-config` volume by `infra/seed-config.sh`
|
||||||
|
(`docker cp`) and mounted at `/app/setup_configuration`, so the init container
|
||||||
|
finds it on Gitea's containerized CI runner too (bind mounts don't reach sibling
|
||||||
|
containers there). The image is the upstream `openzaak/open-zaak` verbatim — no
|
||||||
|
build. Run via `make openzaak-up` (seeds first). See
|
||||||
|
[gitea-actions-gotchas.md](gitea-actions-gotchas.md).
|
||||||
|
|||||||
48
eslint.config.mjs
Normal file
48
eslint.config.mjs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import nx from '@nx/eslint-plugin';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...nx.configs['flat/base'],
|
||||||
|
...nx.configs['flat/typescript'],
|
||||||
|
...nx.configs['flat/javascript'],
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
'**/dist',
|
||||||
|
'**/vite.config.*.timestamp*',
|
||||||
|
'**/vitest.config.*.timestamp*',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||||
|
rules: {
|
||||||
|
'@nx/enforce-module-boundaries': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
enforceBuildableLibDependency: true,
|
||||||
|
allow: ['^.*/eslint(\\.base)?\\.config\\.[cm]?[jt]s$'],
|
||||||
|
// Permissive default — apps/libs are untagged for now. Introduce scope/type tags
|
||||||
|
// when the portal set grows (docs/frontend-decisions.md).
|
||||||
|
depConstraints: [
|
||||||
|
{
|
||||||
|
sourceTag: '*',
|
||||||
|
onlyDependOnLibsWithTags: ['*'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
'**/*.ts',
|
||||||
|
'**/*.tsx',
|
||||||
|
'**/*.cts',
|
||||||
|
'**/*.mts',
|
||||||
|
'**/*.js',
|
||||||
|
'**/*.jsx',
|
||||||
|
'**/*.cjs',
|
||||||
|
'**/*.mjs',
|
||||||
|
],
|
||||||
|
// Override or add rules here
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
372
infra/docker-compose.local.yml
Normal file
372
infra/docker-compose.local.yml
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
# LOCAL development stack — runs with a plain `docker compose up`, no make / no
|
||||||
|
# seed step / no bash. Use this on a local engine (Docker Desktop on Windows or
|
||||||
|
# macOS, or rootless Podman on Linux).
|
||||||
|
#
|
||||||
|
# docker compose -f infra/docker-compose.local.yml up -d --build # podman
|
||||||
|
# docker compose -f infra/docker-compose.local.yml up -d --build --wait # Docker Desktop
|
||||||
|
# docker compose -f infra/docker-compose.local.yml down --volumes
|
||||||
|
#
|
||||||
|
# It is identical to infra/docker-compose.yml EXCEPT that the three config inputs
|
||||||
|
# (OpenZaak data.yaml, Keycloak realms, Flowable BPMN) are **bind-mounted** from
|
||||||
|
# the repo instead of being streamed into external volumes by infra/seed-config.sh.
|
||||||
|
# Bind mounts work here because a local daemon can see your working directory —
|
||||||
|
# the seed dance only exists for the containerized CI runner, where it can't. See
|
||||||
|
# docs/runbooks/gitea-actions-gotchas.md.
|
||||||
|
#
|
||||||
|
# `infra/docker-compose.yml` remains the CI-canonical stack; keep the two in sync.
|
||||||
|
#
|
||||||
|
# Port map (host):
|
||||||
|
# 8000 OpenZaak · 8001 Open Notificaties · 8080 BFF · 8090 Flowable REST
|
||||||
|
# 8100 ACL · 8180 Keycloak (all admin: admin / admin — dev only)
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
# ── OpenZaak (S-01) ──────────────────────────────────────────────────────
|
||||||
|
oz-db:
|
||||||
|
image: docker.io/postgis/postgis:17-3.5
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: openzaak
|
||||||
|
POSTGRES_PASSWORD: openzaak
|
||||||
|
POSTGRES_DB: openzaak
|
||||||
|
command: postgres -c max_connections=300
|
||||||
|
volumes:
|
||||||
|
- oz-db:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U openzaak -d openzaak && psql -U openzaak -d openzaak -c 'SELECT PostGIS_Version();' -q 2>/dev/null"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 30
|
||||||
|
start_period: 15s
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
oz-redis:
|
||||||
|
image: docker.io/library/redis:7
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
oz-init:
|
||||||
|
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-1.28.2}
|
||||||
|
environment: &oz-env
|
||||||
|
DJANGO_SETTINGS_MODULE: openzaak.conf.docker
|
||||||
|
SECRET_KEY: ${OZ_SECRET_KEY:-dev-only-not-for-production}
|
||||||
|
DB_HOST: oz-db
|
||||||
|
DB_NAME: openzaak
|
||||||
|
DB_USER: openzaak
|
||||||
|
DB_PASSWORD: openzaak
|
||||||
|
IS_HTTPS: "no"
|
||||||
|
ALLOWED_HOSTS: "*"
|
||||||
|
CACHE_DEFAULT: oz-redis:6379/0
|
||||||
|
CACHE_AXES: oz-redis:6379/0
|
||||||
|
CELERY_BROKER_URL: redis://oz-redis:6379/1
|
||||||
|
CELERY_RESULT_BACKEND: redis://oz-redis:6379/1
|
||||||
|
DISABLE_2FA: "true"
|
||||||
|
# Publish notifications to NRC (always present in this twin). See ADR-0007.
|
||||||
|
NOTIFICATIONS_DISABLED: "false"
|
||||||
|
OPENZAAK_SUPERUSER_USERNAME: admin
|
||||||
|
DJANGO_SUPERUSER_PASSWORD: admin
|
||||||
|
OPENZAAK_SUPERUSER_EMAIL: admin@localhost
|
||||||
|
RUN_SETUP_CONFIG: "true"
|
||||||
|
command: /setup_configuration.sh
|
||||||
|
# Bind mount (`:z` relabels for SELinux on Linux; a no-op on Docker Desktop).
|
||||||
|
volumes:
|
||||||
|
- ./openzaak/setup_configuration:/app/setup_configuration:ro,z
|
||||||
|
depends_on:
|
||||||
|
oz-db:
|
||||||
|
condition: service_healthy
|
||||||
|
oz-redis:
|
||||||
|
condition: service_started
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
openzaak:
|
||||||
|
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-1.28.2}
|
||||||
|
environment: *oz-env
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import requests,sys; sys.exit(0 if requests.head('http://localhost:8000/admin/').status_code in (200,302) else 1)"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
start_period: 30s
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
depends_on:
|
||||||
|
oz-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
oz-celery:
|
||||||
|
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-1.28.2}
|
||||||
|
environment: *oz-env
|
||||||
|
command: /celery_worker.sh
|
||||||
|
depends_on:
|
||||||
|
oz-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
# ── Open Notificaties / NRC (S-01-c) ─────────────────────────────────────
|
||||||
|
nrc-db:
|
||||||
|
image: docker.io/postgis/postgis:17-3.5
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: opennotificaties
|
||||||
|
POSTGRES_PASSWORD: opennotificaties
|
||||||
|
POSTGRES_DB: opennotificaties
|
||||||
|
command: postgres -c max_connections=300
|
||||||
|
volumes:
|
||||||
|
- nrc-db:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U opennotificaties -d opennotificaties"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
nrc-redis:
|
||||||
|
image: docker.io/library/redis:7
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
nrc-init:
|
||||||
|
# Migrations + setup_configuration (S-01-c): the JWT credential, Autorisaties-API
|
||||||
|
# delegation, and the `zaken` kanaal that let OpenZaak publish. Config is
|
||||||
|
# bind-mounted here (this twin is the local/no-make path). See ADR-0007.
|
||||||
|
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1}
|
||||||
|
environment: &nrc-env
|
||||||
|
DJANGO_SETTINGS_MODULE: nrc.conf.docker
|
||||||
|
SECRET_KEY: ${NRC_SECRET_KEY:-dev-only-not-for-production}
|
||||||
|
DB_HOST: nrc-db
|
||||||
|
DB_NAME: opennotificaties
|
||||||
|
DB_USER: opennotificaties
|
||||||
|
DB_PASSWORD: opennotificaties
|
||||||
|
IS_HTTPS: "no"
|
||||||
|
ALLOWED_HOSTS: "*"
|
||||||
|
CACHE_DEFAULT: nrc-redis:6379/0
|
||||||
|
CACHE_AXES: nrc-redis:6379/0
|
||||||
|
CELERY_BROKER_URL: redis://nrc-redis:6379/1
|
||||||
|
CELERY_RESULT_BACKEND: redis://nrc-redis:6379/1
|
||||||
|
DISABLE_2FA: "true"
|
||||||
|
OPENNOTIFICATIES_SUPERUSER_USERNAME: admin
|
||||||
|
DJANGO_SUPERUSER_PASSWORD: admin
|
||||||
|
OPENNOTIFICATIES_SUPERUSER_EMAIL: admin@localhost
|
||||||
|
RUN_SETUP_CONFIG: "true"
|
||||||
|
NOTIFICATION_SEC_INTERVAL: "5"
|
||||||
|
command: /setup_configuration.sh
|
||||||
|
volumes:
|
||||||
|
- ./opennotificaties/setup_configuration:/app/setup_configuration:ro,z
|
||||||
|
depends_on:
|
||||||
|
nrc-db:
|
||||||
|
condition: service_healthy
|
||||||
|
nrc-redis:
|
||||||
|
condition: service_started
|
||||||
|
openzaak:
|
||||||
|
condition: service_healthy
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
nrc-web:
|
||||||
|
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1}
|
||||||
|
environment: *nrc-env
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import requests,sys; sys.exit(0 if requests.head('http://localhost:8000/admin/').status_code in (200,302) else 1)"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
start_period: 30s
|
||||||
|
ports:
|
||||||
|
- "8001:8000"
|
||||||
|
depends_on:
|
||||||
|
nrc-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
nrc-celery:
|
||||||
|
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1}
|
||||||
|
environment: *nrc-env
|
||||||
|
command: /celery_worker.sh
|
||||||
|
depends_on:
|
||||||
|
nrc-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
# Celery beat drains scheduled notifications to subscribers — required for
|
||||||
|
# delivery, not optional. See ADR-0007.
|
||||||
|
nrc-beat:
|
||||||
|
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1}
|
||||||
|
environment: *nrc-env
|
||||||
|
command: /celery_beat.sh
|
||||||
|
depends_on:
|
||||||
|
nrc-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
# ── Keycloak (S-02) ──────────────────────────────────────────────────────
|
||||||
|
keycloak:
|
||||||
|
image: quay.io/keycloak/keycloak:26.1
|
||||||
|
command: ["start-dev", "--import-realm"]
|
||||||
|
environment:
|
||||||
|
KC_BOOTSTRAP_ADMIN_USERNAME: admin
|
||||||
|
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
|
||||||
|
KEYCLOAK_ADMIN: admin
|
||||||
|
KEYCLOAK_ADMIN_PASSWORD: admin
|
||||||
|
KC_HEALTH_ENABLED: "true"
|
||||||
|
KC_HTTP_ENABLED: "true"
|
||||||
|
ports:
|
||||||
|
- "8180:8080"
|
||||||
|
volumes:
|
||||||
|
- ./keycloak/realms:/opt/keycloak/data/import:ro,z
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
# ── Flowable (S-03) ──────────────────────────────────────────────────────
|
||||||
|
flowable-db:
|
||||||
|
image: docker.io/library/postgres:16
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: flowable
|
||||||
|
POSTGRES_PASSWORD: flowable
|
||||||
|
POSTGRES_DB: flowable
|
||||||
|
volumes:
|
||||||
|
- flowable-db:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U flowable -d flowable"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
flowable-rest:
|
||||||
|
image: docker.io/flowable/flowable-rest:latest
|
||||||
|
environment:
|
||||||
|
SPRING_DATASOURCE_DRIVER-CLASS-NAME: org.postgresql.Driver
|
||||||
|
SPRING_DATASOURCE_URL: jdbc:postgresql://flowable-db:5432/flowable
|
||||||
|
SPRING_DATASOURCE_USERNAME: flowable
|
||||||
|
SPRING_DATASOURCE_PASSWORD: flowable
|
||||||
|
ports:
|
||||||
|
- "8090:8080"
|
||||||
|
depends_on:
|
||||||
|
flowable-db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
flowable-init:
|
||||||
|
image: docker.io/curlimages/curl:latest
|
||||||
|
restart: "no"
|
||||||
|
volumes:
|
||||||
|
- ../workflows/registratie.bpmn:/work/registratie.bpmn:ro,z
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
base=http://flowable-rest:8080/flowable-rest/service/repository/deployments
|
||||||
|
until curl -sf -u rest-admin:test "$$base" >/dev/null 2>&1; do echo "waiting for flowable-rest..."; sleep 3; done
|
||||||
|
if curl -s -u rest-admin:test "$$base?name=registratie" | grep -q '"name":"registratie"'; then
|
||||||
|
echo "registratie already deployed; skip"
|
||||||
|
else
|
||||||
|
curl -sf -u rest-admin:test -F 'file=@/work/registratie.bpmn;filename=registratie.bpmn' "$$base" >/dev/null && echo "deployed registratie"
|
||||||
|
fi
|
||||||
|
depends_on:
|
||||||
|
flowable-rest:
|
||||||
|
condition: service_started
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
# ── ACL ──────────────────────────────────────────────────────────────────
|
||||||
|
acl:
|
||||||
|
build:
|
||||||
|
context: ../services/acl
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: register-referentie/acl:dev
|
||||||
|
environment:
|
||||||
|
Acl__OpenZaak__BaseUrl: http://openzaak:8000/
|
||||||
|
Acl__OpenZaak__ClientId: big-reference-seed
|
||||||
|
Acl__OpenZaak__Secret: insecure-dev-secret-change-me
|
||||||
|
Acl__Defaults__Bronorganisatie: "517439943"
|
||||||
|
Acl__Defaults__VerantwoordelijkeOrganisatie: "517439943"
|
||||||
|
Acl__Defaults__Vertrouwelijkheidaanduiding: openbaar
|
||||||
|
Acl__Defaults__ZaaktypeUrl: ${ACL_ZAAKTYPE_URL:-http://openzaak:8000/catalogi/api/v1/zaaktypen/00000000-0000-0000-0000-000000000000}
|
||||||
|
ports:
|
||||||
|
- "8100:8080"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
depends_on:
|
||||||
|
openzaak:
|
||||||
|
condition: service_healthy
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
# ── BFF ──────────────────────────────────────────────────────────────────
|
||||||
|
bff:
|
||||||
|
build:
|
||||||
|
context: ../services/bff
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: register-referentie/bff:dev
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
# ── Read projection (S-06) ────────────────────────────────────────────────
|
||||||
|
projection-db:
|
||||||
|
image: docker.io/library/postgres:16
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: projection
|
||||||
|
POSTGRES_PASSWORD: projection
|
||||||
|
POSTGRES_DB: projection
|
||||||
|
volumes:
|
||||||
|
- projection-db:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U projection -d projection"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
event-subscriber:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: services/event-subscriber/Dockerfile
|
||||||
|
image: register-referentie/event-subscriber:dev
|
||||||
|
environment:
|
||||||
|
ConnectionStrings__Projection: Host=projection-db;Database=projection;Username=projection;Password=projection
|
||||||
|
EventSubscriber__Webhook__AuthToken: ${NOTIFICATION_WEBHOOK_TOKEN:-Bearer big-reference-notifications}
|
||||||
|
ports:
|
||||||
|
- "8110:8080"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
start_period: 15s
|
||||||
|
depends_on:
|
||||||
|
projection-db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
projection-api:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: services/projection-api/Dockerfile
|
||||||
|
image: register-referentie/projection-api:dev
|
||||||
|
environment:
|
||||||
|
ConnectionStrings__Projection: Host=projection-db;Database=projection;Username=projection;Password=projection
|
||||||
|
ports:
|
||||||
|
- "8120:8080"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
start_period: 15s
|
||||||
|
depends_on:
|
||||||
|
projection-db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
oz-db:
|
||||||
|
nrc-db:
|
||||||
|
flowable-db:
|
||||||
|
projection-db:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
cg:
|
||||||
@@ -1,14 +1,356 @@
|
|||||||
# Local development stack. Grows service-by-service with each slice.
|
# Development stack — boots all infra services plus the ACL and BFF.
|
||||||
# S-00-b: the placeholder BFF with a /health check.
|
#
|
||||||
|
# Consolidates infra/openzaak/, infra/opennotificaties/, infra/keycloak/,
|
||||||
|
# and infra/flowable/ and adds the ACL and BFF services.
|
||||||
|
#
|
||||||
|
# Port map (host):
|
||||||
|
# 8000 OpenZaak ZGW API (admin: admin / admin)
|
||||||
|
# 8001 Open Notificaties (admin: admin / admin)
|
||||||
|
# 8080 BFF GET /health → Healthy
|
||||||
|
# 8090 Flowable REST http://localhost:8090/flowable-rest/service/
|
||||||
|
# 8100 ACL GET /health → Healthy POST /zaken
|
||||||
|
# 8110 Event Subscriber GET /health → Healthy POST /notifications
|
||||||
|
# 8120 projection-api GET /health → Healthy GET /register
|
||||||
|
# 8180 Keycloak (admin: admin / admin)
|
||||||
#
|
#
|
||||||
# docker compose -f infra/docker-compose.yml up -d --build --wait
|
# docker compose -f infra/docker-compose.yml up -d --build --wait
|
||||||
# curl http://localhost:8080/health # -> Healthy
|
#
|
||||||
|
# After first boot, seed the BIG catalogus and note the zaaktype URL:
|
||||||
|
# python infra/openzaak/seed_catalogus.py
|
||||||
|
# Then set ACL_ZAAKTYPE_URL in a .env file or your shell and re-up the acl
|
||||||
|
# service:
|
||||||
|
# export ACL_ZAAKTYPE_URL=http://openzaak:8000/catalogi/api/v1/zaaktypen/<uuid>
|
||||||
|
# docker compose -f infra/docker-compose.yml up -d acl
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
|
# ── OpenZaak (S-01) ──────────────────────────────────────────────────────
|
||||||
|
oz-db:
|
||||||
|
image: docker.io/postgis/postgis:17-3.5
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: openzaak
|
||||||
|
POSTGRES_PASSWORD: openzaak
|
||||||
|
POSTGRES_DB: openzaak
|
||||||
|
command: postgres -c max_connections=300
|
||||||
|
volumes:
|
||||||
|
- oz-db:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
# pg_isready only checks TCP; the second clause verifies PostGIS is installed
|
||||||
|
# so oz-init migrations can safely start (avoids race on cold container start).
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U openzaak -d openzaak && psql -U openzaak -d openzaak -c 'SELECT PostGIS_Version();' -q 2>/dev/null"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 30
|
||||||
|
start_period: 15s
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
oz-redis:
|
||||||
|
image: docker.io/library/redis:7
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
oz-init:
|
||||||
|
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-1.28.2}
|
||||||
|
environment: &oz-env
|
||||||
|
DJANGO_SETTINGS_MODULE: openzaak.conf.docker
|
||||||
|
SECRET_KEY: ${OZ_SECRET_KEY:-dev-only-not-for-production}
|
||||||
|
DB_HOST: oz-db
|
||||||
|
DB_NAME: openzaak
|
||||||
|
DB_USER: openzaak
|
||||||
|
DB_PASSWORD: openzaak
|
||||||
|
IS_HTTPS: "no"
|
||||||
|
ALLOWED_HOSTS: "*"
|
||||||
|
CACHE_DEFAULT: oz-redis:6379/0
|
||||||
|
CACHE_AXES: oz-redis:6379/0
|
||||||
|
CELERY_BROKER_URL: redis://oz-redis:6379/1
|
||||||
|
CELERY_RESULT_BACKEND: redis://oz-redis:6379/1
|
||||||
|
DISABLE_2FA: "true"
|
||||||
|
# Publish notifications to NRC (always present in this full stack). The NRC
|
||||||
|
# service + notifications_config are provisioned by setup_configuration
|
||||||
|
# (infra/openzaak/setup_configuration/data.yaml). See ADR-0007 / S-01-c.
|
||||||
|
NOTIFICATIONS_DISABLED: "false"
|
||||||
|
OPENZAAK_SUPERUSER_USERNAME: admin
|
||||||
|
DJANGO_SUPERUSER_PASSWORD: admin
|
||||||
|
OPENZAAK_SUPERUSER_EMAIL: admin@localhost
|
||||||
|
RUN_SETUP_CONFIG: "true"
|
||||||
|
command: /setup_configuration.sh
|
||||||
|
# data.yaml is streamed into this external volume by infra/seed-config.sh
|
||||||
|
# before start (bind mounts don't reach sibling containers on the CI runner).
|
||||||
|
volumes:
|
||||||
|
- oz-config:/app/setup_configuration:ro
|
||||||
|
depends_on:
|
||||||
|
oz-db:
|
||||||
|
condition: service_healthy
|
||||||
|
oz-redis:
|
||||||
|
condition: service_started
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
openzaak:
|
||||||
|
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-1.28.2}
|
||||||
|
environment: *oz-env
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import requests,sys; sys.exit(0 if requests.head('http://localhost:8000/admin/').status_code in (200,302) else 1)"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
start_period: 30s
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
depends_on:
|
||||||
|
oz-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
oz-celery:
|
||||||
|
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-1.28.2}
|
||||||
|
environment: *oz-env
|
||||||
|
command: /celery_worker.sh
|
||||||
|
depends_on:
|
||||||
|
oz-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
# ── Open Notificaties / NRC (S-01-c) ─────────────────────────────────────
|
||||||
|
nrc-db:
|
||||||
|
image: docker.io/postgis/postgis:17-3.5
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: opennotificaties
|
||||||
|
POSTGRES_PASSWORD: opennotificaties
|
||||||
|
POSTGRES_DB: opennotificaties
|
||||||
|
command: postgres -c max_connections=300
|
||||||
|
volumes:
|
||||||
|
- nrc-db:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U opennotificaties -d opennotificaties"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
nrc-redis:
|
||||||
|
image: docker.io/library/redis:7
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
nrc-init:
|
||||||
|
# Plain base image — nrc-init runs migrations only (see command below), so it
|
||||||
|
# needs no baked config.
|
||||||
|
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1}
|
||||||
|
environment: &nrc-env
|
||||||
|
DJANGO_SETTINGS_MODULE: nrc.conf.docker
|
||||||
|
SECRET_KEY: ${NRC_SECRET_KEY:-dev-only-not-for-production}
|
||||||
|
DB_HOST: nrc-db
|
||||||
|
DB_NAME: opennotificaties
|
||||||
|
DB_USER: opennotificaties
|
||||||
|
DB_PASSWORD: opennotificaties
|
||||||
|
IS_HTTPS: "no"
|
||||||
|
ALLOWED_HOSTS: "*"
|
||||||
|
CACHE_DEFAULT: nrc-redis:6379/0
|
||||||
|
CACHE_AXES: nrc-redis:6379/0
|
||||||
|
CELERY_BROKER_URL: redis://nrc-redis:6379/1
|
||||||
|
CELERY_RESULT_BACKEND: redis://nrc-redis:6379/1
|
||||||
|
DISABLE_2FA: "true"
|
||||||
|
OPENNOTIFICATIES_SUPERUSER_USERNAME: admin
|
||||||
|
DJANGO_SUPERUSER_PASSWORD: admin
|
||||||
|
OPENNOTIFICATIES_SUPERUSER_EMAIL: admin@localhost
|
||||||
|
RUN_SETUP_CONFIG: "true"
|
||||||
|
# nrc-beat fires `execute_notifications` this often to drain scheduled
|
||||||
|
# notifications to subscribers (upstream default 20s). See ADR-0007.
|
||||||
|
NOTIFICATION_SEC_INTERVAL: "5"
|
||||||
|
# Runs migrations + setup_configuration (S-01-c): the JWT credential, the
|
||||||
|
# Autorisaties-API delegation, and the `zaken` kanaal that let OpenZaak publish.
|
||||||
|
# data.yaml is streamed into rr-nrc-config by infra/seed-config.sh (bind mounts
|
||||||
|
# don't reach sibling containers on the CI runner). See data.yaml + ADR-0007.
|
||||||
|
command: /setup_configuration.sh
|
||||||
|
volumes:
|
||||||
|
- nrc-config:/app/setup_configuration:ro
|
||||||
|
depends_on:
|
||||||
|
nrc-db:
|
||||||
|
condition: service_healthy
|
||||||
|
nrc-redis:
|
||||||
|
condition: service_started
|
||||||
|
openzaak:
|
||||||
|
condition: service_healthy
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
nrc-web:
|
||||||
|
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1}
|
||||||
|
environment: *nrc-env
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import requests,sys; sys.exit(0 if requests.head('http://localhost:8000/admin/').status_code in (200,302) else 1)"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
start_period: 30s
|
||||||
|
ports:
|
||||||
|
- "8001:8000"
|
||||||
|
depends_on:
|
||||||
|
nrc-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
nrc-celery:
|
||||||
|
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1}
|
||||||
|
environment: *nrc-env
|
||||||
|
command: /celery_worker.sh
|
||||||
|
depends_on:
|
||||||
|
nrc-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
# Celery beat drains the ScheduledNotification rows the API creates on publish
|
||||||
|
# and hands them to the worker. Without it, notifications are accepted but never
|
||||||
|
# delivered to subscribers — required, not optional. See ADR-0007.
|
||||||
|
nrc-beat:
|
||||||
|
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1}
|
||||||
|
environment: *nrc-env
|
||||||
|
command: /celery_beat.sh
|
||||||
|
depends_on:
|
||||||
|
nrc-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
# ── Keycloak (S-02) ──────────────────────────────────────────────────────
|
||||||
|
keycloak:
|
||||||
|
image: quay.io/keycloak/keycloak:26.1
|
||||||
|
command: ["start-dev", "--import-realm"]
|
||||||
|
environment:
|
||||||
|
KC_BOOTSTRAP_ADMIN_USERNAME: admin
|
||||||
|
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
|
||||||
|
KEYCLOAK_ADMIN: admin
|
||||||
|
KEYCLOAK_ADMIN_PASSWORD: admin
|
||||||
|
KC_HEALTH_ENABLED: "true"
|
||||||
|
KC_HTTP_ENABLED: "true"
|
||||||
|
ports:
|
||||||
|
- "8180:8080"
|
||||||
|
# realm exports are streamed into this external volume by infra/seed-config.sh.
|
||||||
|
volumes:
|
||||||
|
- kc-realms:/opt/keycloak/data/import:ro
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
# ── Flowable (S-03) ──────────────────────────────────────────────────────
|
||||||
|
flowable-db:
|
||||||
|
image: docker.io/library/postgres:16
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: flowable
|
||||||
|
POSTGRES_PASSWORD: flowable
|
||||||
|
POSTGRES_DB: flowable
|
||||||
|
volumes:
|
||||||
|
- flowable-db:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U flowable -d flowable"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
flowable-rest:
|
||||||
|
image: docker.io/flowable/flowable-rest:latest
|
||||||
|
environment:
|
||||||
|
SPRING_DATASOURCE_DRIVER-CLASS-NAME: org.postgresql.Driver
|
||||||
|
SPRING_DATASOURCE_URL: jdbc:postgresql://flowable-db:5432/flowable
|
||||||
|
SPRING_DATASOURCE_USERNAME: flowable
|
||||||
|
SPRING_DATASOURCE_PASSWORD: flowable
|
||||||
|
ports:
|
||||||
|
- "8090:8080"
|
||||||
|
depends_on:
|
||||||
|
flowable-db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
flowable-init:
|
||||||
|
image: docker.io/curlimages/curl:latest
|
||||||
|
restart: "no"
|
||||||
|
# registratie.bpmn is streamed into this external volume by infra/seed-config.sh.
|
||||||
|
volumes:
|
||||||
|
- fl-bpmn:/work:ro
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
base=http://flowable-rest:8080/flowable-rest/service/repository/deployments
|
||||||
|
until curl -sf -u rest-admin:test "$$base" >/dev/null 2>&1; do echo "waiting for flowable-rest..."; sleep 3; done
|
||||||
|
if curl -s -u rest-admin:test "$$base?name=registratie" | grep -q '"name":"registratie"'; then
|
||||||
|
echo "registratie already deployed; skip"
|
||||||
|
else
|
||||||
|
curl -sf -u rest-admin:test -F 'file=@/work/registratie.bpmn;filename=registratie.bpmn' "$$base" >/dev/null && echo "deployed registratie"
|
||||||
|
fi
|
||||||
|
depends_on:
|
||||||
|
flowable-rest:
|
||||||
|
condition: service_started
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
# ── ACL ──────────────────────────────────────────────────────────────────
|
||||||
|
acl:
|
||||||
|
build:
|
||||||
|
context: ../services/acl
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: register-referentie/acl:dev
|
||||||
|
environment:
|
||||||
|
# Overridable so verify-domain can point the ACL at the same OpenZaak host that
|
||||||
|
# owns the seeded zaaktype URL (host-consistent zaak creation, ADR-0009).
|
||||||
|
Acl__OpenZaak__BaseUrl: ${ACL_OPENZAAK_BASEURL:-http://openzaak:8000/}
|
||||||
|
Acl__OpenZaak__ClientId: big-reference-seed
|
||||||
|
Acl__OpenZaak__Secret: insecure-dev-secret-change-me
|
||||||
|
Acl__Defaults__Bronorganisatie: "517439943"
|
||||||
|
Acl__Defaults__VerantwoordelijkeOrganisatie: "517439943"
|
||||||
|
Acl__Defaults__Vertrouwelijkheidaanduiding: openbaar
|
||||||
|
# Override with the real zaaktype URL after running seed_catalogus.py.
|
||||||
|
Acl__Defaults__ZaaktypeUrl: ${ACL_ZAAKTYPE_URL:-http://openzaak:8000/catalogi/api/v1/zaaktypen/00000000-0000-0000-0000-000000000000}
|
||||||
|
ports:
|
||||||
|
- "8100:8080"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
depends_on:
|
||||||
|
openzaak:
|
||||||
|
condition: service_healthy
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
# ── BIG Domain Service (S-05) ──────────────────────────────────────────────
|
||||||
|
# Orchestrates a registration: POST /registrations creates the aggregate and
|
||||||
|
# starts the registratie Flowable process; a hosted worker acquires the
|
||||||
|
# OpenZaakAanmaken job, opens a zaak via the ACL and completes it (ADR-0009).
|
||||||
|
# Talks only to Flowable (Workflow Client, §8.2) and the ACL (§8.1).
|
||||||
|
domain:
|
||||||
|
build:
|
||||||
|
context: ../services/domain
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: register-referentie/domain:dev
|
||||||
|
environment:
|
||||||
|
Flowable__BaseUrl: http://flowable-rest:8080/flowable-rest/
|
||||||
|
Flowable__Username: rest-admin
|
||||||
|
Flowable__Password: test
|
||||||
|
Acl__BaseUrl: http://acl:8080/
|
||||||
|
ports:
|
||||||
|
- "8130:8080"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
depends_on:
|
||||||
|
acl:
|
||||||
|
condition: service_healthy
|
||||||
|
flowable-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
# ── BFF ──────────────────────────────────────────────────────────────────
|
||||||
bff:
|
bff:
|
||||||
build:
|
build:
|
||||||
context: ../services/bff
|
context: ../services/bff
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: register-referentie/bff:dev
|
image: register-referentie/bff:dev
|
||||||
|
environment:
|
||||||
|
# The BFF is the portals' only backend; it validates digid tokens and fans out (ADR-0010).
|
||||||
|
# Keycloak (start-dev) derives the issuer from the request host, so the BFF authority and the
|
||||||
|
# verify token request both use keycloak:8080 to keep the issuer consistent.
|
||||||
|
Keycloak__Authority: http://keycloak:8080/realms/digid
|
||||||
|
Downstream__Domain__BaseUrl: http://domain:8080/
|
||||||
|
Downstream__Projection__BaseUrl: http://projection-api:8080/
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -17,3 +359,100 @@ services:
|
|||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 5
|
retries: 5
|
||||||
start_period: 10s
|
start_period: 10s
|
||||||
|
depends_on:
|
||||||
|
domain:
|
||||||
|
condition: service_healthy
|
||||||
|
projection-api:
|
||||||
|
condition: service_healthy
|
||||||
|
keycloak:
|
||||||
|
condition: service_started
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
# ── Read projection (S-06) ────────────────────────────────────────────────
|
||||||
|
# One Postgres DB backing the rebuildable read projection (PRD §8.4): the Event
|
||||||
|
# Subscriber writes it, projection-api reads it. See ADR-0008.
|
||||||
|
projection-db:
|
||||||
|
image: docker.io/library/postgres:16
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: projection
|
||||||
|
POSTGRES_PASSWORD: projection
|
||||||
|
POSTGRES_DB: projection
|
||||||
|
volumes:
|
||||||
|
- projection-db:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U projection -d projection"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
# Consumes NRC notifications (abonnement callback) and projects zaak-created events
|
||||||
|
# into register_projection. Build context is the repo root: it shares the read model
|
||||||
|
# in services/projection-api/Projection.ReadModel.
|
||||||
|
event-subscriber:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: services/event-subscriber/Dockerfile
|
||||||
|
image: register-referentie/event-subscriber:dev
|
||||||
|
environment:
|
||||||
|
ConnectionStrings__Projection: Host=projection-db;Database=projection;Username=projection;Password=projection
|
||||||
|
# The bearer Open Notificaties must present on the abonnement callback. NRC's
|
||||||
|
# registration probe expects a 401 without it (ADR-0007). Dev-only token.
|
||||||
|
EventSubscriber__Webhook__AuthToken: ${NOTIFICATION_WEBHOOK_TOKEN:-Bearer big-reference-notifications}
|
||||||
|
ports:
|
||||||
|
- "8110:8080"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
start_period: 15s
|
||||||
|
depends_on:
|
||||||
|
projection-db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
# The read side of the projection. Shares Projection.ReadModel, so build context is root.
|
||||||
|
projection-api:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: services/projection-api/Dockerfile
|
||||||
|
image: register-referentie/projection-api:dev
|
||||||
|
environment:
|
||||||
|
ConnectionStrings__Projection: Host=projection-db;Database=projection;Username=projection;Password=projection
|
||||||
|
ports:
|
||||||
|
- "8120:8080"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
start_period: 15s
|
||||||
|
depends_on:
|
||||||
|
projection-db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
oz-db:
|
||||||
|
nrc-db:
|
||||||
|
flowable-db:
|
||||||
|
projection-db:
|
||||||
|
# Config volumes — created and populated out-of-band by infra/seed-config.sh
|
||||||
|
# (docker cp), because bind mounts don't reach sibling containers on the CI
|
||||||
|
# runner. `external` keeps the names deterministic; the seed step manages them.
|
||||||
|
oz-config:
|
||||||
|
external: true
|
||||||
|
name: rr-oz-config
|
||||||
|
nrc-config:
|
||||||
|
external: true
|
||||||
|
name: rr-nrc-config
|
||||||
|
kc-realms:
|
||||||
|
external: true
|
||||||
|
name: rr-kc-realms
|
||||||
|
fl-bpmn:
|
||||||
|
external: true
|
||||||
|
name: rr-fl-bpmn
|
||||||
|
|
||||||
|
networks:
|
||||||
|
cg:
|
||||||
|
|||||||
@@ -40,8 +40,9 @@ services:
|
|||||||
flowable-init:
|
flowable-init:
|
||||||
image: docker.io/curlimages/curl:latest
|
image: docker.io/curlimages/curl:latest
|
||||||
restart: "no"
|
restart: "no"
|
||||||
|
# registratie.bpmn is streamed into this external volume by infra/seed-config.sh.
|
||||||
volumes:
|
volumes:
|
||||||
- ../../workflows/registratie.bpmn:/work/registratie.bpmn:ro,z
|
- fl-bpmn:/work:ro
|
||||||
command:
|
command:
|
||||||
- sh
|
- sh
|
||||||
- -c
|
- -c
|
||||||
@@ -60,6 +61,10 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
flowable-db:
|
flowable-db:
|
||||||
|
# populated out-of-band by infra/seed-config.sh (docker cp) — see that script.
|
||||||
|
fl-bpmn:
|
||||||
|
external: true
|
||||||
|
name: rr-fl-bpmn
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
cg:
|
cg:
|
||||||
|
|||||||
@@ -21,9 +21,15 @@ services:
|
|||||||
KC_HTTP_ENABLED: "true"
|
KC_HTTP_ENABLED: "true"
|
||||||
ports:
|
ports:
|
||||||
- "8180:8080"
|
- "8180:8080"
|
||||||
|
# realm exports are streamed into this external volume by infra/seed-config.sh.
|
||||||
volumes:
|
volumes:
|
||||||
- ./realms:/opt/keycloak/data/import:ro,z
|
- kc-realms:/opt/keycloak/data/import:ro
|
||||||
networks: [cg]
|
networks: [cg]
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
kc-realms:
|
||||||
|
external: true
|
||||||
|
name: rr-kc-realms
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
cg:
|
cg:
|
||||||
|
|||||||
39
infra/notification-sink.py
Executable file
39
infra/notification-sink.py
Executable file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""A throwaway webhook sink for verifying the OpenZaak → NRC notification path.
|
||||||
|
|
||||||
|
NRC delivers abonnement callbacks here as POSTs; each body is printed to stdout
|
||||||
|
(prefixed `NOTIFICATION `) so the verify harness can assert on `docker logs`.
|
||||||
|
|
||||||
|
NRC refuses to register an abonnement whose callback is unauthenticated
|
||||||
|
(`no-auth-on-callback`): when validating it sends a probe and expects the callback
|
||||||
|
to reject a request without the configured `Authorization` value. So this sink
|
||||||
|
enforces that header (EXPECTED_AUTH env) — 401 without it, 204 with it.
|
||||||
|
|
||||||
|
Stdlib only. Listens on :9000. See infra/verify-notifications.sh / S-01-c (#56).
|
||||||
|
"""
|
||||||
|
import http.server
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
EXPECTED_AUTH = os.environ.get("EXPECTED_AUTH", "Bearer notification-sink-token")
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(http.server.BaseHTTPRequestHandler):
|
||||||
|
def do_POST(self):
|
||||||
|
length = int(self.headers.get("content-length", 0))
|
||||||
|
body = self.rfile.read(length).decode("utf-8", "replace")
|
||||||
|
if self.headers.get("Authorization") != EXPECTED_AUTH:
|
||||||
|
self.send_response(401)
|
||||||
|
self.end_headers()
|
||||||
|
return
|
||||||
|
print("NOTIFICATION " + body, flush=True)
|
||||||
|
self.send_response(204)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def log_message(self, *args): # silence default request logging
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
http.server.HTTPServer(("0.0.0.0", 9000), Handler).serve_forever()
|
||||||
|
sys.exit(0)
|
||||||
@@ -19,10 +19,13 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- nrc-db:/var/lib/postgresql/data
|
- nrc-db:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U opennotificaties -d opennotificaties"]
|
# pg_isready only checks TCP; the second clause verifies PostGIS is installed
|
||||||
|
# so nrc-init migrations can safely start (avoids race on cold container start).
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U opennotificaties -d opennotificaties && psql -U opennotificaties -d opennotificaties -c 'SELECT PostGIS_Version();' -q 2>/dev/null"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 3s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 30
|
||||||
|
start_period: 15s
|
||||||
networks: [cg]
|
networks: [cg]
|
||||||
|
|
||||||
nrc-redis:
|
nrc-redis:
|
||||||
@@ -30,7 +33,8 @@ services:
|
|||||||
networks: [cg]
|
networks: [cg]
|
||||||
|
|
||||||
nrc-init:
|
nrc-init:
|
||||||
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-latest}
|
# Plain base image — nrc-init runs migrations only (see command below).
|
||||||
|
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1}
|
||||||
environment: &nrc-env
|
environment: &nrc-env
|
||||||
DJANGO_SETTINGS_MODULE: nrc.conf.docker
|
DJANGO_SETTINGS_MODULE: nrc.conf.docker
|
||||||
SECRET_KEY: ${NRC_SECRET_KEY:-dev-only-not-for-production}
|
SECRET_KEY: ${NRC_SECRET_KEY:-dev-only-not-for-production}
|
||||||
@@ -49,9 +53,17 @@ services:
|
|||||||
DJANGO_SUPERUSER_PASSWORD: admin
|
DJANGO_SUPERUSER_PASSWORD: admin
|
||||||
OPENNOTIFICATIES_SUPERUSER_EMAIL: admin@localhost
|
OPENNOTIFICATIES_SUPERUSER_EMAIL: admin@localhost
|
||||||
RUN_SETUP_CONFIG: "true"
|
RUN_SETUP_CONFIG: "true"
|
||||||
|
# Delivery cadence: nrc-beat fires `execute_notifications` this often to drain
|
||||||
|
# scheduled notifications to subscribers. Upstream default is 20s; 5s keeps the
|
||||||
|
# walking-skeleton + the verify smoke responsive.
|
||||||
|
NOTIFICATION_SEC_INTERVAL: "5"
|
||||||
|
# Runs migrations + setup_configuration (S-01-c): the JWT credential, the
|
||||||
|
# Autorisaties-API delegation, and the `zaken` kanaal that let OpenZaak publish
|
||||||
|
# notifications. data.yaml is streamed into this external volume by
|
||||||
|
# infra/seed-config.sh (same pattern as oz-init). See data.yaml + ADR-0006.
|
||||||
command: /setup_configuration.sh
|
command: /setup_configuration.sh
|
||||||
volumes:
|
volumes:
|
||||||
- ./setup_configuration:/app/setup_configuration:ro,z
|
- nrc-config:/app/setup_configuration:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
nrc-db:
|
nrc-db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -60,7 +72,7 @@ services:
|
|||||||
networks: [cg]
|
networks: [cg]
|
||||||
|
|
||||||
nrc-web:
|
nrc-web:
|
||||||
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-latest}
|
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1}
|
||||||
environment: *nrc-env
|
environment: *nrc-env
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "python", "-c", "import requests,sys; sys.exit(0 if requests.head('http://localhost:8000/admin/').status_code in (200,302) else 1)"]
|
test: ["CMD", "python", "-c", "import requests,sys; sys.exit(0 if requests.head('http://localhost:8000/admin/').status_code in (200,302) else 1)"]
|
||||||
@@ -76,7 +88,7 @@ services:
|
|||||||
networks: [cg]
|
networks: [cg]
|
||||||
|
|
||||||
nrc-celery:
|
nrc-celery:
|
||||||
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-latest}
|
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1}
|
||||||
environment: *nrc-env
|
environment: *nrc-env
|
||||||
command: /celery_worker.sh
|
command: /celery_worker.sh
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -84,8 +96,25 @@ services:
|
|||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
networks: [cg]
|
networks: [cg]
|
||||||
|
|
||||||
|
# Celery beat: periodically fires `execute_notifications`, which drains the
|
||||||
|
# ScheduledNotification rows the API creates on publish and hands them to the
|
||||||
|
# worker for delivery. Without beat, notifications are accepted but never
|
||||||
|
# delivered to subscribers — so it is required, not optional. See ADR-0007.
|
||||||
|
nrc-beat:
|
||||||
|
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1}
|
||||||
|
environment: *nrc-env
|
||||||
|
command: /celery_beat.sh
|
||||||
|
depends_on:
|
||||||
|
nrc-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
nrc-db:
|
nrc-db:
|
||||||
|
# populated out-of-band by infra/seed-config.sh (docker cp) — see that script.
|
||||||
|
nrc-config:
|
||||||
|
external: true
|
||||||
|
name: rr-nrc-config
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
cg:
|
cg:
|
||||||
|
|||||||
@@ -1,5 +1,41 @@
|
|||||||
# Open Notificaties setup_configuration.
|
# Open Notificaties (NRC) setup_configuration (S-01-c, #56).
|
||||||
# Stage 1 (this commit): intentionally minimal — the init container runs
|
# Wires NRC so OpenZaak can publish notifications:
|
||||||
# migrations; no steps enabled yet. The OpenZaak<->NRC notification wiring
|
# - the JWT credential OpenZaak authenticates with,
|
||||||
# (Services, Authorization, JWT, Kanalen) is added next. See ADR-0002 / S-01-c.
|
# - delegation of authorization checks to OpenZaak's Autorisaties API (AC),
|
||||||
{}
|
# - the `zaken` kanaal OpenZaak publishes zaak events on.
|
||||||
|
# Dev-only credentials — not for production. Steps from nrc.setup_configuration.
|
||||||
|
|
||||||
|
# 1. JWT credential NRC uses to verify the token OpenZaak presents.
|
||||||
|
vng_api_common_credentials_config_enable: true
|
||||||
|
vng_api_common_credentials:
|
||||||
|
items:
|
||||||
|
- identifier: big-reference-seed
|
||||||
|
secret: insecure-dev-secret-change-me
|
||||||
|
|
||||||
|
# 2. The Autorisaties API (OpenZaak's AC) NRC consults to authorize publishers.
|
||||||
|
zgw_consumers_config_enable: true
|
||||||
|
zgw_consumers:
|
||||||
|
services:
|
||||||
|
- identifier: openzaak-ac
|
||||||
|
label: OpenZaak Autorisaties API
|
||||||
|
api_type: ac
|
||||||
|
api_root: http://openzaak:8000/autorisaties/api/v1/
|
||||||
|
auth_type: zgw
|
||||||
|
client_id: big-reference-seed
|
||||||
|
secret: insecure-dev-secret-change-me
|
||||||
|
|
||||||
|
# 3. Delegate authorization to that AC.
|
||||||
|
autorisaties_api_config_enable: true
|
||||||
|
autorisaties_api:
|
||||||
|
authorizations_api_service_identifier: openzaak-ac
|
||||||
|
|
||||||
|
# 4. The kanaal OpenZaak publishes zaak events on.
|
||||||
|
notifications_kanalen_config_enable: true
|
||||||
|
notifications_kanalen_config:
|
||||||
|
items:
|
||||||
|
- naam: zaken
|
||||||
|
documentatie_link: https://github.com/VNG-Realisatie/gemma-zaken
|
||||||
|
filters:
|
||||||
|
- bronorganisatie
|
||||||
|
- zaaktype
|
||||||
|
- vertrouwelijkheidaanduiding
|
||||||
|
|||||||
@@ -18,10 +18,13 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- oz-db:/var/lib/postgresql/data
|
- oz-db:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U openzaak -d openzaak"]
|
# pg_isready only checks TCP; the second clause verifies PostGIS is installed
|
||||||
|
# so oz-init migrations can safely start (avoids race on cold container start).
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U openzaak -d openzaak && psql -U openzaak -d openzaak -c 'SELECT PostGIS_Version();' -q 2>/dev/null"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 3s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 30
|
||||||
|
start_period: 15s
|
||||||
networks: [cg]
|
networks: [cg]
|
||||||
|
|
||||||
oz-redis:
|
oz-redis:
|
||||||
@@ -29,7 +32,7 @@ services:
|
|||||||
networks: [cg]
|
networks: [cg]
|
||||||
|
|
||||||
oz-init:
|
oz-init:
|
||||||
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-latest}
|
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-1.28.2}
|
||||||
environment: &oz-env
|
environment: &oz-env
|
||||||
DJANGO_SETTINGS_MODULE: openzaak.conf.docker
|
DJANGO_SETTINGS_MODULE: openzaak.conf.docker
|
||||||
SECRET_KEY: ${OZ_SECRET_KEY:-dev-only-not-for-production}
|
SECRET_KEY: ${OZ_SECRET_KEY:-dev-only-not-for-production}
|
||||||
@@ -44,18 +47,20 @@ services:
|
|||||||
CELERY_BROKER_URL: redis://oz-redis:6379/1
|
CELERY_BROKER_URL: redis://oz-redis:6379/1
|
||||||
CELERY_RESULT_BACKEND: redis://oz-redis:6379/1
|
CELERY_RESULT_BACKEND: redis://oz-redis:6379/1
|
||||||
DISABLE_2FA: "true"
|
DISABLE_2FA: "true"
|
||||||
# Notifications go to Open Notificaties (NRC), which arrives in S-01-c.
|
# Notifications are OFF by default so OpenZaak-only bring-ups (openzaak-up,
|
||||||
# Until then, disable outbound notifications so writes don't 500.
|
# the ACL integration test) don't 500 trying to reach an absent NRC. When
|
||||||
NOTIFICATIONS_DISABLED: "true"
|
# OpenZaak runs together with the NRC stack, set OZ_NOTIFICATIONS_DISABLED=false
|
||||||
|
# (make stack-up does) to publish; the NRC service + notifications_config that
|
||||||
|
# name it are provisioned by setup_configuration (data.yaml, S-01-c).
|
||||||
|
NOTIFICATIONS_DISABLED: "${OZ_NOTIFICATIONS_DISABLED:-true}"
|
||||||
OPENZAAK_SUPERUSER_USERNAME: admin
|
OPENZAAK_SUPERUSER_USERNAME: admin
|
||||||
DJANGO_SUPERUSER_PASSWORD: admin
|
DJANGO_SUPERUSER_PASSWORD: admin
|
||||||
OPENZAAK_SUPERUSER_EMAIL: admin@localhost
|
OPENZAAK_SUPERUSER_EMAIL: admin@localhost
|
||||||
RUN_SETUP_CONFIG: "true"
|
RUN_SETUP_CONFIG: "true"
|
||||||
command: /setup_configuration.sh
|
command: /setup_configuration.sh
|
||||||
|
# data.yaml is streamed into this external volume by infra/seed-config.sh.
|
||||||
volumes:
|
volumes:
|
||||||
# :z relabels for SELinux; the dir/file must be world-readable for the
|
- oz-config:/app/setup_configuration:ro
|
||||||
# container user (rootless Podman uid mapping). See docs/runbooks/openzaak.md.
|
|
||||||
- ./setup_configuration:/app/setup_configuration:ro,z
|
|
||||||
depends_on:
|
depends_on:
|
||||||
oz-db:
|
oz-db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -64,7 +69,7 @@ services:
|
|||||||
networks: [cg]
|
networks: [cg]
|
||||||
|
|
||||||
openzaak:
|
openzaak:
|
||||||
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-latest}
|
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-1.28.2}
|
||||||
environment: *oz-env
|
environment: *oz-env
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "python", "-c", "import requests,sys; sys.exit(0 if requests.head('http://localhost:8000/admin/').status_code in (200,302) else 1)"]
|
test: ["CMD", "python", "-c", "import requests,sys; sys.exit(0 if requests.head('http://localhost:8000/admin/').status_code in (200,302) else 1)"]
|
||||||
@@ -80,7 +85,7 @@ services:
|
|||||||
networks: [cg]
|
networks: [cg]
|
||||||
|
|
||||||
oz-celery:
|
oz-celery:
|
||||||
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-latest}
|
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-1.28.2}
|
||||||
environment: *oz-env
|
environment: *oz-env
|
||||||
command: /celery_worker.sh
|
command: /celery_worker.sh
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -90,6 +95,10 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
oz-db:
|
oz-db:
|
||||||
|
# populated out-of-band by infra/seed-config.sh (docker cp) — see that script.
|
||||||
|
oz-config:
|
||||||
|
external: true
|
||||||
|
name: rr-oz-config
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
cg:
|
cg:
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ SECRET = os.environ.get("OZ_SECRET", "insecure-dev-secret-change-me")
|
|||||||
ZTC = f"{BASE}/catalogi/api/v1"
|
ZTC = f"{BASE}/catalogi/api/v1"
|
||||||
RSIN = "517439943" # elfproef-valid test RSIN
|
RSIN = "517439943" # elfproef-valid test RSIN
|
||||||
|
|
||||||
|
# Opt-in: also publish the zaaktype so OpenZaak's Zaken API accepts a zaak against
|
||||||
|
# it (a concept zaaktype is rejected with `not-published`). Off by default — the
|
||||||
|
# S-01 compose seed keeps it a concept (ADR-0002). The ACL integration test
|
||||||
|
# (S-04a, #46) sets OZ_PUBLISH=1. Publishing requires ≥2 statustypen, ≥1 roltype
|
||||||
|
# and ≥1 resultaattype; the resultaattype is validated against the external
|
||||||
|
# Selectielijst reference API, so this path needs outbound access to it. See ADR-0006.
|
||||||
|
PUBLISH = os.environ.get("OZ_PUBLISH", "").lower() in ("1", "true", "yes")
|
||||||
|
SELECTIELIJST = os.environ.get(
|
||||||
|
"OZ_SELECTIELIJST", "https://selectielijst.openzaak.nl/api/v1").rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
def token():
|
def token():
|
||||||
b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=")
|
b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=")
|
||||||
@@ -52,6 +62,68 @@ def find(path):
|
|||||||
return body.get("results", [])
|
return body.get("results", [])
|
||||||
|
|
||||||
|
|
||||||
|
def selectielijst(path):
|
||||||
|
"""GET the external Selectielijst reference API (no auth). Used only when publishing."""
|
||||||
|
req = urllib.request.Request(f"{SELECTIELIJST}{path}", headers={"Accept": "application/json"})
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as r:
|
||||||
|
return json.loads(r.read())
|
||||||
|
|
||||||
|
|
||||||
|
def publish_zaaktype(zt):
|
||||||
|
"""Add the relations OpenZaak requires to publish, then publish (idempotent).
|
||||||
|
|
||||||
|
Publish validation (verified against OpenZaak 1.28.2) demands: ≥2 statustypen
|
||||||
|
(begin + eind), ≥1 roltype, ≥1 resultaattype. A resultaattype needs a
|
||||||
|
Selectielijst `selectielijstklasse` whose procestype matches the zaaktype's
|
||||||
|
`selectielijstProcestype`, plus a `resultaattypeomschrijving`.
|
||||||
|
"""
|
||||||
|
have_st = {s.get("volgnummer") for s in find(f"/statustypen?zaaktype={zt['url']}&status=alles")}
|
||||||
|
for volgnummer, omschrijving in [(1, "Ontvangen"), (2, "Afgehandeld")]:
|
||||||
|
if volgnummer not in have_st:
|
||||||
|
st, body = api("POST", "/statustypen", {
|
||||||
|
"omschrijving": omschrijving, "zaaktype": zt["url"], "volgnummer": volgnummer})
|
||||||
|
if st != 201:
|
||||||
|
sys.exit(f"create statustype {volgnummer} -> {st}: {json.dumps(body, indent=2)}")
|
||||||
|
print(f"create statustype {volgnummer} ({omschrijving})")
|
||||||
|
|
||||||
|
if find(f"/roltypen?zaaktype={zt['url']}&status=alles"):
|
||||||
|
print("skip roltype Aanvrager")
|
||||||
|
else:
|
||||||
|
st, body = api("POST", "/roltypen", {
|
||||||
|
"zaaktype": zt["url"], "omschrijving": "Aanvrager", "omschrijvingGeneriek": "initiator"})
|
||||||
|
if st != 201:
|
||||||
|
sys.exit(f"create roltype -> {st}: {json.dumps(body, indent=2)}")
|
||||||
|
print("create roltype Aanvrager")
|
||||||
|
|
||||||
|
if find(f"/resultaattypen?zaaktype={zt['url']}&status=alles"):
|
||||||
|
print("skip resultaattype Geregistreerd")
|
||||||
|
else:
|
||||||
|
resultaat = selectielijst("/resultaten?pageSize=1")["results"][0]
|
||||||
|
omschrijvingen = selectielijst("/resultaattypeomschrijvingen")
|
||||||
|
oms = (omschrijvingen if isinstance(omschrijvingen, list) else omschrijvingen["results"])[0]["url"]
|
||||||
|
# The selectielijstklasse and the zaaktype must share a procestype.
|
||||||
|
st, body = api("PATCH", zt["url"], {"selectielijstProcestype": resultaat["procesType"]})
|
||||||
|
if st != 200:
|
||||||
|
sys.exit(f"set procestype -> {st}: {json.dumps(body, indent=2)}")
|
||||||
|
st, body = api("POST", "/resultaattypen", {
|
||||||
|
"zaaktype": zt["url"], "omschrijving": "Geregistreerd",
|
||||||
|
"resultaattypeomschrijving": oms, "selectielijstklasse": resultaat["url"],
|
||||||
|
"archiefnominatie": "blijvend_bewaren",
|
||||||
|
"brondatumArchiefprocedure": {"afleidingswijze": "afgehandeld"},
|
||||||
|
})
|
||||||
|
if st != 201:
|
||||||
|
sys.exit(f"create resultaattype -> {st}: {json.dumps(body, indent=2)}")
|
||||||
|
print("create resultaattype Geregistreerd")
|
||||||
|
|
||||||
|
if zt.get("concept", True):
|
||||||
|
st, body = api("POST", f"{zt['url']}/publish")
|
||||||
|
if st != 200:
|
||||||
|
sys.exit(f"publish zaaktype -> {st}: {json.dumps(body, indent=2)}")
|
||||||
|
print(f"publish zaaktype BIG-REGISTRATIE ({zt['url']})")
|
||||||
|
else:
|
||||||
|
print("skip publish (already published)")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# 1. Catalogus
|
# 1. Catalogus
|
||||||
existing = [c for c in find(f"/catalogussen?domein=BIG") if c.get("domein") == "BIG"]
|
existing = [c for c in find(f"/catalogussen?domein=BIG") if c.get("domein") == "BIG"]
|
||||||
@@ -121,16 +193,28 @@ def main():
|
|||||||
else:
|
else:
|
||||||
print("warn zaaktype already published; cannot add bsn eigenschap")
|
print("warn zaaktype already published; cannot add bsn eigenschap")
|
||||||
|
|
||||||
# Intentionally NOT published. Publishing requires roltypen, resultaattypen
|
# 4. Optionally publish. By default the zaaktype stays a concept: publishing
|
||||||
# and statustypen, which go beyond the "lean / schema-mandatory" zaaktype this
|
# requires roltypen, resultaattypen and statustypen, beyond the "lean /
|
||||||
# slice asks for; they arrive with the workflow/zaak slices. See ADR-0002.
|
# schema-mandatory" zaaktype S-01 asks for (ADR-0002). Set OZ_PUBLISH=1 to add
|
||||||
|
# those relations and publish — needed so a real zaak POST is accepted, which
|
||||||
|
# the ACL integration test (S-04a, #46) exercises. See ADR-0006.
|
||||||
|
if PUBLISH:
|
||||||
|
# Re-fetch: the bsn-eigenschap branch above may hold a stale concept flag.
|
||||||
|
zt = next(z for z in find(f"/zaaktypen?catalogus={cat['url']}&status=alles")
|
||||||
|
if z.get("identificatie") == "BIG-REGISTRATIE")
|
||||||
|
publish_zaaktype(zt)
|
||||||
|
|
||||||
# 4. Verify the JWT client can list the zaaktype (concepts included).
|
# 5. Verify the JWT client can list the zaaktype (concepts included).
|
||||||
zaaktypen = find(f"/zaaktypen?catalogus={cat['url']}&status=alles")
|
zaaktypen = find(f"/zaaktypen?catalogus={cat['url']}&status=alles")
|
||||||
names = [z.get("identificatie") for z in zaaktypen]
|
names = [z.get("identificatie") for z in zaaktypen]
|
||||||
print(f"zaaktypen in BIG: {names}")
|
print(f"zaaktypen in BIG: {names}")
|
||||||
assert "BIG-REGISTRATIE" in names, "BIG-REGISTRATIE not listed"
|
assert "BIG-REGISTRATIE" in names, "BIG-REGISTRATIE not listed"
|
||||||
print("OK — BIG catalogus seeded (BIG-REGISTRATIE concept + bsn eigenschap)")
|
state = "published" if PUBLISH else "concept"
|
||||||
|
# Machine-readable line so callers (e.g. infra/run-domain-check.sh) can capture the
|
||||||
|
# zaaktype URL to configure the ACL's default-fill (ADR-0003/0009).
|
||||||
|
zt_url = next(z["url"] for z in zaaktypen if z.get("identificatie") == "BIG-REGISTRATIE")
|
||||||
|
print(f"ZAAKTYPE_URL {zt_url}")
|
||||||
|
print(f"OK — BIG catalogus seeded (BIG-REGISTRATIE {state} + bsn eigenschap)")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -20,3 +20,22 @@ vng_api_common_applicaties:
|
|||||||
- big-reference-seed
|
- big-reference-seed
|
||||||
label: BIG reference seed client
|
label: BIG reference seed client
|
||||||
heeft_alle_autorisaties: true
|
heeft_alle_autorisaties: true
|
||||||
|
|
||||||
|
# ── OpenZaak → Open Notificaties (NRC) publishing (S-01-c, #56) ─────────────
|
||||||
|
# The NRC service OpenZaak posts notifications to, authenticating with the same
|
||||||
|
# big-reference-seed client (NRC verifies the JWT and authorizes it via the AC).
|
||||||
|
zgw_consumers_config_enable: true
|
||||||
|
zgw_consumers:
|
||||||
|
services:
|
||||||
|
- identifier: nrc
|
||||||
|
label: Open Notificaties
|
||||||
|
api_type: nrc
|
||||||
|
api_root: http://nrc-web:8000/api/v1/
|
||||||
|
auth_type: zgw
|
||||||
|
client_id: big-reference-seed
|
||||||
|
secret: insecure-dev-secret-change-me
|
||||||
|
|
||||||
|
# Point OpenZaak's notifications at that service. Requires NOTIFICATIONS_DISABLED=false.
|
||||||
|
notifications_config_enable: true
|
||||||
|
notifications_config:
|
||||||
|
notifications_api_service_identifier: nrc
|
||||||
|
|||||||
37
infra/run-acl-integration.sh
Executable file
37
infra/run-acl-integration.sh
Executable file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Run the ACL integration tests (Category=Integration) against the OpenZaak that is
|
||||||
|
# ALREADY running — works for any stack: oz-only (`make integration`), the standalone
|
||||||
|
# oz+nrc stack, or the full compose stack (the CI `verify-stack` job). Seeds a
|
||||||
|
# published BIG zaaktype (idempotent), then builds + runs the test image on the stack
|
||||||
|
# network, reaching OpenZaak by container IP (a single-label host isn't URL-valid;
|
||||||
|
# the runner can't reach published ports — see gitea-actions-gotchas.md §5/§6).
|
||||||
|
#
|
||||||
|
# Does NOT manage the stack lifecycle: the caller owns bring-up + teardown. Plain
|
||||||
|
# docker primitives only (docker/podman-portable). See ADR-0006.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
root="$(cd "$here/.." && pwd)"
|
||||||
|
|
||||||
|
# The OpenZaak API container, matched across compose projects + docker/podman naming
|
||||||
|
# (`<project>[-_]openzaak[-_]<n>`); the delimiters exclude oz-db / oz-redis / oz-init.
|
||||||
|
oz="$(docker ps -q --filter 'name=[-_]openzaak[-_]' | head -1)"
|
||||||
|
[ -n "$oz" ] || { echo "ERROR: no running OpenZaak container found — bring the stack up first" >&2; exit 1; }
|
||||||
|
net="$(docker inspect -f '{{range $k,$_ := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' "$oz" | head -1)"
|
||||||
|
oz_ip="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$oz")"
|
||||||
|
oz_base="http://$oz_ip:8000"
|
||||||
|
echo ">> OpenZaak at $oz_base on network $net"
|
||||||
|
|
||||||
|
echo ">> seeding a published BIG zaaktype (idempotent)"
|
||||||
|
sid="$(docker create --network "$net" -e "OZ_BASE=$oz_base" -e OZ_PUBLISH=1 \
|
||||||
|
python:3-slim python /seed.py)"
|
||||||
|
docker cp "$here/openzaak/seed_catalogus.py" "$sid:/seed.py" >/dev/null
|
||||||
|
docker start -a "$sid"
|
||||||
|
docker rm -f "$sid" >/dev/null
|
||||||
|
|
||||||
|
echo ">> building the integration test image"
|
||||||
|
docker build -f "$root/services/acl/Dockerfile.integration" -t rr-acl-integration "$root/services/acl"
|
||||||
|
|
||||||
|
echo ">> running the ACL integration tests (inside the network)"
|
||||||
|
docker run --rm --network "$net" -e "OZ_BASE=$oz_base" rr-acl-integration
|
||||||
58
infra/run-bff-check.sh
Executable file
58
infra/run-bff-check.sh
Executable file
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Verify the BFF end-to-end (S-07) against an ALREADY-RUNNING full stack. The BFF is the portals'
|
||||||
|
# only backend (§8.3): it validates Keycloak digid tokens on the self-service submit and serves the
|
||||||
|
# openbaar register anonymously with only public-safe fields (ADR-0010).
|
||||||
|
#
|
||||||
|
# Checks, in-network (services reached by container IP; Keycloak by its service name so the token's
|
||||||
|
# host-derived issuer matches the BFF's authority — see the compose bff env and ADR-0010):
|
||||||
|
# 1. POST /self-service/registrations without a token -> 401
|
||||||
|
# 2. mint a real digid access token (direct grant) and POST it -> 202 (forwarded to the domain)
|
||||||
|
# 3. GET /openbaar/register (anonymous) -> 200 JSON array, never a bsn
|
||||||
|
#
|
||||||
|
# Does NOT manage the stack lifecycle (the caller owns bring-up + teardown). Plain docker primitives.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ip() { docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$1"; }
|
||||||
|
|
||||||
|
bff="$(docker ps -q --filter 'name=[-_]bff[-_]' | head -1)"
|
||||||
|
kc="$(docker ps -q --filter 'name=keycloak' | head -1)"
|
||||||
|
[ -n "$bff" ] || { echo "ERROR: no running bff container — bring the stack up first" >&2; exit 1; }
|
||||||
|
[ -n "$kc" ] || { echo "ERROR: no running keycloak container — bring the stack up first" >&2; exit 1; }
|
||||||
|
net="$(docker inspect -f '{{range $k,$_ := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' "$bff" | head -1)"
|
||||||
|
bff_ip="$(ip "$bff")"
|
||||||
|
echo ">> bff=$bff_ip network=$net"
|
||||||
|
|
||||||
|
# Helper: run curl inside a throwaway container on the stack network (reaches services by name/IP).
|
||||||
|
net_curl() { docker run --rm --network "$net" curlimages/curl:latest "$@"; }
|
||||||
|
|
||||||
|
echo ">> 1. self-service submit without a token must be 401"
|
||||||
|
code="$(net_curl -s -o /dev/null -w '%{http_code}' -X POST "http://$bff_ip:8080/self-service/registrations" \
|
||||||
|
-H 'Content-Type: application/json' -d '{}')"
|
||||||
|
echo " -> $code"; [ "$code" = "401" ] || { echo "FAIL: expected 401, got $code" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo ">> 2. minting a digid token (direct grant) via keycloak:8080 (host-consistent issuer)"
|
||||||
|
token=""
|
||||||
|
for _ in $(seq 1 20); do
|
||||||
|
token="$(net_curl -s -X POST "http://keycloak:8080/realms/digid/protocol/openid-connect/token" \
|
||||||
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
|
--data-urlencode 'grant_type=password' --data-urlencode 'client_id=big-portal' \
|
||||||
|
--data-urlencode 'username=jan-burger' --data-urlencode 'password=test123' \
|
||||||
|
| sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')"
|
||||||
|
[ -n "$token" ] && break
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
[ -n "$token" ] || { echo "FAIL: could not obtain a digid access token" >&2; exit 1; }
|
||||||
|
echo " -> got a token"
|
||||||
|
|
||||||
|
echo ">> 2b. self-service submit with the token must be 202"
|
||||||
|
code="$(net_curl -s -o /dev/null -w '%{http_code}' -X POST "http://$bff_ip:8080/self-service/registrations" \
|
||||||
|
-H "Authorization: Bearer $token" -H 'Content-Type: application/json' -d '{}')"
|
||||||
|
echo " -> $code"; [ "$code" = "202" ] || { echo "FAIL: expected 202, got $code" >&2; docker logs "$bff" 2>&1 | tail -15 >&2; exit 1; }
|
||||||
|
|
||||||
|
echo ">> 3. openbaar register (anonymous) must be 200 JSON, never a bsn"
|
||||||
|
body="$(net_curl -s "http://$bff_ip:8080/openbaar/register")"
|
||||||
|
echo "$body" | grep -q '^\[' || { echo "FAIL: openbaar did not return a JSON array: $body" >&2; exit 1; }
|
||||||
|
if echo "$body" | grep -q '"bsn"'; then echo "FAIL: openbaar leaked a bsn field" >&2; exit 1; fi
|
||||||
|
|
||||||
|
echo "OK — BFF: 401 without token, 202 with a digid token, anonymous public-safe openbaar register"
|
||||||
68
infra/run-domain-check.sh
Executable file
68
infra/run-domain-check.sh
Executable file
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Verify the BIG Domain Service end-to-end (S-05) against an ALREADY-RUNNING full stack:
|
||||||
|
# domain → Flowable (start the registratie process + external-task worker) → ACL → OpenZaak.
|
||||||
|
# Submits a registration to the domain and asserts the worker opens a zaak in OpenZaak and
|
||||||
|
# records its URL on the aggregate (ADR-0009).
|
||||||
|
#
|
||||||
|
# The seeded zaaktype URL is server-assigned, so it isn't knowable at initial bring-up. This
|
||||||
|
# script therefore seeds a published BIG zaaktype and recreates the `acl` service configured to
|
||||||
|
# default-fill it — pointing the ACL at the SAME OpenZaak host that owns the URL, so zaak creation
|
||||||
|
# is host-consistent (exactly the configuration the ACL integration test proves, ADR-0006). That
|
||||||
|
# one recreate aside, the caller owns stack bring-up + teardown.
|
||||||
|
#
|
||||||
|
# All in-network, reaching services by container IP (a single-label host isn't URL-valid; the
|
||||||
|
# runner can't reach published ports — gitea-actions-gotchas.md §5/§6). Plain docker primitives.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
root="$(cd "$here/.." && pwd)"
|
||||||
|
compose="$root/infra/docker-compose.yml"
|
||||||
|
|
||||||
|
ip() { docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$1"; }
|
||||||
|
|
||||||
|
oz="$(docker ps -q --filter 'name=[-_]openzaak[-_]' | head -1)"
|
||||||
|
dom="$(docker ps -q --filter 'name=domain' | head -1)"
|
||||||
|
[ -n "$oz" ] || { echo "ERROR: no running OpenZaak container — bring the stack up first" >&2; exit 1; }
|
||||||
|
[ -n "$dom" ] || { echo "ERROR: no running domain container — bring the stack up first" >&2; exit 1; }
|
||||||
|
net="$(docker inspect -f '{{range $k,$_ := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' "$oz" | head -1)"
|
||||||
|
oz_ip="$(ip "$oz")"; dom_ip="$(ip "$dom")"
|
||||||
|
oz_base="http://$oz_ip:8000"
|
||||||
|
echo ">> openzaak=$oz_ip domain=$dom_ip network=$net"
|
||||||
|
|
||||||
|
echo ">> seeding a published BIG zaaktype (idempotent) and capturing its URL"
|
||||||
|
sid="$(docker create --network "$net" -e "OZ_BASE=$oz_base" -e OZ_PUBLISH=1 python:3-slim python /seed.py)"
|
||||||
|
docker cp "$here/openzaak/seed_catalogus.py" "$sid:/seed.py" >/dev/null
|
||||||
|
zt_url="$(docker start -a "$sid" | sed -n 's/^ZAAKTYPE_URL //p' | head -1)"
|
||||||
|
docker rm -f "$sid" >/dev/null
|
||||||
|
[ -n "$zt_url" ] || { echo "ERROR: seed did not report a ZAAKTYPE_URL" >&2; exit 1; }
|
||||||
|
echo ">> zaaktype: $zt_url"
|
||||||
|
|
||||||
|
echo ">> recreating the acl service pointed at the seeded zaaktype (host-consistent)"
|
||||||
|
ACL_ZAAKTYPE_URL="$zt_url" ACL_OPENZAAK_BASEURL="$oz_base/" docker compose -f "$compose" up -d acl
|
||||||
|
WAIT_TIMEOUT="${WAIT_TIMEOUT:-120}" bash "$here/wait-healthy.sh" acl
|
||||||
|
|
||||||
|
echo ">> submitting a registration to the domain"
|
||||||
|
loc="$(docker run --rm --network "$net" curlimages/curl:latest \
|
||||||
|
-fsS -D - -o /dev/null -X POST "http://$dom_ip:8080/registrations" \
|
||||||
|
-H 'Content-Type: application/json' -d '{"bsn":"123456782"}' \
|
||||||
|
| sed -n 's/\r$//; s/^[Ll]ocation: //p' | head -1)"
|
||||||
|
[ -n "$loc" ] || { echo "ERROR: POST /registrations returned no Location" >&2; exit 1; }
|
||||||
|
echo ">> registration accepted at $loc"
|
||||||
|
|
||||||
|
echo ">> polling the domain until the worker records the opened zaak"
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
body="$(docker run --rm --network "$net" curlimages/curl:latest \
|
||||||
|
-fsS "http://$dom_ip:8080$loc" 2>/dev/null || true)"
|
||||||
|
if echo "$body" | grep -q '/zaken/api/v1/zaken/'; then
|
||||||
|
echo "OK — the domain opened a zaak and recorded it on the registration:"
|
||||||
|
echo "$body" | cut -c1-300
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "FAIL — the registration never received a zaak URL" >&2
|
||||||
|
echo "--- domain log ---" >&2; docker logs "$dom" 2>&1 | tail -15 >&2
|
||||||
|
acl="$(docker ps -q --filter 'name=[-_]acl[-_]' | head -1)"
|
||||||
|
[ -n "$acl" ] && { echo "--- acl log ---" >&2; docker logs "$acl" 2>&1 | tail -15 >&2; }
|
||||||
|
exit 1
|
||||||
34
infra/run-integration.sh
Executable file
34
infra/run-integration.sh
Executable file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Local convenience: run the ACL integration test against a throwaway OpenZaak-only
|
||||||
|
# stack (fast iteration on the ACL gateway). Brings OpenZaak up, runs the shared
|
||||||
|
# stack-agnostic check (infra/run-acl-integration.sh), then always tears down.
|
||||||
|
#
|
||||||
|
# CI does not use this — there the full stack is brought up once and the same runner
|
||||||
|
# is invoked as a step (see the `verify-stack` job / Makefile `verify-*`). See ADR-0006.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
OZ_COMPOSE="$here/openzaak/docker-compose.yml"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
docker compose -f "$OZ_COMPOSE" down --volumes >/dev/null 2>&1 || true
|
||||||
|
docker volume rm -f rr-oz-config >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
echo ">> bringing OpenZaak up"
|
||||||
|
bash "$here/seed-config.sh" oz
|
||||||
|
docker compose -f "$OZ_COMPOSE" up -d
|
||||||
|
|
||||||
|
echo ">> waiting for the OpenZaak API container to be healthy"
|
||||||
|
for _ in $(seq 1 140); do
|
||||||
|
oz="$(docker ps -q --filter 'name=[-_]openzaak[-_]' | head -1)"
|
||||||
|
if [ -n "$oz" ] && [ "$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "$oz" 2>/dev/null || true)" = healthy ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
[ -n "${oz:-}" ] || { echo "ERROR: OpenZaak never came up" >&2; exit 1; }
|
||||||
|
|
||||||
|
bash "$here/run-acl-integration.sh"
|
||||||
72
infra/run-notification-check.sh
Executable file
72
infra/run-notification-check.sh
Executable file
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Verify the OpenZaak → NRC notification path against an ALREADY-RUNNING oz+nrc stack
|
||||||
|
# (the standalone stack via `make verify-notifications`, or the full compose stack in
|
||||||
|
# the CI `verify-stack` job). Seeds a published BIG zaaktype (idempotent), registers
|
||||||
|
# an abonnement to a webhook sink, creates a zaak, and asserts the sink receives the
|
||||||
|
# `zaken`/`create` notification. All in-network, reaching services by container IP
|
||||||
|
# (single-label hosts aren't URL-valid; the runner can't reach published ports).
|
||||||
|
#
|
||||||
|
# Does NOT manage the stack lifecycle (the caller owns bring-up + teardown), but it
|
||||||
|
# cleans up the throwaway sink/driver it creates. Plain docker primitives only.
|
||||||
|
# See ADR-0007.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
SINK_AUTH="Bearer notification-sink-token"
|
||||||
|
|
||||||
|
cleanup() { docker rm -f rr-nsink rr-nverify >/dev/null 2>&1 || true; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
ip() { docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$1"; }
|
||||||
|
|
||||||
|
oz="$(docker ps -q --filter 'name=[-_]openzaak[-_]' | head -1)"
|
||||||
|
nrc="$(docker ps -q --filter 'name=nrc-web' | head -1)"
|
||||||
|
[ -n "$oz" ] && [ -n "$nrc" ] || { echo "ERROR: OpenZaak and/or NRC not running — bring the stack up first" >&2; exit 1; }
|
||||||
|
net="$(docker inspect -f '{{range $k,$_ := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' "$oz" | head -1)"
|
||||||
|
oz_ip="$(ip "$oz")"; nrc_ip="$(ip "$nrc")"
|
||||||
|
echo ">> network=$net openzaak=$oz_ip nrc=$nrc_ip"
|
||||||
|
|
||||||
|
echo ">> seeding a published BIG zaaktype (idempotent)"
|
||||||
|
sid="$(docker create --network "$net" -e "OZ_BASE=http://$oz_ip:8000" -e OZ_PUBLISH=1 \
|
||||||
|
python:3-slim python /seed.py)"
|
||||||
|
docker cp "$here/openzaak/seed_catalogus.py" "$sid:/seed.py" >/dev/null
|
||||||
|
docker start -a "$sid"
|
||||||
|
docker rm -f "$sid" >/dev/null
|
||||||
|
|
||||||
|
echo ">> starting the webhook sink"
|
||||||
|
docker rm -f rr-nsink >/dev/null 2>&1 || true
|
||||||
|
sink="$(docker create --network "$net" --name rr-nsink -e "EXPECTED_AUTH=$SINK_AUTH" \
|
||||||
|
python:3-slim python /sink.py)"
|
||||||
|
docker cp "$here/notification-sink.py" "$sink:/sink.py" >/dev/null
|
||||||
|
docker start "$sink" >/dev/null
|
||||||
|
sleep 1
|
||||||
|
sink_ip="$(ip rr-nsink)"
|
||||||
|
echo ">> sink at $sink_ip:9000"
|
||||||
|
|
||||||
|
echo ">> registering abonnement + creating a zaak"
|
||||||
|
docker rm -f rr-nverify >/dev/null 2>&1 || true
|
||||||
|
drv="$(docker create --network "$net" --name rr-nverify \
|
||||||
|
-e "OZ_BASE=http://$oz_ip:8000" -e "NRC_BASE=http://$nrc_ip:8000" \
|
||||||
|
-e "SINK_CALLBACK=http://$sink_ip:9000/" -e "SINK_AUTH=$SINK_AUTH" \
|
||||||
|
python:3-slim python /driver.py)"
|
||||||
|
docker cp "$here/verify-notification-driver.py" "$drv:/driver.py" >/dev/null
|
||||||
|
docker start -a "$drv"
|
||||||
|
zaak_url="$(docker logs rr-nverify 2>/dev/null | sed -n 's/^ZAAK_CREATED //p' | head -1)"
|
||||||
|
docker rm -f rr-nverify >/dev/null
|
||||||
|
[ -n "$zaak_url" ] || { echo "ERROR: driver did not create a zaak" >&2; exit 1; }
|
||||||
|
zaak_uuid="${zaak_url##*/}"
|
||||||
|
echo ">> zaak created: $zaak_url"
|
||||||
|
|
||||||
|
echo ">> waiting for the notification to reach the sink"
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
if docker logs rr-nsink 2>&1 | grep -q "$zaak_uuid"; then
|
||||||
|
echo "OK — NRC delivered the zaken notification for zaak $zaak_uuid to the sink"
|
||||||
|
docker logs rr-nsink 2>&1 | grep "$zaak_uuid" | tail -1 | cut -c1-300
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "FAIL — the sink never received a notification for zaak $zaak_uuid" >&2
|
||||||
|
echo "--- sink log ---" >&2; docker logs rr-nsink 2>&1 | tail -8 >&2
|
||||||
|
exit 1
|
||||||
68
infra/run-projection-check.sh
Executable file
68
infra/run-projection-check.sh
Executable file
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Verify the end-to-end read-projection path (S-06) against an ALREADY-RUNNING full stack:
|
||||||
|
# OpenZaak → NRC → Event Subscriber → projection → projection-api. Seeds a published BIG
|
||||||
|
# zaaktype (idempotent), registers an abonnement on the `zaken` kanaal pointing at the real
|
||||||
|
# Event Subscriber's /notifications callback (with the bearer it enforces), creates a zaak,
|
||||||
|
# and asserts projection-api serves a row for that zaak with status INGEDIEND.
|
||||||
|
#
|
||||||
|
# All in-network, reaching services by container IP — single-label hosts aren't URL-valid and
|
||||||
|
# the runner can't reach published ports (gitea-actions-gotchas.md §5/§6). Reuses the
|
||||||
|
# notification driver to register the abonnement + create the zaak. Does NOT manage the stack
|
||||||
|
# lifecycle (the caller owns bring-up + teardown). Plain docker primitives only. See ADR-0007/0008.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
WEBHOOK_AUTH="${NOTIFICATION_WEBHOOK_TOKEN:-Bearer big-reference-notifications}"
|
||||||
|
|
||||||
|
cleanup() { docker rm -f rr-pverify rr-pquery >/dev/null 2>&1 || true; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
ip() { docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$1"; }
|
||||||
|
|
||||||
|
oz="$(docker ps -q --filter 'name=[-_]openzaak[-_]' | head -1)"
|
||||||
|
nrc="$(docker ps -q --filter 'name=nrc-web' | head -1)"
|
||||||
|
es="$(docker ps -q --filter 'name=event-subscriber' | head -1)"
|
||||||
|
proj="$(docker ps -q --filter 'name=projection-api' | head -1)"
|
||||||
|
[ -n "$oz" ] && [ -n "$nrc" ] || { echo "ERROR: OpenZaak and/or NRC not running — bring the stack up first" >&2; exit 1; }
|
||||||
|
[ -n "$es" ] && [ -n "$proj" ] || { echo "ERROR: event-subscriber and/or projection-api not running — bring the stack up first" >&2; exit 1; }
|
||||||
|
net="$(docker inspect -f '{{range $k,$_ := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' "$oz" | head -1)"
|
||||||
|
oz_ip="$(ip "$oz")"; nrc_ip="$(ip "$nrc")"; es_ip="$(ip "$es")"; proj_ip="$(ip "$proj")"
|
||||||
|
echo ">> network=$net openzaak=$oz_ip nrc=$nrc_ip event-subscriber=$es_ip projection-api=$proj_ip"
|
||||||
|
|
||||||
|
echo ">> seeding a published BIG zaaktype (idempotent)"
|
||||||
|
sid="$(docker create --network "$net" -e "OZ_BASE=http://$oz_ip:8000" -e OZ_PUBLISH=1 \
|
||||||
|
python:3-slim python /seed.py)"
|
||||||
|
docker cp "$here/openzaak/seed_catalogus.py" "$sid:/seed.py" >/dev/null
|
||||||
|
docker start -a "$sid"
|
||||||
|
docker rm -f "$sid" >/dev/null
|
||||||
|
|
||||||
|
echo ">> registering abonnement at the Event Subscriber + creating a zaak"
|
||||||
|
docker rm -f rr-pverify >/dev/null 2>&1 || true
|
||||||
|
drv="$(docker create --network "$net" --name rr-pverify \
|
||||||
|
-e "OZ_BASE=http://$oz_ip:8000" -e "NRC_BASE=http://$nrc_ip:8000" \
|
||||||
|
-e "SINK_CALLBACK=http://$es_ip:8080/notifications" -e "SINK_AUTH=$WEBHOOK_AUTH" \
|
||||||
|
python:3-slim python /driver.py)"
|
||||||
|
docker cp "$here/verify-notification-driver.py" "$drv:/driver.py" >/dev/null
|
||||||
|
docker start -a "$drv"
|
||||||
|
zaak_url="$(docker logs rr-pverify 2>/dev/null | sed -n 's/^ZAAK_CREATED //p' | head -1)"
|
||||||
|
docker rm -f rr-pverify >/dev/null
|
||||||
|
[ -n "$zaak_url" ] || { echo "ERROR: driver did not create a zaak" >&2; exit 1; }
|
||||||
|
zaak_uuid="${zaak_url##*/}"
|
||||||
|
echo ">> zaak created: $zaak_url"
|
||||||
|
|
||||||
|
echo ">> polling projection-api for the projected row (status INGEDIEND)"
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
body="$(docker run --rm --network "$net" curlimages/curl:latest \
|
||||||
|
-fsS "http://$proj_ip:8080/register/$zaak_uuid" 2>/dev/null || true)"
|
||||||
|
if echo "$body" | grep -q '"INGEDIEND"'; then
|
||||||
|
echo "OK — projection-api serves zaak $zaak_uuid with status INGEDIEND"
|
||||||
|
echo "$body" | cut -c1-300
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "FAIL — projection-api never served an INGEDIEND row for zaak $zaak_uuid" >&2
|
||||||
|
echo "--- event-subscriber log ---" >&2; docker logs "$es" 2>&1 | tail -10 >&2
|
||||||
|
echo "--- projection-api log ---" >&2; docker logs "$proj" 2>&1 | tail -10 >&2
|
||||||
|
exit 1
|
||||||
46
infra/seed-config.sh
Executable file
46
infra/seed-config.sh
Executable file
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Populate the external named *config* volumes that the upstream services mount,
|
||||||
|
# by `docker cp`-ing files into a throwaway helper container that mounts each one.
|
||||||
|
#
|
||||||
|
# Why: the compose stack uses the upstream images verbatim (no build). On Gitea's
|
||||||
|
# containerized runner, `docker compose` starts the stack as SIBLING containers
|
||||||
|
# via the host daemon, so a workspace bind mount resolves to a path the daemon
|
||||||
|
# can't see and is mounted empty. `docker cp` instead streams bytes over the
|
||||||
|
# Docker API, so the files reach the volume regardless of where the daemon runs.
|
||||||
|
# We use plain docker primitives (volume create / run / cp / rm) rather than
|
||||||
|
# `docker compose create`, because podman-compose (local dev) lacks that
|
||||||
|
# subcommand. Fixed-name `external` volumes keep the names deterministic across
|
||||||
|
# both runtimes. See docs/runbooks/gitea-actions-gotchas.md.
|
||||||
|
#
|
||||||
|
# Usage: seed-config.sh <key> [<key> ...] where key ∈ { oz, kc, fl }
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
HELPER="${SEED_HELPER_IMAGE:-docker.io/library/busybox:stable}"
|
||||||
|
|
||||||
|
populate() { # volume source(file or dir/.)
|
||||||
|
local vol="$1" src="$2" cid
|
||||||
|
docker volume rm -f "$vol" >/dev/null 2>&1 || true
|
||||||
|
docker volume create "$vol" >/dev/null
|
||||||
|
# A *created* (never started) helper is enough: the volume is attached at create
|
||||||
|
# time, `docker cp` writes through to it, and `docker rm` is instant (nothing to
|
||||||
|
# stop). `docker create` is a container subcommand both docker and podman have —
|
||||||
|
# unlike `docker compose create`, which podman-compose lacks.
|
||||||
|
cid="$(docker create -v "$vol:/dest" "$HELPER" true)"
|
||||||
|
docker cp "$src" "$cid:/dest/"
|
||||||
|
docker rm "$cid" >/dev/null
|
||||||
|
echo " seeded $vol"
|
||||||
|
}
|
||||||
|
|
||||||
|
[ "$#" -gt 0 ] || { echo "usage: seed-config.sh <oz|nrc|kc|fl> ..." >&2; exit 2; }
|
||||||
|
|
||||||
|
for key in "$@"; do
|
||||||
|
case "$key" in
|
||||||
|
oz) populate rr-oz-config "$here/openzaak/setup_configuration/." ;;
|
||||||
|
nrc) populate rr-nrc-config "$here/opennotificaties/setup_configuration/." ;;
|
||||||
|
kc) populate rr-kc-realms "$here/keycloak/realms/." ;;
|
||||||
|
fl) populate rr-fl-bpmn "$here/../workflows/registratie.bpmn" ;;
|
||||||
|
*) echo "unknown seed key: $key" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
90
infra/verify-notification-driver.py
Executable file
90
infra/verify-notification-driver.py
Executable file
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Drive the OpenZaak → NRC notification check from *inside* the compose network.
|
||||||
|
|
||||||
|
Registers an abonnement on the `zaken` kanaal pointing at a webhook sink, then
|
||||||
|
creates a zaak against the published BIG zaaktype. OpenZaak publishes a
|
||||||
|
`zaken`/`create` notification; NRC delivers it to the sink. The host harness
|
||||||
|
(infra/verify-notifications.sh) then asserts the sink received it.
|
||||||
|
|
||||||
|
Reached by container IP, not service name: OpenZaak/NRC validate URLs with Django's
|
||||||
|
URLValidator, which rejects a single-label host like `openzaak`. Stdlib only.
|
||||||
|
Env: OZ_BASE, NRC_BASE, SINK_CALLBACK, SINK_AUTH, OZ_CLIENT_ID, OZ_SECRET.
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
OZ = os.environ["OZ_BASE"].rstrip("/")
|
||||||
|
NRC = os.environ["NRC_BASE"].rstrip("/")
|
||||||
|
SINK_CALLBACK = os.environ["SINK_CALLBACK"]
|
||||||
|
SINK_AUTH = os.environ.get("SINK_AUTH", "Bearer notification-sink-token")
|
||||||
|
CID = os.environ.get("OZ_CLIENT_ID", "big-reference-seed")
|
||||||
|
SECRET = os.environ.get("OZ_SECRET", "insecure-dev-secret-change-me")
|
||||||
|
RSIN = "517439943"
|
||||||
|
|
||||||
|
|
||||||
|
def token():
|
||||||
|
b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=")
|
||||||
|
seg = (
|
||||||
|
b64(json.dumps({"alg": "HS256", "typ": "JWT"}, separators=(",", ":")).encode())
|
||||||
|
+ b"."
|
||||||
|
+ b64(json.dumps(
|
||||||
|
{"iss": CID, "iat": int(time.time()), "client_id": CID,
|
||||||
|
"user_id": "verify", "user_representation": "verify"},
|
||||||
|
separators=(",", ":")).encode())
|
||||||
|
)
|
||||||
|
return (seg + b"." + b64(hmac.new(SECRET.encode(), seg, hashlib.sha256).digest())).decode()
|
||||||
|
|
||||||
|
|
||||||
|
def call(method, url, body=None, crs=False):
|
||||||
|
headers = {"Authorization": "Bearer " + token(),
|
||||||
|
"Content-Type": "application/json", "Accept": "application/json"}
|
||||||
|
if crs:
|
||||||
|
headers["Accept-Crs"] = "EPSG:4326"
|
||||||
|
headers["Content-Crs"] = "EPSG:4326"
|
||||||
|
data = json.dumps(body).encode() if body is not None else None
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(
|
||||||
|
urllib.request.Request(url, data=data, method=method, headers=headers), timeout=30
|
||||||
|
) as r:
|
||||||
|
return r.status, json.loads(r.read() or "null")
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return e.code, json.loads(e.read() or "null")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
status, ab = call("POST", f"{NRC}/api/v1/abonnement", {
|
||||||
|
"callbackUrl": SINK_CALLBACK,
|
||||||
|
"auth": SINK_AUTH,
|
||||||
|
"kanalen": [{"naam": "zaken", "filters": {}}],
|
||||||
|
})
|
||||||
|
if status != 201:
|
||||||
|
sys.exit(f"create abonnement -> {status}: {json.dumps(ab)}")
|
||||||
|
print(f"abonnement: {ab['url']}")
|
||||||
|
|
||||||
|
status, body = call(
|
||||||
|
"GET", f"{OZ}/catalogi/api/v1/zaaktypen?identificatie=BIG-REGISTRATIE&status=definitief")
|
||||||
|
results = body.get("results", []) if status == 200 else []
|
||||||
|
if not results:
|
||||||
|
sys.exit("no published BIG-REGISTRATIE zaaktype — seed with OZ_PUBLISH=1 first")
|
||||||
|
zaaktype = results[0]["url"]
|
||||||
|
|
||||||
|
status, zaak = call("POST", f"{OZ}/zaken/api/v1/zaken", {
|
||||||
|
"bronorganisatie": RSIN, "verantwoordelijkeOrganisatie": RSIN,
|
||||||
|
"zaaktype": zaaktype, "startdatum": time.strftime("%Y-%m-%d"),
|
||||||
|
"vertrouwelijkheidaanduiding": "openbaar",
|
||||||
|
}, crs=True)
|
||||||
|
if status != 201:
|
||||||
|
sys.exit(f"create zaak -> {status}: {json.dumps(zaak)}")
|
||||||
|
# The harness greps the sink for this exact URL.
|
||||||
|
print(f"ZAAK_CREATED {zaak['url']}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
41
infra/verify-notifications.sh
Executable file
41
infra/verify-notifications.sh
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Local convenience: verify the OpenZaak → NRC notification path against a throwaway
|
||||||
|
# oz+nrc stack. Brings both up (notifications enabled), runs the shared stack-agnostic
|
||||||
|
# check (infra/run-notification-check.sh), then always tears down.
|
||||||
|
#
|
||||||
|
# CI does not use this — there the full stack is brought up once and the same runner
|
||||||
|
# is invoked as a step (see the `verify-stack` job / Makefile `verify-*`). See ADR-0007.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
OZ_COMPOSE="$here/openzaak/docker-compose.yml"
|
||||||
|
NRC_COMPOSE="$here/opennotificaties/docker-compose.yml"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
docker compose -f "$OZ_COMPOSE" -f "$NRC_COMPOSE" down --volumes >/dev/null 2>&1 || true
|
||||||
|
docker volume rm -f rr-oz-config rr-nrc-config >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
wait_healthy() { # name-regex
|
||||||
|
local re="$1" cid
|
||||||
|
for _ in $(seq 1 140); do
|
||||||
|
cid="$(docker ps -q --filter "name=$re" | head -1)"
|
||||||
|
if [ -n "$cid" ] && [ "$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "$cid" 2>/dev/null || true)" = healthy ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo ">> bringing up OpenZaak + Open Notificaties (notifications enabled)"
|
||||||
|
bash "$here/seed-config.sh" oz nrc
|
||||||
|
OZ_NOTIFICATIONS_DISABLED=false docker compose -f "$OZ_COMPOSE" -f "$NRC_COMPOSE" up -d
|
||||||
|
|
||||||
|
echo ">> waiting for OpenZaak + NRC to be healthy"
|
||||||
|
wait_healthy '[-_]openzaak[-_]' || { echo "ERROR: OpenZaak not healthy" >&2; exit 1; }
|
||||||
|
wait_healthy 'nrc-web' || { echo "ERROR: NRC not healthy" >&2; exit 1; }
|
||||||
|
|
||||||
|
bash "$here/run-notification-check.sh"
|
||||||
36
infra/wait-healthy.sh
Executable file
36
infra/wait-healthy.sh
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Wait until the named compose services report a healthy healthcheck.
|
||||||
|
#
|
||||||
|
# Portable across `docker compose` (CI) and `podman-compose` (local dev): it uses
|
||||||
|
# plain `docker ps` + `docker inspect`, so it needs neither `docker compose
|
||||||
|
# up --wait` (podman-compose doesn't implement that flag) nor host port access
|
||||||
|
# (the containerized CI runner can't reach published ports). It also sidesteps the
|
||||||
|
# `--wait`-fails-when-a-one-shot-exits issue, since we only poll long-running
|
||||||
|
# services that declare a healthcheck. See docs/runbooks/gitea-actions-gotchas.md.
|
||||||
|
#
|
||||||
|
# Usage: WAIT_TIMEOUT=420 wait-healthy.sh <service> [<service> ...]
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
timeout="${WAIT_TIMEOUT:-420}"
|
||||||
|
deadline=$(( $(date +%s) + timeout ))
|
||||||
|
|
||||||
|
# compose service name -> container id. The name filter matches both docker
|
||||||
|
# compose ("infra-openzaak-1") and podman-compose ("infra_openzaak_1") naming.
|
||||||
|
cid_for() { docker ps -aq --filter "name=$1" | head -1; }
|
||||||
|
|
||||||
|
for svc in "$@"; do
|
||||||
|
echo "waiting for '$svc' to be healthy (timeout ${timeout}s)..."
|
||||||
|
while :; do
|
||||||
|
cid="$(cid_for "$svc")"
|
||||||
|
status=""
|
||||||
|
[ -n "$cid" ] && status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$cid" 2>/dev/null || true)"
|
||||||
|
[ "$status" = "healthy" ] && { echo " '$svc' is healthy"; break; }
|
||||||
|
if [ "$(date +%s)" -ge "$deadline" ]; then
|
||||||
|
echo "TIMEOUT: '$svc' not healthy (status=${status:-no-container})" >&2
|
||||||
|
docker ps -a --filter "name=$svc" >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
done
|
||||||
7
libs/api-client/README.md
Normal file
7
libs/api-client/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# api-client
|
||||||
|
|
||||||
|
This library was generated with [Nx](https://nx.dev).
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `nx test api-client` to execute the unit tests.
|
||||||
34
libs/api-client/eslint.config.mjs
Normal file
34
libs/api-client/eslint.config.mjs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import nx from '@nx/eslint-plugin';
|
||||||
|
import baseConfig from '../../eslint.config.mjs';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...nx.configs['flat/angular'],
|
||||||
|
...nx.configs['flat/angular-template'],
|
||||||
|
...baseConfig,
|
||||||
|
{
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
rules: {
|
||||||
|
'@angular-eslint/directive-selector': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
type: 'attribute',
|
||||||
|
prefix: 'lib',
|
||||||
|
style: 'camelCase',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@angular-eslint/component-selector': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
type: 'element',
|
||||||
|
prefix: 'lib',
|
||||||
|
style: 'kebab-case',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.html'],
|
||||||
|
// Override or add rules here
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
17
libs/api-client/orval.config.ts
Normal file
17
libs/api-client/orval.config.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { defineConfig } from 'orval';
|
||||||
|
|
||||||
|
// Generates the BFF client (Angular HttpClient service + models) from the committed
|
||||||
|
// OpenAPI contract. Never hand-edit the generated output — re-run `nx run api-client:generate`
|
||||||
|
// after the BFF spec changes (CLAUDE.md §10; docs/frontend-decisions.md).
|
||||||
|
export default defineConfig({
|
||||||
|
bff: {
|
||||||
|
input: '../../services/bff/openapi.json',
|
||||||
|
output: {
|
||||||
|
target: './src/lib/generated/bff-api.ts',
|
||||||
|
client: 'angular',
|
||||||
|
mode: 'single',
|
||||||
|
clean: true,
|
||||||
|
prettier: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
20
libs/api-client/project.json
Normal file
20
libs/api-client/project.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "api-client",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "libs/api-client/src",
|
||||||
|
"prefix": "lib",
|
||||||
|
"projectType": "library",
|
||||||
|
"tags": [],
|
||||||
|
"targets": {
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nx/eslint:lint"
|
||||||
|
},
|
||||||
|
"generate": {
|
||||||
|
"executor": "nx:run-commands",
|
||||||
|
"options": {
|
||||||
|
"command": "orval --config orval.config.ts",
|
||||||
|
"cwd": "libs/api-client"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
libs/api-client/src/index.ts
Normal file
3
libs/api-client/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// The BFF API client is generated from services/bff/openapi.json (orval); never hand-edit
|
||||||
|
// src/lib/generated. Re-run `nx run api-client:generate` after the BFF spec changes.
|
||||||
|
export * from './lib/generated/bff-api';
|
||||||
50
libs/api-client/src/lib/bff-api.spec.ts
Normal file
50
libs/api-client/src/lib/bff-api.spec.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { provideHttpClient } from '@angular/common/http';
|
||||||
|
import {
|
||||||
|
HttpTestingController,
|
||||||
|
provideHttpClientTesting,
|
||||||
|
} from '@angular/common/http/testing';
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import {
|
||||||
|
BffApiV1Service,
|
||||||
|
type OpenbaarEntry,
|
||||||
|
type SubmitAccepted,
|
||||||
|
} from '../index';
|
||||||
|
|
||||||
|
describe('BffApiV1Service (generated from services/bff/openapi.json)', () => {
|
||||||
|
let service: BffApiV1Service;
|
||||||
|
let http: HttpTestingController;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [provideHttpClient(), provideHttpClientTesting()],
|
||||||
|
});
|
||||||
|
service = TestBed.inject(BffApiV1Service);
|
||||||
|
http = TestBed.inject(HttpTestingController);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => http.verify());
|
||||||
|
|
||||||
|
it('submits a registration via POST /self-service/registrations', () => {
|
||||||
|
let result: SubmitAccepted | undefined;
|
||||||
|
service.postSelfServiceRegistrations().subscribe((r) => (result = r));
|
||||||
|
|
||||||
|
const req = http.expectOne('/self-service/registrations');
|
||||||
|
expect(req.request.method).toBe('POST');
|
||||||
|
req.flush({ registrationId: 'reg-1', status: 'Ingediend' });
|
||||||
|
|
||||||
|
expect(result?.registrationId).toBe('reg-1');
|
||||||
|
expect(result?.status).toBe('Ingediend');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reads the openbaar register via GET /openbaar/register with the query', () => {
|
||||||
|
let rows: OpenbaarEntry[] | undefined;
|
||||||
|
service.getOpenbaarRegister({ q: 'abc' }).subscribe((r) => (rows = r));
|
||||||
|
|
||||||
|
const req = http.expectOne((r) => r.url === '/openbaar/register');
|
||||||
|
expect(req.request.method).toBe('GET');
|
||||||
|
expect(req.request.params.get('q')).toBe('abc');
|
||||||
|
req.flush([{ id: 'abc-111', status: 'INGEDIEND' }]);
|
||||||
|
|
||||||
|
expect(rows?.[0].id).toBe('abc-111');
|
||||||
|
});
|
||||||
|
});
|
||||||
216
libs/api-client/src/lib/generated/bff-api.ts
Normal file
216
libs/api-client/src/lib/generated/bff-api.ts
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
/**
|
||||||
|
* Generated by orval v8.19.0 🍺
|
||||||
|
* Do not edit manually.
|
||||||
|
* Bff.Api | v1
|
||||||
|
* OpenAPI spec version: 1.0.0
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
HttpClient,
|
||||||
|
HttpHeaders,
|
||||||
|
HttpResponse as AngularHttpResponse
|
||||||
|
} from '@angular/common/http';
|
||||||
|
import type {
|
||||||
|
HttpContext,
|
||||||
|
HttpEvent,
|
||||||
|
HttpParams
|
||||||
|
} from '@angular/common/http';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
inject
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Observable
|
||||||
|
} from 'rxjs';
|
||||||
|
|
||||||
|
export interface OpenbaarEntry {
|
||||||
|
id: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubmitAccepted {
|
||||||
|
registrationId: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetOpenbaarRegisterParams = {
|
||||||
|
q?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface HttpClientOptions {
|
||||||
|
readonly headers?: HttpHeaders | Record<string, string | string[]>;
|
||||||
|
readonly context?: HttpContext;
|
||||||
|
readonly params?:
|
||||||
|
| HttpParams
|
||||||
|
| Record<string, string | number | boolean | Array<string | number | boolean>>;
|
||||||
|
readonly reportProgress?: boolean;
|
||||||
|
readonly withCredentials?: boolean;
|
||||||
|
readonly credentials?: RequestCredentials;
|
||||||
|
readonly keepalive?: boolean;
|
||||||
|
readonly priority?: RequestPriority;
|
||||||
|
readonly cache?: RequestCache;
|
||||||
|
readonly mode?: RequestMode;
|
||||||
|
readonly redirect?: RequestRedirect;
|
||||||
|
readonly referrer?: string;
|
||||||
|
readonly integrity?: string;
|
||||||
|
readonly referrerPolicy?: ReferrerPolicy;
|
||||||
|
readonly transferCache?: {includeHeaders?: string[]} | boolean;
|
||||||
|
readonly timeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type HttpClientBodyOptions = HttpClientOptions & {
|
||||||
|
readonly observe?: 'body';
|
||||||
|
};
|
||||||
|
|
||||||
|
type HttpClientEventOptions = HttpClientOptions & {
|
||||||
|
readonly observe: 'events';
|
||||||
|
};
|
||||||
|
|
||||||
|
type HttpClientResponseOptions = HttpClientOptions & {
|
||||||
|
readonly observe: 'response';
|
||||||
|
};
|
||||||
|
|
||||||
|
type HttpClientObserveOptions = HttpClientOptions & {
|
||||||
|
readonly observe?: 'body' | 'events' | 'response';
|
||||||
|
};
|
||||||
|
|
||||||
|
type AngularHttpParamValue = string | number | boolean | Array<string | number | boolean>;
|
||||||
|
type AngularHttpParamValueWithNullable = AngularHttpParamValue | null;
|
||||||
|
|
||||||
|
function filterParams(
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
requiredNullableKeys?: ReadonlySet<string>,
|
||||||
|
preserveRequiredNullables?: false,
|
||||||
|
passthroughKeys?: undefined,
|
||||||
|
): Record<string, AngularHttpParamValue>;
|
||||||
|
function filterParams(
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
requiredNullableKeys: ReadonlySet<string> | undefined,
|
||||||
|
preserveRequiredNullables: true,
|
||||||
|
passthroughKeys?: undefined,
|
||||||
|
): Record<string, AngularHttpParamValueWithNullable>;
|
||||||
|
function filterParams(
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
requiredNullableKeys: ReadonlySet<string> | undefined,
|
||||||
|
preserveRequiredNullables: boolean | undefined,
|
||||||
|
passthroughKeys: ReadonlySet<string>,
|
||||||
|
): Record<string, unknown>;
|
||||||
|
function filterParams(
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
requiredNullableKeys: ReadonlySet<string> = new Set(),
|
||||||
|
preserveRequiredNullables = false,
|
||||||
|
passthroughKeys: ReadonlySet<string> = new Set(),
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const filteredParams: Record<string, unknown> = {};
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
if (passthroughKeys.has(key)) {
|
||||||
|
if (value !== undefined) {
|
||||||
|
filteredParams[key] = value;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const filtered = value.filter(
|
||||||
|
(item) =>
|
||||||
|
item != null &&
|
||||||
|
(typeof item === 'string' ||
|
||||||
|
typeof item === 'number' ||
|
||||||
|
typeof item === 'boolean'),
|
||||||
|
) as Array<string | number | boolean>;
|
||||||
|
if (filtered.length) {
|
||||||
|
filteredParams[key] = filtered;
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
preserveRequiredNullables &&
|
||||||
|
value === null &&
|
||||||
|
requiredNullableKeys.has(key)
|
||||||
|
) {
|
||||||
|
filteredParams[key] = null;
|
||||||
|
} else if (
|
||||||
|
value != null &&
|
||||||
|
(typeof value === 'string' ||
|
||||||
|
typeof value === 'number' ||
|
||||||
|
typeof value === 'boolean')
|
||||||
|
) {
|
||||||
|
filteredParams[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class BffApiV1Service {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
postSelfServiceRegistrations<TData = SubmitAccepted>( options?: HttpClientBodyOptions): Observable<TData>;
|
||||||
|
postSelfServiceRegistrations<TData = SubmitAccepted>( options?: HttpClientEventOptions): Observable<HttpEvent<TData>>;
|
||||||
|
postSelfServiceRegistrations<TData = SubmitAccepted>( options?: HttpClientResponseOptions): Observable<AngularHttpResponse<TData>>;
|
||||||
|
postSelfServiceRegistrations<TData = SubmitAccepted>(
|
||||||
|
options?: HttpClientObserveOptions): Observable<TData | HttpEvent<TData> | AngularHttpResponse<TData>> {
|
||||||
|
if (options?.observe === 'events') {
|
||||||
|
return this.http.post<TData>(
|
||||||
|
`/self-service/registrations`,
|
||||||
|
undefined,{
|
||||||
|
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||||
|
observe: 'events',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.observe === 'response') {
|
||||||
|
return this.http.post<TData>(
|
||||||
|
`/self-service/registrations`,
|
||||||
|
undefined,{
|
||||||
|
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||||
|
observe: 'response',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.post<TData>(
|
||||||
|
`/self-service/registrations`,
|
||||||
|
undefined,{
|
||||||
|
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||||
|
observe: 'body',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getOpenbaarRegister<TData = OpenbaarEntry[]>(params?: GetOpenbaarRegisterParams, options?: HttpClientBodyOptions): Observable<TData>;
|
||||||
|
getOpenbaarRegister<TData = OpenbaarEntry[]>(params?: GetOpenbaarRegisterParams, options?: HttpClientEventOptions): Observable<HttpEvent<TData>>;
|
||||||
|
getOpenbaarRegister<TData = OpenbaarEntry[]>(params?: GetOpenbaarRegisterParams, options?: HttpClientResponseOptions): Observable<AngularHttpResponse<TData>>;
|
||||||
|
getOpenbaarRegister<TData = OpenbaarEntry[]>(
|
||||||
|
params?: GetOpenbaarRegisterParams, options?: HttpClientObserveOptions): Observable<TData | HttpEvent<TData> | AngularHttpResponse<TData>> {
|
||||||
|
const filteredParams = filterParams({...params, ...options?.params}, new Set<string>([]));
|
||||||
|
|
||||||
|
if (options?.observe === 'events') {
|
||||||
|
return this.http.get<TData>(
|
||||||
|
`/openbaar/register`,{
|
||||||
|
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||||
|
observe: 'events',
|
||||||
|
params: filteredParams,}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.observe === 'response') {
|
||||||
|
return this.http.get<TData>(
|
||||||
|
`/openbaar/register`,{
|
||||||
|
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||||
|
observe: 'response',
|
||||||
|
params: filteredParams,}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.get<TData>(
|
||||||
|
`/openbaar/register`,{
|
||||||
|
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||||
|
observe: 'body',
|
||||||
|
params: filteredParams,}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
5
libs/api-client/src/test-setup.ts
Normal file
5
libs/api-client/src/test-setup.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import '@angular/compiler';
|
||||||
|
import '@analogjs/vitest-angular/setup-snapshots';
|
||||||
|
import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed';
|
||||||
|
|
||||||
|
setupTestBed({ zoneless: false });
|
||||||
31
libs/api-client/tsconfig.json
Normal file
31
libs/api-client/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"isolatedModules": true,
|
||||||
|
"target": "es2022",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"emitDecoratorMetadata": false,
|
||||||
|
"module": "preserve"
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"strictTemplates": true
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.lib.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
26
libs/api-client/tsconfig.lib.json
Normal file
26
libs/api-client/tsconfig.lib.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"inlineSources": true,
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": [
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"vite.config.ts",
|
||||||
|
"vite.config.mts",
|
||||||
|
"vitest.config.ts",
|
||||||
|
"vitest.config.mts",
|
||||||
|
"src/**/*.test.tsx",
|
||||||
|
"src/**/*.spec.tsx",
|
||||||
|
"src/**/*.test.js",
|
||||||
|
"src/**/*.spec.js",
|
||||||
|
"src/**/*.test.jsx",
|
||||||
|
"src/**/*.spec.jsx",
|
||||||
|
"src/test-setup.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
29
libs/api-client/tsconfig.spec.json
Normal file
29
libs/api-client/tsconfig.spec.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": [
|
||||||
|
"vitest/globals",
|
||||||
|
"vitest/importMeta",
|
||||||
|
"vite/client",
|
||||||
|
"node",
|
||||||
|
"vitest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"vite.config.ts",
|
||||||
|
"vite.config.mts",
|
||||||
|
"vitest.config.ts",
|
||||||
|
"vitest.config.mts",
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.test.tsx",
|
||||||
|
"src/**/*.spec.tsx",
|
||||||
|
"src/**/*.test.js",
|
||||||
|
"src/**/*.spec.js",
|
||||||
|
"src/**/*.test.jsx",
|
||||||
|
"src/**/*.spec.jsx",
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
],
|
||||||
|
"files": ["src/test-setup.ts"]
|
||||||
|
}
|
||||||
28
libs/api-client/vite.config.mts
Normal file
28
libs/api-client/vite.config.mts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/// <reference types='vitest' />
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import angular from '@analogjs/vite-plugin-angular';
|
||||||
|
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||||
|
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||||
|
|
||||||
|
export default defineConfig(() => ({
|
||||||
|
root: __dirname,
|
||||||
|
cacheDir: '../../node_modules/.vite/libs/api-client',
|
||||||
|
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||||
|
// Uncomment this if you are using workers.
|
||||||
|
// worker: {
|
||||||
|
// plugins: () => [ nxViteTsPaths() ],
|
||||||
|
// },
|
||||||
|
test: {
|
||||||
|
name: 'api-client',
|
||||||
|
watch: false,
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||||
|
setupFiles: ['src/test-setup.ts'],
|
||||||
|
reporters: ['default'],
|
||||||
|
coverage: {
|
||||||
|
reportsDirectory: '../../coverage/libs/api-client',
|
||||||
|
provider: 'v8' as const,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
7
libs/auth/README.md
Normal file
7
libs/auth/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# auth
|
||||||
|
|
||||||
|
This library was generated with [Nx](https://nx.dev).
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `nx test auth` to execute the unit tests.
|
||||||
34
libs/auth/eslint.config.mjs
Normal file
34
libs/auth/eslint.config.mjs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import nx from '@nx/eslint-plugin';
|
||||||
|
import baseConfig from '../../eslint.config.mjs';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...nx.configs['flat/angular'],
|
||||||
|
...nx.configs['flat/angular-template'],
|
||||||
|
...baseConfig,
|
||||||
|
{
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
rules: {
|
||||||
|
'@angular-eslint/directive-selector': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
type: 'attribute',
|
||||||
|
prefix: 'lib',
|
||||||
|
style: 'camelCase',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@angular-eslint/component-selector': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
type: 'element',
|
||||||
|
prefix: 'lib',
|
||||||
|
style: 'kebab-case',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.html'],
|
||||||
|
// Override or add rules here
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
13
libs/auth/project.json
Normal file
13
libs/auth/project.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "auth",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "libs/auth/src",
|
||||||
|
"prefix": "lib",
|
||||||
|
"projectType": "library",
|
||||||
|
"tags": [],
|
||||||
|
"targets": {
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nx/eslint:lint"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
libs/auth/src/index.ts
Normal file
4
libs/auth/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './lib/auth.service';
|
||||||
|
export * from './lib/digid-auth.service';
|
||||||
|
export * from './lib/digid-auth.providers';
|
||||||
|
export * from './lib/authenticated.guard';
|
||||||
16
libs/auth/src/lib/auth.service.ts
Normal file
16
libs/auth/src/lib/auth.service.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Signal } from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The portal's view of the signed-in user. An abstraction over the OIDC library so components and
|
||||||
|
* guards depend on a small, mockable surface (the real implementation is DigiadAuthService).
|
||||||
|
*/
|
||||||
|
export abstract class AuthService {
|
||||||
|
/** Whether a DigiD session is active. */
|
||||||
|
abstract readonly isAuthenticated: Signal<boolean>;
|
||||||
|
/** The citizen-service number from the DigiD token, once authenticated. */
|
||||||
|
abstract readonly bsn: Signal<string | undefined>;
|
||||||
|
/** Start the DigiD login (redirects to Keycloak). */
|
||||||
|
abstract login(): void;
|
||||||
|
/** End the session. */
|
||||||
|
abstract logout(): void;
|
||||||
|
}
|
||||||
42
libs/auth/src/lib/authenticated.guard.spec.ts
Normal file
42
libs/auth/src/lib/authenticated.guard.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { signal } from '@angular/core';
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { authenticatedGuard } from './authenticated.guard';
|
||||||
|
|
||||||
|
class FakeAuth extends AuthService {
|
||||||
|
readonly isAuthenticated = signal(false);
|
||||||
|
readonly bsn = signal<string | undefined>(undefined);
|
||||||
|
login(): void {
|
||||||
|
/* spied in tests */
|
||||||
|
}
|
||||||
|
logout(): void {
|
||||||
|
/* noop */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runGuard(authenticated: boolean) {
|
||||||
|
const auth = new FakeAuth();
|
||||||
|
auth.isAuthenticated.set(authenticated);
|
||||||
|
const loginSpy = vi.spyOn(auth, 'login');
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [{ provide: AuthService, useValue: auth }],
|
||||||
|
});
|
||||||
|
const result = TestBed.runInInjectionContext(() =>
|
||||||
|
authenticatedGuard(null as never, null as never),
|
||||||
|
);
|
||||||
|
return { result, loginSpy };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('authenticatedGuard', () => {
|
||||||
|
it('allows the route for an authenticated user', () => {
|
||||||
|
const { result, loginSpy } = runGuard(true);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(loginSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('blocks and starts DigiD login when not authenticated', () => {
|
||||||
|
const { result, loginSpy } = runGuard(false);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(loginSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
13
libs/auth/src/lib/authenticated.guard.ts
Normal file
13
libs/auth/src/lib/authenticated.guard.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { inject } from '@angular/core';
|
||||||
|
import { CanActivateFn } from '@angular/router';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
|
/** Allow the route only when a DigiD session is active; otherwise start the login. */
|
||||||
|
export const authenticatedGuard: CanActivateFn = () => {
|
||||||
|
const auth = inject(AuthService);
|
||||||
|
if (auth.isAuthenticated()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
auth.login();
|
||||||
|
return false;
|
||||||
|
};
|
||||||
50
libs/auth/src/lib/digid-auth.providers.ts
Normal file
50
libs/auth/src/lib/digid-auth.providers.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
|
||||||
|
import {
|
||||||
|
authInterceptor,
|
||||||
|
LogLevel,
|
||||||
|
provideAuth,
|
||||||
|
withAppInitializerAuthCheck,
|
||||||
|
} from 'angular-auth-oidc-client';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { DigiadAuthService } from './digid-auth.service';
|
||||||
|
|
||||||
|
export interface DigiadAuthOptions {
|
||||||
|
/** The Keycloak `digid` realm issuer, as reachable from the browser. */
|
||||||
|
authority: string;
|
||||||
|
/** Where Keycloak redirects back to after login (usually the app origin). */
|
||||||
|
redirectUrl: string;
|
||||||
|
/** The BFF origin whose requests get the bearer token attached (secure route). */
|
||||||
|
secureApiOrigin: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure DigiD login (Keycloak `digid` realm, public client `big-portal`, auth-code + PKCE) and
|
||||||
|
* bind {@link AuthService} to the OIDC-backed implementation. Register {@link authInterceptor} in the
|
||||||
|
* app's HttpClient so BFF calls carry the token.
|
||||||
|
*/
|
||||||
|
export function provideDigiadAuth(options: DigiadAuthOptions): EnvironmentProviders {
|
||||||
|
return makeEnvironmentProviders([
|
||||||
|
provideAuth(
|
||||||
|
{
|
||||||
|
config: {
|
||||||
|
authority: options.authority,
|
||||||
|
redirectUrl: options.redirectUrl,
|
||||||
|
postLogoutRedirectUri: options.redirectUrl,
|
||||||
|
clientId: 'big-portal',
|
||||||
|
scope: 'openid profile',
|
||||||
|
responseType: 'code',
|
||||||
|
silentRenew: true,
|
||||||
|
useRefreshToken: true,
|
||||||
|
secureRoutes: [options.secureApiOrigin],
|
||||||
|
logLevel: LogLevel.Warn,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Run checkAuth() at startup so the DigiD callback (?code=…) is processed before the router
|
||||||
|
// and guard run — without it the guard sees "not authenticated" and re-triggers login (loop).
|
||||||
|
withAppInitializerAuthCheck(),
|
||||||
|
),
|
||||||
|
{ provide: AuthService, useClass: DigiadAuthService },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { authInterceptor };
|
||||||
29
libs/auth/src/lib/digid-auth.service.ts
Normal file
29
libs/auth/src/lib/digid-auth.service.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { inject, Injectable, Signal } from '@angular/core';
|
||||||
|
import { toSignal } from '@angular/core/rxjs-interop';
|
||||||
|
import { OidcSecurityService } from 'angular-auth-oidc-client';
|
||||||
|
import { map } from 'rxjs';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
|
/** DigiD-backed AuthService over angular-auth-oidc-client (Keycloak `digid` realm). */
|
||||||
|
@Injectable()
|
||||||
|
export class DigiadAuthService extends AuthService {
|
||||||
|
private readonly oidc = inject(OidcSecurityService);
|
||||||
|
|
||||||
|
readonly isAuthenticated: Signal<boolean> = toSignal(
|
||||||
|
this.oidc.isAuthenticated$.pipe(map((result) => result.isAuthenticated)),
|
||||||
|
{ initialValue: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
readonly bsn: Signal<string | undefined> = toSignal(
|
||||||
|
this.oidc.userData$.pipe(map((data) => (data.userData as { bsn?: string } | null)?.bsn)),
|
||||||
|
{ initialValue: undefined },
|
||||||
|
);
|
||||||
|
|
||||||
|
override login(): void {
|
||||||
|
this.oidc.authorize();
|
||||||
|
}
|
||||||
|
|
||||||
|
override logout(): void {
|
||||||
|
this.oidc.logoff().subscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
5
libs/auth/src/test-setup.ts
Normal file
5
libs/auth/src/test-setup.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import '@angular/compiler';
|
||||||
|
import '@analogjs/vitest-angular/setup-snapshots';
|
||||||
|
import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed';
|
||||||
|
|
||||||
|
setupTestBed({ zoneless: false });
|
||||||
31
libs/auth/tsconfig.json
Normal file
31
libs/auth/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"isolatedModules": true,
|
||||||
|
"target": "es2022",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"emitDecoratorMetadata": false,
|
||||||
|
"module": "preserve"
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"strictTemplates": true
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.lib.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
26
libs/auth/tsconfig.lib.json
Normal file
26
libs/auth/tsconfig.lib.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"inlineSources": true,
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": [
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"vite.config.ts",
|
||||||
|
"vite.config.mts",
|
||||||
|
"vitest.config.ts",
|
||||||
|
"vitest.config.mts",
|
||||||
|
"src/**/*.test.tsx",
|
||||||
|
"src/**/*.spec.tsx",
|
||||||
|
"src/**/*.test.js",
|
||||||
|
"src/**/*.spec.js",
|
||||||
|
"src/**/*.test.jsx",
|
||||||
|
"src/**/*.spec.jsx",
|
||||||
|
"src/test-setup.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
29
libs/auth/tsconfig.spec.json
Normal file
29
libs/auth/tsconfig.spec.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": [
|
||||||
|
"vitest/globals",
|
||||||
|
"vitest/importMeta",
|
||||||
|
"vite/client",
|
||||||
|
"node",
|
||||||
|
"vitest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"vite.config.ts",
|
||||||
|
"vite.config.mts",
|
||||||
|
"vitest.config.ts",
|
||||||
|
"vitest.config.mts",
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.test.tsx",
|
||||||
|
"src/**/*.spec.tsx",
|
||||||
|
"src/**/*.test.js",
|
||||||
|
"src/**/*.spec.js",
|
||||||
|
"src/**/*.test.jsx",
|
||||||
|
"src/**/*.spec.jsx",
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
],
|
||||||
|
"files": ["src/test-setup.ts"]
|
||||||
|
}
|
||||||
28
libs/auth/vite.config.mts
Normal file
28
libs/auth/vite.config.mts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/// <reference types='vitest' />
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import angular from '@analogjs/vite-plugin-angular';
|
||||||
|
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||||
|
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||||
|
|
||||||
|
export default defineConfig(() => ({
|
||||||
|
root: __dirname,
|
||||||
|
cacheDir: '../../node_modules/.vite/libs/auth',
|
||||||
|
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||||
|
// Uncomment this if you are using workers.
|
||||||
|
// worker: {
|
||||||
|
// plugins: () => [ nxViteTsPaths() ],
|
||||||
|
// },
|
||||||
|
test: {
|
||||||
|
name: 'auth',
|
||||||
|
watch: false,
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||||
|
setupFiles: ['src/test-setup.ts'],
|
||||||
|
reporters: ['default'],
|
||||||
|
coverage: {
|
||||||
|
reportsDirectory: '../../coverage/libs/auth',
|
||||||
|
provider: 'v8' as const,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
7
libs/ui/README.md
Normal file
7
libs/ui/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# ui
|
||||||
|
|
||||||
|
This library was generated with [Nx](https://nx.dev).
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `nx test ui` to execute the unit tests.
|
||||||
34
libs/ui/eslint.config.mjs
Normal file
34
libs/ui/eslint.config.mjs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import nx from '@nx/eslint-plugin';
|
||||||
|
import baseConfig from '../../eslint.config.mjs';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...nx.configs['flat/angular'],
|
||||||
|
...nx.configs['flat/angular-template'],
|
||||||
|
...baseConfig,
|
||||||
|
{
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
rules: {
|
||||||
|
'@angular-eslint/directive-selector': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
type: 'attribute',
|
||||||
|
prefix: 'lib',
|
||||||
|
style: 'camelCase',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@angular-eslint/component-selector': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
type: 'element',
|
||||||
|
prefix: 'lib',
|
||||||
|
style: 'kebab-case',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.html'],
|
||||||
|
// Override or add rules here
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
13
libs/ui/project.json
Normal file
13
libs/ui/project.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "ui",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "libs/ui/src",
|
||||||
|
"prefix": "lib",
|
||||||
|
"projectType": "library",
|
||||||
|
"tags": [],
|
||||||
|
"targets": {
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nx/eslint:lint"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
libs/ui/src/index.ts
Normal file
11
libs/ui/src/index.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// NL Design System building blocks — Utrecht is NL DS's reference Angular implementation
|
||||||
|
// (docs/frontend-decisions.md). The portals depend on the design system through this one lib.
|
||||||
|
// The design tokens/theme CSS is imported once, at the app level (apps/*/src/styles.css).
|
||||||
|
//
|
||||||
|
// The Utrecht v3 components are NgModule-based (not standalone), so we re-export the module.
|
||||||
|
// A standalone component consumes them by importing UtrechtComponentsModule in its `imports`
|
||||||
|
// (CLAUDE.md §10 forbids *declaring* NgModules in our code, not consuming a third-party one).
|
||||||
|
// Re-export the whole Utrecht package: importing UtrechtComponentsModule pulls every component it
|
||||||
|
// exports into the compiler's scope, so the AOT build needs all of them resolvable through this
|
||||||
|
// barrel (a partial re-export fails with NG3004 for the first unused component).
|
||||||
|
export * from '@utrecht/component-library-angular';
|
||||||
7
libs/ui/src/lib/ui.spec.ts
Normal file
7
libs/ui/src/lib/ui.spec.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { UtrechtComponentsModule } from '../index';
|
||||||
|
|
||||||
|
describe('ui barrel', () => {
|
||||||
|
it('re-exports the NL Design System (Utrecht) module', () => {
|
||||||
|
expect(UtrechtComponentsModule).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
5
libs/ui/src/test-setup.ts
Normal file
5
libs/ui/src/test-setup.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import '@angular/compiler';
|
||||||
|
import '@analogjs/vitest-angular/setup-snapshots';
|
||||||
|
import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed';
|
||||||
|
|
||||||
|
setupTestBed({ zoneless: false });
|
||||||
31
libs/ui/tsconfig.json
Normal file
31
libs/ui/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"isolatedModules": true,
|
||||||
|
"target": "es2022",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"emitDecoratorMetadata": false,
|
||||||
|
"module": "preserve"
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"strictTemplates": true
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.lib.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
26
libs/ui/tsconfig.lib.json
Normal file
26
libs/ui/tsconfig.lib.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"inlineSources": true,
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": [
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"vite.config.ts",
|
||||||
|
"vite.config.mts",
|
||||||
|
"vitest.config.ts",
|
||||||
|
"vitest.config.mts",
|
||||||
|
"src/**/*.test.tsx",
|
||||||
|
"src/**/*.spec.tsx",
|
||||||
|
"src/**/*.test.js",
|
||||||
|
"src/**/*.spec.js",
|
||||||
|
"src/**/*.test.jsx",
|
||||||
|
"src/**/*.spec.jsx",
|
||||||
|
"src/test-setup.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
29
libs/ui/tsconfig.spec.json
Normal file
29
libs/ui/tsconfig.spec.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": [
|
||||||
|
"vitest/globals",
|
||||||
|
"vitest/importMeta",
|
||||||
|
"vite/client",
|
||||||
|
"node",
|
||||||
|
"vitest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"vite.config.ts",
|
||||||
|
"vite.config.mts",
|
||||||
|
"vitest.config.ts",
|
||||||
|
"vitest.config.mts",
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.test.tsx",
|
||||||
|
"src/**/*.spec.tsx",
|
||||||
|
"src/**/*.test.js",
|
||||||
|
"src/**/*.spec.js",
|
||||||
|
"src/**/*.test.jsx",
|
||||||
|
"src/**/*.spec.jsx",
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
],
|
||||||
|
"files": ["src/test-setup.ts"]
|
||||||
|
}
|
||||||
28
libs/ui/vite.config.mts
Normal file
28
libs/ui/vite.config.mts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/// <reference types='vitest' />
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import angular from '@analogjs/vite-plugin-angular';
|
||||||
|
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||||
|
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||||
|
|
||||||
|
export default defineConfig(() => ({
|
||||||
|
root: __dirname,
|
||||||
|
cacheDir: '../../node_modules/.vite/libs/ui',
|
||||||
|
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||||
|
// Uncomment this if you are using workers.
|
||||||
|
// worker: {
|
||||||
|
// plugins: () => [ nxViteTsPaths() ],
|
||||||
|
// },
|
||||||
|
test: {
|
||||||
|
name: 'ui',
|
||||||
|
watch: false,
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||||
|
setupFiles: ['src/test-setup.ts'],
|
||||||
|
reporters: ['default'],
|
||||||
|
coverage: {
|
||||||
|
reportsDirectory: '../../coverage/libs/ui',
|
||||||
|
provider: 'v8' as const,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
11
mkdocs.yml
11
mkdocs.yml
@@ -23,7 +23,18 @@ nav:
|
|||||||
- Product Requirements: PRD.md
|
- Product Requirements: PRD.md
|
||||||
- Architecture:
|
- Architecture:
|
||||||
- "ADR-0001: Loose coupling": architecture/adr-0001-loose-coupling.md
|
- "ADR-0001: Loose coupling": architecture/adr-0001-loose-coupling.md
|
||||||
|
- "ADR-0002: Catalogus design": architecture/adr-0002-catalogus-design.md
|
||||||
|
- "ADR-0003: ACL default-fill": architecture/adr-0003-default-fill.md
|
||||||
|
- "ADR-0004: BDD framework": architecture/adr-0004-bdd-framework.md
|
||||||
|
- "ADR-0005: Mutation testing": architecture/adr-0005-mutation-testing.md
|
||||||
|
- "ADR-0006: ACL integration test provisioning": architecture/adr-0006-integration-test-provisioning.md
|
||||||
|
- "ADR-0007: OpenZaak → NRC notification wiring": architecture/adr-0007-notification-wiring.md
|
||||||
|
- "ADR-0008: Read projection store": architecture/adr-0008-read-projection-store.md
|
||||||
|
- "ADR-0009: External-task job worker": architecture/adr-0009-external-task-job-worker.md
|
||||||
|
- "ADR-0010: BFF OIDC validation": architecture/adr-0010-bff-oidc.md
|
||||||
- Working in Gitea: gitea-workflow.md
|
- Working in Gitea: gitea-workflow.md
|
||||||
|
- Frontend decisions: frontend-decisions.md
|
||||||
|
- Demo script: demo-script.md
|
||||||
- Runbooks:
|
- Runbooks:
|
||||||
- CI: runbooks/ci.md
|
- CI: runbooks/ci.md
|
||||||
|
|
||||||
|
|||||||
120
nx.json
Normal file
120
nx.json
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/nx/schemas/nx-schema.json",
|
||||||
|
"namedInputs": {
|
||||||
|
"default": [
|
||||||
|
"{projectRoot}/**/*",
|
||||||
|
"sharedGlobals"
|
||||||
|
],
|
||||||
|
"production": [
|
||||||
|
"default",
|
||||||
|
"!{projectRoot}/.eslintrc.json",
|
||||||
|
"!{projectRoot}/eslint.config.mjs",
|
||||||
|
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
|
||||||
|
"!{projectRoot}/tsconfig.spec.json",
|
||||||
|
"!{projectRoot}/src/test-setup.[jt]s",
|
||||||
|
"!{projectRoot}/jest.config.[jt]s",
|
||||||
|
"!{projectRoot}/test-setup.[jt]s"
|
||||||
|
],
|
||||||
|
"sharedGlobals": []
|
||||||
|
},
|
||||||
|
"targetDefaults": {
|
||||||
|
"@angular/build:application": {
|
||||||
|
"cache": true,
|
||||||
|
"dependsOn": [
|
||||||
|
"^build"
|
||||||
|
],
|
||||||
|
"inputs": [
|
||||||
|
"production",
|
||||||
|
"^production"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@nx/eslint:lint": {
|
||||||
|
"cache": true,
|
||||||
|
"inputs": [
|
||||||
|
"default",
|
||||||
|
"^default",
|
||||||
|
"{workspaceRoot}/.eslintrc.json",
|
||||||
|
"{workspaceRoot}/.eslintignore",
|
||||||
|
"{workspaceRoot}/eslint.config.mjs",
|
||||||
|
"{workspaceRoot}/tools/eslint-rules/**/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@nx/esbuild:esbuild": {
|
||||||
|
"cache": true,
|
||||||
|
"dependsOn": [
|
||||||
|
"^build"
|
||||||
|
],
|
||||||
|
"inputs": [
|
||||||
|
"production",
|
||||||
|
"^production"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@nx/js:tsc": {
|
||||||
|
"cache": true,
|
||||||
|
"dependsOn": [
|
||||||
|
"^build"
|
||||||
|
],
|
||||||
|
"inputs": [
|
||||||
|
"production",
|
||||||
|
"^production"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@nx/vitest:test": {
|
||||||
|
"cache": true,
|
||||||
|
"inputs": [
|
||||||
|
"default",
|
||||||
|
"^production"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@angular/build:unit-test": {
|
||||||
|
"cache": true,
|
||||||
|
"inputs": [
|
||||||
|
"default",
|
||||||
|
"^production"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"plugin": "@nx/eslint/plugin",
|
||||||
|
"options": {
|
||||||
|
"targetName": "lint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"plugin": "@nx/vite/plugin",
|
||||||
|
"options": {
|
||||||
|
"buildTargetName": "build",
|
||||||
|
"serveTargetName": "serve",
|
||||||
|
"devTargetName": "dev",
|
||||||
|
"previewTargetName": "preview",
|
||||||
|
"serveStaticTargetName": "serve-static",
|
||||||
|
"typecheckTargetName": "typecheck",
|
||||||
|
"buildDepsTargetName": "build-deps",
|
||||||
|
"watchDepsTargetName": "watch-deps"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"plugin": "@nx/vitest",
|
||||||
|
"options": {
|
||||||
|
"testTargetName": "test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"generators": {
|
||||||
|
"@nx/angular:application": {
|
||||||
|
"e2eTestRunner": "playwright",
|
||||||
|
"linter": "eslint",
|
||||||
|
"style": "css",
|
||||||
|
"unitTestRunner": "vitest-analog"
|
||||||
|
},
|
||||||
|
"@nx/angular:library": {
|
||||||
|
"linter": "eslint",
|
||||||
|
"unitTestRunner": "vitest-analog"
|
||||||
|
},
|
||||||
|
"@nx/angular:component": {
|
||||||
|
"style": "css"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"analytics": false
|
||||||
|
}
|
||||||
71
package.json
Normal file
71
package.json
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"name": "register-referentie",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular-devkit/build-angular": "21.2.7",
|
||||||
|
"@angular/common": "21.2.9",
|
||||||
|
"@angular/compiler": "21.2.9",
|
||||||
|
"@angular/core": "21.2.9",
|
||||||
|
"@angular/forms": "21.2.9",
|
||||||
|
"@angular/platform-browser": "21.2.9",
|
||||||
|
"@angular/router": "21.2.9",
|
||||||
|
"@utrecht/component-library-angular": "^3.0.2",
|
||||||
|
"@utrecht/design-tokens": "^6.2.1",
|
||||||
|
"angular-auth-oidc-client": "^21.0.2",
|
||||||
|
"rxjs": "~7.8.0",
|
||||||
|
"zone.js": "0.16.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@analogjs/vite-plugin-angular": "2.2.0",
|
||||||
|
"@analogjs/vitest-angular": "2.2.0",
|
||||||
|
"@angular-devkit/core": "21.2.7",
|
||||||
|
"@angular-devkit/schematics": "21.2.7",
|
||||||
|
"@angular/build": "21.2.7",
|
||||||
|
"@angular/cli": "21.2.7",
|
||||||
|
"@angular/compiler-cli": "21.2.9",
|
||||||
|
"@angular/language-service": "21.2.9",
|
||||||
|
"@eslint/js": "^9.8.0",
|
||||||
|
"@nx/angular": "23.0.1",
|
||||||
|
"@nx/devkit": "23.0.1",
|
||||||
|
"@nx/esbuild": "23.0.1",
|
||||||
|
"@nx/eslint": "23.0.1",
|
||||||
|
"@nx/eslint-plugin": "23.0.1",
|
||||||
|
"@nx/js": "23.0.1",
|
||||||
|
"@nx/vite": "23.0.1",
|
||||||
|
"@nx/vitest": "23.0.1",
|
||||||
|
"@nx/web": "23.0.1",
|
||||||
|
"@nx/workspace": "23.0.1",
|
||||||
|
"@oxc-project/runtime": "^0.115.0",
|
||||||
|
"@schematics/angular": "21.2.7",
|
||||||
|
"@swc-node/register": "1.11.1",
|
||||||
|
"@swc/core": "1.15.8",
|
||||||
|
"@swc/helpers": "0.5.18",
|
||||||
|
"@testing-library/angular": "^19.4.1",
|
||||||
|
"@types/node": "20.19.9",
|
||||||
|
"@typescript-eslint/utils": "^8.40.0",
|
||||||
|
"@vitest/coverage-v8": "~4.1.0",
|
||||||
|
"@vitest/ui": "~4.1.0",
|
||||||
|
"angular-eslint": "21.3.1",
|
||||||
|
"axe-core": "^4.12.1",
|
||||||
|
"esbuild": "^0.27.0",
|
||||||
|
"eslint": "^9.8.0",
|
||||||
|
"eslint-config-prettier": "^10.0.0",
|
||||||
|
"jiti": "2.4.2",
|
||||||
|
"jsdom": "~22.1.0",
|
||||||
|
"jsonc-eslint-parser": "^2.1.0",
|
||||||
|
"nx": "23.0.1",
|
||||||
|
"orval": "^8.19.0",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
|
"ts-node": "10.9.1",
|
||||||
|
"tslib": "^2.3.0",
|
||||||
|
"typescript": "~5.9.2",
|
||||||
|
"typescript-eslint": "^8.40.0",
|
||||||
|
"vite": "8.0.9",
|
||||||
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
|
"vitest": "~4.1.0",
|
||||||
|
"vitest-axe": "^0.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
18863
pnpm-lock.yaml
generated
Normal file
18863
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user