Skip to content

feat(fabric-admin): Fabric Admin section with API token generator#1533

Merged
DavidCockerill merged 5 commits into
stagefrom
feat/fabric-admin-section
Jul 22, 2026
Merged

feat(fabric-admin): Fabric Admin section with API token generator#1533
DavidCockerill merged 5 commits into
stagefrom
feat/fabric-admin-section

Conversation

@DavidCockerill

Copy link
Copy Markdown
Member

What

Adds a Fabric Admin section to Studio — an extensible home for internal admin tooling — and ships its first tool: a short-lived API Token generator.

  • New /fabric-admin area (src/features/fabricAdmin/): a layout shell (FabricAdminShell) with a sub-nav rail and an <Outlet/>, so future admin tools drop in as sibling routes.
  • API Token generator (apiToken/index.tsx): a Generate button that calls POST /Admin/ApiToken and shows the returned token (read-only field + copy) with its expiry and a short usage note. The token authenticates as the signed-in admin, with their permissions.
  • Access gating: the section and its navbar entry are shown only to fabric admins (useAdminMode()fabricRole of fabric_admin/super_user) and are hidden entirely in local Studio.
  • 401 handling relies on the sibling PR fix(auth): sign out and redirect on 401 from the CM API #1523 (sign out + redirect on a 401 from the CM API), so an expired admin session is handled gracefully.

Why

We're forcing internal fabric_admin accounts onto Google SSO (see the companion central-manager PR). SSO gives humans a browser session but no password for programmatic/API access. This page lets an admin mint a short-lived, SSO-derived token for CLI/API use without reintroducing a password. It also gives us a place to grow the rest of the admin surface.

Depends on / related

  • central-manager fabric-admin-auth-hardening — provides the POST /Admin/ApiToken endpoint this page calls (plus the SSO-session hardening). That endpoint must ship first, or Generate returns 404.
  • Studio fix(auth): sign out and redirect on 401 from the CM API #1523 — 401 auto-logout interceptor (the safety net for expired admin sessions).

Testing

  • vitest run — full suite green (241 files, 1722 passing).
  • tsc -b, oxlint, dprint check — all clean.
  • Route-tree wiring follows the getParentRoute/addChildren lockstep rule (CLAUDE.md).
  • Note: end-to-end happy-path (real Google SSO → Generate) needs this branch deployed to dev; local Studio can't complete the remote OAuth callback.

Notes

  • Drafted with assistance from Claude (Opus 4.8); reviewed by David before opening.

New top-level "Fabric Admin" area, gated to fabric admins / super users,
built as an extensible section (layout + sub-nav rail) so future admin
endpoints slot in as sub-pages. First page generates a short-lived API
token for programmatic access.

- Navbar: fabric-admin-gated "Fabric Admin" item (isAdminMode).
- routes: fabricAdminLayoutRoute (under dashboardLayout) + api-token index,
  wired into rootRouteTree keeping getParentRoute/addChildren in lockstep.
- FabricAdminShell: SubNavRail shell, render-gated via useCloudAuth/isAdminMode
  (dashboard guard still handles the unauthenticated redirect).
- ApiToken page: generate button -> POST /Admin/ApiToken, shows the token
  (read-only field + copy) with expiry and a "copy it now" note.
- ApiTokenResult type in api.patch.d.ts; endpoint isn't in generated types
  so the mutation casts the URL + response (like getCurrentUser).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces a new Fabric Admin section, allowing administrators to generate short-lived API tokens for programmatic access. It includes the addition of a 'Fabric Admin' link in the navigation bar, a shell layout with sub-navigation, and the API token generation interface and mutation. Feedback on the changes suggests removing the local 'onError' handler from the token generation mutation to prevent duplicate error notifications, and correcting the 'variant="submit"' prop on the generate button to 'type="button"'.

