feat(web-ui): admin UI for public API keys (issues #438/#439) - #551
Merged
Weegy merged 2 commits intoJul 30, 2026
Merged
Conversation
Adds /admin/api-keys — create/list/revoke for the server-to-server bearer
credentials exposed by @omadia/channel-api's admin router
(/api/public/v1/admin/keys). Modeled on admin/webhooks (the closest
existing 'secret shown once' pattern) and admin/mcp for list/create/
loading/empty/error state conventions.
- app/_lib/api.ts: ApiKeyPublicView type + listApiKeys/createApiKey/
revokeApiKey client functions.
- app/admin/api-keys/: page.tsx + ApiKeysPanel.tsx + shared.tsx
(status badge, chip, card/input classnames — feature-scoped, same
pattern as admin/webhooks/_components/shared.tsx).
- Reveal-once token UI: shown only in create-response state, dismissed
explicitly (never auto-hidden), never re-fetched — the list endpoint
never returns a token field.
- scopes:[] footgun avoided: the only real scope (chat:write) is a
checkbox that must stay checked to submit; unchecking blocks the
create button with an explanation rather than silently sending [] or
omitting the field against the operator's intent.
- Revoke requires an explicit two-step confirm (danger-variant Button,
text/edge only, no filled red) before calling the API.
- admin/page.tsx: unconditional static card under the Access group,
matching the convention already used for webhooks/mcp (plugin-backed
but not gated behind requiresNavFrom/registerNav).
- messages/{en,de}.json: adminApiKeys namespace + admin.index.cards.apiKeys.
- 11 new vitest cases covering empty/loaded/revoked states, the create
scope-checkbox footgun (asserts payload.scopes is never []), the
reveal-once flow, and the two-step revoke confirmation.
Fixes 5 blocking findings + 1 lower-severity note from a codex adversarial review of the /admin/api-keys page (issues #438/#439): 1. Token-overwrite bug: creating a second key while a first key's one-time token reveal was still showing silently overwrote it in React state. `canSubmit` now requires `!revealed`, and the create form (button + all fields) stays disabled until the reveal is explicitly dismissed. 2. Race conditions: a single global `pendingId`/`confirmingId` let concurrent per-row revokes clobber each other's busy/confirm UI state, and unsequenced list reloads could resolve out of order. Both are now `ReadonlySet<string>` keyed per key id, and `reload()` uses a monotonic sequence ref so only the most recently issued reload's result applies. 3. Lume state-color violation: several error texts rendered in danger color without the mandatory 1px edge. Added `errorTextCls`/ `errorInlineCls` (border + tint + text) to shared.tsx and applied them at all 6 flagged spots. 4. Hard i18n rule violation: the error helper returned raw backend response bodies, and the created-date display used `toLocaleString()`. `toFriendlyError(err, t)` now maps known backend error shapes to translated `adminApiKeys.errors.*` catalog keys (mirrors `admin/registries/page.tsx`), never rendering the raw body. Dates now use `useFormatter().dateTime(...)`. 5. Missing documentation: added a CHANGELOG bullet and a middleware-agent-handoff.md section describing the new admin UI page. Lower severity: decimal rate-limit input ("60.7") is now rejected via Number.isInteger instead of being silently truncated. Two new interaction-level tests exercise findings 1 and 2 directly via React Testing Library. Re-reviewed by codex (prReady: true).
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the admin UI for the public API keys introduced by #438/#439: create,
list, and revoke server-to-server bearer credentials for
POST /api/public/v1/chatat/admin/api-keys— create/list/revoke against/api/public/v1/admin/keys, gated by the same operator-session cookie everyother admin page uses.
Stacked on #549 (
feat/issue-438-439-public-api) since that hasn't mergedyet — this PR targets that branch, not
main.What's in this page
integers only), and the single
chat:writescope checkbox (hardcoded —it's the only scope that exists today).
once, right after creation, and is never re-derived from a list reload
(the list endpoint never returns a token field). The create form —
button and every field — is disabled while that reveal is still on
screen, so a second create can never silently overwrite the first key's
only-ever-shown token before it's copied.
tracked per key id (not a single shared value), so concurrent revokes on
different rows don't clobber each other's UI state.
out-of-order response (a slower in-flight fetch) overwriting a newer
one's result.
not_found,operator_auth.unavailable,auth.missing/auth.invalid,invalid_request) map to translated catalog messages underadminApiKeys.errors.*; nothing renders the raw backend response body.useFormatter().dateTime(...), nottoLocaleString().Review history
A codex adversarial review round found 5 blocking findings against the
initial version of this page (token-overwrite bug, two race conditions,
a Lume state-color violation, and a hard i18n-rule violation) plus one
lower-severity note (decimal rate-limit input silently truncated). All 5
are fixed in this PR — see the commit for the fix-by-fix breakdown — and
a re-review round confirmed the fixes.
Test plan
npm run typecheck— cleannpm run lint(api-keys dir) — cleannpm run i18n:check— 0 errors, no new "identical to en" warningsnpx vitest run— 409/409 tests, 54/54 files (under the repo'spinned Node 22.22.3 — a system Node version mismatch in the sandbox
breaks 5 unrelated, pre-existing tests in
toolTemplates.test.tsonnewer Node; confirmed unrelated to this diff)
the concurrent-revoke race-condition fix directly via React Testing
Library
fireEventunrelated pre-existing dev-server bug (Turbopack/Tailwind 500s on
every page due to a literal
text-[color:var(...)]example string indoc comments in two untouched test files); not something this PR
introduced or is in scope to fix
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.