Skip to content

feat(dashboard): API Keys page — create, list, revoke, connect AI agent (DEV-950)#2049

Open
brunod-e wants to merge 5 commits into
feat/user-api-keysfrom
feat/dashboard-api-keys
Open

feat(dashboard): API Keys page — create, list, revoke, connect AI agent (DEV-950)#2049
brunod-e wants to merge 5 commits into
feat/user-api-keysfrom
feat/dashboard-api-keys

Conversation

@brunod-e

Copy link
Copy Markdown
Collaborator

Summary

Frontend for self-service API keys (DEV-950), stacked on #2047. Adds the API Keys page from the Figma design: signed-in users create, view, and revoke personal API keys for the MCP server / public API.

What's included

  • API Keys page (/{dao}/api-keys, new "API" sidebar entry), gated behind sign-in (prompts the login modal when signed out).
  • Create flow: name modal → mints via the User API → one-time plaintext reveal modal with copy (never retrievable again).
  • Table: name, status (Active/Revoked), created, last used (from Authful via the backend enrichment in feat(user-api): self-service API keys via scoped Authful provisioning (DEV-950) #2047). Row action revokes with a destructive confirm.
  • Connect your AI agent: Claude Code / Cursor / Codex tabs with a copy-paste MCP install command.
  • Hand-written apiKeysClient on the /api/user proxy (session cookie) + a React Query hook.

Scope notes (agreed)

  • Columns implemented: name/status/created/last used. Usage ("N calls this month") is deferred — it needs per-key metrics from Gateful (separate data path), out of scope here.
  • The install command shows a <YOUR_API_KEY> placeholder (the plaintext is only shown once at creation); real tokens use the act_ prefix.
  • Wired into the main DAO sidebar; whitelabel sidebar entry is a follow-up.
  • Rotate + onboarding-tooltip frames from the design are deferred.

Verification

  • pnpm typecheck clean; eslint clean on the new/changed files.
  • Backend enabled on Railway dev (Authful PROVISIONING_API_KEY + User API AUTHFUL_URL/AUTHFUL_PROVISIONING_API_KEY), so the page works end to end against dev.

Refs DEV-950.

🤖 Generated with Claude Code

… AI agent)

Signed-in users manage personal API keys for the MCP server / public API:
create (name -> one-time plaintext reveal), list (name/status/created/last
used), and revoke, plus a Connect-your-AI-agent section with copy-paste
install commands for Claude Code, Cursor, and Codex. Talks to the User API
via the /api/user proxy (session cookie); gated behind sign-in and reachable
from the new API sidebar entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 10, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
anticapture-storybook Ready Ready Preview, Comment Jul 10, 2026 7:57pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
anticapture Ignored Ignored Jul 10, 2026 7:57pm

Request Review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a7ac0b8606

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +15 to +19
`claude mcp add anticapture --transport http ${MCP_URL} --header "X-API-KEY: ${key}"`,
Cursor: (key: string) =>
`cursor mcp add anticapture --transport http ${MCP_URL} --header "X-API-KEY: ${key}"`,
Codex: (key: string) =>
`codex mcp add anticapture --url ${MCP_URL} --header "X-API-KEY: ${key}"`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use the deployed MCP endpoint and bearer header

These copied commands point users at https://mcp.anticapture.com/v1 and send X-API-KEY, but the repo's public proxy exposes the MCP server under /mcp (infra/proxy/default.conf.template) and the MCP/Gateful auth path only forwards and validates Authorization: Bearer <token> (packages/anticapture-client/src/configure-upstream-client.ts, apps/gateful/src/auth/token-auth.ts). Anyone following the new Connect your AI agent section will get a non-working 404/401 setup even with a freshly created key.

Useful? React with 👍 / 👎.

const queryClient = useQueryClient();

