feat(behandelportal): WP-65a beoordeling detail (read) + fix unreachable medewerker login
CI / changes (pull_request) Successful in 17s
CI / lint (pull_request) Failing after 54s
CI / frontend (pull_request) Successful in 2m38s
CI / storybook-a11y (pull_request) Failing after 3m28s
CI / backend (pull_request) Successful in 2m1s
CI / semgrep (pull_request) Successful in 1m9s
CI / e2e (pull_request) Successful in 2m55s
CI / api-client-drift (pull_request) Successful in 2m1s

New GET /beoordeling/{id} shows one aanvraag's status, linked documents, and a
canBesluiten decision flag, gated by the same CanBeoordelen capability as the
werkvoorraad list. Reads through IZaakSource.ListCases rather than a new seam
method (WP-66 needs one anyway for the real write); owner BSN is masked.

Fixes a real gap found while wiring this up: the behandelportal's login was still
WP-61's copied citizen/BSN DigiD flow, so nothing ever sent X-Medewerker and the
werkvoorraad screen (WP-64) always denied in a real browser. A dev-only
medewerkerInterceptor (mirrors the existing ?role= stand-in as ?rollen=) fixes that.

WP-65's own Risks note authorized splitting read from write across sessions given
its size; this is the read half. The decision-recording mutation is next (65b).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-08-03 09:01:09 +02:00
co-authored by Claude Sonnet 5
parent fe69caee63
commit 4133b30e5d
29 changed files with 1195 additions and 58 deletions
@@ -0,0 +1,19 @@
import { HttpInterceptorFn } from '@angular/common/http';
import { MEDEWERKER_ID, currentRollen } from './medewerker';
/**
* Dev-only: stamps every API request as the fixed stand-in medewerker (`X-Medewerker`/
* `X-Rollen`), so `StubIdentityProvider` resolves a `MedewerkerCaller` instead of falling
* through to its zorgverlener default. Unlike `roleInterceptor`'s allow-listed endpoints,
* this is the app's whole identity — every request needs it, since this app has no
* citizen-scoped screens to keep separate (see `CallerIdentity.Zorgverlener()`'s guard: a
* medewerker hitting a citizen-scoped SSP endpoint would 500, but no such endpoint exists
* here). Real employee-SSO login is out of scope for this POC (ADR-0002 §3 — the two
* apps' login flows are expected to diverge; this stand-in is that flow's placeholder).
*/
export const medewerkerInterceptor: HttpInterceptorFn = (req, next) =>
req.url.includes('/api/v1/')
? next(
req.clone({ setHeaders: { 'X-Medewerker': MEDEWERKER_ID, 'X-Rollen': currentRollen() } }),
)
: next(req);
@@ -0,0 +1,23 @@
/**
* Dev-only medewerker rollen stand-in (the reading MECHANISM — mirrors
* `@shared/infrastructure/role.ts`'s `?role=` idiom, but app-local: `auth` is
* deliberately not shared between ssp and behandelportal, ADR-0002 §3). Until a real
* employee-SSO login exists, every request from this app identifies as one fixed
* medewerker; `?rollen=` lets a dev exercise the deny path (`?rollen=geen`) the same
* way `?role=` exercises ssp's role-gated pages.
*
* **Sticky within the tab (sessionStorage)**, same reasoning as `currentRole()`: a
* plain in-app navigation drops the query param, which would silently revert to the
* default and mask a deliberately-chosen `?rollen=geen`.
*/
const STORAGE_KEY = 'dev-rollen';
export const MEDEWERKER_ID = 'medewerker-1';
export function currentRollen(): string {
const fromUrl = new URLSearchParams(window.location.search).get('rollen');
if (fromUrl !== null) {
sessionStorage.setItem(STORAGE_KEY, fromUrl);
return fromUrl;
}
return sessionStorage.getItem(STORAGE_KEY) ?? 'behandelaar';
}