## What & why
Finishes **S-12 · Behandel-portal — werkbak + beoordeling**. The backend sub-slices (S-12a/b/c-1/c-2) were merged, but the slice's stated outcome — a behandel *portal* with medewerker login, a werkbak, and decide — had no frontend. This adds it.
- **`libs/auth`**: `MedewerkerAuthService` + `provideMedewerkerAuth` (Keycloak `medewerker` realm), a `roles`/`hasRole` surface on the shared `AuthService`, and a realm-roles protocol mapper so the SPA can read `behandelaar`/`teamlead` from the token. The BFF remains the security boundary (ADR-0013).
- **`apps/behandel`**: a new Nx Angular app mirroring self-service — medewerker OIDC login and a **werkbak** page listing registrations awaiting beoordeling (`GET /behandel/werkbak`) with per-row **Goedkeuren/Afwijzen** actions (`POST /behandel/registrations/{id}/decide`) that refresh the list. NL DS/Utrecht, standalone + signals.
- **e2e**: the walking-skeleton happy path now approves through the real portal (behandelaar logs in, finds the row by reference, clicks Goedkeuren) instead of the temporary admin endpoint.
- **infra/docs**: behandel service in compose (`:8142`, depends on Keycloak); added to the smoke `WAIT_SVCS` + CI log dump; `frontend-decisions.md` and `demo-script.md` updated.
Closes #13
## Definition of Done
- [x] Linked Gitea issue (above).
- [x] Failing test committed before the implementation.
- [x] Implementation makes the test pass; refactor commit if structure improved.
- [x] Conventional Commits referencing the issue (`refs #13`).
- [ ] CI green — all Gitea Actions jobs.
- [x] `docker compose up` from a fresh clone reaches green health checks within 3 minutes. *(behandel image + container verified locally; full stack gated in CI.)*
- [x] Docs updated if behaviour, contracts, or operations changed.
- [x] ADR added — ADR-0013 (merged with the backend sub-slices) already covers the wiring; no new decision here.
- [x] Demo note in `docs/demo-script.md`.
## Notes for reviewers
- Verified locally: auth + behandel + all frontend projects pass lint & unit tests (incl. axe WCAG 2.1 AA); production build green; the behandel Docker image builds and serves with the correct baked `medewerker` config + SPA fallback.
- The full compose-up smoke, e2e, and mutation are CI-gated (known local full-stack verify limits).
- **Follow-ups (not in scope):** the `WerkbakItem` contract has no citizen name (werkbak shows the BSN) — adding one is a BFF+domain contract change; and the domain's temporary admin `approve` endpoint is now unused by the e2e and could be removed.
Reviewed-on: #87
44 lines
1.5 KiB
TypeScript
44 lines
1.5 KiB
TypeScript
import { TestBed } from '@angular/core/testing';
|
|
import { OidcSecurityService } from 'angular-auth-oidc-client';
|
|
import { of } from 'rxjs';
|
|
import { MedewerkerAuthService } from './medewerker-auth.service';
|
|
|
|
function makeService(userData: unknown, authenticated = true) {
|
|
const oidc = {
|
|
isAuthenticated$: of({ isAuthenticated: authenticated }),
|
|
userData$: of({ userData }),
|
|
authorize: vi.fn(),
|
|
logoff: vi.fn(() => of(null)),
|
|
};
|
|
TestBed.configureTestingModule({
|
|
providers: [MedewerkerAuthService, { provide: OidcSecurityService, useValue: oidc }],
|
|
});
|
|
return { svc: TestBed.inject(MedewerkerAuthService), oidc };
|
|
}
|
|
|
|
describe('MedewerkerAuthService', () => {
|
|
it('exposes the realm roles carried in the token', () => {
|
|
const { svc } = makeService({ realm_access: { roles: ['behandelaar', 'teamlead'] } });
|
|
expect(svc.roles()).toEqual(['behandelaar', 'teamlead']);
|
|
expect(svc.hasRole('behandelaar')).toBe(true);
|
|
expect(svc.hasRole('beheerder')).toBe(false);
|
|
});
|
|
|
|
it('has no roles when the token omits realm_access', () => {
|
|
const { svc } = makeService({ preferred_username: 'merel-behandelaar' });
|
|
expect(svc.roles()).toEqual([]);
|
|
expect(svc.hasRole('behandelaar')).toBe(false);
|
|
});
|
|
|
|
it('reflects the OIDC authenticated state', () => {
|
|
const { svc } = makeService({}, true);
|
|
expect(svc.isAuthenticated()).toBe(true);
|
|
});
|
|
|
|
it('starts login by delegating to the OIDC library', () => {
|
|
const { svc, oidc } = makeService({});
|
|
svc.login();
|
|
expect(oidc.authorize).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|