From 2c88cb0db0b9339564fc493d32f141274367ef8b Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Thu, 16 Jul 2026 09:17:11 +0200 Subject: [PATCH] docs(behandel): frontend decision log + demo-script for the behandel portal (refs #13) Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/demo-script.md | 27 +++++++++++++++++++++++++++ docs/frontend-decisions.md | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/docs/demo-script.md b/docs/demo-script.md index 2426f9e..8a240f3 100644 --- a/docs/demo-script.md +++ b/docs/demo-script.md @@ -248,3 +248,30 @@ curl -fsS "http://localhost:8140/openbaar/register?q=$ref" | jq > The openbaar register's "Referentie" column and its search now use this reference — the exact value > the citizen saw on submit. Asserted end-to-end by the Playwright happy path. + +## S-12 — Behandel portal: werkbak + beoordeling (#13, ADR-0013) + +A behandelaar now works submitted registrations in a real portal instead of the temporary admin +endpoint. After a citizen submits (as above), the workflow parks the registration at the Flowable +`Beoordelen` user task, and it shows up in the **werkbak**. The behandelaar logs in against the +Keycloak `medewerker` realm and decides — **goedkeuren** (→ INGESCHREVEN via the ACL, per ADR-0011) +or **afwijzen** — which also completes the Beoordelen task so the process advances. + +```text +# 1. Open the behandel portal and log in as a behandelaar (medewerker realm): +# http://localhost:8142/ → merel-behandelaar / test123 +# +# 2. The werkbak lists the registrations awaiting beoordeling (referentie / bsn / status). +# Find the reference from the submit confirmation and click "Goedkeuren" on that row. +# +# 3. The row drops off the werkbak (its Beoordelen task is completed) and the openbaar register +# (http://localhost:8141/) now shows that reference as INGESCHREVEN. +``` + +**The path:** behandel portal → BFF `POST /behandel/registrations/{id}/decide` (behandelaar policy, +`medewerker` realm) → domain applies the decision + completes the Flowable `Beoordelen` task → +ACL → NRC → event-subscriber → projection → openbaar register shows INGESCHREVEN. + +> The full round-trip — DigiD submit → public INGEDIEND → behandelaar goedkeurt in the werkbak → +> public INGESCHREVEN — is the Playwright happy path (`tests/e2e/registration.spec.ts`), which now +> drives the behandel portal in place of the old admin endpoint. diff --git a/docs/frontend-decisions.md b/docs/frontend-decisions.md index 19c65e9..e6a7b11 100644 --- a/docs/frontend-decisions.md +++ b/docs/frontend-decisions.md @@ -117,3 +117,37 @@ with the submit form (S-08c, #67); any deviation from NL DS will be recorded her (id + status); `bsn`/`naam` never leave the BFF. The e2e asserts the bsn never renders. - **Loads on open, filters on search.** `RegisterPage` fetches the full register on construction and re-queries `/openbaar/register?q=` on search — no client-side filtering, the BFF owns the query. + +## Behandel portal (S-12, #13) + +The staff portal where a behandelaar works the **werkbak** (registrations awaiting beoordeling) and +decides each — goedkeuren or afwijzen. `apps/behandel` mirrors `apps/self-service`; the net-new +frontend work is the medewerker realm auth and the werkbak/decide page. Wiring rationale is in +**ADR-0013**; this entry records the frontend-specific choices. + +- **Medewerker realm auth, reusing `libs/auth`.** Staff authenticate against the Keycloak + `medewerker` realm (public client `big-portal`), not `digid`. Rather than fork the auth lib, the + abstract `AuthService` grew a **`roles`/`hasRole` surface** (empty for realms without roles, e.g. + `digid`), and a parallel **`MedewerkerAuthService` + `provideMedewerkerAuth`** were added — same + auth-code + PKCE config, bound to the medewerker realm, reading the nested `realm_access.roles` + claim. The library's own `authInterceptor` attaches the token to the relative `/behandel/` calls + (secure route), exactly as self-service does for `/self-service/`. +- **Roles reach the frontend via a realm mapper.** Keycloak emits realm roles in the access token by + default but not the ID token/userinfo the SPA reads, so the medewerker `big-portal` client gets a + **realm-roles protocol mapper** (`realm_access.roles`, added to id + userinfo tokens). The + **BFF remains the security boundary** (`behandelaar` policy, 401/403 on `/behandel/*`, ADR-0013); + the frontend role signal is for display/UX, and the werkbak page surfaces a load failure (e.g. a + 403 for a non-behandelaar) rather than swallowing it. +- **Same-origin via nginx, like the other portals.** The compose `behandel` image serves the built + app and reverse-proxies `/behandel` to the BFF (relative calls, no CORS). Served on `:8142`, + health-checked over IPv4 (`127.0.0.1`), depends on Keycloak for the medewerker realm. +- **Werkbak = decide-and-refresh.** `WerkbakPage` loads `GET /behandel/werkbak` on open and renders a + row per registration (referentie/bsn/status). Goedkeuren/afwijzen `POST /behandel/registrations/ + {id}/decide` and then reload the werkbak, so the handled item drops off (its Flowable `Beoordelen` + task is completed). Per-row decide buttons carry an `aria-label` including the reference, so the + e2e (and screen readers) can target a specific registration in a shared werkbak. +- **Testing.** Component tests use `@testing-library/angular` with `BffApiV1Service`/`AuthService` + mocked and the axe WCAG 2.1 AA check; an `app.config.spec` drives the real interceptor + api-client + to assert the medewerker token attaches to `/behandel/*` (and not to the anonymous openbaar call). + The full DigiD-submit → behandel-decide → public INGESCHREVEN round-trip is the Playwright happy + path.