Skip to content

refactor(frontend): gate analytics + admin nav on permissions - #558

Merged
zaira-bibi merged 2 commits into
mainfrom
zaira/feat/permission-gated-nav
Jul 31, 2026
Merged

refactor(frontend): gate analytics + admin nav on permissions#558
zaira-bibi merged 2 commits into
mainfrom
zaira/feat/permission-gated-nav

Conversation

@zaira-bibi

@zaira-bibi zaira-bibi commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

📚 Stacked on #539 (analytics dashboard) → which is on #533 (GET /permissions/can). Review/merge #533#539 → this; the diff is against #539's branch. Follow-up to the analytics-dashboard nav gating; part of the analytics epic #434.

What

Render the Analytics and Admin sidebar entries through a small config-driven, permission-gated mechanism that mirrors how plugin entries render — replacing the ad-hoc canViewAnalytics boolean (Analytics) and the coarse is_admin flag (Admin).

Changes

  • refactor(frontend): GATED_NAV config + resolveNavPermissions — resolves each gated entry once per token via GET /permissions/can (one check per item, failing closed) into a Record<key, boolean>
  • refactor(frontend): shared NavItem component (extracted from the repeated nav-link blocks)
  • refactor(frontend): the dashboard layout resolves navPermissions once and threads it to both sidebars, which render GATED_NAV.filter(i => navPermissions?.[i.key]).map(NavItem); canViewAnalytics removed
  • refactor(frontend): Admin nav gates on email.whitelist.read (whitelist scope) instead of is_admin
  • fix(frontend): the whitelist page's client guard is aligned to the same email.whitelist.read check (was !is_admin), so nav visibility and page access agree
  • test(frontend): resolveNavPermissions, NavItem, rewritten AppSidebar gating (per key), WhitelistPage guard

How to Test

  1. make test.frontend.vitest → green (190 tests).
  2. cd frontend && bun run typecheck, make lint.frontend, make test.frontend.api → all clean.
  3. Manual (backend + frontend running): a user with analytics.read sees the Analytics entry; a user with email.whitelist.read sees the Admin entry and can open /dashboard/admin/whitelist; a user with neither sees neither entry, and direct navigation to the whitelist page redirects to /dashboard.

Notes

This PR description was written with the assistance of an LLM (Claude).

@zaira-bibi
zaira-bibi force-pushed the zaira/feat/permission-gated-nav branch from 77ad884 to 32cc715 Compare July 27, 2026 12:05
@zaira-bibi
zaira-bibi force-pushed the zaira/feat/permission-gated-nav branch from 32cc715 to 98c3b89 Compare July 27, 2026 12:12
@zaira-bibi
zaira-bibi force-pushed the zaira/feat/permission-gated-nav branch from 98c3b89 to 712e99a Compare July 27, 2026 12:56
@zaira-bibi
zaira-bibi force-pushed the zaira/feat/permission-gated-nav branch from 712e99a to 49bf0c5 Compare July 28, 2026 07:01
@zaira-bibi
zaira-bibi force-pushed the zaira/feat/permission-gated-nav branch from 49bf0c5 to ca82116 Compare July 28, 2026 08:00
@zaira-bibi

Copy link
Copy Markdown
Contributor Author

@claude-review

@claude

claude Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Claude finished @zaira-bibi's task in 2m 22s —— View job


Claude Code Review

Reviewed the diff against origin/zaira/feat/analytics-dashboard-ui (10 files, +322/-72). This is a clean, well-scoped refactor that replaces the ad-hoc canViewAnalytics boolean and the coarse is_admin flag with a config-driven, permission-gated nav mechanism. Nicely done overall — good abstraction, strong test coverage, and the fail-closed behaviour is consistent throughout.

✅ Strengths

  • GATED_NAV + resolveNavPermissions is a clean, extensible pattern — adding a gated entry is now a single config object. Checks run in parallel (Promise.all), fail closed per-item, and log with context (no bare except-style swallowing).
  • NavItem correctly de-duplicates the repeated link markup; keyed by item.key (not array index) ✔.
  • Effect hygiene — both permission effects use an active cleanup flag to avoid post-unmount state updates, and depend only on [token]. Good.
  • Guard/nav alignment — moving the whitelist page guard to the same email.whitelist.read check so nav visibility and page access agree is the right call.
  • Tests cover the config shape, resolveNavPermissions (map/scope pass-through/fail-closed), NavItem rendering, per-key AppSidebar gating, and the WhitelistPage allow/deny guard. Matches the TDD expectation for new functionality.