Comment thread src/features/admin/apiToken/index.tsx
Comment thread src/features/admin/apiToken/index.tsx
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 51.81% 5544 / 10700
🔵 Statements 52.36% 5933 / 11330
🔵 Functions 44.24% 1369 / 3094
🔵 Branches 44.81% 3726 / 8315
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/components/Navbar.tsx 30% 23.07% 15.78% 32.43% 32-42, 98-121, 164-303
src/features/admin/routes.ts 71.42% 100% 50% 71.42% 14-15
src/features/admin/apiToken/mutations/useGenerateApiToken.ts 100% 100% 100% 100%
src/features/admin/components/AdminShell.tsx 14.28% 0% 0% 20% 17-32
src/hooks/useAuth.ts 43.47% 50% 46.15% 47.36% 17-26, 58-64
src/router/rootRouteTree.ts 100% 50% 100% 100%
Generated in workflow #1566 for commit 21cfcd1 by the Vitest Coverage Report Action

@DavidCockerill
DavidCockerill marked this pull request as ready for review July 17, 2026 18:09
@DavidCockerill
DavidCockerill requested a review from a team as a code owner July 17, 2026 18:09
Review feedback (Gemini): the global MutationCache.onError in
react-query/queryClient already toasts mutation failures, so the local
onError produced a duplicate toast.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@kriszyp kriszyp 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.

I see there are comments about super user; I don't know if a super user is really a valid concern, if you manage to login as the super user, I assume you don't need a token. So I think this looks good.

Comment thread src/features/fabricAdmin/components/FabricAdminShell.tsx Outdated
Comment thread src/components/Navbar.tsx Outdated
…ription

Review feedback (Kris): isAdminMode includes super_user, but the token
endpoint requires an SSO session only fabric_admin accounts have — a
password-authenticated super user would see the page and only ever get
403. New isFabricAdmin helper (accepts the cloud/local user union)
gates the shell and navbar item. Navbar now derives it from its
existing useOverallAuth() subscription instead of useAdminMode(),
which added a second authStore listener per Navbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dawsontoth

Copy link
Copy Markdown
Contributor

I can't verify this locally since fabric admins must use Google auth, even locally... pushing to dev, though I don't think I have a user there either... how did you verify locally, David?

@dawsontoth dawsontoth 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.

Let's just call it Admin, drop Fabric from the name. We're already on Fabric, that's understood. And there's no Admin for anyone except us, so it's safe ambiguation of the term.

Review feedback (Dawson): we're already on Fabric and there's no admin
area for anyone but us, so "Admin" is unambiguous. Renames the route
(/fabric-admin -> /admin), nav label, section heading, feature dir
(features/fabricAdmin -> features/admin), and FabricAdminShell ->
AdminShell. The Harper role identifier (fabric_admin / fabricRole /
isFabricAdmin) is unchanged — that's the actual role name, not the
section label — and it now aligns with CM's existing /Admin namespace.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@DavidCockerill

Copy link
Copy Markdown
Member Author

Done in 7446b78 — renamed to Admin throughout: route /fabric-admin/admin, the nav label and section heading, the feature dir (features/fabricAdminfeatures/admin), and FabricAdminShellAdminShell. Left the Harper role identifier alone (fabric_admin / fabricRole / isFabricAdmin) since that's the actual role name, not the section label — and as a bonus "Admin" now lines up with CM's existing /Admin namespace. Full suite green (1722), tsc/lint/format clean.

On verifying locally — you're right that you can't, and neither could I do a true end-to-end run: real Google SSO won't complete against remote CM from localhost (the OAuth callback lands on the dev domain). What I checked locally was appearance only — I seeded a fake admin user and stubbed the generate-token mutation for a pure-visual pass, then reverted it. The happy path (real SSO → mint) was never exercised locally.

The actual unblock for "fabric admins must use Google even locally" is the CM side: central-manager#459 adds FABRIC_ADMIN_SSO_ENFORCED, which is off in local/dev, so a fabric admin can password-login there and click through the whole Admin tab (including minting a token) with no Google. The catch: #459 isn't deployed to dev yet. So the real verification path is — once #459 lands on dev, sign in on dev studio with a dev fabric_admin account + password and exercise it there. If you don't have a dev fabric-admin user, David can grant one via PATCH /Admin/User (or alter_user) on dev. Happy to walk through it once #459 is up.

