Skip to content

feat: premium API keys page redesign#290

Closed
DevanshuNEU wants to merge 1 commit into
OpenCodeIntel:mainfrom
DevanshuNEU:redesign/api-keys-premium
Closed

feat: premium API keys page redesign#290
DevanshuNEU wants to merge 1 commit into
OpenCodeIntel:mainfrom
DevanshuNEU:redesign/api-keys-premium

Conversation

@DevanshuNEU

@DevanshuNEU DevanshuNEU commented Mar 8, 2026

Copy link
Copy Markdown
Collaborator

What

Full visual overhaul of APIKeysPage.tsx. Addresses CEO feedback on PR #289 (generic design). All existing functionality preserved.

Changes

  • Table layout with column headers (Name | Key | Tier | Last used | Status)
  • Skeleton loading state replacing spinner
  • Hover-reveal revoke button -- less visual noise at rest
  • One-time key reveal modal with amber security warning and usage hint
  • Inline copy button with 2s checkmark feedback state
  • Color-coded tier badges: violet (enterprise), blue (pro), zinc (free)
  • Security callout with Shield icon
  • Footer summary: active count + remaining key slots
  • AlertDialog for revoke confirmation (replaces plain Dialog)
  • Header matches UsagePage pattern exactly (icon + title + subtitle)

What is NOT changed

  • All API calls, endpoints, and URL patterns
  • React Query setup and cache invalidation
  • All state variables and handlers
  • TypeScript interfaces
  • Named export APIKeysPage

Closes

Supersedes the visual layer from PR #289. Backend from #288 is unchanged.

Summary by CodeRabbit

  • New Features
    • Introduced a comprehensive API Keys management page with the ability to generate, view, and revoke API keys.
    • Added modal dialogs for generating new keys and displaying generated key details with copy-to-clipboard functionality.
    • Implemented confirmation flow for key revocation with security safeguards.
    • Added key tier badges, last-used timestamps, and empty-state messaging for improved user experience.

Visual overhaul to Stripe/Vercel quality. All functionality preserved.
- Table layout with column headers (Name | Key | Tier | Last used | Status)
- Skeleton loading state replacing spinner
- Hover-reveal revoke button (reduces visual noise)
- One-time key reveal modal with amber security warning and usage hint
- Inline copy button with 2s checkmark state
- Color-coded tier badges (free/pro/enterprise)
- Security callout with Shield icon
- Footer summary showing active count and remaining slots
- AlertDialog for revoke confirmation (was plain Dialog)
- Matches UsagePage header pattern exactly
@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
📝 Walkthrough

Walkthrough

Introduces a new API Keys management page component providing a complete interface for users to view, generate, and revoke API keys. The implementation includes data fetching from a /keys endpoint, modals for key generation and display, confirmation dialogs for revocation, clipboard support, and comprehensive error handling with loading states.

Changes

Cohort / File(s) Summary
API Keys Management Page
frontend/src/pages/APIKeysPage.tsx
New 525-line page component implementing full API key lifecycle management: fetches keys from authenticated /keys endpoint with loading skeleton UI, supports generating new keys via modal with name input and secure reveal display, handles key revocation with confirmation dialog and query invalidation, includes subcomponents for tier badges, copy-to-clipboard button, timeAgo formatting, empty state prompts, responsive grid-like table layout with key preview/status columns, and accessibility features (keyboard enter-to-submit, proper ARIA dialogs).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #288: Implements complementary backend API endpoints and service methods for /keys GET, DELETE /keys/{key_id}, list_keys(), and revoke_key_by_id() operations that directly support the data-fetching and mutation operations in this frontend component.

Poem

🐰 Keys to the kingdom, organized with care,
Generate and revoke with a modal flair!
Copy them quick before they fade away,
API keys managed the bunny way! 🔑✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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 accurately describes the main change: a redesign of the API keys page with premium features. It directly aligns with the PR's primary objective of overhauling the visual presentation and UX of the APIKeysPage component.

✏️ 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: 6