const query = useQuery({
queryKey: QUERY_KEY,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Scope the API key query cache by user

Because /me/api-keys is session-scoped, using a fixed query key lets React Query synchronously return the previous signed-in user's cached key list as soon as a different session becomes authenticated, until the refetch completes. In an account-switch/shared-browser flow, the second user can briefly see the first user's key labels/status; include session.user.id in the query key or clear/remove this query on auth changes.

Useful? React with 👍 / 👎.

@isadorable-png isadorable-png left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🎨 UI Review

Automated review · ⚠️ Figma link found (figma.com/design/mUgy2KpQ3gJ07yZaUaXu8l, node 3315-19892) but the Figma MCP is unreachable in this run (config/auth error, not a "not found") — visual validation skipped. The dashboard's own Vercel preview is marked Ignored on this PR (only anticapture-storybook deployed), so there's no live preview either. Grounded instead in: the diff, the DS component inventory, and the ClickUp spec task (86ac8e1y5, "🔐 Login & API Keys" — very detailed, node IDs per screen listed in its "Design status" section) plus its linked Authful task (DEV-950).


API Keys page — access gating (question for author)

ApiKeysManager.tsx:33 gates the entire page behind sign-in (if (!isAuthed) return <...sign-in prompt.../>). The ClickUp spec explicitly designs an "anonymous-first funnel": "the API Keys page is reachable logged out. Empty state ("NO KEY YET") + "+ Create key". Clicking Create key while disconnected triggers the sign-in modal, then returns the user to the create flow with any typed name preserved (resume intent)." — also called out as an acceptance criterion. This PR instead blocks the page itself and has no "resume intent" (typed name preserved across login). The PR's own scope notes list Rotate and the onboarding tooltip as deferred, but don't mention this — please confirm whether the full-page gate is an agreed V1 cut or should follow the anonymous-first design. [Code-only]

Empty / signed-out states — component reuse

Both the signed-out gate (ApiKeysManager.tsx:34-45) and the "no keys yet" empty state (ApiKeysManager.tsx:76-79) are hand-rolled <div>s. The project already has a BlankSlate component (shared/components/design-system/blank-slate/BlankSlate.tsx) built for exactly this — icon + uppercase mono title + description — which happens to match the spec's "NO KEY YET" styling far better than the current sentence-case copy ("No API keys yet. Create one to connect your AI agent."). Swap both states to BlankSlate and align the copy with the spec string. [Code-only]

ApiKeysTable — component reuse & touch target

  • Hand-rolled <table>/<thead>/<tbody> (ApiKeysTable.tsx:20-41) duplicates the project's own DS table primitives (TableRow, TableHeader, TableHead, TableCell, etc. in design-system/table/components) — rebuild on those so hover/border states match every other data table in the app.
  • The row action is a bare <button> wrapping <Trash2 className="size-4" /> with no padding (ApiKeysTable.tsx:66-73) — hit area is ~16×16px, well under the 44×44px minimum touch target. Use IconButton (design-system/buttons/icon-button/IconButton) instead. [Code-only]

Row action model — question for author

Spec's row menu is Rotate API key · Disable API key · divider · Delete API key for an active key (Activate API key · divider · Delete API key for a disabled one) — a recoverable Disable/Activate toggle distinct from permanent delete. This PR collapses everything to a single destructive Revoke (permanent), and the badge/table copy says "Active/Revoked" instead of the spec's "Active/Disabled." The PR's scope notes mention Rotate as deferred but not this — worth a quick confirm that revoke-only is the agreed V1 cut. [Code-only]

Connect your AI agent — component reuse & touch target

  • The Claude Code / Cursor / Codex picker (ConnectAgentSection.tsx:35-48) is a hand-rolled toggle-button row; the project already has a SegmentedControl component (design-system/segmented-control) built for exactly this (radiogroup semantics, active/inactive styling, size variants) — swap in for consistency.
  • The copy button here and in SaveApiKeyModal.tsx is an unpadded <Copy/>/<Check/> icon in a raw <button> — same sub-44px touch-target issue as the table row action. Use IconButton. [Code-only]

Mobile

The touch-target gaps above (revoke button, copy buttons, client chips) apply directly on mobile, where these are the only way to trigger the action — no hidden lg:* alternative exists, so the small hit areas aren't just cosmetic. [Code-only]


Generated by Claude Code — automated UI review routine


Generated by Claude Code

brunod-e and others added 3 commits July 10, 2026 16:53
…-auth

- the three connect-wallet buttons called RainbowKit's openConnectModal
  directly, so the custom LoginModal never appeared; they now go through
  useLogin().openLogin
- the better-auth client baseURL was missing the service's /api/auth base
  path, so every auth call (get-session, siwe/nonce, ...) 404'd through
  the /api/user proxy
- the modal shows the ANTICAPTURE wordmark per the Figma (whitelabel keeps
  the neutral mark); the wordmark SVG now uses currentColor, with the
  watermark call-site compensating for its previously baked-in opacity

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…design

The page is user-scoped, not DAO-scoped: it now lives at /api-keys with an
"API" entry in the main sidebar (and mobile menu), and the misplaced entry
in the DAO sidebar is gone.

Visual pass against the Figma spec:
- table rebuilt on the design-system Table (contrast header, Name/Status/
  Created/Last Used columns, per-row ... options menu with revoke)
- connect section: client chips as design-system buttons, a key selector
  ("Key: <label>"), labeled Copy button inside the code block, and an
  activity status line driven by lastUsedAt
- keys created this session keep their plaintext in memory so the connect
  command ships with the real key (truncated on screen, full on copy)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8fca5038b2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

{created && (
<SaveApiKeyModal
open={!!created}
onOpenChange={(open) => !open && setCreated(null)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve one-time keys until explicit confirmation

When the save dialog is dismissed via the modal's X button, overlay click, or Escape key, Radix calls onOpenChange(false), and this handler immediately clears created. Because this plaintext key is never retrievable again, a user who accidentally closes the dialog before copying it loses the only copy and has to revoke/recreate the key; clear this state only from the explicit Done path, or disable passive dismissal for this one-time secret.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants