Repository navigation
feat(admin): mint API tokens for a staff owner with a role and lifetime, list and revoke them - #1992
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive management interface for Admin/Staff API tokens, replacing the previous simple token generation mechanism. It adds features to mint tokens with specific roles, owners, and lifetimes, as well as capabilities to list, filter, and revoke active tokens. Key additions include the MintStaffApiTokenForm, StaffUserPicker, and StaffApiTokenTable components, along with corresponding React Query hooks, Zod validation schemas, design documentation, and extensive unit tests. I have no review comments or feedback to provide as no issues were identified.
ea7d9dc to
7d160a0
Compare
7d160a0 to
e1dff41
Compare
…me, list and revoke them Replaces the single "Generate token" button with central manager's new staff token model (HarperFast/central-manager#973): an admin picks the owner by email, a role, a name and a lifetime bounded by the role's cap; the token is shown once and kept out of the mutation cache as before. Every staff role sees its own tokens with their status and can revoke them; admins see everyone's. Refs #1985 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Nothing central manager exposes says which staff role another account holds, so the mint's 403 for a role wider than the owner's, which names the owner's role, is placed under the role field; every other failure toasts as before. Refs #1985 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…past customers Review round 1: matches now belong to the prefix the typing settled on and Enter never reaches the form; the staff search reads pages until staff turn up among customers sharing the prefix; a failed user lookup falls back to the id without a toast; the rail shows the page on apiToken:self or apiToken:create; the mutation-cache note no longer claims sign-out keeps the cache; dead exports and narrating comments removed; a form test covers the mint path and the refusal. Refs #1985 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tcome, revoke unseen tokens Review round 2: a refusal clears when the owner or role changes; the list refreshes on settle so a mint whose answer was lost still shows; a token minted after the page was left is revoked; the picker only searches while open, Enter never submits even after Escape, and a search that ran out of pages says to type more; the signed-in account is named without a lookup. Refs #1985 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…onestly, stop stale paging Review round 3: the "type more" hint shows whenever the search ran out of pages, matches or not; an unseen-token revoke whose answer was lost goes through the shared error handler and refreshes the list; clearing the owner no longer flags an empty owner; the search honours the query's abort signal. Refs #1985 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…oken stays live Review round 4: closing the picker cancels the search in flight (disabling the query alone does not abort it); a refused unseen-token revoke says the token is still live and where to revoke it, while a lost answer keeps the shared wording; a mint that failed after the page was left still toasts. Refs #1985 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…apiToken:self Closing review round: the apiToken:create fallback would show the page on a central manager that also lacks the list endpoint and the owner-aware mint, so the form would mint the admin a self token whatever it asked for. The page waits for the permission that ships with the API it drives. Narrating comments trimmed. Refs #1985 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Round 6: an API without staffPermissions at all predates the token API this page drives, so the fallback that grants fabric_admin every permission must not show it. Duplicate test comments removed. Refs #1985 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Closing round: a search that stops at 20 staff with a full page behind it is reported as truncated, so the 21st colleague is not read as nonexistent. Comments that duplicated the design note trimmed. Refs #1985 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Round 8: minting again while a card was showing replaced that secret before it was copied, leaving a live token nobody could use. The form now waits for the card to be dismissed. Three narrating comments trimmed. Refs #1985 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…oke finds nothing Round 9: a 404 on the post-unmount revoke meant "still live; revoke it from the list", sending the admin after a token that does not exist. Narrating comments trimmed. Refs #1985 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…g wildcard Found by the live smoke against the rig: the deployed Harper line (5.3) reads a trailing `*` as a literal, so the owner picker never matched anyone. The FIQL `email=starts_with=<prefix>` form works there and on main. Refs #1985 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
a8b5704 to
aab467a
Compare
…nd shorten the dates The table overflows sideways on a normal viewport, which scrolled the one control off the right edge; it now leads the row, and the three timestamps drop their seconds. Refs #1985 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s and an icon revoke Name and role share a cell, the minter sits under the owner, Created, Expires and Last used read as a stamp with "in 13 days" beneath, ended rows dim as a whole, and Revoke is a leading icon button with a tooltip. Refs #1985 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…out a sideways scroll The cell default is nowrap, so the table could only overflow. Cells that hold prose now wrap, the owner folds under the token name below lg, Last used and Created leave at xl and 2xl, and Revoke is a labelled button again. Refs #1985 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…u for Revoke Token name over its masked id, owner and minter as an initial with name and address, role as a coloured dot, a date with its relative reading, a status badge, and a "⋮" menu that copies the id or revokes a live token behind the confirm. The owner folds under the token below lg; Last used and Created leave at xl and 2xl, so the table never scrolls sideways. Refs #1985 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s wrap The role column joins the owner under the token name below md, dates may wrap below sm, and a user's name and address wrap anywhere rather than truncating, which had fixed the column's minimum at the whole name. Refs #1985 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…such Cells had no horizontal padding, so the first column sat on the border. A 404 on the owner or minter lookup now reads "Removed account" over the id instead of a bare question mark; other failures still show the id. Refs #1985 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…all a 404 clean-up done Review: two submits entering the handler before the first answer sent two mints and replaced the first secret; a synchronous lock now holds until the response is handled. A 404 on the post-unmount revoke is reported as an unconfirmed clean-up naming the token and owner, since central manager answers 404 for another owner's token when the caller lacks apiToken:create. Refs #1985 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Addressed both review findings in bb63d30: a synchronous submit lock on the mint form (with a racing-double-submit test that fails without it), and a 404 on the post-unmount revoke now reported as an unconfirmed clean-up naming the token and owner rather than "already gone". Gates are green; re-requesting review. 🤖 Generated by Anthropic Claude (Fable 5.1) with Claude Code; posted via @DavidCockerill. |
…y what the row shows Review: a background refresh that failed replaced the table, and its revoke menus, with an error; the last known rows now stay under a warning with Retry. The filter box matches the masked and full token id and the owner's and minter's address and name, resolved through the cells' own lookups. Refs #1985 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Addressed the two remaining review findings in a064a1e: a failed background refresh keeps the last known tokens (and their menus) on the page with a Retry, and the filter box matches the shown id and the owner's and minter's address. Gates are green; re-requesting review. 🤖 Generated by Anthropic Claude (Fable 5.1) with Claude Code; posted via @DavidCockerill. |
⊙ Problem
The Admin → API Token page is one "Generate token" button: a one-hour token acting as the signed-in admin, never listed, never revoked. Central manager now mints staff tokens for a named owner with a role and a role-capped lifetime, and lists and revokes them (HarperFast/central-manager#973, PR HarperFast/central-manager#975). The page had to catch up.
🤔 Is a whole page warranted, or would the button do?
Yes: the owner, the role and the lifetime are the point of HarperFast/central-manager#973, and a token nobody can see or revoke is what it replaces. The page is the Studio half of that issue (#1985) and ships with it; the old empty-body mint still works on the server, so nothing here is urgent on its own.
💡 Solution
Admin/User, "Me" shortcut), role (least privilege first), name, and a lifetime in hours bounded by the role's cap mirrored from central manager.apiToken:self(every staff role), the form only toapiToken:create: staff who cannot mint still see and revoke their own tokens.🤔 Why does the role picker offer every role when the server refuses one wider than the owner's?
Nothing central manager exposes says which staff role another account holds; exposing it on
Admin/Useris a central manager follow-up. So the picker offers every role, defaults to read-only, and the server's refusal, which names the owner's role, is shown under the Role field. One field on the user record reverses this.🤔 Which review findings were accepted rather than fixed?
Sign-out mid-mint can leave an unheld token live until it expires or idles 5 days: the clean-up revoke has no session; the token stays listed and revocable. Labels cost one cached lookup per other staff id. post-review delta: the 404 revoke wording, comment trims and the
starts_withquery, not re-reviewed — narrow fixes inside code every full pass read.⚖️ Alternatives
david/576-org-api-tokensbranch, not onstage; fold together once both land.🔧 Changes
Product and architecture tour
Product and architecture tour
What the page does now
The mint, from form to list
From Mint to the show-once card
The secret goes to the page's state before the mutation forgets it; a token minted after the page was left gets a revoke attempt, and the admin is told when that attempt fails.
sequenceDiagram participant Admin participant Form as MintStaffApiTokenForm participant CM as central manager participant Page as ApiTokenIndex Admin->>Form: owner, role, name, lifetime Form->>CM: POST /Admin/ApiToken/ alt 403 role wider than the owner holds CM-->>Form: refusal naming the owner's role Form->>Admin: message under the Role field else minted CM-->>Form: token with its secret Form->>Page: onMinted (page state) Form->>Form: reset() the mutation Page->>Admin: show-once card Form->>CM: GET /Admin/ApiToken/ (list refresh) else minted after the page was left Form->>CM: DELETE /Admin/ApiToken/:id Form->>Admin: warning toast endLifetime caps mirror central manager's
STAFF_API_TOKEN_LIFETIMES:fabric_readonlyup to 14 days (default 14),fabric_supportandfabric_staffup to 7 days (default 7),fabric_adminup to 12 hours (default 1). The server refuses longer lifetimes; the form caps the input.Choosing an owner
The user table holds customers and staff and offers no "ends with" match. The picker reads
GET /Admin/User/?email=starts_with=<prefix>page by page until it finds staff, keeps only the staff domain, and asks for more typing when it runs out of pages or hits its limit. Results match the settled prefix; Enter never reaches the mint form; closing cancels the search.Central manager does not expose another account's staff role. The role list offers every staff role; the server's refusal names the owner's role and appears under the Role field.
The list
Statuses: Active authenticates and can be revoked here; Idle went unused for 5 days before its expiry and is refused; Expired reached its expiry; Revoked was revoked here and stays listed for 30 days. Owner and minter ids are rendered as addresses through
GET /Admin/User/:id, one cached lookup per distinct id other than the signed-in account; a failed lookup shows the id.Files
index.tsx— the page: the form gated onapiToken:createand held while a card is showing, the show-once card, the filterable table, whose filter matches the shown id and the owner's and minter's address and which keeps the last known rows, with a Retry, through a failed refresh.MintStaffApiTokenForm.tsx— react-hook-form + zod form; changing the role resets the lifetime to that role's default; the server's role refusal lands under the role field and clears when the owner changes, other failures toast; a second mint waits while a secret is on screen; a second submit cannot race the first; a token minted after the page was left is revoked, with a refusal, a lost answer and a 404 each worded for what they mean, a 404 being an unconfirmed clean-up that names the token and owner.StaffUserPicker.tsx— debounced email-prefix search with a keyboard-navigable listbox; matches belong to the settled prefix only, it searches only while open and closing cancels the search, Enter never reaches the form, and a search that ran out of pages says to type more beside whatever it found.StaffApiTokenTable.tsx— the table, laid out like the console: token name over its masked id, a status badge per row, columns that leave as the viewport narrows instead of scrolling sideways, and a row menu at the end;StaffUserCell.tsxshows a user as an initial, name and address (a removed account says so over its id, any other failed lookup shows the id, the signed-in account needs none);StaffApiTokenRowMenu.tsxcopies the id or revokes a live token behind the confirm;StaffUserLabel.tsxnames the owner inline on the card;MintedTokenCard.tsxis the show-once card with a Done that drops the secret.staffTokenRoles.ts— the role list and the lifetime caps mirrored from central manager;mintTokenSchema.ts— the form schema whose lifetime cap follows the role.getStaffApiTokens.ts— the list query;staffUsers.ts— the staff search that pages past customers, keeps the staff domain and reports a cut at the limit, and the user lookup that reports a removed account or falls back to the id.useGenerateApiToken.ts— the mint: typed payload,gcTime: 0, opts out of the global toast, refreshes the list on any outcome;useRevokeApiToken.ts— the revoke that marks the cached row revoked at once, then refetches.AdminShell.tsx— the rail item is "API Tokens", shown onapiToken:selfand never through the role-name fallback for an API withoutstaffPermissions;routes.ts— the page title.api.patch.d.ts—StaffApiToken,MintedStaffApiToken,StaffApiTokenStatus;apiToken:selfadded toStaffPermission.DESIGN.md— the secret-hygiene invariant, the mirrored caps, the owner-role gap, the permission gating.✅ Verification
david/staff-api-tokens; every step below passed, and it caught the*-vs-starts_withquery bug fixed here.Live smoke steps
DELETE /Admin/ApiToken/:idanswered 200 and the row flipped to Revoked.*as a literal, so the owner search matched nobody; it now sendsemail=starts_with=<prefix>, which also works on main.Unit tests
staffTokenRoles.test.ts— caps pinned to central manager's numbers.mintTokenSchema.test.ts— the lifetime cap follows the role.getStaffApiTokens.test.ts,staffUsers.test.ts— request shapes, staff-domain filter, paging past customers, truncation, silent lookup fallback.useGenerateApiToken.test.ts— payload, cache hygiene, list refresh on any outcome.index.test.tsx— cached rows and a Retry survive a failed refresh; the filter matches the owner address and the masked id.StaffApiTokenRowMenu.test.tsx— Revoke only for a live token, copy the id, one revoke for two quick confirms.StaffUserCell.test.ts— initials from the name, else the address.StaffApiTokenTable.test.ts— the relative-time reading picks days, hours or minutes, never seconds; the id mask keeps head and tail.useRevokeApiToken.test.ts— cached row marked revoked at once.StaffUserPicker.test.tsx— debounce, pick by click / Enter / Me, no stale matches, Enter never submits, no search once closed, cancellation, the type-more hint.MintStaffApiTokenForm.test.tsx— default payload, page-then-reset order, refusal under the role field and cleared on owner change, unseen token revoked with each failure worded, one mint for two racing submits, no mint while a secret is on screen, empty name refused.adminShellVisibility.test.ts— who sees the page.Related PRs: #1780 overlaps (StaffPermission union), #1975 overlaps (StaffPermission union, admin visibility fixture), #1988 overlaps (StaffPermission union, admin visibility fixture), #1989 overlaps (admin visibility fixture)
🤖 Generated with Claude Code
Complexity: medium
Review-Coverage: authored=claude; ran=gemini,codex; adjudicated=domain; blocked=cursor-grok(lost); declined=cursor-composer,cursor-kimi,cursor-muse,muse; rounds=9; full=5 @ a064a1e
Review-Attention: study ~20m (critical: mintTokenSchema.ts; decisions: do-less-alternative, auto-revoke-unseen-token, client-side-staff-filter, legacy-api-hides-token-page, role-refusal-via-403) @ a064a1e