🤖 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 177-183: The inline Copy button currently copies the masked
apiKey.key_preview (from the list API) which is not a usable credential; update
the UI in APIKeysPage so the row-level copy either is removed or is explicitly
relabeled as a preview copy. Concretely, remove or conditionally replace the
<CopyButton value={apiKey.key_preview} /> next to apiKey.key_preview with a
non-misleading control (e.g., a disabled copy or a button labeled "Copy Preview"
that clearly indicates it's a preview-only value) and keep the existing
isRevoked guard; adjust any CopyButton props or aria-labels so consumers see
"Preview" instead of implying a real key is copied.
- Around line 163-167: The grid uses fixed pixel column widths which get clipped
due to the parent using overflow-hidden; update the container and column defs in
the div where className is built in APIKeysPage.tsx (the grid div around the API
key rows, referenced alongside isRevoked) to allow narrow viewports by (1)
making the first column shrink correctly with min-w-0 (add the utility/class
that sets min-width:0 on the grid item or container) and (2) replace the rigid
grid-cols-[1fr_200px_80px_110px_120px] with flexible minmax values (e.g., use
minmax(...) or relative widths so right-hand columns can overflow) and ensure
the row wrapper allows horizontal scrolling on small screens by switching
overflow-hidden to overflow-x-auto (apply this change to both occurrences noted
around lines 163-167 and 456-464).
- Around line 295-320: Add a "generation in flight" guard and disable the input
while generating: introduce a boolean state (e.g., isGenerating) used by the
existing button disable logic, update handleSubmit to return early if
isGenerating, call setIsGenerating(true) before invoking onGenerate and
setIsGenerating(false) when the generate promise resolves/rejects; also pass
isGenerating to the Input (disabled prop) and change the onKeyDown handler to
ignore Enter when isGenerating so pressing Enter cannot submit while a request
is in flight. Ensure you reference handleSubmit, onGenerate, and the
Input/id="key-name" element when making these changes.
- Around line 205-215: The revoke button is hidden from keyboard and touch users
due to "opacity-0 group-hover:opacity-100"; update the button styling around the
button component (the <button> using onRevoke, revoking, className, cn and
rendering <Trash2>) to ensure it's visible to focus and coarse-pointer devices
by adding focus-visible and focus-within visibility classes and a non-hover path
for small/touch layouts (e.g., add focus-visible:opacity-100
focus-within:opacity-100 and a small-screen or coarse-pointer alternative such
as sm:opacity-100 or a media-query based class) so keyboard and touch users can
always see and activate the revoke control.
- Around line 348-352: The query key for useQuery in APIKeysPage is global
('api-keys') causing cross-user cache leakage; update the queryKey used in
useQuery (the call that currently passes ['api-keys'] and calls
fetchKeys(token)) to include the current user's identity (e.g., session.user.id
from useAuth) so caches are partitioned per user, and adjust the enabled
condition to require the user id (e.g., enabled when token and session.user.id
are truthy) so the query only runs for the correct user.
- Around line 227-228: Make the one-time secret reveal dialog fully
non-dismissible except via the explicit "Done" button: prevent Escape and
outside-click closing on the Dialog that currently renders Dialog and
DialogContent (open onOpenChange={onClose}) by adding handlers to swallow
onEscapeKeyDown and onInteractOutside (or the framework-equivalent props) so
they call event.preventDefault()/event.stopPropagation() when in reveal mode,
and also hide or disable the X/close button rendered inside DialogContent during
the reveal (render it conditionally or set disabled when the one-time secret
reveal state is active). Apply the same change to the other reveal dialog
instance at the second location (lines ~273-279) so both dialogs are only
dismissible via the Done button.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 07de3dac-a474-4c30-b87f-99584bae4b3d

📥 Commits

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

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

Comment on lines +163 to +167
<div className={cn(
'group grid items-center gap-4 px-4 py-3.5 border-b border-border last:border-0 transition-colors',
'grid-cols-[1fr_200px_80px_110px_120px]',
isRevoked ? 'opacity-50' : 'hover:bg-muted/20',
)}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Allow the new grid table to fit narrow viewports.

These fixed-width columns need roughly 600px before content, but the wrapper also uses overflow-hidden. On phones or split panes, the right-hand columns get clipped instead of scrollable.

Suggested fix
    <div className={cn(
-     'group grid items-center gap-4 px-4 py-3.5 border-b border-border last:border-0 transition-colors',
+     'group min-w-[640px] grid items-center gap-4 px-4 py-3.5 border-b border-border last:border-0 transition-colors',
      'grid-cols-[1fr_200px_80px_110px_120px]',
      isRevoked ? 'opacity-50' : 'hover:bg-muted/20',
    )}>
