feat: API Keys dashboard page -- generate, list, revoke (OPE-167)#289
Conversation
New page at /dashboard/api-keys for managing MCP API keys: - Generate modal: name input, creates key, shows raw key ONCE with copy-to-clipboard and 'will not be shown again' warning - Key list: name, preview (ci_...suffix), tier badge, active/revoked status, last used timestamp, revoke button - Empty state with helpful context about what keys are for - Quick setup hint pointing to Claude Desktop config path - Sidebar: added 'API Keys' nav item with KeyRound icon - Command palette: added API Keys navigation entry - Max 5 active keys enforced (button disabled at limit) Uses existing patterns: useAuth(), API_URL, Bearer token, shadcn/ui Card/Dialog/Badge/Button, toast from sonner. Backend endpoints from PR OpenCodeIntel#288: - POST /api/v1/keys/generate - GET /api/v1/keys - DELETE /api/v1/keys/{key_id} TypeScript clean, build passes.
|
@DevanshuNEU is attempting to deploy a commit to the Dev's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
Caution Review failedThe pull request is closed. Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe PR adds a new API Keys management feature to the dashboard with a dedicated page supporting key listing, generation, revocation, and clipboard interactions. Dashboard routing, sidebar navigation, and command palette are updated to expose the feature. The UsagePage receives a design overhaul with updated layout, styling, and component structures. Changes
Sequence DiagramssequenceDiagram
participant User
participant UI as APIKeysPage UI
participant Query as useQuery Hook
participant API as Backend API
participant Cache as React Query Cache
User->>UI: Load API Keys page
UI->>Query: Trigger initial fetch
Query->>API: GET /api/keys
API-->>Query: Return keys list
Query-->>Cache: Update cache
Query-->>UI: Render keys list
User->>UI: Click Generate Key
UI->>UI: Show GenerateKeyModal
User->>UI: Enter name & submit
UI->>API: POST /api/keys (name)
API-->>UI: Return generated key
UI->>UI: Display NewKeyModal with key preview
User->>UI: Copy key to clipboard
UI-->>User: Visual feedback
User->>UI: Click Revoke on key
UI->>UI: Show RevokeConfirmModal
User->>UI: Confirm revocation
UI->>API: DELETE /api/keys/{id}
API-->>UI: Success response
UI->>Cache: Invalidate keys query
Cache->>Query: Refetch keys
Query-->>UI: Re-render updated list
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
frontend/src/pages/APIKeysPage.tsx (2)
138-154: Consider adding confirmation before revoking.Revoking an API key is destructive and irreversible. A confirmation dialog would prevent accidental revocations, especially since the button is right next to each key in the list.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/APIKeysPage.tsx` around lines 138 - 154, The handleRevoke function currently deletes an API key immediately; add a user confirmation step (e.g., window.confirm or a modal) before proceeding so accidental clicks can't revoke keys. Update handleRevoke to prompt the user and only continue to setRevoking, call fetch(`${API_URL}/keys/${keyId}`, { method: 'DELETE', ... }), and fetchKeys() if the user confirms; if they cancel, simply return without changing state. Ensure the confirmation uses the keyId or key name in the prompt for clarity and keep setRevoking(null) in the finally block to clear state.
273-286: Quick setup hint is macOS-specific.The path
~/Library/Application Support/Claude/only applies to macOS. Consider noting this or providing paths for other platforms.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/APIKeysPage.tsx` around lines 273 - 286, The quick setup hint currently displays a macOS-only path; update the JSX in the APIKeysPage component (the Card/CardContent block that renders when activeKeys.length > 0) to make the platform specificity explicit and/or include equivalent paths for other OSes (e.g., Windows: %APPDATA%\\Claude\\claude_desktop_config.json, Linux: ~/.config/Claude/claude_desktop_config.json). Modify the text to either prepend "macOS:" before the shown path or render a small OS-aware switch (using the browser userAgent or a simple labeled list) so users on Windows/Linux see appropriate paths. Ensure you edit only the Card content and keep the existing styling and conditional rendering.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/pages/APIKeysPage.tsx`:
- Around line 63-77: The CopyButton's handleCopy calls
navigator.clipboard.writeText without handling failures; wrap the writeText call
in a try/catch inside CopyButton.handleCopy, only setCopied(true) when writeText
succeeds, call setCopied(false) on failure (or leave unchanged), log the error
(console.error) and surface a failure to the user (e.g., call an existing
toast/notification helper or set an error state/aria-live message); optionally
implement a DOM fallback (document.execCommand('copy')) inside the catch before
reporting failure so CopyButton and its handleCopy remain robust across
permission/insecure-context errors.
- Around line 91-109: Replace the manual fetchKeys useCallback + useEffect with
React Query's useQuery: create a query with queryKey ['api-keys'] that uses the
token in the Authorization header to GET `${API_URL}/keys`, map the returned
data to setKeys (or directly use data.keys in the component) and use
isLoading/isError instead of setLoading; handle errors via useQuery's onError to
call toast.error('Failed to load API keys'); remove the fetchKeys function and
its useEffect, and ensure any mutations call queryClient.invalidateQueries({
queryKey: ['api-keys'] }) to refresh the list.
---
Nitpick comments:
In `@frontend/src/pages/APIKeysPage.tsx`:
- Around line 138-154: The handleRevoke function currently deletes an API key
immediately; add a user confirmation step (e.g., window.confirm or a modal)
before proceeding so accidental clicks can't revoke keys. Update handleRevoke to
prompt the user and only continue to setRevoking, call
fetch(`${API_URL}/keys/${keyId}`, { method: 'DELETE', ... }), and fetchKeys() if
the user confirms; if they cancel, simply return without changing state. Ensure
the confirmation uses the keyId or key name in the prompt for clarity and keep
setRevoking(null) in the finally block to clear state.
- Around line 273-286: The quick setup hint currently displays a macOS-only
path; update the JSX in the APIKeysPage component (the Card/CardContent block
that renders when activeKeys.length > 0) to make the platform specificity
explicit and/or include equivalent paths for other OSes (e.g., Windows:
%APPDATA%\\Claude\\claude_desktop_config.json, Linux:
~/.config/Claude/claude_desktop_config.json). Modify the text to either prepend
"macOS:" before the shown path or render a small OS-aware switch (using the
browser userAgent or a simple labeled list) so users on Windows/Linux see
appropriate paths. Ensure you edit only the Card content and keep the existing
styling and conditional rendering.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: b0ce76e7-caaf-4600-b2f7-feaef02965dc
📒 Files selected for processing (4)
frontend/src/components/Dashboard.tsxfrontend/src/components/dashboard/CommandPalette.tsxfrontend/src/components/dashboard/Sidebar.tsxfrontend/src/pages/APIKeysPage.tsx
… confirm, platform paths 1. CopyButton: wrapped clipboard.writeText in try/catch, shows toast.error on failure (matches DocsCodeBlock.tsx pattern) 2. React Query: replaced manual fetchKeys/useEffect/useState with useQuery + queryClient.invalidateQueries. Matches the established useCachedQuery.ts pattern used everywhere else in the dashboard. Generate and revoke both invalidate ['api-keys'] query. 3. Revoke confirmation: added a Dialog that shows key name and warns 'applications using this key will stop working immediately'. Matches SettingsPage delete confirmation pattern. 4. Platform-aware config paths: detects OS from userAgent, shows macOS/Windows/Linux paths accordingly instead of hardcoded macOS. Build passes, TS clean.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/pages/APIKeysPage.tsx`:
- Around line 77-80: The copy and revoke icon-only Buttons lack accessible
names; add clear aria-labels (or aria-labelledby) to the Buttons so screen
readers know the action and which key it targets. For the copy Button (where
handleCopy is used and icons Check/Copy are rendered) add an aria-label that
includes the key identifier or masked key (e.g., `aria-label={"Copy API key " +
keyName}` or toggle to `"Copied API key " + keyName` when copied), and do the
same for the revoke Button (the button that calls your revoke handler, e.g.,
handleRevoke) with a descriptive label like `aria-label={"Revoke API key " +
keyName}`; ensure any dynamic text uses the same variables used to render the
key so each button uniquely identifies its target.
- Around line 128-153: handleGenerate can be invoked multiple times (e.g., Enter
key) while an API request is in flight, producing duplicate keys; guard the
entry by early-returning when generating is true and also prevent form
submission/Enter from triggering generation while generating. Specifically, in
the handleGenerate function add a check if (generating) return to block
re-entry, and ensure the form or keyboard handler that currently triggers
handleGenerate respects the generating flag (either by changing the submit
button to type="button" or updating the form onSubmit handler to call
preventDefault and skip calling handleGenerate when generating is true) so only
one request can be in flight and duplicate key creation is prevented.
- Around line 144-145: The frontend is reading the wrong response field—replace
usage of data.api_key with data.key when setting the one-time secret so
setGeneratedKey receives the plaintext key the backend returns; locate the
fetch/response handling that calls res.json() and setGeneratedKey and update it
to read data.key (and adjust any variable names or null checks accordingly).
- Around line 122-126: The useQuery call in APIKeysPage currently ignores query
errors by defaulting data to [] which hides failures; update the useQuery
destructuring to include error (e.g., const { data: keys = [], error, isLoading
} = useQuery(...)) and then, inside the APIKeysPage render logic (where it
currently shows "No API keys yet"), check for error and render an error state
(or return an error message/component) before falling back to the empty-state
UI; apply the same change to the other useQuery usages noted (around the blocks
that reference fetchKeys and the queryKey ['api-keys'] at the other ranges) so
authorization/server errors surface instead of being treated as an empty list.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: b8649112-81b0-4f3e-9d55-da725a6ed517
📒 Files selected for processing (1)
frontend/src/pages/APIKeysPage.tsx
Matches DashboardHome which uses full-width layout. The constrained width left half the screen empty and looked odd.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (3)
frontend/src/pages/APIKeysPage.tsx (3)
122-126:⚠️ Potential issue | 🟠 MajorSurface query failures instead of falling through to the empty state.
Line 122 defaults
datato[], so a 401/403/500 still renders the "No API keys yet" branch at Line 214. That masks auth/server failures as a valid empty account.Also applies to: 183-190, 214-230
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/APIKeysPage.tsx` around lines 122 - 126, The query result is being defaulted to an empty array which masks failures (401/403/500) as "No API keys yet"; remove the default in the useQuery destructure so data can be undefined on error (change "const { data: keys = [], isLoading }" to not default data), then update the render logic that currently shows the empty-state branch (the code that checks keys/length around the API keys list) to first check useQuery's isError and error and render an error state/message when present; apply the same fix pattern for the other similar useQuery usages that default data (the ones calling fetchKeys / queryKey ['api-keys'] and the analogous blocks referenced in the review).
77-80:⚠️ Potential issue | 🟠 MajorAdd accessible names to the icon-only actions.
These buttons are currently unlabeled for assistive tech. The copy action should expose a generated-key label, and the revoke action should identify the targeted key by name.
Suggested fix
- <Button variant="ghost" size="icon" onClick={handleCopy} className="h-8 w-8"> + <Button + variant="ghost" + size="icon" + onClick={handleCopy} + className="h-8 w-8" + aria-label={copied ? 'Copied generated API key' : 'Copy generated API key'} + > @@ <Button variant="ghost" size="sm" onClick={() => setRevokeConfirm(key)} disabled={revoking === key.id} className="text-destructive hover:text-destructive hover:bg-destructive/10" + aria-label={`Revoke API key ${key.name}`} >Also applies to: 270-282
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/APIKeysPage.tsx` around lines 77 - 80, The icon-only Button in APIKeysPage.tsx lacks accessible names; update the Button used with handleCopy to include an aria-label like `Copy generated key` (or a dynamic label incorporating the key id/name) so screen readers know its purpose, and similarly add an aria-label to the revoke icon Button (e.g., `Revoke key: ${key.name}`) for the revoke handler (e.g., handleRevoke/revokeKey); keep the visual icon-only UI but ensure each Button element includes a clear aria-label or aria-labelledby that references the target key's name.
128-153:⚠️ Potential issue | 🟠 MajorBlock re-entry while key creation is in flight.
handleGenerate()still runs whengeneratingis already true, and the Enter shortcut at Line 350 bypasses the disabled button state. A fast double-submit can create multiple keys, but only the last plaintext secret is shown.Suggested fix
const handleGenerate = async () => { - if (!token || !keyName.trim()) return + if (generating || !token || !keyName.trim()) return setGenerating(true) @@ <Input id="key-name" placeholder="e.g. Claude Desktop, CI/CD, Development" value={keyName} onChange={(e) => setKeyName(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleGenerate()} + onKeyDown={(e) => e.key === 'Enter' && !generating && handleGenerate()} + disabled={generating} autoFocus />Also applies to: 345-350, 359-362
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/APIKeysPage.tsx` around lines 128 - 153, handleGenerate can be re-entered while a creation is in-flight (generating state), so add a stable guard using a ref: create a generatingRef (useRef(false)), set generatingRef.current = true immediately when starting (alongside setGenerating(true)) and set it back to false in finally; at the top of handleGenerate check if (generatingRef.current) return to prevent re-entry; also update the Enter-key shortcut handler (the onKeyDown/submit handler that currently calls handleGenerate on Enter) to check generatingRef.current and preventDefault/return when true so Enter cannot bypass the disabled button state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/pages/APIKeysPage.tsx`:
- Around line 174-178: The current closeGenerateDialog() clears generatedKey
(via setGeneratedKey(null)) and is used for every dialog close path, so
backdrop/Escape dismiss will wipe the one-time secret; change the dialog close
handling so closeGenerateDialog() is only used for cancellation flows while
backdrop/Escape/escKeyDown or onClose handlers after a successful create do NOT
call setGeneratedKey(null). Concretely, keep closeGenerateDialog (which still
calls setGenerateOpen(false) and setKeyName('')) for cancel paths, add a
separate cancelGenerateDialog or conditional close handler that does not clear
generatedKey after a successful creation, and update the Dialog
onClose/Backdrop/Escape bindings (where generatedKey is wired) to use the
non-destructive handler or check generatedKey before clearing so the one-time
key remains visible until the user explicitly dismisses it.
- Around line 122-124: The query cache key for useQuery is too broad—change the
key passed to useQuery (where queryKey is currently ['api-keys']) to include the
current user's identifier (e.g., session?.user?.id) so cached results are scoped
per user; update the useQuery call that invokes fetchKeys(token) (and any
related destructuring of data: keys) to use a key like ['api-keys',
session?.user?.id] to prevent cross-session cache leakage.
---
Duplicate comments:
In `@frontend/src/pages/APIKeysPage.tsx`:
- Around line 122-126: The query result is being defaulted to an empty array
which masks failures (401/403/500) as "No API keys yet"; remove the default in
the useQuery destructure so data can be undefined on error (change "const {
data: keys = [], isLoading }" to not default data), then update the render logic
that currently shows the empty-state branch (the code that checks keys/length
around the API keys list) to first check useQuery's isError and error and render
an error state/message when present; apply the same fix pattern for the other
similar useQuery usages that default data (the ones calling fetchKeys / queryKey
['api-keys'] and the analogous blocks referenced in the review).
- Around line 77-80: The icon-only Button in APIKeysPage.tsx lacks accessible
names; update the Button used with handleCopy to include an aria-label like
`Copy generated key` (or a dynamic label incorporating the key id/name) so
screen readers know its purpose, and similarly add an aria-label to the revoke
icon Button (e.g., `Revoke key: ${key.name}`) for the revoke handler (e.g.,
handleRevoke/revokeKey); keep the visual icon-only UI but ensure each Button
element includes a clear aria-label or aria-labelledby that references the
target key's name.
- Around line 128-153: handleGenerate can be re-entered while a creation is
in-flight (generating state), so add a stable guard using a ref: create a
generatingRef (useRef(false)), set generatingRef.current = true immediately when
starting (alongside setGenerating(true)) and set it back to false in finally; at
the top of handleGenerate check if (generatingRef.current) return to prevent
re-entry; also update the Enter-key shortcut handler (the onKeyDown/submit
handler that currently calls handleGenerate on Enter) to check
generatingRef.current and preventDefault/return when true so Enter cannot bypass
the disabled button state.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 055f280e-ded6-45f1-85e6-8af345d745b4
📒 Files selected for processing (1)
frontend/src/pages/APIKeysPage.tsx
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (5)
frontend/src/pages/APIKeysPage.tsx (5)
209-215:⚠️ Potential issue | 🟠 MajorMake the revoke action visible and named for non-pointer users.
This control stays hidden unless the row is hovered, so keyboard users can land on an effectively invisible button. It also relies on
title, which is not a reliable accessible name. Add focus-based visibility (group-focus-within/focus-visible) and an explicitaria-label, e.g.Revoke API key ${apiKey.name}.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/APIKeysPage.tsx` around lines 209 - 215, The revoke button is invisible to keyboard users and lacks an accessible name; update the button rendered when !isRevoked (the element with onClick={onRevoke}, disabled={revoking}) to also become visible on keyboard focus by adding focus-based utility classes (e.g. include group-focus-within:opacity-100 and/or focus-visible:opacity-100 alongside group-hover:opacity-100) and add an explicit accessible name via aria-label (e.g. `Revoke API key ${apiKey.name}`) so screen reader users can identify the action.
288-290:⚠️ Potential issue | 🟠 MajorGuard key creation against double-submit.
The disabled button is not enough here: Enter can still re-enter
handleGenerate()whilegeneratingis true. That can mint multiple keys, but only the last plaintext is surfaced. Add ageneratingguard in bothhandleSubmit()andhandleGenerate(), and suppress the Enter shortcut while the request is in flight.Also applies to: 307-307, 343-345
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/APIKeysPage.tsx` around lines 288 - 290, Add a "generating" guard to prevent double-submit: in handleSubmit() and handleGenerate() check and return early if generating is true before proceeding, and set generating true at the start of handleGenerate() and reset it on completion/failure. Also disable or ignore the Enter key shortcut while generating (e.g., in the key handler that triggers handleSubmit()) so Enter cannot invoke handleGenerate() during the in-flight request. Update references: handleSubmit, handleGenerate, and the Enter key handler to use the shared generating flag.
356-358:⚠️ Potential issue | 🔴 CriticalRead the generated secret from the backend field the endpoint actually returns.
The paired generate endpoint has been returning the plaintext in
key, notapi_key. If this stays as-is, the creation flow succeeds but the one-time secret is never shown, so the user cannot recover it. Validate the response field before closing the entry modal.🐛 Proposed fix
const data = await res.json() - setGenerateOpen(false) - setGeneratedKey(data.api_key) + if (!data.key) { + throw new Error('Generate response did not include the API key') + } + setGenerateOpen(false) + setGeneratedKey(data.key)#!/bin/bash set -euo pipefail echo "=== Inspect the generate endpoint implementation ===" fd "rate_limiter.py" backend --type f -x sed -n '170,210p' {} echo echo "=== Search for response-field names used by key generation ===" rg -n -C3 'api_key|["'"'"']key["'"'"']|/keys/generate|keys/generate' backend --type py🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/APIKeysPage.tsx` around lines 356 - 358, The response handler currently reads data.api_key and closes the modal before verifying the field; change it to read the actual backend field data.key (use the unique symbols setGeneratedKey and setGenerateOpen) and only call setGenerateOpen(false) after confirming data.key exists; if data.key is missing, keep the modal open and surface an error (e.g., show validation or set an error state) so the one-time secret is not lost.
337-340:⚠️ Potential issue | 🟠 MajorScope the React Query cache to the signed-in user.
['api-keys']is global. After an auth/session switch, React Query can reuse the previous user's cached list until a refetch lands. Include the current user identifier in both the query key and the invalidation key, e.g.['api-keys', session?.user?.id].#!/bin/bash set -euo pipefail echo "=== APIKeysPage uses a global api-keys query key ===" rg -n -C2 "queryKey:\s*\['api-keys'" frontend/src/pages/APIKeysPage.tsx echo echo "=== Compare against existing user-scoped query-key patterns ===" rg -n -C2 "queryKey:.*(userId|session\?\.user\?\.id)|queryKey:\s*\[[^]]*userId" frontend/src --type ts --type tsx echo echo "=== Check whether auth changes clear React Query caches centrally ===" rg -n -C3 "onAuthStateChange|queryClient\.(clear|removeQueries|invalidateQueries)" frontend/src --type ts --type tsxAlso applies to: 359-359, 378-378
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/APIKeysPage.tsx` around lines 337 - 340, The query key for useQuery in APIKeysPage.tsx is currently global (['api-keys']) and must be scoped to the signed-in user; update the useQuery call that uses queryFn: () => fetchKeys(token) (and any related calls like queryClient.invalidateQueries or removeQueries) to include the current user identifier (e.g. session?.user?.id or userId) so the key becomes ['api-keys', session?.user?.id], and ensure any places that invalidate or remove the API keys cache (calls referencing 'api-keys') use the same two-item key so cache operations are user-scoped and won't leak between sessions.
225-228:⚠️ Potential issue | 🟠 MajorPreserve the generated secret until the user explicitly dismisses it.
onOpenChange={onClose}plusonClose={() => setGeneratedKey(null)}means an accidental backdrop click or Escape discards the only plaintext copy. KeepgeneratedKeyintact for passive close paths and clear it only from an explicit "Done" action.Also applies to: 481-483
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/APIKeysPage.tsx` around lines 225 - 228, The modal NewKeyModal currently ties Dialog's onOpenChange to onClose which calls setGeneratedKey(null), causing accidental backdrop clicks/Escape to erase the only plaintext API key; change the behavior so onOpenChange does not clear generatedKey and only the explicit "Done" (or a dedicated close button handler) calls setGeneratedKey(null). Concretely, remove or disable clearing logic from the Dialog's onOpenChange prop in NewKeyModal (and the duplicate modal instance), keep generatedKey/state intact on passive closes, and wire setGeneratedKey(null) only to the explicit done/acknowledge action (e.g., handleDone or onDone button handler) so the secret is preserved until user confirmation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/pages/APIKeysPage.tsx`:
- Around line 183-187: The CopyButton currently copies apiKey.key_preview (a
masked/incomplete value) which misleads users; update the UI so persisted rows
do not offer a misleading "copy key": either remove the CopyButton when
rendering the persisted API key (the code block referencing apiKey.key_preview
together with {!isRevoked && <CopyButton ...>}) or change its label/tooltip to
an explicit "Copy preview" and adjust the CopyButton invocation to use a prop
that displays "Copy preview" (or similar) so users know they're copying only a
preview, not the full key. Ensure changes are applied where apiKey.key_preview
and CopyButton are used and keep the existing isRevoked check logic.
---
Duplicate comments:
In `@frontend/src/pages/APIKeysPage.tsx`:
- Around line 209-215: The revoke button is invisible to keyboard users and
lacks an accessible name; update the button rendered when !isRevoked (the
element with onClick={onRevoke}, disabled={revoking}) to also become visible on
keyboard focus by adding focus-based utility classes (e.g. include
group-focus-within:opacity-100 and/or focus-visible:opacity-100 alongside
group-hover:opacity-100) and add an explicit accessible name via aria-label
(e.g. `Revoke API key ${apiKey.name}`) so screen reader users can identify the
action.
- Around line 288-290: Add a "generating" guard to prevent double-submit: in
handleSubmit() and handleGenerate() check and return early if generating is true
before proceeding, and set generating true at the start of handleGenerate() and
reset it on completion/failure. Also disable or ignore the Enter key shortcut
while generating (e.g., in the key handler that triggers handleSubmit()) so
Enter cannot invoke handleGenerate() during the in-flight request. Update
references: handleSubmit, handleGenerate, and the Enter key handler to use the
shared generating flag.
- Around line 356-358: The response handler currently reads data.api_key and
closes the modal before verifying the field; change it to read the actual
backend field data.key (use the unique symbols setGeneratedKey and
setGenerateOpen) and only call setGenerateOpen(false) after confirming data.key
exists; if data.key is missing, keep the modal open and surface an error (e.g.,
show validation or set an error state) so the one-time secret is not lost.
- Around line 337-340: The query key for useQuery in APIKeysPage.tsx is
currently global (['api-keys']) and must be scoped to the signed-in user; update
the useQuery call that uses queryFn: () => fetchKeys(token) (and any related
calls like queryClient.invalidateQueries or removeQueries) to include the
current user identifier (e.g. session?.user?.id or userId) so the key becomes
['api-keys', session?.user?.id], and ensure any places that invalidate or remove
the API keys cache (calls referencing 'api-keys') use the same two-item key so
cache operations are user-scoped and won't leak between sessions.
- Around line 225-228: The modal NewKeyModal currently ties Dialog's
onOpenChange to onClose which calls setGeneratedKey(null), causing accidental
backdrop clicks/Escape to erase the only plaintext API key; change the behavior
so onOpenChange does not clear generatedKey and only the explicit "Done" (or a
dedicated close button handler) calls setGeneratedKey(null). Concretely, remove
or disable clearing logic from the Dialog's onOpenChange prop in NewKeyModal
(and the duplicate modal instance), keep generatedKey/state intact on passive
closes, and wire setGeneratedKey(null) only to the explicit done/acknowledge
action (e.g., handleDone or onDone button handler) so the secret is preserved
until user confirmation.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 5275b5b6-d249-44b5-815e-bc32c0e46a8b
📒 Files selected for processing (1)
frontend/src/pages/APIKeysPage.tsx
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (5)
frontend/src/pages/APIKeysPage.tsx (5)
241-267:⚠️ Potential issue | 🟠 MajorEnter can still submit twice while generation is in flight.
handleSubmit()only checksname.trim(), so the input's Enter handler bypasses the disabled button state and can create multiple keys before the first request finishes.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/APIKeysPage.tsx` around lines 241 - 267, handleSubmit currently only guards against empty names, so the onKeyDown Enter handler can trigger it while a generation is in flight; update handleSubmit to also bail out when generating is true (or otherwise disable further submits) and ensure the onKeyDown path uses the same guarded function. Specifically, modify the handleSubmit function referenced here (and any callers like the onKeyDown handler and the Button onClick) to check the generating flag before calling onGenerate and resetting state, so Enter cannot submit while generating.
318-320:⚠️ Potential issue | 🟠 MajorScope the query cache key to the current user.
['api-keys']is global, so after a session switch the next user can briefly see the previous user's cached list until the refetch completes. Include the authenticated user identifier in the key.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/APIKeysPage.tsx` around lines 318 - 320, The query key for useQuery is currently ['api-keys'] which is global and allows cross-user cache leakage; update the queryKey in the useQuery call that invokes fetchKeys(token) to include the current authenticated user's identifier (for example user.id, user.email, or token subject) so it becomes ['api-keys', currentUserId] (or similar) to scope the cache per-user and ensure cached lists don't show across sessions; modify the useQuery invocation that references fetchKeys and token accordingly.
175-180:⚠️ Potential issue | 🟠 MajorAdd an accessible name to the revoke action.
This button is icon-only, and
titleis not a reliable accessible name. Screen readers still need an explicit label for which key will be revoked.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/APIKeysPage.tsx` around lines 175 - 180, The revoke button is icon-only and uses title which is not reliable for assistive tech; update the button (the element with onClick={onRevoke} and disabled={revoking}) to include an explicit accessible name via aria-label (or aria-labelledby) that includes the specific key identifier (e.g., key.name or key.id / apiKey.id) so screen readers announce which key will be revoked; ensure the label is dynamic and unique per key (for example "Revoke API key {key.name}" or "Revoke API key {apiKey.id}").
159-161:⚠️ Potential issue | 🟠 MajorDon't present a masked preview as a copyable key.
apiKey.key_previewis intentionally incomplete, but this still copies it and shows a success state. That makes a non-functional credential look usable.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/APIKeysPage.tsx` around lines 159 - 161, The UI currently renders CopyButton with apiKey.key_preview which copies an intentionally incomplete value; update the rendering logic in APIKeysPage.tsx so CopyButton never copies the preview. Instead, only render or enable CopyButton when the real plaintext key is available (e.g., a property like apiKey.plaintext or apiKey.full_key) and apiKey is not revoked (use the existing isRevoked check), otherwise render a non-copyable preview (keep the <code> showing apiKey.key_preview but remove or disable CopyButton). Locate the CopyButton usage and the apiKey.key_preview usage in the component and make the copy action conditional on the presence of the real key.
192-193:⚠️ Potential issue | 🟠 MajorBackdrop/Escape currently discard the only copy of the secret.
onOpenChange={onClose}makes an accidental outside click or Escape cleargeneratedKey. For a one-time credential, only an explicit dismiss path should destroy it.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/APIKeysPage.tsx` around lines 192 - 193, The Dialog currently uses onOpenChange={onClose}, which causes backdrop clicks/Escape to call onClose and wipe the only copy of generatedKey; instead remove the onOpenChange prop and control closing only via an explicit close action: render the Dialog with a fixed open prop (e.g., open={true} or controlled by a dedicated state), add a DialogClose or explicit button that calls onClose (the same function that clears generatedKey), and ensure no automatic backdrop/Escape handler clears generatedKey; reference the Dialog component, the onClose handler, and the generatedKey state to implement this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/package.json`:
- Line 18: Remove the unused Radix alert-dialog dependency and wrapper: delete
the dependency entry for "@radix-ui/react-alert-dialog" from package.json and
uninstall it (npm/yarn/pnpm remove), delete the wrapper file
frontend/src/components/ui/alert-dialog.tsx (and any exported symbol it
declares, e.g., AlertDialog/AlertDialogTrigger/AlertDialogContent), and remove
any imports/usages or re-export entries that reference that file (check
component index barrels). After removal, run install/build to ensure no missing
imports remain.
In `@frontend/src/pages/APIKeysPage.tsx`:
- Around line 219-226: The Usage block in APIKeysPage.tsx currently only shows
the generic "Authorization: Bearer '<your-key>'" string; add a short Claude
Desktop quick-start hint immediately under or alongside that code snippet
indicating the config location (e.g., "Claude Desktop → Settings → API Keys
(paste your key into 'API Key' or 'Custom API')" ) so users can find where to
configure the app; update the JSX inside the rounded div that contains the
<Terminal /> header and the code element with the "Authorization: Bearer" text
(the usage block) to include a small, muted hint line (matching existing
text-muted-foreground styling and small font size) that references Claude
Desktop setup instructions and keeps layout consistent.
- Line 8: The import of the Skeleton primitive in APIKeysPage.tsx (import {
Skeleton } from '@/components/ui/skeleton') breaks the build because that module
doesn't exist; either add the shared Skeleton component at the referenced module
path (exporting a Skeleton named export) or remove/replace the import and use an
existing placeholder component (e.g., an existing Spinner/Loader or Skeleton
alternative) within the APIKeysPage component so the build no longer imports a
missing module. Ensure the replacement preserves the same named usage (Skeleton)
or update all usages in APIKeysPage.tsx to the new component name.
---
Duplicate comments:
In `@frontend/src/pages/APIKeysPage.tsx`:
- Around line 241-267: handleSubmit currently only guards against empty names,
so the onKeyDown Enter handler can trigger it while a generation is in flight;
update handleSubmit to also bail out when generating is true (or otherwise
disable further submits) and ensure the onKeyDown path uses the same guarded
function. Specifically, modify the handleSubmit function referenced here (and
any callers like the onKeyDown handler and the Button onClick) to check the
generating flag before calling onGenerate and resetting state, so Enter cannot
submit while generating.
- Around line 318-320: The query key for useQuery is currently ['api-keys']
which is global and allows cross-user cache leakage; update the queryKey in the
useQuery call that invokes fetchKeys(token) to include the current authenticated
user's identifier (for example user.id, user.email, or token subject) so it
becomes ['api-keys', currentUserId] (or similar) to scope the cache per-user and
ensure cached lists don't show across sessions; modify the useQuery invocation
that references fetchKeys and token accordingly.
- Around line 175-180: The revoke button is icon-only and uses title which is
not reliable for assistive tech; update the button (the element with
onClick={onRevoke} and disabled={revoking}) to include an explicit accessible
name via aria-label (or aria-labelledby) that includes the specific key
identifier (e.g., key.name or key.id / apiKey.id) so screen readers announce
which key will be revoked; ensure the label is dynamic and unique per key (for
example "Revoke API key {key.name}" or "Revoke API key {apiKey.id}").
- Around line 159-161: The UI currently renders CopyButton with
apiKey.key_preview which copies an intentionally incomplete value; update the
rendering logic in APIKeysPage.tsx so CopyButton never copies the preview.
Instead, only render or enable CopyButton when the real plaintext key is
available (e.g., a property like apiKey.plaintext or apiKey.full_key) and apiKey
is not revoked (use the existing isRevoked check), otherwise render a
non-copyable preview (keep the <code> showing apiKey.key_preview but remove or
disable CopyButton). Locate the CopyButton usage and the apiKey.key_preview
usage in the component and make the copy action conditional on the presence of
the real key.
- Around line 192-193: The Dialog currently uses onOpenChange={onClose}, which
causes backdrop clicks/Escape to call onClose and wipe the only copy of
generatedKey; instead remove the onOpenChange prop and control closing only via
an explicit close action: render the Dialog with a fixed open prop (e.g.,
open={true} or controlled by a dedicated state), add a DialogClose or explicit
button that calls onClose (the same function that clears generatedKey), and
ensure no automatic backdrop/Escape handler clears generatedKey; reference the
Dialog component, the onClose handler, and the generatedKey state to implement
this change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 03290a21-8ee7-4b35-9d0f-87f3a613df81
⛔ Files ignored due to path filters (1)
frontend/bun.lockis excluded by!**/*.lock
📒 Files selected for processing (3)
frontend/package.jsonfrontend/src/components/ui/alert-dialog.tsxfrontend/src/pages/APIKeysPage.tsx
a1e34de to
e44193b
Compare
Complete visual overhaul: - Key cards with tier-colored left accent stripe (amber/indigo/zinc) - Revoke on hover only, connect guide with tabbed config snippets - JetBrains Mono key preview, full-width layout - Empty state with feature hints, emerald key in generate dialog - No functional changes, same API calls and React Query patterns
Redesigned UsagePage to match the new API Keys design language: - Tier badge: same amber/indigo/zinc style as API Keys cards - Resource cards: 3-column grid with large numbers, progress bar on repos - Features: row-based layout with icon boxes, descriptions, and emerald checkmarks for enabled / indigo Pro badge for locked - Full-width layout (removed max-w-4xl) - Section labels: uppercase tracking-wider muted text - Upgrade CTA: button in header + subtle banner, not heavy card - Loading skeleton: matches 3-col + feature list layout - Cost tracking: dashed border placeholder, dimmed Same data, same hooks, same API. Pure visual upgrade.
Security:
- handleGenerate guards against double-submit (if generating, return)
- Enter key also checks !generating before triggering
- Dialog prevents backdrop/Escape close when generatedKey is displayed
(only explicit 'Done' button clears the one-time key)
- Removed CopyInline from key_preview (preview is intentionally
incomplete, copying it gives users a broken value)
Accessibility:
- CopyInline button has dynamic aria-label ('Copy/Copied' + label)
- Revoke button has aria-label='Revoke API key {name}'
- Removed unused X import
Error handling:
- useQuery destructures error alongside data/isLoading
- Error state renders destructive-styled message instead of empty list
- Query key scoped to user: ['api-keys', userId] prevents cross-user
cache leakage between sessions
- invalidateQueries calls updated to match scoped key
Skipped findings (verified not applicable):
- Radix alert-dialog removal: not in package.json
- Skeleton import: not imported in this file
- data.api_key -> data.key: backend returns api_key (line 94)
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
What
New dashboard page at
/dashboard/api-keysfor self-service API key management. No more manual Python scripts, SQL queries, or hash confusion.Screenshot context
Sits alongside the existing Repositories and Usage pages. Same design language -- dark theme, shadcn/ui, Tailwind.
Features
Key generation
Key list
ci_...xYz12345), tier badge (enterprise/pro/free), status (Active/Revoked)Empty state
Quick setup hint
Navigation
Files changed
frontend/src/pages/APIKeysPage.tsx(new, 350 lines)frontend/src/components/Dashboard.tsx(+2 lines, route)frontend/src/components/dashboard/Sidebar.tsx(+2 lines, nav item)frontend/src/components/dashboard/CommandPalette.tsx(+1 line, palette entry)Backend dependency
Requires PR #288 endpoints: POST /keys/generate, GET /keys, DELETE /keys/{id}
Testing
TypeScript clean, build passes.
Summary by CodeRabbit
Release Notes
New Features
Improvements