Compare commits
2
Commits
19f2e9b734
...
8c54ede6eb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c54ede6eb | ||
|
|
62cb34b60f |
@@ -43,54 +43,54 @@ WP-19's own file), so it's a separate manual/CI step, not chained into the other
|
||||
Gates land before the work they cover; each lint rule lands in the same WP as the fixes
|
||||
for its existing violations, so every WP ends green.
|
||||
|
||||
| WP | Title | Phase | Status |
|
||||
| ---------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------- | ------ |
|
||||
| [WP-01](WP-01-axe-ci-gate.md) | Axe-on-every-story CI gate | 0 · gates | done |
|
||||
| [WP-02](WP-02-check-tokens.md) | Harden `check:tokens` + fix what it catches | 0 · gates | done |
|
||||
| [WP-03](WP-03-contracts-purity.md) | Boundaries I: contracts purity + ApiClient confinement | 0 · gates | done |
|
||||
| [WP-04](WP-04-ui-not-infrastructure.md) | Boundaries II: `ui ↛ infrastructure` + showcase sanction | 0 · gates | done |
|
||||
| [WP-05](WP-05-parse-boundaries.md) | Parse-don't-validate closure + MDX | 1 · FP/DDD | done |
|
||||
| [WP-06](WP-06-typed-async.md) | Generic async template contexts — kill `$any()` | 1 · FP/DDD | done |
|
||||
| [WP-07](WP-07-brief-idioms.md) | Brief on the shared idioms + RemoteData MDX | 1 · FP/DDD | done |
|
||||
| [WP-08](WP-08-store-idiom.md) | One store idiom + machine naming + TEA MDX | 1 · FP/DDD | done |
|
||||
| [WP-09](WP-09-pure-logic.md) | Pure-logic closure: dates + missing command specs | 1 · FP/DDD | done |
|
||||
| [WP-10](WP-10-button-fidelity.md) | CIBG button fidelity | 2 · CIBG | done |
|
||||
| [WP-11](WP-11-markup-fidelity.md) | CIBG markup fidelity: application-link + absent-class triage | 2 · CIBG | done |
|
||||
| [WP-12](WP-12-datablock.md) | CIBG Datablock for application data | 2 · CIBG | done |
|
||||
| [WP-13](WP-13-cibg-gap-register.md) | CIBG-gap register + hygiene + MDX | 2 · CIBG | done |
|
||||
| [WP-14](WP-14-storybook-taxonomy.md) | Storybook taxonomy reorg + Layers MDX | 3 · Storybook | done |
|
||||
| [WP-15](WP-15-missing-stories.md) | Missing stories: shell + brief components | 3 · Storybook | done |
|
||||
| [WP-16](WP-16-component-a11y.md) | Component a11y: description wiring + alert role | 4 · a11y | done |
|
||||
| [WP-17](WP-17-app-a11y.md) | App-level a11y: route focus, template lint, WCAG checklist | 4 · a11y | done |
|
||||
| [WP-18](WP-18-abac-capability-spine.md) | ABAC capability spine (Principal + capabilities, phase P1) | 5 · productie-volwassenheid | done |
|
||||
| [WP-19](WP-19-e2e-smoke.md) | Playwright e2e smoke | 5 · productie-volwassenheid | done |
|
||||
| [WP-20](WP-20-second-locale.md) | Second locale proof | 5 · productie-volwassenheid | done |
|
||||
| [WP-21](WP-21-resilience-seams.md) | Resilience seams (correlation-id, idempotency, retry) | 5 · productie-volwassenheid | done |
|
||||
| [WP-22](WP-22-durable-persistence.md) | Durable persistence (optional tier) | 5 · productie-volwassenheid | done |
|
||||
| [WP-23](WP-23-org-template-backend.md) | Org-template backend + admin role | 6 · Brief v2 | done |
|
||||
| [WP-24](WP-24-letter-canvas.md) | Letter canvas (edit on the letter) | 6 · Brief v2 | done |
|
||||
| [WP-25](WP-25-letter-preview-html.md) | Server-rendered letter preview (HTML; PDF deferred) | 6 · Brief v2 | done |
|
||||
| [WP-26](WP-26-org-template-editor.md) | Admin org-template editor | 6 · Brief v2 | done |
|
||||
| [WP-27](WP-27-brief-ux-layer.md) | Brief UX layer (undo/redo, standaardbrief, diff) | 6 · Brief v2 | done |
|
||||
| [WP-28](WP-28-brief-v2-demo-polish.md) | Brief v2 demo polish (scenarios, e2e, docs) | 6 · Brief v2 | todo |
|
||||
| [WP-29](WP-29-stamdata-beheer-editor.md) | Stamdata beheer editor (low-code, PR-emitting) | follow-on · ADR-0004 | done |
|
||||
| [WP-30](WP-30-ci-perf-followups.md) | CI performance follow-ups (node_modules cache, runner image, path filters) | follow-on · CI/infra | todo |
|
||||
| [WP-31](WP-31-shared-store-helpers.md) | Shared store helpers (ActionState/SaveState, history, debounced-save, RemoteData) | 7 · refinements | done |
|
||||
| [WP-32](WP-32-stamdata-undo.md) | Undo/redo in the stamdata editor | 7 · refinements | done |
|
||||
| [WP-33](WP-33-dev-switchers.md) | In-app dev switchers (scenario + role) | 7 · refinements | done |
|
||||
| [WP-34](WP-34-adres-phone-brp-readonly.md) | Adres: phone field + BRP address read-only | 7 · refinements | done |
|
||||
| [WP-35](WP-35-one-concept-per-type.md) | One Concept per case type (server-enforced) | 7 · refinements | done |
|
||||
| [WP-36](WP-36-admin-cases.md) | Admin cases page + admin delete | 7 · refinements | done |
|
||||
| [WP-37](WP-37-dev-switcher-reset.md) | Dev-switcher reset fix (scenario/role URL param) | 8 · platform/DX/showcase | done |
|
||||
| [WP-38](WP-38-dependency-graph-boundaries.md) | Dependency graph + declarative boundaries (visualize + enforce) | 8 · platform/DX/showcase | done |
|
||||
| [WP-39](WP-39-showcase-snippets-animations.md) | Showcase: linked code snippets + teaching animations | 8 · platform/DX/showcase | done |
|
||||
| [WP-40](WP-40-pii-kernel.md) | PII kernel: branded `Bsn` VO (elfproef) + masked-value atom | 8 · platform/DX/showcase | todo |
|
||||
| [WP-41](WP-41-persisted-authz-audit.md) | Persisted, queryable authz/PII-reveal audit (no PII) | 8 · platform/DX/showcase | todo |
|
||||
| [WP-42](WP-42-privacy-security-showcase.md) | Privacy & security showcase page (mask + no-PII log) | 8 · platform/DX/showcase | todo |
|
||||
| [WP-43](WP-43-scaffold-generators.md) | Runnable generators: value-object / form-machine / bff-endpoint / ui-component | 8 · platform/DX/showcase | todo |
|
||||
| [WP-44](WP-44-context-generator.md) | Runnable generator: `gen:context` | 8 · platform/DX/showcase | todo |
|
||||
| [WP-45](WP-45-create-ssp-generator.md) | `create-ssp` bootstrap generator (mechanise new-ssp) | 8 · platform/DX/showcase | todo |
|
||||
| [WP-46](WP-46-vitest-coverage.md) | Vitest coverage (report + report-only thresholds) | 8 · platform/DX/showcase | done |
|
||||
| WP | Title | Phase | Status |
|
||||
| ---------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------- | ------- |
|
||||
| [WP-01](WP-01-axe-ci-gate.md) | Axe-on-every-story CI gate | 0 · gates | done |
|
||||
| [WP-02](WP-02-check-tokens.md) | Harden `check:tokens` + fix what it catches | 0 · gates | done |
|
||||
| [WP-03](WP-03-contracts-purity.md) | Boundaries I: contracts purity + ApiClient confinement | 0 · gates | done |
|
||||
| [WP-04](WP-04-ui-not-infrastructure.md) | Boundaries II: `ui ↛ infrastructure` + showcase sanction | 0 · gates | done |
|
||||
| [WP-05](WP-05-parse-boundaries.md) | Parse-don't-validate closure + MDX | 1 · FP/DDD | done |
|
||||
| [WP-06](WP-06-typed-async.md) | Generic async template contexts — kill `$any()` | 1 · FP/DDD | done |
|
||||
| [WP-07](WP-07-brief-idioms.md) | Brief on the shared idioms + RemoteData MDX | 1 · FP/DDD | done |
|
||||
| [WP-08](WP-08-store-idiom.md) | One store idiom + machine naming + TEA MDX | 1 · FP/DDD | done |
|
||||
| [WP-09](WP-09-pure-logic.md) | Pure-logic closure: dates + missing command specs | 1 · FP/DDD | done |
|
||||
| [WP-10](WP-10-button-fidelity.md) | CIBG button fidelity | 2 · CIBG | done |
|
||||
| [WP-11](WP-11-markup-fidelity.md) | CIBG markup fidelity: application-link + absent-class triage | 2 · CIBG | done |
|
||||
| [WP-12](WP-12-datablock.md) | CIBG Datablock for application data | 2 · CIBG | done |
|
||||
| [WP-13](WP-13-cibg-gap-register.md) | CIBG-gap register + hygiene + MDX | 2 · CIBG | done |
|
||||
| [WP-14](WP-14-storybook-taxonomy.md) | Storybook taxonomy reorg + Layers MDX | 3 · Storybook | done |
|
||||
| [WP-15](WP-15-missing-stories.md) | Missing stories: shell + brief components | 3 · Storybook | done |
|
||||
| [WP-16](WP-16-component-a11y.md) | Component a11y: description wiring + alert role | 4 · a11y | done |
|
||||
| [WP-17](WP-17-app-a11y.md) | App-level a11y: route focus, template lint, WCAG checklist | 4 · a11y | done |
|
||||
| [WP-18](WP-18-abac-capability-spine.md) | ABAC capability spine (Principal + capabilities, phase P1) | 5 · productie-volwassenheid | done |
|
||||
| [WP-19](WP-19-e2e-smoke.md) | Playwright e2e smoke | 5 · productie-volwassenheid | done |
|
||||
| [WP-20](WP-20-second-locale.md) | Second locale proof | 5 · productie-volwassenheid | done |
|
||||
| [WP-21](WP-21-resilience-seams.md) | Resilience seams (correlation-id, idempotency, retry) | 5 · productie-volwassenheid | done |
|
||||
| [WP-22](WP-22-durable-persistence.md) | Durable persistence (optional tier) | 5 · productie-volwassenheid | done |
|
||||
| [WP-23](WP-23-org-template-backend.md) | Org-template backend + admin role | 6 · Brief v2 | done |
|
||||
| [WP-24](WP-24-letter-canvas.md) | Letter canvas (edit on the letter) | 6 · Brief v2 | done |
|
||||
| [WP-25](WP-25-letter-preview-html.md) | Server-rendered letter preview (HTML; PDF deferred) | 6 · Brief v2 | done |
|
||||
| [WP-26](WP-26-org-template-editor.md) | Admin org-template editor | 6 · Brief v2 | done |
|
||||
| [WP-27](WP-27-brief-ux-layer.md) | Brief UX layer (undo/redo, standaardbrief, diff) | 6 · Brief v2 | done |
|
||||
| [WP-28](WP-28-brief-v2-demo-polish.md) | Brief v2 demo polish (scenarios, e2e, docs) | 6 · Brief v2 | todo |
|
||||
| [WP-29](WP-29-stamdata-beheer-editor.md) | Stamdata beheer editor (low-code, PR-emitting) | follow-on · ADR-0004 | done |
|
||||
| [WP-30](WP-30-ci-perf-followups.md) | CI performance follow-ups (node_modules cache, runner image, path filters) | follow-on · CI/infra | todo |
|
||||
| [WP-31](WP-31-shared-store-helpers.md) | Shared store helpers (ActionState/SaveState, history, debounced-save, RemoteData) | 7 · refinements | done |
|
||||
| [WP-32](WP-32-stamdata-undo.md) | Undo/redo in the stamdata editor | 7 · refinements | done |
|
||||
| [WP-33](WP-33-dev-switchers.md) | In-app dev switchers (scenario + role) | 7 · refinements | done |
|
||||
| [WP-34](WP-34-adres-phone-brp-readonly.md) | Adres: phone field + BRP address read-only | 7 · refinements | done |
|
||||
| [WP-35](WP-35-one-concept-per-type.md) | One Concept per case type (server-enforced) | 7 · refinements | done |
|
||||
| [WP-36](WP-36-admin-cases.md) | Admin cases page + admin delete | 7 · refinements | done |
|
||||
| [WP-37](WP-37-dev-switcher-reset.md) | Dev-switcher reset fix (scenario/role URL param) | 8 · platform/DX/showcase | done |
|
||||
| [WP-38](WP-38-dependency-graph-boundaries.md) | Dependency graph + declarative boundaries (visualize + enforce) | 8 · platform/DX/showcase | done |
|
||||
| [WP-39](WP-39-showcase-snippets-animations.md) | Showcase: linked code snippets + teaching animations | 8 · platform/DX/showcase | done |
|
||||
| [WP-40](WP-40-pii-kernel.md) | PII kernel: branded `Bsn` VO (elfproef) + masked-value atom | 8 · platform/DX/showcase | done |
|
||||
| [WP-41](WP-41-persisted-authz-audit.md) | Persisted, queryable authz/PII-reveal audit (no PII) | 8 · platform/DX/showcase | todo |
|
||||
| [WP-42](WP-42-privacy-security-showcase.md) | Privacy & security showcase page (mask + no-PII log) | 8 · platform/DX/showcase | partial |
|
||||
| [WP-43](WP-43-scaffold-generators.md) | Runnable generators: value-object / form-machine / bff-endpoint / ui-component | 8 · platform/DX/showcase | todo |
|
||||
| [WP-44](WP-44-context-generator.md) | Runnable generator: `gen:context` | 8 · platform/DX/showcase | todo |
|
||||
| [WP-45](WP-45-create-ssp-generator.md) | `create-ssp` bootstrap generator (mechanise new-ssp) | 8 · platform/DX/showcase | todo |
|
||||
| [WP-46](WP-46-vitest-coverage.md) | Vitest coverage (report + report-only thresholds) | 8 · platform/DX/showcase | done |
|
||||
|
||||
Sequencing dependencies (stated in the WPs too): 01 before 10–15 (axe covers story churn);
|
||||
03/04 before 05–09 (boundaries stop new violations during refactors); 06 before 07 (typed
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
# WP-40 — PII kernel: branded `Bsn` VO + masked-value atom
|
||||
|
||||
Status: todo
|
||||
Status: done
|
||||
Phase: 8 — platform/DX/showcase
|
||||
Priority: P2
|
||||
|
||||
## Outcome
|
||||
|
||||
- `shared/kernel/bsn.ts` — `Bsn` branded VO + `parseBsn` with the **elfproef** (11-test) checksum
|
||||
(+spec). Wired into the DigiD login boundary (`digid.adapter`): login now does real BSN
|
||||
validation instead of "any 9 digits" — login hint + e2e BSNs updated to a valid one (`123456782`).
|
||||
- `shared/kernel/pii.ts` — pure `maskBsn`/`maskTail`/`REDACTED` (+spec), the functional core. Moved
|
||||
out of `debug-state/mask.ts`, which keeps only `redactProfile` (it depends on the registratie
|
||||
`BigProfile`, so it can't live in `shared/kernel` — boundary; debug-state is the sanctioned
|
||||
cross-context devtool). `mask.spec` ported to `pii.spec`.
|
||||
- `<app-masked-value>` atom (`shared/ui/masked-value`) + story — masked-by-default value with an
|
||||
optional reveal affordance; **centralises** the `.includes('*')` masked-detection that
|
||||
`behandel-scherm` used to sniff inline (now refactored to the atom). The atom only emits
|
||||
`reveal`; the caller keeps the step-up confirm + audited fetch.
|
||||
- Deviation (flagged): `Session.bsn` stays typed `string` — the brand guards the login ENTRY, but
|
||||
Session crosses a persistence boundary where the restore path deliberately drops the bsn
|
||||
(privacy) and JSON can't carry a brand, so re-typing it would add a `''`/nullable ripple for no
|
||||
real gain. Backend `MaskTail` twin unchanged (still in sync).
|
||||
|
||||
## Why
|
||||
|
||||
Masking is real but ad-hoc: pure helpers live in `debug-state/mask.ts`, the BSN is a bare `string`
|
||||
@@ -29,6 +47,6 @@ AVG art. 9 (BSN = special category) territory; the pieces should be first-class
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] `parseBsn` accepts valid elfproef numbers, rejects bad checksum/length (spec).
|
||||
- [ ] `<app-masked-value>` renders masked by default with an accessible reveal; behandel-scherm uses it.
|
||||
- [ ] Pure maskers have specs (no TestBed); backend `MaskTail` parity noted; `npm run ci` green.
|
||||
- [x] `parseBsn` accepts valid elfproef numbers, rejects bad checksum/length/all-zeros (spec).
|
||||
- [x] `<app-masked-value>` renders masked by default with an optional reveal; behandel-scherm uses it (no more inline sniff).
|
||||
- [x] Pure maskers have specs (no TestBed); backend `MaskTail` parity noted; `npm run ci` green.
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
# WP-42 — Privacy & security showcase page
|
||||
|
||||
Status: todo
|
||||
Status: partial — mask/parse showcase done; audit half pending WP-41
|
||||
Phase: 8 — platform/DX/showcase
|
||||
Priority: P2
|
||||
Depends on: WP-40, WP-41
|
||||
|
||||
## Outcome (mask/parse half — done, on user request ahead of WP-41)
|
||||
|
||||
Added a "6 · PII — maskeren & parsen" section to `/concepts` demonstrating the WP-40 pieces with
|
||||
FP + atomic design, framed for AVG art. 9 / data-minimisation: a live `<app-masked-value>` atom
|
||||
(masked-by-default BSN that reveals on click; note points to the real step-up + audited reveal in
|
||||
behandel-scherm) and a live `parseBsn` elfproef parse mirroring the postcode demo. Both show the
|
||||
real linked source via the WP-39 snippet mechanism (`// #region showcase:parseBsn` in bsn.ts,
|
||||
`showcase:mask` in pii.ts, registered in `gen-snippets.mjs` → `snippets.generated.ts`, drift-gated).
|
||||
No i18n (showcase is Dutch-only teaching text). No behaviour change outside the showcase.
|
||||
|
||||
**Still pending (needs WP-41):** the "log PII / no-PII audit trail" half — visualizing the
|
||||
persisted authz/reveal audit — plus an optional Foundations MDX writeup.
|
||||
|
||||
## Why
|
||||
|
||||
Once the reusable privacy pieces exist (WP-40 masked-value atom + pure maskers, WP-41 persisted
|
||||
|
||||
@@ -12,7 +12,7 @@ import { expect, test } from '@playwright/test';
|
||||
// here: a real reload cycle, not a no-op button.
|
||||
test('dashboard error state renders, retry re-fetches (and fails again)', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.getByLabel('BSN').fill('123456789');
|
||||
await page.getByLabel('BSN').fill('123456782');
|
||||
await page.getByRole('button', { name: 'Inloggen met DigiD' }).click();
|
||||
await expect(page).toHaveURL(/\/dashboard$/);
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import { expect, test } from '@playwright/test';
|
||||
// stricter future test might.
|
||||
test('login → dashboard → registratie wizard → submitted', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.getByLabel('BSN').fill('123456789');
|
||||
await page.getByLabel('BSN').fill('123456782');
|
||||
await page.getByLabel('Wachtwoord').fill('demo');
|
||||
await page.getByRole('button', { name: 'Inloggen met DigiD' }).click();
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ const SOURCES = [
|
||||
'src/app/registratie/domain/value-objects/postcode.ts',
|
||||
'src/app/registratie/domain/change-request.machine.ts',
|
||||
'src/app/herregistratie/domain/intake.machine.ts',
|
||||
'src/app/shared/kernel/bsn.ts',
|
||||
'src/app/shared/kernel/pii.ts',
|
||||
];
|
||||
|
||||
const START = /\/\/\s*#region showcase:([\w-]+)/;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Result, ok, err } from '@shared/kernel/fp';
|
||||
import { Result, ok } from '@shared/kernel/fp';
|
||||
import { parseBsn } from '@shared/kernel/bsn';
|
||||
import { Session } from '../domain/session';
|
||||
|
||||
/** Infrastructure: talks to the (mock) DigiD identity provider. */
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DigidAdapter {
|
||||
// ponytail: fake DigiD — any 9-digit BSN authenticates to a fixed identity.
|
||||
// Swap for a real OIDC redirect flow when there's a backend.
|
||||
// ponytail: fake DigiD — any elfproef-valid BSN authenticates to a fixed identity.
|
||||
// Real BSN validation (parseBsn, WP-40) is the trust boundary; swap the fixed identity
|
||||
// for a real OIDC redirect flow when there's an IdP.
|
||||
async authenticate(bsn: string): Promise<Result<string, Session>> {
|
||||
const t = bsn.trim();
|
||||
if (!/^\d{9}$/.test(t))
|
||||
return err($localize`:@@validation.bsn:Voer een geldig BSN van 9 cijfers in.`);
|
||||
return ok({ bsn: t, naam: 'Dr. A. (Anna) de Vries' });
|
||||
const r = parseBsn(bsn);
|
||||
return r.ok ? ok({ bsn: r.value, naam: 'Dr. A. (Anna) de Vries' }) : r;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,14 +22,14 @@ import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
fieldId="bsn"
|
||||
required
|
||||
i18n-description="@@login.bsnDescription"
|
||||
description="9 cijfers (demo: vul iets in)"
|
||||
description="9-cijferig BSN, elfproef-geldig (demo: 123456782)"
|
||||
>
|
||||
<app-text-input
|
||||
inputId="bsn"
|
||||
hasDescription
|
||||
[(ngModel)]="bsn"
|
||||
name="bsn"
|
||||
placeholder="123456789"
|
||||
placeholder="123456782"
|
||||
/>
|
||||
</app-form-field>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Component, ElementRef, computed, input, output, viewChild } from '@angu
|
||||
import { PlaceholderOption } from '@shared/ui/rich-text-editor/rich-text-editor.component';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
import { HeadingComponent } from '@shared/ui/heading/heading.component';
|
||||
import { MaskedValueComponent } from '@shared/ui/masked-value/masked-value.component';
|
||||
import { StepperComponent } from '@shared/ui/stepper/stepper.component';
|
||||
import { Besluit, Brief, CaseContext, LibraryPassage } from '@brief/domain/brief';
|
||||
import { inferSelection } from '@brief/domain/besluit';
|
||||
@@ -25,6 +26,7 @@ import { BesluitPanelComponent } from '@brief/ui/besluit-panel/besluit-panel.com
|
||||
imports: [
|
||||
ButtonComponent,
|
||||
HeadingComponent,
|
||||
MaskedValueComponent,
|
||||
StepperComponent,
|
||||
LetterCanvasComponent,
|
||||
DiagnosticsPanelComponent,
|
||||
@@ -92,10 +94,13 @@ import { BesluitPanelComponent } from '@brief/ui/besluit-panel/besluit-panel.com
|
||||
<span>{{ caseContext().aanvraagReferentie }}</span>
|
||||
<span>{{ caseContext().zorgverlenerNaam }}</span>
|
||||
<span>
|
||||
{{ bigLabel() }} {{ caseContext().bigNummer }}
|
||||
@if (canRevealBigNummer() && isMasked()) {
|
||||
<app-button variant="subtle" (click)="onReveal()">{{ revealLabel() }}</app-button>
|
||||
}
|
||||
{{ bigLabel() }}
|
||||
<app-masked-value
|
||||
[value]="caseContext().bigNummer"
|
||||
[canReveal]="canRevealBigNummer()"
|
||||
[revealLabel]="revealLabel()"
|
||||
(reveal)="onReveal()"
|
||||
/>
|
||||
</span>
|
||||
<span>{{ caseContext().beroep }}</span>
|
||||
</div>
|
||||
@@ -172,10 +177,6 @@ export class BehandelSchermComponent {
|
||||
locate = output<Diagnostic>();
|
||||
revealBigNummer = output<void>();
|
||||
|
||||
/** The BIG-nummer arrives masked (contains `*`); once revealed the swapped value has
|
||||
no `*`, so the reveal action hides itself — no separate "revealed" flag needed. */
|
||||
protected isMasked = computed(() => this.caseContext().bigNummer.includes('*'));
|
||||
|
||||
/** Step-up (PRD-0002 §5d) stubbed as a native confirm — the extra verification gesture
|
||||
before an audited PII reveal. ponytail: real systems prompt MFA / recent re-auth. */
|
||||
protected onReveal() {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parseBsn } from './bsn';
|
||||
|
||||
describe('parseBsn (elfproef)', () => {
|
||||
it('accepts a valid BSN (passes the elfproef)', () => {
|
||||
const r = parseBsn('123456782'); // Σ d·w = 154, divisible by 11
|
||||
expect(r.ok && r.value).toBe('123456782');
|
||||
});
|
||||
|
||||
it('rejects a 9-digit number that fails the elfproef', () => {
|
||||
expect(parseBsn('123456789').ok).toBe(false); // sum 147, not divisible
|
||||
});
|
||||
|
||||
it('rejects wrong length / non-digits / all zeros', () => {
|
||||
expect(parseBsn('12345').ok).toBe(false);
|
||||
expect(parseBsn('abcdefghi').ok).toBe(false);
|
||||
expect(parseBsn('000000000').ok).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Brand, Result, ok, err } from '@shared/kernel/fp';
|
||||
|
||||
/**
|
||||
* Value object: a Dutch **BSN** (burgerservicenummer) — art. 9 GDPR/AVG special-category
|
||||
* data. "Parse, don't validate": a `Bsn` is a distinct type from a raw string, mintable only
|
||||
* via `parseBsn`, so holding one is proof it passed the **elfproef** (11-test) checksum, not
|
||||
* just a 9-digit shape. Format/checksum only — identity is still faked in this POC (DigiD stub).
|
||||
*/
|
||||
export type Bsn = Brand<string, 'Bsn'>;
|
||||
|
||||
// Positional weights for the elfproef: 9·d1 + 8·d2 + … + 2·d8 − 1·d9 ≡ 0 (mod 11).
|
||||
const WEIGHTS = [9, 8, 7, 6, 5, 4, 3, 2, -1];
|
||||
|
||||
// #region showcase:parseBsn
|
||||
export function parseBsn(raw: string): Result<string, Bsn> {
|
||||
const t = raw.trim();
|
||||
if (!/^\d{9}$/.test(t)) {
|
||||
return err($localize`:@@validation.bsn:Voer een geldig BSN van 9 cijfers in.`);
|
||||
}
|
||||
const sum = [...t].reduce((acc, ch, i) => acc + Number(ch) * WEIGHTS[i], 0);
|
||||
if (t === '000000000' || sum % 11 !== 0) {
|
||||
return err(
|
||||
$localize`:@@validation.bsnElfproef:Dit is geen geldig BSN (klopt niet met de elfproef).`,
|
||||
);
|
||||
}
|
||||
return ok(t as Bsn); // holding a Bsn is proof it passed the elfproef
|
||||
}
|
||||
// #endregion showcase:parseBsn
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { maskBsn, maskTail } from './pii';
|
||||
|
||||
describe('pii maskers', () => {
|
||||
it('maskBsn keeps the last 3 digits', () => {
|
||||
expect(maskBsn('123456789')).toBe('******789');
|
||||
});
|
||||
|
||||
it('maskTail keeps the requested tail length', () => {
|
||||
expect(maskTail('abcdef', 2)).toBe('****ef');
|
||||
});
|
||||
|
||||
it('masks the whole value when it is not longer than the kept tail', () => {
|
||||
expect(maskBsn('12')).toBe('**');
|
||||
expect(maskBsn('')).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* PII masking — pure functional core (WP-40). Data-minimisation helpers shared by the app
|
||||
* (dev state panel, the masked-value atom, anywhere sensitive data is shown). No framework,
|
||||
* no domain imports. The backend keeps a `MaskTail` twin in sync (see Program.cs).
|
||||
*/
|
||||
export const REDACTED = '‹redacted›';
|
||||
|
||||
// #region showcase:mask
|
||||
/** Keep the last `keep` characters, mask the rest with `*`. */
|
||||
export function maskTail(value: string, keep: number): string {
|
||||
if (value.length <= keep) return '*'.repeat(value.length);
|
||||
return '*'.repeat(value.length - keep) + value.slice(-keep);
|
||||
}
|
||||
|
||||
/** Mask a BSN / BIG-nummer for display: keep the last 3 digits, mask the rest. */
|
||||
export function maskBsn(value: string): string {
|
||||
return maskTail(value, 3);
|
||||
}
|
||||
// #endregion showcase:mask
|
||||
@@ -8,7 +8,8 @@ import { Role } from '@shared/domain/role';
|
||||
import { ROLES, currentRole, setRole } from '@shared/infrastructure/role';
|
||||
import { Scenario, SCENARIOS, currentScenario, setScenario } from '@shared/infrastructure/scenario';
|
||||
import { stripDevParams } from '@shared/infrastructure/dev-params';
|
||||
import { maskBsn, redactProfile } from './mask';
|
||||
import { maskBsn } from '@shared/kernel/pii';
|
||||
import { redactProfile } from './mask';
|
||||
|
||||
// CIBG-GAP EXTENSION: n/a — devtool, no corresponding CIBG concept; deliberately
|
||||
// off-theme by design (see the ponytail note below), see cibg-gaps.mdx.
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { maskBsn } from './mask';
|
||||
|
||||
describe('maskBsn', () => {
|
||||
it('keeps the last 3 digits and masks the rest', () => {
|
||||
expect(maskBsn('123456789')).toBe('******789');
|
||||
});
|
||||
|
||||
it('handles short and empty input without throwing', () => {
|
||||
expect(maskBsn('12')).toBe('**');
|
||||
expect(maskBsn('')).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -1,23 +1,14 @@
|
||||
import { BigProfile } from '@registratie/domain/big-profile';
|
||||
|
||||
const REDACTED = '‹redacted›';
|
||||
|
||||
/** Keep the last `keep` characters, mask the rest. */
|
||||
function maskTail(value: string, keep: number): string {
|
||||
if (value.length <= keep) return '*'.repeat(value.length);
|
||||
return '*'.repeat(value.length - keep) + value.slice(-keep);
|
||||
}
|
||||
|
||||
/** Redact a BSN for the dev state view: keep the last 3 digits, mask the rest. */
|
||||
export function maskBsn(bsn: string): string {
|
||||
return maskTail(bsn, 3);
|
||||
}
|
||||
import { REDACTED, maskTail } from '@shared/kernel/pii';
|
||||
|
||||
/**
|
||||
* Data minimisation for the dev "show the Model" panel: keep the structural /
|
||||
* decision-relevant fields (status, beroep, dates of registration) but redact
|
||||
* direct personal identifiers (name, address, date of birth) and mask the BIG
|
||||
* number. The panel is for inspecting state SHAPE, never for reading PII.
|
||||
* number. The panel is for inspecting state SHAPE, never for reading PII. The
|
||||
* generic maskers live in `@shared/kernel/pii`; this stays here because it depends
|
||||
* on the registratie `BigProfile` domain type (debug-state is the sanctioned
|
||||
* cross-context devtool).
|
||||
*/
|
||||
export function redactProfile(p: BigProfile): unknown {
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Component, computed, input, output } from '@angular/core';
|
||||
import { ButtonComponent } from '@shared/ui/button/button.component';
|
||||
|
||||
/**
|
||||
* Atom: a possibly-masked sensitive value (BSN, BIG-nummer, …) with an optional, audited
|
||||
* reveal affordance (WP-40). The value arrives masked from the server (data-minimisation)
|
||||
* and is swapped for the full value on reveal; the reveal button shows only when the value
|
||||
* is still masked AND the caller says the principal may reveal it. Centralises the
|
||||
* masked-detection that consumers used to sniff inline. The atom only emits `reveal`; the
|
||||
* caller owns the step-up gesture + the audited fetch (see behandel-scherm).
|
||||
*
|
||||
* ponytail: masked-detection is the mask character (`*`) — a POC heuristic. A server-sent
|
||||
* `masked` boolean would remove the sniff; wire it here without touching consumers.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-masked-value',
|
||||
imports: [ButtonComponent],
|
||||
template: `
|
||||
<span class="value">{{ value() }}</span>
|
||||
@if (canReveal() && masked()) {
|
||||
<app-button variant="subtle" (click)="reveal.emit()">{{ revealLabel() }}</app-button>
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class MaskedValueComponent {
|
||||
value = input.required<string>();
|
||||
canReveal = input(false);
|
||||
revealLabel = input($localize`:@@maskedValue.reveal:Tonen`);
|
||||
reveal = output<void>();
|
||||
|
||||
protected masked = computed(() => this.value().includes('*'));
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { Meta, StoryObj } from '@storybook/angular';
|
||||
import { MaskedValueComponent } from './masked-value.component';
|
||||
|
||||
const meta: Meta<MaskedValueComponent> = {
|
||||
title: 'Design System/Atoms/Masked Value',
|
||||
component: MaskedValueComponent,
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<MaskedValueComponent>;
|
||||
|
||||
/** Masked + the principal may reveal → the reveal button shows. */
|
||||
export const RevealableMasked: Story = {
|
||||
args: { value: '******601', canReveal: true, revealLabel: 'Toon BIG-nummer' },
|
||||
};
|
||||
|
||||
/** Masked but no reveal right → just the masked value, no affordance. */
|
||||
export const MaskedNoReveal: Story = {
|
||||
args: { value: '******601', canReveal: false },
|
||||
};
|
||||
|
||||
/** Already revealed (no mask character) → no reveal button even with the right. */
|
||||
export const Revealed: Story = {
|
||||
args: { value: '990000000012', canReveal: true },
|
||||
};
|
||||
@@ -11,6 +11,9 @@ import { HerregistratieWizardComponent } from '@herregistratie/ui/herregistratie
|
||||
import { IntakeWizardComponent } from '@herregistratie/ui/intake-wizard/intake-wizard.component';
|
||||
import { Registration } from '@registratie/domain/registration';
|
||||
import { parsePostcode } from '@registratie/domain/value-objects/postcode';
|
||||
import { parseBsn } from '@shared/kernel/bsn';
|
||||
import { maskBsn } from '@shared/kernel/pii';
|
||||
import { MaskedValueComponent } from '@shared/ui/masked-value/masked-value.component';
|
||||
import { SNIPPETS } from './snippets.generated';
|
||||
import { highlightTs } from './highlight-ts';
|
||||
|
||||
@@ -39,6 +42,7 @@ function fakeResource<T>(status: string, value?: T, error?: Error): Resource<T>
|
||||
RegistrationSummaryComponent,
|
||||
HerregistratieWizardComponent,
|
||||
IntakeWizardComponent,
|
||||
MaskedValueComponent,
|
||||
],
|
||||
styles: [
|
||||
`
|
||||
@@ -371,6 +375,65 @@ function fakeResource<T>(status: string, value?: T, error?: Error): Resource<T>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 6. PII: mask + parse -->
|
||||
<section class="section">
|
||||
<app-heading [level]="2">6 · PII — maskeren & parsen</app-heading>
|
||||
<p class="lead">
|
||||
Een BSN is bijzondere persoonsgegevens (AVG art. 9). Dataminimalisatie: standaard
|
||||
gemaskeerd tonen, alleen tonen na een vastgelegde handeling; en "parse, don't validate" op
|
||||
het gevoeligste veld — een pure functie die de <em>elfproef</em> afdwingt.
|
||||
</p>
|
||||
<div class="cols">
|
||||
<div class="card card--good">
|
||||
<p class="tag good">Maskeren — atom</p>
|
||||
<p>
|
||||
BSN:
|
||||
<app-masked-value
|
||||
[value]="bsnShown()"
|
||||
[canReveal]="true"
|
||||
revealLabel="Toon BSN"
|
||||
(reveal)="bsnRevealed.set(true)"
|
||||
/>
|
||||
</p>
|
||||
<figure class="linked">
|
||||
<pre [innerHTML]="code['mask']"></pre>
|
||||
<figcaption class="src">↳ {{ src['mask'] }}</figcaption>
|
||||
</figure>
|
||||
<p class="note">
|
||||
Standaard gemaskeerd; het echte tonen is step-up-geverifieerd én vastgelegd (zie het
|
||||
behandelscherm). De atom bevat de maskeer-detectie — geen los <code>*</code>-gesnuffel
|
||||
bij elke gebruiker.
|
||||
</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<p class="tag good">Parse (elfproef) → Result</p>
|
||||
<app-text-input
|
||||
inputId="bsn"
|
||||
[ngModel]="bsnRaw()"
|
||||
(ngModelChange)="bsnRaw.set($event)"
|
||||
name="bsn"
|
||||
placeholder="Typ een BSN, bijv. 123456782"
|
||||
/>
|
||||
@let b = bsnParsed();
|
||||
@if (bsnRaw()) {
|
||||
<div animate.enter="app-item-enter">
|
||||
@if (b.ok) {
|
||||
<p class="tag good">ok</p>
|
||||
<pre>Bsn ="{{ b.value }}"</pre>
|
||||
} @else {
|
||||
<p class="tag bad">err</p>
|
||||
<pre>{{ b.error }}</pre>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<figure class="linked">
|
||||
<pre [innerHTML]="code['parseBsn']"></pre>
|
||||
<figcaption class="src">↳ {{ src['parseBsn'] }}</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</app-page-shell>
|
||||
`,
|
||||
})
|
||||
@@ -394,6 +457,14 @@ export class ConceptsPage {
|
||||
raw = signal('');
|
||||
parsed = computed(() => parsePostcode(this.raw()));
|
||||
|
||||
// 6 · PII demo. Masked-by-default value that reveals locally (the real reveal is
|
||||
// step-up-gated + audited elsewhere); plus a live elfproef parse mirroring the postcode demo.
|
||||
demoBsn = '123456782';
|
||||
bsnRevealed = signal(false);
|
||||
bsnShown = computed(() => (this.bsnRevealed() ? this.demoBsn : maskBsn(this.demoBsn)));
|
||||
bsnRaw = signal('');
|
||||
bsnParsed = computed(() => parseBsn(this.bsnRaw()));
|
||||
|
||||
// Deliberately-wrong illustrations (no real source to link — they show the anti-pattern).
|
||||
private readonly illustrations: Record<string, string> = {
|
||||
unionBad: `interface Registration {
|
||||
@@ -419,5 +490,7 @@ errors = signal<...>({});
|
||||
parse: 'registratie/domain/value-objects/postcode.ts',
|
||||
machine: 'registratie/domain/change-request.machine.ts',
|
||||
steps: 'herregistratie/domain/intake.machine.ts',
|
||||
parseBsn: 'shared/kernel/bsn.ts',
|
||||
mask: 'shared/kernel/pii.ts',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
export const SNIPPETS: Record<string, string> = {
|
||||
"fold": "/** Exhaustive fold: you must handle every case, checked at compile time. */\nexport function foldRemote<E, T, R>(\n rd: RemoteData<E, T>,\n h: { loading: () => R; empty: () => R; failure: (e: E) => R; success: (v: T) => R },\n): R {\n switch (rd.tag) {\n case 'Loading':\n return h.loading();\n case 'Empty':\n return h.empty();\n case 'Failure':\n return h.failure(rd.error);\n case 'Success':\n return h.success(rd.value);\n default:\n return assertNever(rd); // add a variant → compile error until handled\n }\n}",
|
||||
"machine": "export type ChangeRequestState =\n | { tag: 'Editing'; draft: Draft; errors: Errors } // draft/errors exist ONLY while editing\n | { tag: 'Submitting'; data: Valid } // carries the parsed value, no errors\n | { tag: 'Submitted'; data: Valid; referentie: string }\n | { tag: 'Failed'; data: Valid; error: string };",
|
||||
"mask": "/** Keep the last `keep` characters, mask the rest with `*`. */\nexport function maskTail(value: string, keep: number): string {\n if (value.length <= keep) return '*'.repeat(value.length);\n return '*'.repeat(value.length - keep) + value.slice(-keep);\n}\n\n/** Mask a BSN / BIG-nummer for display: keep the last 3 digits, mask the rest. */\nexport function maskBsn(value: string): string {\n return maskTail(value, 3);\n}",
|
||||
"parse": "export function parsePostcode(raw: string): Result<string, Postcode> {\n const t = raw.trim().toUpperCase();\n if (!/^[1-9]\\d{3}\\s?[A-Z]{2}$/.test(t)) {\n return err($localize`:@@validation.postcode:Voer een geldige postcode in, bijv. 1234 AB.`);\n }\n // Normalise to \"1234 AB\" — the parser also cleans up.\n return ok(t.replace(/^(\\d{4})\\s?([A-Z]{2})$/, '$1 $2') as Postcode);\n}",
|
||||
"parseBsn": "export function parseBsn(raw: string): Result<string, Bsn> {\n const t = raw.trim();\n if (!/^\\d{9}$/.test(t)) {\n return err($localize`:@@validation.bsn:Voer een geldig BSN van 9 cijfers in.`);\n }\n const sum = [...t].reduce((acc, ch, i) => acc + Number(ch) * WEIGHTS[i], 0);\n if (t === '000000000' || sum % 11 !== 0) {\n return err(\n $localize`:@@validation.bsnElfproef:Dit is geen geldig BSN (klopt niet met de elfproef).`,\n );\n }\n return ok(t as Bsn); // holding a Bsn is proof it passed the elfproef\n}",
|
||||
"steps": "/** The fixed step list. Number of steps never changes; questions reveal inline. */\nexport const STEPS: StepId[] = ['buitenland', 'werk', 'review'];",
|
||||
"union": "export type RegistrationStatus =\n | { tag: 'Geregistreerd'; herregistratieDatum: string } // only this variant carries the date\n | { tag: 'Geschorst'; geschorstTot: string; reden: string }\n | { tag: 'Doorgehaald'; doorgehaaldOp: string; reden: string };",
|
||||
};
|
||||
|
||||
@@ -6,8 +6,24 @@
|
||||
<source>Voer een geldig BSN van 9 cijfers in.</source>
|
||||
<target datatype="html">Enter a valid 9-digit BSN.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/auth/infrastructure/digid.adapter.ts</context>
|
||||
<context context-type="linenumber">13</context>
|
||||
<context context-type="sourcefile">src/app/shared/kernel/bsn.ts</context>
|
||||
<context context-type="linenumber">15</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="validation.bsnElfproef" datatype="html">
|
||||
<source>Dit is geen geldig BSN (klopt niet met de elfproef).</source>
|
||||
<target datatype="html">This is not a valid BSN (fails the eleven-test checksum).</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/shared/kernel/bsn.ts</context>
|
||||
<context context-type="linenumber">20</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="maskedValue.reveal" datatype="html">
|
||||
<source>Tonen</source>
|
||||
<target datatype="html">Show</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/shared/ui/masked-value/masked-value.component.ts</context>
|
||||
<context context-type="linenumber">27</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.verplichteVelden" datatype="html">
|
||||
@@ -35,8 +51,8 @@
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="login.bsnDescription" datatype="html">
|
||||
<source>9 cijfers (demo: vul iets in)</source>
|
||||
<target datatype="html">9 digits (demo: fill in anything)</target>
|
||||
<source>9-cijferig BSN, elfproef-geldig (demo: 123456782)</source>
|
||||
<target datatype="html">9-digit BSN, valid eleven-test checksum (demo: 123456782)</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/auth/ui/login-form/login-form.component.ts</context>
|
||||
<context context-type="linenumber">25,28</context>
|
||||
|
||||
+41
-27
@@ -2,13 +2,6 @@
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="nl" datatype="plaintext" original="ng2.template">
|
||||
<body>
|
||||
<trans-unit id="validation.bsn" datatype="html">
|
||||
<source>Voer een geldig BSN van 9 cijfers in.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/auth/infrastructure/digid.adapter.ts</context>
|
||||
<context context-type="linenumber">13</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="form.verplichteVelden" datatype="html">
|
||||
<source>* verplichte velden</source>
|
||||
<context-group purpose="location">
|
||||
@@ -32,7 +25,7 @@
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="login.bsnDescription" datatype="html">
|
||||
<source>9 cijfers (demo: vul iets in)</source>
|
||||
<source>9-cijferig BSN, elfproef-geldig (demo: 123456782)</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/auth/ui/login-form/login-form.component.ts</context>
|
||||
<context context-type="linenumber">25,28</context>
|
||||
@@ -322,81 +315,81 @@
|
||||
<source>Opnieuw indienen</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">221</context>
|
||||
<context context-type="linenumber">222</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.submit" datatype="html">
|
||||
<source>Indienen ter beoordeling</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">222</context>
|
||||
<context context-type="linenumber">223</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.step.beoordelen" datatype="html">
|
||||
<source>Beoordelen</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">226</context>
|
||||
<context context-type="linenumber">227</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.step.opstellen" datatype="html">
|
||||
<source>Brief opstellen</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">227</context>
|
||||
<context context-type="linenumber">228</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">231</context>
|
||||
<context context-type="linenumber">232</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.step.indienen" datatype="html">
|
||||
<source>Indienen</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">228</context>
|
||||
<context context-type="linenumber">229</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.process" datatype="html">
|
||||
<source>Herregistratie behandelen</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">230</context>
|
||||
<context context-type="linenumber">231</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.case.heading" datatype="html">
|
||||
<source>Aanvraag herregistratie</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">232</context>
|
||||
<context context-type="linenumber">233</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.case.big" datatype="html">
|
||||
<source>BIG-nummer</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">233</context>
|
||||
<context context-type="linenumber">234</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.case.reveal" datatype="html">
|
||||
<source>Toon BIG-nummer</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">234</context>
|
||||
<context context-type="linenumber">235</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.case.revealConfirm" datatype="html">
|
||||
<source>Extra verificatie vereist. Het tonen van het BIG-nummer wordt vastgelegd. Doorgaan?</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">236</context>
|
||||
<context context-type="linenumber">237</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.preview.open" datatype="html">
|
||||
<source>Voorbeeld</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">238</context>
|
||||
<context context-type="linenumber">239</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/letter-composer/letter-composer.component.ts</context>
|
||||
@@ -407,21 +400,21 @@
|
||||
<source>Openen als document (PDF)</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">240</context>
|
||||
<context context-type="linenumber">241</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="common.close" datatype="html">
|
||||
<source>Sluiten</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">242</context>
|
||||
<context context-type="linenumber">243</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.submitHint" datatype="html">
|
||||
<source>Vul eerst alle verplichte secties en los fouten op.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/brief/ui/behandel-scherm/behandel-scherm.component.ts</context>
|
||||
<context context-type="linenumber">244</context>
|
||||
<context context-type="linenumber">245</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="brief.besluit.positief" datatype="html">
|
||||
@@ -1070,11 +1063,11 @@
|
||||
<source>Maak een keuze.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/herregistratie/domain/intake.machine.ts</context>
|
||||
<context context-type="linenumber">100</context>
|
||||
<context context-type="linenumber">102</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/herregistratie/domain/intake.machine.ts</context>
|
||||
<context context-type="linenumber">112</context>
|
||||
<context context-type="linenumber">114</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/registratie/domain/registratie-wizard.machine.ts</context>
|
||||
@@ -1085,7 +1078,7 @@
|
||||
<source>Vul een land in.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/herregistratie/domain/intake.machine.ts</context>
|
||||
<context context-type="linenumber">103</context>
|
||||
<context context-type="linenumber">105</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="herregWizard.processName" datatype="html">
|
||||
@@ -1681,7 +1674,7 @@
|
||||
<source>Voer een geldige postcode in, bijv. 1234 AB.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/registratie/domain/value-objects/postcode.ts</context>
|
||||
<context context-type="linenumber">13</context>
|
||||
<context context-type="linenumber">14</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="validation.telefoon" datatype="html">
|
||||
@@ -2614,6 +2607,20 @@
|
||||
<context context-type="linenumber">28</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="validation.bsn" datatype="html">
|
||||
<source>Voer een geldig BSN van 9 cijfers in.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/shared/kernel/bsn.ts</context>
|
||||
<context context-type="linenumber">17</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="validation.bsnElfproef" datatype="html">
|
||||
<source>Dit is geen geldig BSN (klopt niet met de elfproef).</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/shared/kernel/bsn.ts</context>
|
||||
<context context-type="linenumber">21</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="header.nav.huisstijl" datatype="html">
|
||||
<source>Huisstijl</source>
|
||||
<context-group purpose="location">
|
||||
@@ -2936,6 +2943,13 @@
|
||||
<context context-type="linenumber">31</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="maskedValue.reveal" datatype="html">
|
||||
<source>Tonen</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/shared/ui/masked-value/masked-value.component.ts</context>
|
||||
<context context-type="linenumber">28</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="placeholderChip.auto" datatype="html">
|
||||
<source>wordt automatisch ingevuld</source>
|
||||
<context-group purpose="location">
|
||||
|
||||
Reference in New Issue
Block a user