-        <div className="rounded-lg border border-border overflow-hidden">
+        <div className="rounded-lg border border-border overflow-x-auto">
           {/* Column headers */}
-          <div className="grid gap-4 px-4 py-2.5 bg-muted/40 border-b border-border grid-cols-[1fr_200px_80px_110px_120px]">
+          <div className="min-w-[640px] grid gap-4 px-4 py-2.5 bg-muted/40 border-b border-border grid-cols-[1fr_200px_80px_110px_120px]">

Also applies to: 456-464

🤖 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 163 - 167, The grid uses
fixed pixel column widths which get clipped due to the parent using
overflow-hidden; update the container and column defs in the div where className
is built in APIKeysPage.tsx (the grid div around the API key rows, referenced
alongside isRevoked) to allow narrow viewports by (1) making the first column
shrink correctly with min-w-0 (add the utility/class that sets min-width:0 on
the grid item or container) and (2) replace the rigid
grid-cols-[1fr_200px_80px_110px_120px] with flexible minmax values (e.g., use
minmax(...) or relative widths so right-hand columns can overflow) and ensure
the row wrapper allows horizontal scrolling on small screens by switching
overflow-hidden to overflow-x-auto (apply this change to both occurrences noted
around lines 163-167 and 456-464).

Comment on lines +177 to +183
{/* Key preview + inline copy */}
<div className="flex items-center gap-2 min-w-0">
<code className="text-xs font-mono text-muted-foreground truncate flex-1">
{apiKey.key_preview}
</code>
{!isRevoked && <CopyButton value={apiKey.key_preview} />}
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The row-level copy action currently copies an unusable value.

The list API (backend/services/rate_limiter.py:233-250) only returns a masked key_preview, so this button copies ci_...suffix, not a working credential. Either remove the action here or relabel it as preview-only; plain “Copy” strongly implies the real key is recoverable from the list view.

🤖 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 177 - 183, The inline Copy
button currently copies the masked apiKey.key_preview (from the list API) which
is not a usable credential; update the UI in APIKeysPage so the row-level copy
either is removed or is explicitly relabeled as a preview copy. Concretely,
remove or conditionally replace the <CopyButton value={apiKey.key_preview} />
next to apiKey.key_preview with a non-misleading control (e.g., a disabled copy
or a button labeled "Copy Preview" that clearly indicates it's a preview-only
value) and keep the existing isRevoked guard; adjust any CopyButton props or
aria-labels so consumers see "Preview" instead of implying a real key is copied.

Comment on lines +205 to +215
<button
onClick={onRevoke}
disabled={revoking}
className={cn(
'opacity-0 group-hover:opacity-100 p-1 rounded transition-all',
'text-muted-foreground/40 hover:text-destructive hover:bg-destructive/10',
)}
title="Revoke key"
>
<Trash2 className="w-3.5 h-3.5" />
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep the revoke control visible for keyboard and touch users.

opacity-0 group-hover:opacity-100 makes the action effectively invisible on focus, and coarse-pointer devices never get hover at all. Add a focus-visible/focus-within path and don’t hide it on small/touch layouts.

Suggested fix
          <button
            onClick={onRevoke}
            disabled={revoking}
            className={cn(
-             'opacity-0 group-hover:opacity-100 p-1 rounded transition-all',
+             'opacity-100 sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100 p-1 rounded transition-all',
+             'focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
              'text-muted-foreground/40 hover:text-destructive hover:bg-destructive/10',
            )}
+           aria-label={`Revoke key ${apiKey.name}`}
            title="Revoke key"
          >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
onClick={onRevoke}
disabled={revoking}
className={cn(
'opacity-0 group-hover:opacity-100 p-1 rounded transition-all',
'text-muted-foreground/40 hover:text-destructive hover:bg-destructive/10',
)}
title="Revoke key"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
<button
onClick={onRevoke}
disabled={revoking}
className={cn(
'opacity-100 sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100 p-1 rounded transition-all',
'focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
'text-muted-foreground/40 hover:text-destructive hover:bg-destructive/10',
)}
aria-label={`Revoke key ${apiKey.name}`}
title="Revoke key"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
🤖 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 205 - 215, The revoke button
is hidden from keyboard and touch users due to "opacity-0
group-hover:opacity-100"; update the button styling around the button component
(the <button> using onRevoke, revoking, className, cn and rendering <Trash2>) to
ensure it's visible to focus and coarse-pointer devices by adding focus-visible
and focus-within visibility classes and a non-hover path for small/touch layouts
(e.g., add focus-visible:opacity-100 focus-within:opacity-100 and a small-screen
or coarse-pointer alternative such as sm:opacity-100 or a media-query based
class) so keyboard and touch users can always see and activate the revoke
control.

Comment on lines +227 to +228
<Dialog open onOpenChange={onClose}>
<DialogContent className="sm:max-w-md">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

wc -l frontend/src/pages/APIKeysPage.tsx

Repository: OpenCodeIntel/opencodeintel

Length of output: 109


🏁 Script executed:

cat -n frontend/src/pages/APIKeysPage.tsx | sed -n '200,300p'

Repository: OpenCodeIntel/opencodeintel

Length of output: 4294


🏁 Script executed:

head -50 frontend/src/pages/APIKeysPage.tsx | grep -E "import.*Dialog|from.*dialog"

Repository: OpenCodeIntel/opencodeintel

Length of output: 140


🏁 Script executed:

cd frontend && fd -e "tsx" -e "ts" | xargs grep -l "export.*Dialog" | head -5

Repository: OpenCodeIntel/opencodeintel

Length of output: 98


🏁 Script executed:

cat -n frontend/src/pages/APIKeysPage.tsx | sed -n '1,30p'

Repository: OpenCodeIntel/opencodeintel

Length of output: 1175


🏁 Script executed:

find frontend -name "dialog*" -type f | head -10

Repository: OpenCodeIntel/opencodeintel

Length of output: 108


🏁 Script executed:

cat frontend/src/components/ui/dialog.tsx

Repository: OpenCodeIntel/opencodeintel

Length of output: 3805


🏁 Script executed:

cat -n frontend/src/pages/APIKeysPage.tsx | sed -n '50,150p' | grep -E "onClose|apiKey|setApiKey"

Repository: OpenCodeIntel/opencodeintel

Length of output: 53


🏁 Script executed:

cat -n frontend/src/pages/APIKeysPage.tsx | sed -n '400,525p'

Repository: OpenCodeIntel/opencodeintel

Length of output: 5990


🏁 Script executed:

cat -n frontend/src/pages/APIKeysPage.tsx | sed -n '340,380p'

Repository: OpenCodeIntel/opencodeintel

Length of output: 1781


🏁 Script executed:

rg "onInteractOutside|onEscapeKeyDown" frontend/src --type tsx | head -10

Repository: OpenCodeIntel/opencodeintel

Length of output: 98


🏁 Script executed:

rg "onInteractOutside|onEscapeKeyDown" frontend/src -t ts -t js | head -10

Repository: OpenCodeIntel/opencodeintel

Length of output: 53


🏁 Script executed:

rg "DialogContent" frontend/src -A 3 -t ts -t js | grep -E "onInteractOutside|onEscapeKeyDown" | head -5

Repository: OpenCodeIntel/opencodeintel

Length of output: 53


🏁 Script executed:

rg "DialogPrimitive.Close" frontend/src -A 5 -B 5

Repository: OpenCodeIntel/opencodeintel

Length of output: 2029


🏁 Script executed:

python3 << 'EOF'
# Check Radix UI Dialog documentation for valid props
# Based on the component code, DialogContent accepts ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
# which means all standard Radix UI Content props are available

print("DialogContent wraps DialogPrimitive.Content from `@radix-ui/react-dialog`")
print("Valid props from Radix UI Dialog.Content include:")
print("- onInteractOutside: (event: PointerEvent) => void")
print("- onEscapeKeyDown: (event: KeyboardEvent) => void")
print("\nThese are standard Radix UI Dialog.Content props.")
EOF

Repository: OpenCodeIntel/opencodeintel

Length of output: 344


🏁 Script executed:

cat -n frontend/src/components/ui/dialog.tsx | sed -n '32,45p'

Repository: OpenCodeIntel/opencodeintel

Length of output: 1253


🏁 Script executed:

cat -n frontend/src/pages/APIKeysPage.tsx | sed -n '223,281p'

Repository: OpenCodeIntel/opencodeintel

Length of output: 2973


Prevent accidental dismissal of the one-time secret reveal dialog.

The dialog can be closed by pressing Escape, clicking outside, or clicking the X button, permanently discarding the only copy of the generated API key. For a one-time reveal with an irreplaceable secret, dismissal should only be possible through the explicit "Done" button.