🟡 Suggestions (non-blocking)

Medium — WhitelistPage fetches before the permission check resolves (inline)
The fetchEntries effect runs on mount while access === "checking", so getWhitelist(token) fires for every user — including those about to be denied (an unauthorized GET /whitelist → 403 → state.error, just before the redirect). Gate it on access === "allowed". Not a security issue (endpoint enforces its own permission), but avoids a wasted request and an error flash. Fix this →

Low — infinite spinner when token is absent
WhitelistPage.tsx:87-100 returns early on !token, leaving access === "checking" forever (permanent "Loading whitelist..." spinner). This relies on the dashboard layout guaranteeing an authenticated token before this page mounts — which appears to hold — but it's a fragile implicit assumption. A brief note or an explicit unauthenticated branch would make it robust.

Low — duplicated spinner JSX
WhitelistPage.tsx:165-174 (the checking spinner) is identical to the state.loading spinner at 176-185. Could be collapsed into one if (access === "checking" || state.loading) branch.

Security

No concerns. UI gating is correctly framed as convenience, not enforcement; every gated entry still fails closed, and the backend enforces each endpoint independently.

Verdict

Approve-worthy pending the maintainer's call on the Medium suggestion. No blocking issues; the fetch-timing item is a small correctness/efficiency polish rather than a defect.
· branch zaira/feat/permission-gated-nav

Comment thread frontend/app/dashboard/admin/whitelist/WhitelistPage.tsx

@regisb regisb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I'm definitely not the right person to review this but as far as I'm concerned, this is good to go.

@zaira-bibi
zaira-bibi force-pushed the zaira/feat/permission-gated-nav branch from ca82116 to 5364ec0 Compare July 28, 2026 17:20

@hamza-56 hamza-56 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice follow-up to #539

Comment thread frontend/app/dashboard/admin/whitelist/WhitelistPage.tsx
Comment thread frontend/components/AppSidebar.tsx Outdated
Comment thread frontend/components/AppSidebar.tsx
Comment thread frontend/app/dashboard/admin/whitelist/WhitelistPage.tsx
Comment thread frontend/app/dashboard/layout.tsx Outdated
Comment thread frontend/components/NavItem/NavItem.tsx
Comment thread frontend/app/dashboard/layout.tsx Outdated
@zaira-bibi
zaira-bibi force-pushed the zaira/feat/permission-gated-nav branch 3 times, most recently from 6be06db to 494a62d Compare July 31, 2026 08:41
@zaira-bibi
zaira-bibi force-pushed the zaira/feat/permission-gated-nav branch from 494a62d to c9260c0 Compare July 31, 2026 16:46
Base automatically changed from zaira/feat/analytics-dashboard-ui to main July 31, 2026 17:23
zaira-bibi and others added 2 commits July 31, 2026 22:23
Render the Analytics and Admin sidebar entries through a small
config-driven, permission-gated mechanism that mirrors how plugin
entries render, instead of the ad-hoc canViewAnalytics boolean
(Analytics) and the coarse is_admin flag (Admin). A GATED_NAV config
declares each entry's (permission, scope); resolveNavPermissions
resolves them once per token via GET /permissions/can (one check per
item, failing closed) into a map the dashboard layout threads to the
sidebars, which render the permitted entries through a shared NavItem.

Admin now gates on email.whitelist.read (whitelist scope) rather than
is_admin, and the whitelist page's own client guard is aligned to the
same permission so nav visibility and page access agree. UI gating
fails closed and is not a security boundary — endpoints still enforce
their own permissions and return 403.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The layout test added alongside the analytics dashboard mocks checkPermission
and asserts the canViewAnalytics boolean, both of which this branch replaces
with resolveNavPermissions and a navPermissions map — so all three cases
failed once the branch was rebased onto it. Assert the resolved map threaded
to the sidebar instead, keeping each original intent, including the guard that
a failed re-resolve after a token change cannot leave the previous user's
entries in place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@zaira-bibi
zaira-bibi force-pushed the zaira/feat/permission-gated-nav branch from c9260c0 to 05b8440 Compare July 31, 2026 17:23
@zaira-bibi
zaira-bibi merged commit 89ee6bc into main Jul 31, 2026
6 checks passed
@zaira-bibi
zaira-bibi deleted the zaira/feat/permission-gated-nav branch July 31, 2026 17:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants