Skip to content

feat: API Keys dashboard page -- generate, list, revoke (OPE-167)#289

Merged
DevanshuNEU merged 6 commits into
OpenCodeIntel:mainfrom
DevanshuNEU:feat/api-keys-dashboard
Mar 8, 2026
Merged

feat: API Keys dashboard page -- generate, list, revoke (OPE-167)#289
DevanshuNEU merged 6 commits into
OpenCodeIntel:mainfrom
DevanshuNEU:feat/api-keys-dashboard

Conversation

@DevanshuNEU

@DevanshuNEU DevanshuNEU commented Mar 8, 2026

Copy link
Copy Markdown
Collaborator

What

New dashboard page at /dashboard/api-keys for 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

  • 'Create Key' button -> name input modal -> generates key -> shows raw key ONCE
  • Copy-to-clipboard with visual feedback (Check icon)
  • Warning: 'Store this key securely. You will not be able to see it again.'
  • Button disabled when 5 active keys reached (backend enforced too)

Key list

  • Name, preview (ci_...xYz12345), tier badge (enterprise/pro/free), status (Active/Revoked)
  • Last used relative timestamp ('2h ago', 'Never used')
  • Revoke button with loading state

Empty state

  • Helpful context: 'Generate a key to connect Claude Desktop, Claude Code, Cursor, or any MCP client'
  • 'Create Your First Key' CTA

Quick setup hint

  • Points to Claude Desktop config path for immediate use

Navigation

  • Sidebar: 'API Keys' with KeyRound icon (between Usage and Documentation)
  • Command palette: 'API Keys' navigation entry

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

    • Added comprehensive API Keys management page with the ability to generate, view, revoke, and copy API keys with status tracking.
    • Added API Keys navigation menu item for easy access to key management.
  • Improvements

    • Enhanced Usage page with improved tier badge styling and more descriptive resource and feature layouts.

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

vercel Bot commented Mar 8, 2026

Copy link
Copy Markdown

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

@coderabbitai

coderabbitai Bot commented Mar 8, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a2732cda-af1b-4cb3-8048-edbcd6094a1d

📥 Commits

Reviewing files that changed from the base of the PR and between 873db8f and 1e94e8f.

📒 Files selected for processing (2)
  • frontend/src/pages/APIKeysPage.tsx
  • frontend/src/pages/UsagePage.tsx

📝 Walkthrough

Walkthrough

The 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

Cohort / File(s) Summary
Navigation Updates
frontend/src/components/Dashboard.tsx, frontend/src/components/dashboard/CommandPalette.tsx, frontend/src/components/dashboard/Sidebar.tsx
Added API Keys route, navigation item, and icon import across dashboard components to expose new feature in UI navigation layers.
API Keys Management
frontend/src/pages/APIKeysPage.tsx
New comprehensive page implementing full API key lifecycle: client-side data fetching via useQuery, skeleton/empty states, key list rendering with metadata (creation date, tier badge, last used, status), generate flow with modal and one-time display dialog, revoke flow with confirmation modal, copy-to-clipboard functionality, and cache invalidation on mutations.
Usage Page Redesign
frontend/src/pages/UsagePage.tsx
Redesigned header with tier badge and styling map; replaced two-column layout with responsive three-column resource grid; replaced compact feature rows with descriptive FeatureRow component including per-feature status indicators; updated skeleton loading structure and removed obsolete tier color constants.

Sequence Diagrams

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 A new feature hops into view,
API keys with a modal or two,
Generate, revoke, and copy with care,
The dashboard now has keys to share! ✨🔑

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main feature: an API Keys dashboard page with generate, list, and revoke capabilities, matching the changeset's core objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 67115a9 and 289e0f1.

📒 Files selected for processing (4)
  • frontend/src/components/Dashboard.tsx
  • frontend/src/components/dashboard/CommandPalette.tsx
  • frontend/src/components/dashboard/Sidebar.tsx
  • frontend/src/pages/APIKeysPage.tsx

Comment thread frontend/src/pages/APIKeysPage.tsx Outdated
Comment thread frontend/src/pages/APIKeysPage.tsx Outdated
… 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.
@vercel

vercel Bot commented Mar 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
opencodeintel Ready Ready Preview, Comment Mar 8, 2026 7:41pm

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 289e0f1 and 0ab7225.

📒 Files selected for processing (1)
  • frontend/src/pages/APIKeysPage.tsx

Comment thread frontend/src/pages/APIKeysPage.tsx Outdated
Comment thread frontend/src/pages/APIKeysPage.tsx Outdated
Comment thread frontend/src/pages/APIKeysPage.tsx
Comment thread frontend/src/pages/APIKeysPage.tsx
Matches DashboardHome which uses full-width layout. The constrained
width left half the screen empty and looked odd.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (3)
frontend/src/pages/APIKeysPage.tsx (3)

122-126: ⚠️ Potential issue | 🟠 Major

Surface query failures instead of falling through to the empty state.

Line 122 defaults data to [], 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 | 🟠 Major

Add 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 | 🟠 Major

Block re-entry while key creation is in flight.

handleGenerate() still runs when generating is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0ab7225 and e44193b.

📒 Files selected for processing (1)
  • frontend/src/pages/APIKeysPage.tsx

Comment thread frontend/src/pages/APIKeysPage.tsx Outdated
Comment thread frontend/src/pages/APIKeysPage.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (5)
frontend/src/pages/APIKeysPage.tsx (5)

209-215: ⚠️ Potential issue | 🟠 Major

Make 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 explicit aria-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 | 🟠 Major

Guard key creation against double-submit.

The disabled button is not enough here: Enter can still re-enter handleGenerate() while generating is true. That can mint multiple keys, but only the last plaintext is surfaced. Add a generating guard in both handleSubmit() and handleGenerate(), 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 | 🔴 Critical

Read the generated secret from the backend field the endpoint actually returns.

The paired generate endpoint has been returning the plaintext in key, not api_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 | 🟠 Major

Scope 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 tsx

Also 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 | 🟠 Major

Preserve the generated secret until the user explicitly dismisses it.

onOpenChange={onClose} plus onClose={() => setGeneratedKey(null)} means an accidental backdrop click or Escape discards the only plaintext copy. Keep generatedKey intact 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

📥 Commits

Reviewing files that changed from the base of the PR and between e44193b and c93f95c.

📒 Files selected for processing (1)
  • frontend/src/pages/APIKeysPage.tsx

Comment thread frontend/src/pages/APIKeysPage.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (5)
frontend/src/pages/APIKeysPage.tsx (5)

241-267: ⚠️ Potential issue | 🟠 Major

Enter can still submit twice while generation is in flight.

handleSubmit() only checks name.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 | 🟠 Major

Scope 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 | 🟠 Major

Add an accessible name to the revoke action.

This button is icon-only, and title is 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 | 🟠 Major

Don't present a masked preview as a copyable key.

apiKey.key_preview is 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 | 🟠 Major

Backdrop/Escape currently discard the only copy of the secret.

onOpenChange={onClose} makes an accidental outside click or Escape clear generatedKey. 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

📥 Commits

Reviewing files that changed from the base of the PR and between c93f95c and 873db8f.

⛔ Files ignored due to path filters (1)
  • frontend/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • frontend/package.json
  • frontend/src/components/ui/alert-dialog.tsx
  • frontend/src/pages/APIKeysPage.tsx

Comment thread frontend/package.json Outdated
Comment thread frontend/src/pages/APIKeysPage.tsx Outdated
Comment thread frontend/src/pages/APIKeysPage.tsx Outdated
@DevanshuNEU DevanshuNEU force-pushed the feat/api-keys-dashboard branch from a1e34de to e44193b Compare March 8, 2026 18:14
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)
@DevanshuNEU

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Mar 8, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@DevanshuNEU DevanshuNEU merged commit 487c61d into OpenCodeIntel:main Mar 8, 2026
8 checks passed
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.

1 participant