The suggested fix prevents Escape and outside clicks, but still leaves the X close button functional. To fully address this, the close button should also be disabled or removed during the reveal.

Suggested fix
-    <Dialog open onOpenChange={onClose}>
-      <DialogContent className="sm:max-w-md">
+    <Dialog open onOpenChange={(open) => {
+      if (!open) onClose()
+    }}>
+      <DialogContent
+        className="sm:max-w-md"
+        onInteractOutside={(e) => e.preventDefault()}
+        onEscapeKeyDown={(e) => e.preventDefault()}
+      >

Note: Also address the X close button (rendered unconditionally in DialogContent) by disabling it or making it conditional during secret reveal.

Also applies to: 273-279

🤖 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 227 - 228, Make the one-time
secret reveal dialog fully non-dismissible except via the explicit "Done"
button: prevent Escape and outside-click closing on the Dialog that currently
renders Dialog and DialogContent (open onOpenChange={onClose}) by adding
handlers to swallow onEscapeKeyDown and onInteractOutside (or the
framework-equivalent props) so they call
event.preventDefault()/event.stopPropagation() when in reveal mode, and also
hide or disable the X/close button rendered inside DialogContent during the
reveal (render it conditionally or set disabled when the one-time secret reveal
state is active). Apply the same change to the other reveal dialog instance at
the second location (lines ~273-279) so both dialogs are only dismissible via
the Done button.

Comment on lines +295 to +320
const [name, setName] = useState('')
const handleSubmit = () => {
if (!name.trim()) return
onGenerate(name.trim())
setName('')
}
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="sm:max-w-sm">
<DialogHeader>
<DialogTitle className="text-base">New API key</DialogTitle>
<DialogDescription>
Give it a name so you know where it is used.
</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<Label htmlFor="key-name" className="text-xs text-muted-foreground">Key name</Label>
<Input
id="key-name"
placeholder="e.g. Claude Desktop, CI/CD, Local dev"
value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSubmit()}
autoFocus
className="h-9 text-sm"
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Block repeat submissions while key generation is in flight.

The button disables, but the input stays live and Enter still calls handleSubmit(). That can send multiple POST /keys/generate requests for a non-idempotent operation and accidentally mint extra active keys.

Suggested fix
  const [name, setName] = useState('')
  const handleSubmit = () => {
-   if (!name.trim()) return
+   if (generating || !name.trim()) return
    onGenerate(name.trim())
    setName('')
  }
  return (
    <Dialog open={open} onOpenChange={onClose}>
      <DialogContent className="sm:max-w-sm">
        <DialogHeader>
          <DialogTitle className="text-base">New API key</DialogTitle>
          <DialogDescription>
            Give it a name so you know where it is used.
          </DialogDescription>
        </DialogHeader>
        <div className="space-y-2">
          <Label htmlFor="key-name" className="text-xs text-muted-foreground">Key name</Label>
          <Input
            id="key-name"
            placeholder="e.g. Claude Desktop, CI/CD, Local dev"
            value={name}
            onChange={(e) => setName(e.target.value)}
            onKeyDown={(e) => e.key === 'Enter' && handleSubmit()}
+           disabled={generating}
            autoFocus
            className="h-9 text-sm"
          />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [name, setName] = useState('')
const handleSubmit = () => {
if (!name.trim()) return
onGenerate(name.trim())
setName('')
}
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="sm:max-w-sm">
<DialogHeader>
<DialogTitle className="text-base">New API key</DialogTitle>
<DialogDescription>
Give it a name so you know where it is used.
</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<Label htmlFor="key-name" className="text-xs text-muted-foreground">Key name</Label>
<Input
id="key-name"
placeholder="e.g. Claude Desktop, CI/CD, Local dev"
value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSubmit()}
autoFocus
className="h-9 text-sm"
/>
const [name, setName] = useState('')
const handleSubmit = () => {
if (generating || !name.trim()) return
onGenerate(name.trim())
setName('')
}
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="sm:max-w-sm">
<DialogHeader>
<DialogTitle className="text-base">New API key</DialogTitle>
<DialogDescription>
Give it a name so you know where it is used.
</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<Label htmlFor="key-name" className="text-xs text-muted-foreground">Key name</Label>
<Input
id="key-name"
placeholder="e.g. Claude Desktop, CI/CD, Local dev"
value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSubmit()}
disabled={generating}
autoFocus
className="h-9 text-sm"
/>
🤖 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 295 - 320, Add a "generation
in flight" guard and disable the input while generating: introduce a boolean
state (e.g., isGenerating) used by the existing button disable logic, update
handleSubmit to return early if isGenerating, call setIsGenerating(true) before
invoking onGenerate and setIsGenerating(false) when the generate promise
resolves/rejects; also pass isGenerating to the Input (disabled prop) and change
the onKeyDown handler to ignore Enter when isGenerating so pressing Enter cannot
submit while a request is in flight. Ensure you reference handleSubmit,
onGenerate, and the Input/id="key-name" element when making these changes.

