fix(ci): unbreak backend format, storybook-a11y, and e2e jobs
CI / frontend (push) Successful in 2m12s
CI / backend (push) Successful in 1m37s
CI / e2e (push) Successful in 3m33s
CI / storybook-a11y (push) Successful in 8m23s
CI / semgrep (push) Successful in 1m4s
CI / api-client-drift (push) Successful in 1m52s

- backend: dotnet format the WP-51 migration (2-space indent, no BOM)
  to match .editorconfig — dotnet format --verify-no-changes was failing.
- storybook: stub FeatureFlagStore (WP-47) in shell/site-header stories
  alongside AccessStore, fixing NG0201 no-provider errors; bump the
  storybook-a11y container's memory cap 4g→6g (build-storybook +
  compodoc measured ~5.8GB peak RSS, leaving too little headroom).
- backend: fix a startup-breaking bug in the new (WP-52) POST
  /zgw/notificaties handler — it took ZgwOptions as a minimal-API
  parameter, which isn't registered in DI, so ASP.NET's endpoint-table
  build threw on every request once the route was registered (incl.
  /swagger, which is why Playwright's webServer health check timed
  out). Close over the existing `zgw` local instead.
- e2e: brief-v2.spec.ts's "Voorbeeld" button locator was ambiguous
  once a second "Voorbeeld met testwaarden" button existed (Playwright
  name matching is substring-based) — added `exact: true`. Also fixed
  the sent-letter preview flow to match app-letter-composer's actual
  behavior (single click → fetch, no in-page dialog, unlike
  app-behandel-scherm's), and fixed a watermark assertion that checked
  for the always-present `.preview-watermark` CSS class name instead
  of the conditionally-rendered "VOORBEELD" marker text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-30 07:18:07 +02:00
co-authored by Claude Sonnet 5
parent 274e17cd51
commit c4dd846fbb
8 changed files with 122 additions and 42 deletions
@@ -1,38 +1,38 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BigRegister.Api.Data.Migrations
{
/// <inheritdoc />
public partial class ZaakAndDrcUrls : Migration
{
/// <inheritdoc />
public partial class ZaakAndDrcUrls : Migration
protected override void Up(MigrationBuilder migrationBuilder)
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "DrcUrl",
table: "Documents",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "DrcUrl",
table: "Documents",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ZaakUrl",
table: "Applications",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DrcUrl",
table: "Documents");
migrationBuilder.DropColumn(
name: "ZaakUrl",
table: "Applications");
}
migrationBuilder.AddColumn<string>(
name: "ZaakUrl",
table: "Applications",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DrcUrl",
table: "Documents");
migrationBuilder.DropColumn(
name: "ZaakUrl",
table: "Applications");
}
}
}
+32
View File
@@ -1,3 +1,5 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using BigRegister.Api.Contracts;
@@ -353,6 +355,36 @@ api.MapGet("/admin/cases", (HttpContext ctx, IZaakSource zaken) => CasesAdmin(ct
.Produces<List<ApplicationSummaryDto>>()
.ProducesProblem(StatusCodes.Status403Forbidden);
// OpenZaak's Notificaties API (NRC) calls this on every zaak event once an `abonnement` is
// provisioned (WP-52, out-of-band — see openzaak-integration.md, no app code subscribes it).
// The caller is NRC, not a user: no Principal, so this audits via AuthzAuditStore directly
// rather than the Principal-shaped AuditAuthz helper below. A plain shared secret (not a
// JWT — that's only for this BFF's OUTBOUND ZGW calls) compared in fixed time; an unconfigured
// secret always rejects.
api.MapPost("/zgw/notificaties", (HttpContext ctx, NotificatieDto body) =>
{
var expected = zgw.NotificatieAuthorization;
var actual = ctx.Request.Headers.Authorization.ToString();
var allowed = !string.IsNullOrEmpty(expected)
&& CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(actual), Encoding.UTF8.GetBytes(expected));
var cid = ctx.Items.TryGetValue("CorrelationId", out var v) ? (string)v! : "none";
app.Logger.LogInformation(
"authz action={Action} resource={Resource} decision={Decision} role={Role} correlationId={Cid}",
"zgw:notificatie", body.HoofdObject, allowed ? "allow" : "deny", "nrc", cid);
AuthzAuditStore.Record("zgw:notificatie", body.HoofdObject, allowed, "nrc", cid);
if (!allowed) return Results.Unauthorized();
// ponytail: nothing to invalidate — /admin/cases above already reads IZaakSource fresh
// every call, no cache exists anywhere in this backend. Add real invalidation here if/when
// one is introduced; today a valid notification's only effect is the audit trail proving
// the webhook round-trip works.
return Results.NoContent();
})
// NRC calls this directly, not the FE — same "hand-written, no client codegen" seam as
// /uploads and /brief/reveal-bignummer.
.ExcludeFromDescription();
// Admin delete removes ANY case (any owner, submitted or not) — unlike the user-facing
// DELETE /applications/{id}. A missing id is a 404.
api.MapDelete("/admin/cases/{id}", (string id, HttpContext ctx) => CasesAdmin(ctx, () =>
@@ -0,0 +1,19 @@
using System.Text.Json.Serialization;
namespace BigRegister.Api.Zgw;
/// <summary>
/// The Notificaties API (NRC) webhook body (WP-52) — the standard ZGW notification shape POSTed
/// to a subscribed <c>abonnement</c>'s <c>callbackUrl</c> on every zaak event. Only
/// <see cref="HoofdObject"/> (the zaak's URL — not PII) is read today, for the audit trail; the
/// rest is parsed because it's the real payload shape a live OpenZaak actually sends, not
/// because this endpoint acts on it yet.
/// </summary>
public sealed record NotificatieDto(
[property: JsonPropertyName("kanaal")] string Kanaal,
[property: JsonPropertyName("hoofdObject")] string HoofdObject,
[property: JsonPropertyName("resource")] string Resource,
[property: JsonPropertyName("resourceUrl")] string ResourceUrl,
[property: JsonPropertyName("actie")] string Actie,
[property: JsonPropertyName("aanmaakdatum")] DateTimeOffset Aanmaakdatum,
[property: JsonPropertyName("kenmerken")] Dictionary<string, string>? Kenmerken);
+15 -2
View File
@@ -8,8 +8,9 @@ namespace BigRegister.Api.Zgw;
///
/// The ZGW standard is FIVE separate services, each its own base URL — slice 1 (WP-49) only
/// needed the Zaken API (ZRC) and, to resolve human labels for a zaaktype, the Catalogi API
/// (ZTC). WP-50 (create-zaak) stayed on those two; WP-51 adds the Documenten API (DRC).
/// BRC/NRC arrive with later slices (WP-52+).
/// (ZTC). WP-50 (create-zaak) stayed on those two; WP-51 adds the Documenten API (DRC); WP-52
/// adds the Notificaties API (NRC) — inbound only, see <see cref="NotificatieAuthorization"/>.
/// BRC arrives with a later slice, if ever.
/// </summary>
public sealed class ZgwOptions
{
@@ -52,4 +53,16 @@ public sealed class ZgwOptions
/// URL (Catalogi), so create-document (WP-51) knows which type to register per category —
/// the document analogue of <see cref="ZaaktypeUrls"/>.</summary>
public Dictionary<string, string> InformatieobjecttypeUrls { get; init; } = new();
/// <summary>Notificaties API (NRC) base URL (WP-52) — documentation/provisioning only, no
/// code in this app calls it: subscribing an <c>abonnement</c> is a one-time out-of-band
/// step (see <c>openzaak-integration.md</c>), not something the BFF does at runtime.</summary>
public string NrcBaseUrl { get; init; } = "";
/// <summary>The exact <c>Authorization</c> header value NRC must send on every
/// <c>POST /zgw/notificaties</c> callback (WP-52) — a plain shared secret set into the
/// <c>abonnement</c>'s <c>auth</c> field when provisioning, not a JWT. Empty (the default)
/// means every notification is rejected — an unconfigured secret must never mean "accept
/// anything".</summary>
public string NotificatieAuthorization { get; init; } = "";
}