feat(registratie): WP-36 — admin cases page + admin delete

Admin-only overview of all cases across owners + an admin delete, gated by a new
`cases:manage` capability (Authz role→cap + CanManageCases + CasesAdmin gate;
FE capability + guard + nav + role.interceptor prefix — the org-template/stamdata
recipe). Backend adds ApplicationStore.ListAll()/DeleteAny() and GET /admin/cases +
DELETE /admin/cases/{id}; admin delete removes ANY case incl. submitted. Page lives
in registratie/ui (owns the Aanvraag aggregate; reuses aanvraag-view + parse),
routed /beheer/zaken; delete guarded by a native confirm, optimistic with rollback.
Typed client regenerated (documents the new endpoints + owner field).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-23 12:23:34 +02:00
co-authored by Claude Opus 4.8
parent d1abd35b0d
commit 446ea9474b
23 changed files with 786 additions and 9 deletions
+32
View File
@@ -309,6 +309,27 @@ api.MapPost("/applications/{id}/submit", (string id, SubmitApplicationRequest re
.ProducesProblem(StatusCodes.Status409Conflict)
.Produces(StatusCodes.Status404NotFound);
// --- Admin cases (WP-36): cross-owner list + admin delete, gated by `cases:manage`. ---
api.MapGet("/admin/cases", (HttpContext ctx) => CasesAdmin(ctx, () =>
{
var now = DateTimeOffset.UtcNow;
return Results.Ok(ApplicationStore.ListAll().Select(a => a.ToAdminSummaryDto(now)).ToList());
}))
.Produces<List<ApplicationSummaryDto>>()
.ProducesProblem(StatusCodes.Status403Forbidden);
// 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, () =>
{
if (!ApplicationStore.DeleteAny(id)) return Results.NotFound();
app.Logger.LogInformation("admin case delete id={Id}", id);
return Results.NoContent();
}))
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status404NotFound)
.ProducesProblem(StatusCodes.Status403Forbidden);
// PRD-0002 §6: coarse, role-derived capabilities for nav/menu-level checks (NOT
// tied to a specific brief's live status — see BriefDecisionsDto for that).
api.MapGet("/me", (HttpContext ctx) => new MeDto(Authz.RoleCapabilities(Authz.ResolvePrincipal(ctx))))
@@ -515,6 +536,17 @@ IResult StamdataAdmin(HttpContext ctx, Func<IResult> action)
statusCode: StatusCodes.Status403Forbidden);
}
// One gate for every admin-cases endpoint — the enforce twin of the `cases:manage`
// capability RoleCapabilities emits (single Authz source, WP-36). A denial is audited.
IResult CasesAdmin(HttpContext ctx, Func<IResult> action)
{
var principal = Authz.ResolvePrincipal(ctx);
if (Authz.CanManageCases(principal)) return action();
AuditAuthz(ctx, "cases:manage", "cases", false, principal);
return Results.Problem(detail: "Alleen een beheerder mag aanvragen beheren.",
statusCode: StatusCodes.Status403Forbidden);
}
static StamdataColumnDto ToColumnDto(StamdataColumn c) => new(c.Name, c.Type, c.IsKey, c.Options);
// Authorization audit (PRD-0002 §8): access-relevant decisions recorded with NO PII —