Comment on lines +348 to +352
const { data: keys = [], isLoading, isError } = useQuery({
queryKey: ['api-keys'],
queryFn: () => fetchKeys(token),
enabled: !!token,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

head -100 frontend/src/pages/APIKeysPage.tsx

Repository: OpenCodeIntel/opencodeintel

Length of output: 3177


🏁 Script executed:

# Check for imports and session usage
rg "import.*session|useAuth|useSession|session\?\.user" frontend/src/pages/APIKeysPage.tsx -A 2 -B 2

Repository: OpenCodeIntel/opencodeintel

Length of output: 531


🏁 Script executed:

# Look for other useQuery patterns with cache key scoping in the codebase
rg "queryKey.*\[.*,.*\]" frontend/src --type ts --type tsx -A 1 | head -50

Repository: OpenCodeIntel/opencodeintel

Length of output: 98


🏁 Script executed:

# Check if there's a types.ts file with auth-related interfaces
find frontend/src -name "types.ts" -o -name "auth.ts" -o -name "session.ts" | head -20

Repository: OpenCodeIntel/opencodeintel

Length of output: 141


🏁 Script executed:

# Understand the token source and how it relates to user identity
rg "token|fetchKeys" frontend/src/pages/APIKeysPage.tsx -B 5 -A 2 | head -80

Repository: OpenCodeIntel/opencodeintel

Length of output: 1807


🏁 Script executed:

# Check the AuthContext to understand the session structure
cat frontend/src/contexts/AuthContext.tsx | head -150

Repository: OpenCodeIntel/opencodeintel

Length of output: 3354


🏁 Script executed:

# Also check if there are other useQuery calls in the frontend that already use user identity in queryKey
rg "queryKey.*\[.*user|queryKey.*\[.*session" frontend/src -A 2 -B 1

Repository: OpenCodeIntel/opencodeintel

Length of output: 1049


🏁 Script executed:

# Check hooks/useCachedQuery.ts mentioned in learnings to understand the caching pattern
cat frontend/src/hooks/useCachedQuery.ts 2>/dev/null || echo "File not found, searching for it:"

Repository: OpenCodeIntel/opencodeintel

Length of output: 6300


🏁 Script executed:

fd "useCachedQuery" frontend/src

Repository: OpenCodeIntel/opencodeintel

Length of output: 107


Scope the API keys query cache by user identity.

['api-keys'] is shared globally, so a sign-out/sign-in cycle can briefly expose the previous user's cached keys until the refetch completes. Since session.user.id is available from useAuth(), include it in the query key to partition caches per user, matching the pattern already used in useUserUsage.

Suggested fix
  const { data: keys = [], isLoading, isError } = useQuery({
-   queryKey: ['api-keys'],
+   queryKey: ['api-keys', session?.user.id],
    queryFn: () => fetchKeys(token),
-   enabled: !!token,
+   enabled: !!token && !!session?.user.id,
  })
🤖 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 348 - 352, The query key for
useQuery in APIKeysPage is global ('api-keys') causing cross-user cache leakage;
update the queryKey used in useQuery (the call that currently passes
['api-keys'] and calls fetchKeys(token)) to include the current user's identity
(e.g., session.user.id from useAuth) so caches are partitioned per user, and
adjust the enabled condition to require the user id (e.g., enabled when token
and session.user.id are truthy) so the query only runs for the correct user.

@DevanshuNEU

Copy link
Copy Markdown
Collaborator Author

Closing — this duplicated work already in PR #289. Redesign will be applied directly to feat/api-keys-dashboard branch instead.

@DevanshuNEU DevanshuNEU closed this Mar 8, 2026
@DevanshuNEU DevanshuNEU deleted the redesign/api-keys-premium branch March 8, 2026 17:45
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