feat(admin): runtime feature flags (catalog-in-code, admin toggle, FE+backend)

Catalog declared in code (Domain/Features/FeatureFlags.cs, build-validated), on/off state
persisted in SQLite (FeatureFlagStore + migration). GET /flags (drives FE gating) + admin
PUT /admin/flags/{key} (new flags:manage capability + FlagsAdmin gate). Enforced end-to-end:
the `inschrijving-open` flag hides the Inschrijven nav item + dashboard action (FE) AND makes
POST /applications for a registratie 403 when off (backend). FE FeatureFlagStore mirrors
AccessStore (enabled() deny-by-default); admin toggle page at /beheer/functies in ADMIN_LINKS.
+4 backend tests, /me cap-list updated, client regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-23 22:29:48 +02:00
co-authored by Claude Opus 4.8
parent ed264be714
commit 67802c68b4
28 changed files with 1154 additions and 52 deletions
@@ -43,7 +43,7 @@ public static class Authz
public static IReadOnlyList<string> RoleCapabilities(Principal principal) => principal.Role switch
{
PrincipalRole.Approver => new[] { "brief:approve", "brief:reject", "brief:send" },
PrincipalRole.Admin => new[] { "orgtemplate:edit", "stamdata:edit", "cases:manage" },
PrincipalRole.Admin => new[] { "orgtemplate:edit", "stamdata:edit", "cases:manage", "flags:manage" },
_ => Array.Empty<string>(),
};
@@ -74,6 +74,9 @@ public static class Authz
/// list + admin delete.
public static bool CanManageCases(Principal principal) => principal.Role == PrincipalRole.Admin;
/// Feature-flag management (WP-47): admin-only, resource-independent — role IS the decision.
public static bool CanManageFeatureFlags(Principal principal) => principal.Role == PrincipalRole.Admin;
/// Field-level PII (PRD-0002 §5c, phase P2): the case screen's BIG-nummer ships
/// masked by default; only the behandelaar (Drafter) composing the case — the actor
/// whose behandel-scherm shows the field — may reveal it. Role-based in the POC; a
@@ -0,0 +1,22 @@
namespace BigRegister.Domain.Features;
/// A known feature flag — DECLARED in code (this catalog, build-validated), TOGGLED at runtime
/// (on/off state persisted in SQLite by FeatureFlagStore). Same split as stamdata (catalog is
/// config-as-code) × org-templates (runtime state in the DB): what flags exist is code; whether
/// they're on is operational config an admin flips.
public sealed record FeatureFlagDef(string Key, string Description, bool DefaultEnabled);
public static class FeatureFlags
{
/// Whether self-service registration (inschrijving) is open. When off, the FE hides the
/// "Inschrijven" action and POST /applications for a `registratie` is refused (server-enforced).
public const string InschrijvingOpen = "inschrijving-open";
public static readonly IReadOnlyList<FeatureFlagDef> Catalog = new[]
{
new FeatureFlagDef(
InschrijvingOpen,
"Zelf-inschrijving in het BIG-register is opengesteld.",
DefaultEnabled: true),
};
}