— Claude Opus 4.8 (on behalf of @DavidCockerill)

@kriszyp kriszyp 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.

Looks like there is an edge case to cover, but looks good.


Proposed inline comments (anchors failed):

  • src/features/fabricAdmin/apiToken/mutations/useGenerateApiToken.ts:17: Could we keep this bearer token out of the global mutation cache once the page stops using it? Each mutate stores the successful ApiTokenResult; generating again or unmounting only makes the old entry inactive, and logout currently clears QueryCache but not MutationCache, so the credential remains readable through queryClient.getMutationCache() until GC. Setting gcTime: 0 here would discard each token as soon as its observer is removed. If we want the token to live only in ApiTokenIndex state even while mounted, we can also call the mutation's reset() after copying the success result into state.
  • src/hooks/useAuth.ts:54: Could we pin the role boundary introduced in the follow-up commit? A small table test for isFabricAdmin (fabric_admin true; super_user, least_privileged, a local user, and null false) would prevent the exact regression this re-review fixed. The current mutation test mocks only the transport and cannot catch the route being exposed to password-authenticated super_user users again.

@kriszyp kriszyp 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.

Another suggestion

}

export function useGenerateApiTokenMutation() {
return useMutation<ApiTokenResult, Error, void>({ mutationFn: generateApiToken });

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.

Could we avoid returning the bearer credential through React Query mutation state? useMutation stores successful data in the global MutationCache; with the browser default it remains there for five minutes after this route unmounts, and logoutOnSuccess() clears only the QueryCache. That means a generated operationToken survives navigation/logout in memory (and is directly shown in the mutation inspector in development builds). Please keep the secret out of shared mutation state, or reset/remove this mutation with immediate GC after moving the value into local component state, and add a test asserting the cache no longer contains operationToken after success/unmount/logout.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 21cfcd1. The mutation now sets gcTime: 0, and the page calls reset() right after copying the token into local component state — so the credential lives only in ApiTokenIndex state, is dropped from the shared MutationCache immediately, and can't survive navigation/logout (or show in the mutation inspector). Added a test that generates a token and asserts the MutationCache contains no operationToken after success and is empty after unmount.

— Claude Opus 4.8 (on behalf of @DavidCockerill)

…oundary

Review feedback (Kris):
- The minted token is a bearer credential but was stored in React Query's
  global MutationCache — readable via getMutationCache(), shown in the
  mutation inspector, and NOT cleared on logout (logoutOnSuccess clears
  only the QueryCache). Set gcTime:0 and reset() after copying it into
  local component state, so it lives only in ApiTokenIndex state. Test
  asserts the cache holds no token after success and is empty after unmount.
- Add an isFabricAdmin table test pinning the role boundary (fabric_admin
  true; super_user / least_privileged / local user / null false) so the
  section can't be re-exposed to a password-auth super_user.

Full suite 1725 passing; tsc/lint/format clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@DavidCockerill

Copy link
Copy Markdown
Member Author

Both of your suggestions are in (21cfcd1):

  • Token out of the MutationCachegcTime: 0 + reset() after copying into local state; test asserts the cache holds no token after success and is empty after unmount (replied inline).
  • isFabricAdmin boundary test (your earlier note; the inline anchor had failed) — added a table test: fabric_admin→true; super_user/least_privileged/local-user/null→false, so the section can't be re-exposed to a password-auth super_user without a test catching it.

Full suite 1725 green; tsc/lint/format clean. Thanks for the careful passes.

— Claude Opus 4.8 (on behalf of @DavidCockerill)

@DavidCockerill
DavidCockerill added this pull request to the merge queue Jul 22, 2026
Merged via the queue into stage with commit 81b2cd8 Jul 22, 2026
2 checks passed
@DavidCockerill
DavidCockerill deleted the feat/fabric-admin-section branch July 22, 2026 13:52
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.

3 participants