feat/mod-gates
needs attentionf9f2669 · incrementalPR #294reviewed 2026-07-10 16:48 UTC0H · 2M · 4L · 5I- Purpose
- Fix prod bug where enabling Tarifas per-org had no visible effect for non-admin members; extend per-org module entitlements to Sites and Assets.
- Goal
- Make soft-launch gate entitlement-aware so platform admins can activate/deactivate all three gated modules per organization with immediate effect.
- Sub-goals
- SG-1: ORG_MODULES + OrgModuleKeySchema widened (tarifas, sites, assets)
- SG-2: soft-launch.ts entitlement-aware; guard + layout + sidebar + admin toggles + tests
- SG-3: Preview test bed (in progress)
- What
- Review-fix pass: OrgModuleKey type import replacing local union; tri-state enabledModules signal consolidated in dashboard layout; credentials tab now receives enabledModules for tab visibility; sidebar requiredModule logic fixed to === 'live' (was negated 'greyed'/'hidden'); GATED_PATH_MODULES gets explicit OrgModuleKey type annotation; doc updates.
- Why
- Second commit tightens type safety, fixes a sidebar requiredModule bug (hidden items were doubly-gated), consolidates the enabledModules derivation to a single signal in layout.tsx, and ensures credentials tab visibility matches SoftLaunchGuard behavior for entitled orgs.
- Areas
- .claude/rules/api-patterns.md+13−0apps/platform/CLAUDE.md+1−1apps/platform/src/app/[locale]/(dashboard)/admin+12−8apps/platform/src/app/[locale]/(dashboard)/credentials+12−6apps/platform/src/app/[locale]/(dashboard)+18−12apps/platform/src/components/DashboardSidebar.tsx+18−16apps/platform/src/lib/soft-launch.ts+26−3
- Blast
- 7 files, +100/−46 in this commit; cumulative branch 13 files, +256/−63. All changes contained to platform app UI layer and documentation — no schema migrations, no domain logic, no API contract changes.
Findings · 12
security4
requiredModule fix is security-neutral — client-side UX, by design
apps/platform/src/components/DashboardSidebar.tsx:94
Old/new code produce the same visible-nav outcome for every nav state. Hidden items are excluded either via the hidden flag (new) or the requiredModule entitlement filter (old). Net security delta: zero. The system's declared model is client-side UX; SoftLaunchGuard at the layout level is the route gate.
useOrganization(user?.orgId) with undefined orgId safely disables the query
apps/platform/src/app/[locale]/(dashboard)/credentials/layout.tsx:25
The hook has `enabled: !!publicId` — when user?.orgId is undefined, the query is disabled and no request is made. No risk of fetching with an undefined org ID or hitting another org.
enabledModules sourced from server-enforced org membership — no cross-org spoofing possible
apps/platform/src/app/[locale]/(dashboard)/layout.tsx:76
GET /api/organizations/:publicId calls requireOrgAccess(..., 'member') before returning data. A user cannot receive another org's enabledModules by supplying a foreign publicId — they would fail the membership check and receive 403/404.
Fail-closed tri-state on org fetch error correctly degrades to deny
apps/platform/src/app/[locale]/(dashboard)/layout.tsx:76
orgError → enabledModules = [] → gated routes redirect to /bills/contratos. Correct security posture for a fail-closed gate.
conventions2
`import type` comment explains WHAT, not WHY
apps/platform/src/app/[locale]/(dashboard)/admin/_components/OrgDetailPanel.tsx:44
The first sentence ('Type-only import — erased at compile time, safe for the client bundle') restates what `import type` communicates by name. The genuinely non-obvious part is the second sentence (forbidden to import ORG_MODULES *value*). Trim to the non-obvious WHY only.
GATED_PATH_MODULES loses literal-string narrowing after removing `as const`
apps/platform/src/lib/soft-launch.ts:44
With `as const` a path-string typo (e.g. '/sties') would fail to compile. With the explicit `readonly (readonly [string, OrgModuleKey])[]` annotation, path strings widen to `string` — typos are silently accepted. Safe for 5 entries; worth restoring `as const` if the table grows.
tests3
requiredModule fix in DashboardSidebar untested at component level
apps/platform/src/components/DashboardSidebar.tsx:94
The fix (`=== 'live' ? 'sites' : undefined` replacing `=== 'greyed' ? undefined : 'sites'`) is correct, but the downstream `visibleNavItems` filter that consumes `requiredModule` has no component test. The old code had a concrete bug for `hidden` modules (item was doubly-gated by entitlement AND hidden flag); the new code is correct. Without a test, any future refactor of the ternary direction reintroduces the bug silently. A narrow sidebar unit test covering all three nav-state values per module would pin this.
credentials/layout showMetricsTab wiring fix has no callsite test
apps/platform/src/app/[locale]/(dashboard)/credentials/layout.tsx:36
The pure `isGated('/credentials/metrics', false, ['assets'])` contract is covered by soft-launch.test.ts. What's missing is a component/render test that verifies `CredentialsLayout` actually passes `org?.enabledModules` to `isGated`. The bug being fixed had exactly this shape — missing argument at a specific callsite. A narrow RTL render (mock useOrganization returning enabledModules: ['assets'], assert Metrics tab present) would close the regression window.
Tri-state enabledModules derivation in layout.tsx has no unit test
apps/platform/src/app/[locale]/(dashboard)/layout.tsx:76
The three-branch expression is minimal and fail-closed. Pure underlying functions (isGated, navState) are thoroughly tested. Acceptable to leave without a test — noted for completeness.
improvement3
credentials/layout re-fetches org data via implicit cache-key contract
apps/platform/src/app/[locale]/(dashboard)/credentials/layout.tsx:27
The sub-layout calls `useOrganization(user?.orgId)` independently and relies on an inline comment for correctness ('same query key + staleTime as the dashboard layout — served from cache'). This is a fragile implicit contract: if the dashboard layout's query key or staleTime ever changes, this silently refetches or returns stale data. The right fix is to thread `enabledModules` through context (extend `OrganizationContext` or add a thin `ModuleEntitlementContext`). Not a current runtime bug, but a maintainability risk.
Double null-coalescing: `enabledModules ?? []` passed to sidebar that already does `?? []` internally
apps/platform/src/app/[locale]/(dashboard)/layout.tsx:161
DashboardSidebar already does `const modules = enabledModules ?? []` at line 78. The external `?? []` collapses the tri-state before it enters the sidebar, masking the undefined-in-flight signal. Passing `enabledModules` directly (without `?? []`) would be cleaner. Not incorrect — sidebar currently renders greyed correctly either way.
Rename `orgModules` → `modules` loses specificity
apps/platform/src/components/DashboardSidebar.tsx:78
`orgModules` communicated the value is the org's enabled modules vs platform flags. `modules` is generic. In a component that mixes platform-admin bypass with per-org entitlement logic, the original name was more self-documenting. Cosmetic, no behavior change.